diff --git a/api/handler_test.go b/api/handler_test.go index c805f5b0c26..475c25bbb05 100644 --- a/api/handler_test.go +++ b/api/handler_test.go @@ -65,7 +65,7 @@ func createDefaultAdminUser(authService auth.Service, t *testing.T) *authmodel.C Username: "admin", } - creds, err := auth.SetupAdminUser(authService, user) + creds, err := auth.SetupAdminUser(authService, &authmodel.SuperuserConfiguration{User: *user}) testutil.Must(t, err) return creds } diff --git a/auth/model/model.go b/auth/model/model.go index f3d37fa9a95..9b14908431a 100644 --- a/auth/model/model.go +++ b/auth/model/model.go @@ -31,6 +31,13 @@ type User struct { Username string `db:"display_name" json:"display_name"` } +// SuperuserConfiguration requests a particular configuration for a superuser. +type SuperuserConfiguration struct { + User + AccessKeyID string + SecretAccessKey string +} + type Group struct { ID int `db:"id"` CreatedAt time.Time `db:"created_at"` diff --git a/auth/service.go b/auth/service.go index f5a3b077001..7b81219b674 100644 --- a/auth/service.go +++ b/auth/service.go @@ -58,6 +58,7 @@ type Service interface { // credentials CreateCredentials(username string) (*model.Credential, error) + AddCredentials(username, accessKeyID, secretAccessKey string) (*model.Credential, error) DeleteCredentials(username, accessKeyID string) error GetCredentialsForUser(username, accessKeyID string) (*model.Credential, error) GetCredentials(accessKeyID string) (*model.Credential, error) @@ -639,10 +640,14 @@ func (s *DBAuthService) ListPolicies(params *model.PaginationParams) ([]*model.P } func (s *DBAuthService) CreateCredentials(username string) (*model.Credential, error) { + accessKeyID := genAccessKeyID() + secretAccessKey := genAccessSecretKey() + return s.AddCredentials(username, accessKeyID, secretAccessKey) +} + +func (s *DBAuthService) AddCredentials(username, accessKeyID, secretAccessKey string) (*model.Credential, error) { now := time.Now() - accessKey := genAccessKeyID() - secretKey := genAccessSecretKey() - encryptedKey, err := s.encryptSecret(secretKey) + encryptedKey, err := s.encryptSecret(secretAccessKey) if err != nil { return nil, err } @@ -652,8 +657,8 @@ func (s *DBAuthService) CreateCredentials(username string) (*model.Credential, e return nil, err } c := &model.Credential{ - AccessKeyID: accessKey, - AccessSecretKey: secretKey, + AccessKeyID: accessKeyID, + AccessSecretKey: secretAccessKey, AccessSecretKeyEncryptedBytes: encryptedKey, IssuedDate: now, UserID: user.ID, diff --git a/auth/setup.go b/auth/setup.go index a6b7189f8d3..2704a080ca1 100644 --- a/auth/setup.go +++ b/auth/setup.go @@ -198,7 +198,7 @@ func SetupBaseGroups(authService Service, ts time.Time) error { return nil } -func SetupAdminUser(authService Service, user *model.User) (*model.Credential, error) { +func SetupAdminUser(authService Service, superuser *model.SuperuserConfiguration) (*model.Credential, error) { now := time.Now() // Setup the basic groups and policies @@ -207,10 +207,10 @@ func SetupAdminUser(authService Service, user *model.User) (*model.Credential, e return nil, err } - return AddAdminUser(authService, user) + return AddAdminUser(authService, superuser) } -func AddAdminUser(authService Service, user *model.User) (*model.Credential, error) { +func AddAdminUser(authService Service, user *model.SuperuserConfiguration) (*model.Credential, error) { const adminGroupName = "Admins" // verify admin group exists @@ -220,7 +220,7 @@ func AddAdminUser(authService Service, user *model.User) (*model.Credential, err } // create admin user - err = authService.CreateUser(user) + err = authService.CreateUser(&user.User) if err != nil { return nil, fmt.Errorf("create user - %w", err) } @@ -229,19 +229,27 @@ func AddAdminUser(authService Service, user *model.User) (*model.Credential, err return nil, fmt.Errorf("add user to group - %w", err) } - // Generate and return a key pair - creds, err := authService.CreateCredentials(user.Username) - if err != nil { - return nil, fmt.Errorf("create credentials for %s %w", user.Username, err) + var creds *model.Credential + if user.AccessKeyID == "" { + // Generate and return a key pair + creds, err = authService.CreateCredentials(user.Username) + if err != nil { + return nil, fmt.Errorf("create credentials for %s %w", user.Username, err) + } + } else { + creds, err = authService.AddCredentials(user.Username, user.AccessKeyID, user.SecretAccessKey) + if err != nil { + return nil, fmt.Errorf("add credentials for %s %w", user.Username, err) + } } return creds, nil } func CreateInitialAdminUser(authService Service, metadataManger MetadataManager, username string) (*model.Credential, error) { - adminUser := &model.User{ + adminUser := &model.SuperuserConfiguration{User: model.User{ CreatedAt: time.Now(), Username: username, - } + }} // create first admin user cred, err := SetupAdminUser(authService, adminUser) if err != nil { diff --git a/cmd/lakefs/cmd/init.go b/cmd/lakefs/cmd/init.go index 3b2641b8ffa..a4f146daa7b 100644 --- a/cmd/lakefs/cmd/init.go +++ b/cmd/lakefs/cmd/init.go @@ -31,7 +31,11 @@ var initCmd = &cobra.Command{ dbPool := db.BuildDatabaseConnection(cfg.GetDatabaseParams()) defer dbPool.Close() - userName, _ := cmd.Flags().GetString("user-name") + userName, err := cmd.Flags().GetString("user-name") + if err != nil { + fmt.Printf("user-name: %s\n", err) + os.Exit(1) + } authService := auth.NewDBAuthService( dbPool, @@ -64,6 +68,7 @@ var initCmd = &cobra.Command{ //nolint:gochecknoinits func init() { rootCmd.AddCommand(initCmd) - initCmd.Flags().String("user-name", "", "an identifier for the user (e.g. \"jane.doe\")") + f := initCmd.Flags() + f.String("user-name", "", "an identifier for the user (e.g. \"jane.doe\")") _ = initCmd.MarkFlagRequired("user-name") } diff --git a/cmd/lakefs/cmd/superuser.go b/cmd/lakefs/cmd/superuser.go index 9c1ffc00b46..b31060f4145 100644 --- a/cmd/lakefs/cmd/superuser.go +++ b/cmd/lakefs/cmd/superuser.go @@ -24,7 +24,21 @@ var superuserCmd = &cobra.Command{ dbPool := db.BuildDatabaseConnection(cfg.GetDatabaseParams()) defer dbPool.Close() - userName, _ := cmd.Flags().GetString("user-name") + userName, err := cmd.Flags().GetString("user-name") + if err != nil { + fmt.Printf("user-name: %s\n", err) + os.Exit(1) + } + accessKeyID, err := cmd.Flags().GetString("access-key-id") + if err != nil { + fmt.Printf("access-key-id: %s\n", err) + os.Exit(1) + } + secretAccessKey, err := cmd.Flags().GetString("secret-access-key") + if err != nil { + fmt.Printf("secret-access-key: %s\n", err) + os.Exit(1) + } authService := auth.NewDBAuthService( dbPool, @@ -33,9 +47,13 @@ var superuserCmd = &cobra.Command{ authMetadataManager := auth.NewDBMetadataManager(config.Version, dbPool) metadataProvider := stats.BuildMetadataProvider(logging.Default(), cfg) metadata := stats.NewMetadata(logging.Default(), cfg, authMetadataManager, metadataProvider) - credentials, err := auth.AddAdminUser(authService, &model.User{ - CreatedAt: time.Now(), - Username: userName, + credentials, err := auth.AddAdminUser(authService, &model.SuperuserConfiguration{ + User: model.User{ + CreatedAt: time.Now(), + Username: userName, + }, + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, }) if err != nil { fmt.Printf("Failed to setup admin user: %s\n", err) @@ -59,6 +77,10 @@ var superuserCmd = &cobra.Command{ //nolint:gochecknoinits func init() { rootCmd.AddCommand(superuserCmd) - superuserCmd.Flags().String("user-name", "", "an identifier for the user (e.g. \"jane.doe\")") + f := superuserCmd.Flags() + f.String("user-name", "", "an identifier for the user (e.g. \"jane.doe\")") + f.String("access-key-id", "", "create this access key ID for the user (for ease of integration)") + f.String("secret-access-key", "", "use this access key secret (potentially insecure, use carefully for ease of integration)") + _ = superuserCmd.MarkFlagRequired("user-name") } diff --git a/docs/assets/js/swagger.yml b/docs/assets/js/swagger.yml index a76ea237145..55ea157c4fb 100644 --- a/docs/assets/js/swagger.yml +++ b/docs/assets/js/swagger.yml @@ -2026,10 +2026,10 @@ paths: - export - branches operationId: run - summary: export branch + summary: hook to be called in order to execute continuous export on branch responses: 201: - description: export successfully started + description: continuous export successfully started schema: description: "export ID" type: string @@ -2111,3 +2111,4 @@ paths: $ref: "#/definitions/config" 401: $ref: "#/responses/Unauthorized" + diff --git a/loadtest/local_load_test.go b/loadtest/local_load_test.go index 3205638a09e..49e87f8d02a 100644 --- a/loadtest/local_load_test.go +++ b/loadtest/local_load_test.go @@ -84,11 +84,13 @@ func TestLocalLoad(t *testing.T) { ts := httptest.NewServer(handler) defer ts.Close() - user := &authmodel.User{ - CreatedAt: time.Now(), - Username: "admin", + superuser := &authmodel.SuperuserConfiguration{ + User: authmodel.User{ + CreatedAt: time.Now(), + Username: "admin", + }, } - credentials, err := auth.SetupAdminUser(authService, user) + credentials, err := auth.SetupAdminUser(authService, superuser) testutil.Must(t, err) testConfig := Config{ diff --git a/nessie/auth_test.go b/nessie/auth_test.go new file mode 100644 index 00000000000..bf8f660b299 --- /dev/null +++ b/nessie/auth_test.go @@ -0,0 +1,62 @@ +package nessie + +import ( + "net/url" + "testing" + + "github.com/treeverse/lakefs/auth" + "github.com/treeverse/lakefs/auth/crypt" + "github.com/treeverse/lakefs/auth/model" + "github.com/treeverse/lakefs/config" + "github.com/treeverse/lakefs/db" + + httptransport "github.com/go-openapi/runtime/client" + "github.com/spf13/viper" + genclient "github.com/treeverse/lakefs/api/gen/client" +) + +const ( + accessKeyID = "Sneakers" + secretAccessKey = "Setec Astronomy" +) + +func TestSuperuserWithPassedCreds(t *testing.T) { + ctx, _, _ := setupTest(t) + + cfg := config.NewConfig() + dbParams := cfg.GetDatabaseParams() + pool := db.BuildDatabaseConnection(dbParams) + + authService := auth.NewDBAuthService( + pool, + crypt.NewSecretStore(cfg.GetAuthEncryptionSecret()), + cfg.GetAuthCacheConfig()) + + _, err := auth.SetupAdminUser(authService, &model.SuperuserConfiguration{ + User: model.User{ + Username: "cosmo", + }, + AccessKeyID: accessKeyID, + SecretAccessKey: secretAccessKey, + }) + if err != nil { + t.Fatal("failed to setup admin user: ", err) + } + + // Set up the client to use this authentication. + endpointURL := viper.GetString("endpoint_url") + u, err := url.Parse(endpointURL) + if err != nil { + t.Fatalf("Failed to parse endpoint URL %s: %s", endpointURL, err) + } + apiBasePath := genclient.DefaultBasePath + if u.Path != "" { + apiBasePath = u.Path + } + r := httptransport.New(u.Host, apiBasePath, []string{u.Scheme}) + r.DefaultAuthentication = httptransport.BasicAuth(accessKeyID, secretAccessKey) + client.Transport = r + + // Use it for some minimal test + listRepositories(t, ctx) +}