diff --git a/.github/workflows/esti.yaml b/.github/workflows/esti.yaml index 4e11b090bf4..4a64de76eb6 100644 --- a/.github/workflows/esti.yaml +++ b/.github/workflows/esti.yaml @@ -1353,7 +1353,7 @@ jobs: fi run-system-aws-s3-basic-auth: - name: Run latest lakeFS app on AWS S3 + name: Run latest lakeFS app on AWS S3 + Basic Auth needs: [deploy-image, login-to-amazon-ecr] runs-on: ubuntu-22.04 env: diff --git a/cmd/lakefs/cmd/superuser.go b/cmd/lakefs/cmd/superuser.go index db78f5ce4cf..a3fdb0539e9 100644 --- a/cmd/lakefs/cmd/superuser.go +++ b/cmd/lakefs/cmd/superuser.go @@ -25,6 +25,12 @@ import ( var superuserCmd = &cobra.Command{ Use: "superuser", Short: "Create additional user with admin credentials", + Long: `Create additional user with admin credentials. +This command can be used to import an admin user when moving from lakeFS version +with previously configured users to a lakeFS with basic auth version. +To do that provide the user name as well as the access key ID to import. +If the wrong user or credentials were chosen it is possible to delete the user and perform the action again. +`, Run: func(cmd *cobra.Command, args []string) { cfg := loadConfig() if cfg.Auth.UIConfig.RBAC == config.AuthRBACExternal { @@ -60,7 +66,32 @@ var superuserCmd = &cobra.Command{ fmt.Printf("Failed to open KV store: %s\n", err) os.Exit(1) } - authService := acl.NewAuthService(kvStore, crypt.NewSecretStore([]byte(cfg.Auth.Encrypt.SecretKey)), authparams.ServiceCache(cfg.Auth.Cache)) + + var authService auth.Service + secretStore := crypt.NewSecretStore([]byte(cfg.Auth.Encrypt.SecretKey)) + authLogger := logger.WithField("service", "auth_api") + addToAdmins := true + switch { + case cfg.IsAuthBasic(): + authService = auth.NewBasicAuthService(kvStore, secretStore, authparams.ServiceCache(cfg.Auth.Cache), authLogger) + addToAdmins = false + case cfg.IsAuthUISimplified() && cfg.IsAuthenticationTypeAPI(): // ACL server + authService, err = auth.NewAPIAuthService( + cfg.Auth.API.Endpoint, + cfg.Auth.API.Token.SecureValue(), + cfg.Auth.AuthenticationAPI.ExternalPrincipalsEnabled, + secretStore, + authparams.ServiceCache(cfg.Auth.Cache), + authLogger) + if err != nil { + fmt.Printf("Failed to initialize auth service: %s\n", err) + os.Exit(1) + } + // TODO (niro): This needs to be removed + default: + authService = acl.NewAuthService(kvStore, secretStore, authparams.ServiceCache(cfg.Auth.Cache)) + } + authMetadataManager := auth.NewKVMetadataManager(version.Version, cfg.Installation.FixedID, cfg.Database.Type, kvStore) metadataProvider := stats.BuildMetadataProvider(logger, cfg) @@ -72,7 +103,7 @@ var superuserCmd = &cobra.Command{ }, AccessKeyID: accessKeyID, SecretAccessKey: secretAccessKey, - }, true) + }, addToAdmins) if err != nil { fmt.Printf("Failed to setup admin user: %s\n", err) os.Exit(1) diff --git a/contrib/auth/acl/service.go b/contrib/auth/acl/service.go index b6a95c25eac..b739b40123e 100644 --- a/contrib/auth/acl/service.go +++ b/contrib/auth/acl/service.go @@ -243,7 +243,7 @@ func (s *AuthService) ListUserCredentials(ctx context.Context, username string, if err != nil { return nil, nil, err } - creds, err := model.ConvertCredDataList(s.secretStore, msgs) + creds, err := model.ConvertCredDataList(s.secretStore, msgs, false) if err != nil { return nil, nil, err } diff --git a/esti/golden/lakefs/help.golden b/esti/golden/lakefs/help.golden new file mode 100644 index 00000000000..ec64da1cd63 --- /dev/null +++ b/esti/golden/lakefs/help.golden @@ -0,0 +1,23 @@ +lakeFS is a data lake management platform + +Usage: + lakefs [command] + +Available Commands: + completion Generate completion script + flare collect configuration, environment variables, and logs for debugging and troubleshooting + help Help about any command + migrate Manage migrations + run Run lakeFS + setup Setup a new lakeFS instance with initial credentials + superuser Create additional user with admin credentials + +Flags: + -c, --config string config file (default is $HOME/.lakefs.yaml) + -h, --help help for lakefs + --local-settings Use lakeFS local default configuration + --quickstart Use lakeFS quickstart configuration + -t, --toggle Help message for toggle + -v, --version version for lakefs + +Use "lakefs [command] --help" for more information about a command. diff --git a/esti/lakectl_test.go b/esti/lakectl_test.go index 5501346f866..5cb093bf13d 100644 --- a/esti/lakectl_test.go +++ b/esti/lakectl_test.go @@ -412,13 +412,9 @@ func TestLakectlAuthUsers(t *testing.T) { "ID": userName, } isSupported := !isBasicAuth() - expected := "Not implemented\n501 Not Implemented\n" - if isSupported { - expected = "user not found\n404 Not Found\n" - } // Not Found - RunCmdAndVerifyFailure(t, Lakectl()+" auth users delete --id "+userName, false, expected, vars) + RunCmdAndVerifyFailure(t, Lakectl()+" auth users delete --id "+userName, false, "user not found\n404 Not Found\n", vars) // Check unique if isSupported { @@ -427,6 +423,7 @@ func TestLakectlAuthUsers(t *testing.T) { RunCmdAndVerifyFailure(t, Lakectl()+" auth users create --id "+userName, false, "Already exists\n409 Conflict\n", vars) // Cleanup + expected := "user not found\n404 Not Found\n" if isSupported { expected = "User deleted successfully\n" } diff --git a/esti/lakectl_util.go b/esti/lakectl_util.go index 72c8bb03257..33a2b13d845 100644 --- a/esti/lakectl_util.go +++ b/esti/lakectl_util.go @@ -34,10 +34,12 @@ var ( rePhysicalAddress = regexp.MustCompile(`/data/[0-9a-v]{20}/(?:[0-9a-v]{20}(?:,.+)?)?`) reVariable = regexp.MustCompile(`\$\{([^${}]+)}`) rePreSignURL = regexp.MustCompile(`https://\S+\?\S+`) + reSecretAccessKey = regexp.MustCompile(`secret_access_key: \S{16,128}`) + reAccessKeyID = regexp.MustCompile(`access_key_id: AKIA\S{12,124}`) ) func lakectlLocation() string { - return viper.GetString("lakectl_dir") + "/lakectl" + return viper.GetString("binaries_dir") + "/lakectl" } func LakectlWithParams(accessKeyID, secretAccessKey, endPointURL string) string { @@ -145,6 +147,8 @@ func sanitize(output string, vars map[string]string) string { s = normalizeCommitID(s) s = normalizeChecksum(s) s = normalizeShortCommitID(s) + s = normalizeAccessKeyID(s) + s = normalizeSecretAccessKey(s) return s } @@ -267,3 +271,11 @@ func normalizeEndpoint(output string, endpoint string) string { func normalizePreSignURL(output string) string { return rePreSignURL.ReplaceAllString(output, "") } + +func normalizeAccessKeyID(output string) string { + return reAccessKeyID.ReplaceAllString(output, "access_key_id: ") +} + +func normalizeSecretAccessKey(output string) string { + return reSecretAccessKey.ReplaceAllString(output, "secret_access_key: ") +} diff --git a/esti/lakefs_test.go b/esti/lakefs_test.go new file mode 100644 index 00000000000..84c49f2ee18 --- /dev/null +++ b/esti/lakefs_test.go @@ -0,0 +1,34 @@ +package esti + +import "testing" + +func TestLakefsHelp(t *testing.T) { + RunCmdAndVerifySuccessWithFile(t, Lakefs(), false, "lakefs/help", emptyVars) + RunCmdAndVerifySuccessWithFile(t, Lakefs()+" --help", false, "lakefs/help", emptyVars) + RunCmdAndVerifySuccessWithFile(t, Lakefs(), true, "lakefs/help", emptyVars) + RunCmdAndVerifySuccessWithFile(t, Lakefs()+" --help", true, "lakefs/help", emptyVars) +} + +func TestLakefsSuperuser_basic(t *testing.T) { + RequirePostgresDB(t) + lakefsCmd := Lakefs() + outputString := "credentials:\n access_key_id: \n secret_access_key: \n" + username := t.Name() + expectFailure := false + if isBasicAuth() { + lakefsCmd = LakefsWithBasicAuth() + outputString = "already exists" + expectFailure = true + } + runCmdAndVerifyContainsText(t, lakefsCmd+" superuser --user-name "+username, expectFailure, false, outputString, nil) +} + +func TestLakefsSuperuser_alreadyExists(t *testing.T) { + RequirePostgresDB(t) + lakefsCmd := Lakefs() + if isBasicAuth() { + lakefsCmd = LakefsWithBasicAuth() + } + // On init - the AdminUsername is already created and expected error should be "already exist" (also in basic auth mode) + RunCmdAndVerifyFailureContainsText(t, lakefsCmd+" superuser --user-name "+AdminUsername, false, "already exists", nil) +} diff --git a/esti/lakefs_util.go b/esti/lakefs_util.go new file mode 100644 index 00000000000..f19d2058914 --- /dev/null +++ b/esti/lakefs_util.go @@ -0,0 +1,41 @@ +package esti + +import ( + "strconv" + "testing" + + "github.com/spf13/viper" +) + +func LakefsWithParams(connectionString string) string { + return LakefsWithParamsWithBasicAuth(connectionString, false) +} + +func LakefsWithParamsWithBasicAuth(connectionString string, basicAuth bool) string { + lakefsCmdline := "LAKEFS_DATABASE_TYPE=postgres" + + " LAKEFS_DATABASE_POSTGRES_CONNECTION_STRING=" + connectionString + + " LAKEFS_AUTH_INTERNAL_BASIC=" + strconv.FormatBool(basicAuth) + + " LAKEFS_BLOCKSTORE_TYPE=" + viper.GetString("blockstore_type") + + " LAKEFS_AUTH_ENCRYPT_SECRET_KEY='some random secret string' " + lakefsLocation() + + return lakefsCmdline +} + +func lakefsLocation() string { + return viper.GetString("binaries_dir") + "/lakefs" +} + +func LakefsWithBasicAuth() string { + return LakefsWithParamsWithBasicAuth(viper.GetString("database_connection_string"), true) +} + +func Lakefs() string { + return LakefsWithParams(viper.GetString("database_connection_string")) +} + +func RequirePostgresDB(t *testing.T) { + dbString := viper.GetString("database_connection_string") + if dbString == "" { + t.Skip("skip test - not postgres") + } +} diff --git a/esti/main_test.go b/esti/main_test.go index 1a8a108497a..e0e9f442a3b 100644 --- a/esti/main_test.go +++ b/esti/main_test.go @@ -23,6 +23,7 @@ import ( const ( DefaultAdminAccessKeyID = "AKIAIOSFDNN7EXAMPLEQ" DefaultAdminSecretAccessKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + AdminUsername = "esti" ) type ( @@ -234,7 +235,7 @@ func TestMain(m *testing.M) { } params := testutil.SetupTestingEnvParams{ - Name: "esti", + Name: AdminUsername, StorageNS: "esti-system-testing", } diff --git a/esti/ops/docker-compose-acl.yaml b/esti/ops/docker-compose-acl.yaml index f041b323296..f4ca572090c 100644 --- a/esti/ops/docker-compose-acl.yaml +++ b/esti/ops/docker-compose-acl.yaml @@ -69,7 +69,7 @@ services: - ESTI_SETUP_LAKEFS - ESTI_AWS_SECRET_ACCESS_KEY - ESTI_ENDPOINT_URL=http://lakefs:8000 - - ESTI_LAKECTL_DIR=/app + - ESTI_BINARIES_DIR=/app - ESTI_DATABASE_CONNECTION_STRING=postgres://lakefs:lakefs@postgres/postgres?sslmode=disable - ESTI_GOTEST_FLAGS - ESTI_FLAGS diff --git a/esti/ops/docker-compose-dynamodb.yaml b/esti/ops/docker-compose-dynamodb.yaml index 0cf3c87bfab..0edbdf33d1e 100644 --- a/esti/ops/docker-compose-dynamodb.yaml +++ b/esti/ops/docker-compose-dynamodb.yaml @@ -52,7 +52,7 @@ services: - ESTI_SETUP_LAKEFS - ESTI_AWS_SECRET_ACCESS_KEY - ESTI_ENDPOINT_URL=http://lakefs:8000 - - ESTI_LAKECTL_DIR=/app + - ESTI_BINARIES_DIR=/app - ESTI_DATABASE_KV_ENABLED - ESTI_KV_MIGRATION=${ESTI_KV_MIGRATION:-none} - ESTI_POST_MIGRATE diff --git a/esti/ops/docker-compose-external-db.yaml b/esti/ops/docker-compose-external-db.yaml index 4a1dce62cc9..d752fc65ac6 100644 --- a/esti/ops/docker-compose-external-db.yaml +++ b/esti/ops/docker-compose-external-db.yaml @@ -47,7 +47,7 @@ services: - ESTI_SETUP_LAKEFS - ESTI_AWS_SECRET_ACCESS_KEY - ESTI_ENDPOINT_URL=http://lakefs:8000 - - ESTI_LAKECTL_DIR=/app + - ESTI_BINARIES_DIR=/app - ESTI_GOTEST_FLAGS - ESTI_FLAGS - ESTI_LARGE_OBJECT_PATH diff --git a/esti/ops/docker-compose.yaml b/esti/ops/docker-compose.yaml index 46f60c83bdb..f1a8db63c6c 100644 --- a/esti/ops/docker-compose.yaml +++ b/esti/ops/docker-compose.yaml @@ -55,7 +55,7 @@ services: - ESTI_SETUP_LAKEFS - ESTI_AWS_SECRET_ACCESS_KEY - ESTI_ENDPOINT_URL=http://lakefs:8000 - - ESTI_LAKECTL_DIR=/app + - ESTI_BINARIES_DIR=/app - ESTI_DATABASE_CONNECTION_STRING=postgres://lakefs:lakefs@postgres/postgres?sslmode=disable - ESTI_GOTEST_FLAGS - ESTI_FLAGS diff --git a/pkg/auth/basic_service.go b/pkg/auth/basic_service.go index e6d84b3b56c..337c31bec25 100644 --- a/pkg/auth/basic_service.go +++ b/pkg/auth/basic_service.go @@ -16,8 +16,9 @@ import ( ) const ( - basicPartitionKey = "basicAuth" + BasicPartitionKey = "basicAuth" SuperAdminKey = "superAdmin" + MaxUsers = 1 MaxCredentialsPerUser = 1 ) @@ -73,7 +74,7 @@ func (s *BasicAuthService) getUser(ctx context.Context) (*model.User, error) { // Single user, always stored under this key! userKey := model.UserPath(SuperAdminKey) m := model.UserData{} - _, err := kv.GetMsg(ctx, s.store, basicPartitionKey, userKey, &m) + _, err := kv.GetMsg(ctx, s.store, BasicPartitionKey, userKey, &m) if err != nil { if errors.Is(err, kv.ErrNotFound) { err = ErrNotFound @@ -92,53 +93,57 @@ func (s *BasicAuthService) CreateUser(ctx context.Context, user *model.User) (st } userKey := model.UserPath(SuperAdminKey) - err := kv.SetMsgIf(ctx, s.store, basicPartitionKey, userKey, model.ProtoFromUser(user), nil) + err := kv.SetMsgIf(ctx, s.store, BasicPartitionKey, userKey, model.ProtoFromUser(user), nil) if err != nil { if errors.Is(err, kv.ErrPredicateFailed) { err = ErrAlreadyExists } - return "", fmt.Errorf("failed to create user (auth.UserKey %s): %w", userKey, err) + return "", fmt.Errorf("failed to create user (%s): %w", user.Username, err) } return user.Username, err } +func (s *BasicAuthService) DeleteUser(ctx context.Context, username string) error { + if _, err := s.GetUser(ctx, username); err != nil { + return err + } + + // delete user + userPath := model.UserPath(SuperAdminKey) + if err := s.store.Delete(ctx, []byte(BasicPartitionKey), userPath); err != nil { + return fmt.Errorf("delete user (%s): %w", username, err) + } + + // Delete user credentials + return s.deleteUserCredentials(ctx, SuperAdminKey, BasicPartitionKey, "") +} + func (s *BasicAuthService) ListUsers(ctx context.Context, _ *model.PaginationParams) ([]*model.User, *model.Paginator, error) { + var users []*model.User user, err := s.getUser(ctx) if err != nil { - return nil, nil, err + if !errors.Is(err, ErrNotFound) { + return nil, nil, err + } + } else { + users = append(users, user) } - return []*model.User{user}, &model.Paginator{Amount: 1}, nil + return users, &model.Paginator{Amount: MaxUsers}, nil } func (s *BasicAuthService) GetCredentials(ctx context.Context, accessKeyID string) (*model.Credential, error) { return s.cache.GetCredential(accessKeyID, func() (*model.Credential, error) { - m := &model.UserData{} - itr, err := kv.NewPrimaryIterator(ctx, s.store, m.ProtoReflect().Type(), basicPartitionKey, model.UserPath(""), kv.IteratorOptionsAfter([]byte(""))) - if err != nil { - return nil, fmt.Errorf("scan users: %w", err) - } - defer itr.Close() - - for itr.Next() { - entry := itr.Entry() - user, ok := entry.Value.(*model.UserData) - if !ok { - return nil, fmt.Errorf("failed to cast: %w", err) - } - c := model.CredentialData{} - credentialsKey := model.CredentialPath(user.Username, accessKeyID) - _, err := kv.GetMsg(ctx, s.store, basicPartitionKey, credentialsKey, &c) - if err != nil && !errors.Is(err, kv.ErrNotFound) { - return nil, err - } - if err == nil { - return model.CredentialFromProto(s.secretStore, &c) - } - } - if err = itr.Err(); err != nil { + c := model.CredentialData{} + credentialsKey := model.CredentialPath(SuperAdminKey, accessKeyID) + _, err := kv.GetMsg(ctx, s.store, BasicPartitionKey, credentialsKey, &c) + switch { + case errors.Is(err, kv.ErrNotFound): + return nil, fmt.Errorf("credentials %w", ErrNotFound) + case err == nil: + return model.CredentialFromProto(s.secretStore, &c) + default: return nil, err } - return nil, fmt.Errorf("credentials %w", ErrNotFound) }) } @@ -153,7 +158,12 @@ func (s *BasicAuthService) CreateCredentials(ctx context.Context, username strin } func (s *BasicAuthService) AddCredentials(ctx context.Context, username, accessKeyID, secretAccessKey string) (*model.Credential, error) { - currCreds, err := s.listUserCredentials(ctx, username) + _, err := s.GetUser(ctx, username) + if err != nil { + return nil, err + } + + currCreds, err := s.listUserCredentials(ctx, SuperAdminKey, BasicPartitionKey, "") if err != nil { return nil, err } @@ -161,9 +171,26 @@ func (s *BasicAuthService) AddCredentials(ctx context.Context, username, accessK if len(currCreds) >= MaxCredentialsPerUser { return nil, fmt.Errorf("exceeded number of allowed credentials: %w", ErrInvalidRequest) } + + // Handle user import flow from previous auth service + if accessKeyID != "" && secretAccessKey == "" { + return s.importUserCredentials(ctx, username, accessKeyID) + } + return s.addCredentials(ctx, username, accessKeyID, secretAccessKey) } +func (s *BasicAuthService) importUserCredentials(ctx context.Context, username, accessKeyID string) (*model.Credential, error) { + creds, err := s.listUserCredentials(ctx, username, model.PartitionKey, accessKeyID) + if err != nil { + return nil, err + } + if len(creds) < 1 { + return nil, fmt.Errorf("no credentials found for user (%s): %w", username, ErrNotFound) + } + return s.addCredentials(ctx, username, creds[0].AccessKeyID, creds[0].SecretAccessKey) +} + func (s *BasicAuthService) addCredentials(ctx context.Context, username, accessKeyID, secretAccessKey string) (*model.Credential, error) { encryptedKey, err := model.EncryptSecret(s.secretStore, secretAccessKey) if err != nil { @@ -179,8 +206,8 @@ func (s *BasicAuthService) addCredentials(ctx context.Context, username, accessK }, Username: username, } - credentialsKey := model.CredentialPath(username, c.AccessKeyID) - err = kv.SetMsgIf(ctx, s.store, basicPartitionKey, credentialsKey, model.ProtoFromCredential(c), nil) + credentialsKey := model.CredentialPath(SuperAdminKey, c.AccessKeyID) + err = kv.SetMsgIf(ctx, s.store, BasicPartitionKey, credentialsKey, model.ProtoFromCredential(c), nil) if err != nil { if errors.Is(err, kv.ErrPredicateFailed) { err = ErrAlreadyExists @@ -191,22 +218,47 @@ func (s *BasicAuthService) addCredentials(ctx context.Context, username, accessK return c, nil } -func (s *BasicAuthService) listUserCredentials(ctx context.Context, username string) ([]*model.Credential, error) { +func (s *BasicAuthService) deleteUserCredentials(ctx context.Context, username, partition, prefix string) error { var credential model.CredentialData credentialsKey := model.CredentialPath(username, "") var ( it kv.MessageIterator err error ) - it, err = kv.NewSecondaryIterator(ctx, s.store, (&credential).ProtoReflect().Type(), basicPartitionKey, credentialsKey, []byte("")) + it, err = kv.NewPrimaryIterator(ctx, s.store, (&credential).ProtoReflect().Type(), partition, credentialsKey, kv.IteratorOptionsFrom([]byte(prefix))) + if err != nil { + return fmt.Errorf("create iterator: %w", err) + } + defer it.Close() + + for it.Next() { + entry := it.Entry() + if err = s.store.Delete(ctx, []byte(partition), entry.Key); err != nil { + return fmt.Errorf("delete credentials: %w", err) + } + } + if err = it.Err(); err != nil { + return fmt.Errorf("iterate credentials: %w", err) + } + + return nil +} + +func (s *BasicAuthService) listUserCredentials(ctx context.Context, username, partition, prefix string) ([]*model.Credential, error) { + var credential model.CredentialData + credentialsKey := model.CredentialPath(username, prefix) + var ( + it kv.MessageIterator + err error + ) + it, err = kv.NewPrimaryIterator(ctx, s.store, (&credential).ProtoReflect().Type(), partition, credentialsKey, kv.IteratorOptionsAfter([]byte(""))) if err != nil { return nil, fmt.Errorf("create iterator: %w", err) } defer it.Close() - amount := 2 entries := make([]proto.Message, 0) - for len(entries) < amount && it.Next() { + for len(entries) < MaxCredentialsPerUser && it.Next() { entry := it.Entry() value := entry.Value entries = append(entries, value) @@ -215,7 +267,7 @@ func (s *BasicAuthService) listUserCredentials(ctx context.Context, username str return nil, fmt.Errorf("iterate credentials: %w", err) } - creds, err := model.ConvertCredDataList(s.secretStore, entries) + creds, err := model.ConvertCredDataList(s.secretStore, entries, true) if err != nil { return nil, err } @@ -230,10 +282,6 @@ func (s *BasicAuthService) SecretStore() crypt.SecretStore { return s.secretStore } -func (s *BasicAuthService) DeleteUser(_ context.Context, _ string) error { - return ErrNotImplemented -} - func (s *BasicAuthService) GetUserByID(_ context.Context, _ string) (*model.User, error) { return nil, ErrNotImplemented } diff --git a/pkg/auth/basic_service_test.go b/pkg/auth/basic_service_test.go new file mode 100644 index 00000000000..ac7bf59e4bc --- /dev/null +++ b/pkg/auth/basic_service_test.go @@ -0,0 +1,193 @@ +package auth_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "github.com/treeverse/lakefs/pkg/auth" + "github.com/treeverse/lakefs/pkg/auth/crypt" + "github.com/treeverse/lakefs/pkg/auth/model" + authparams "github.com/treeverse/lakefs/pkg/auth/params" + "github.com/treeverse/lakefs/pkg/kv" + "github.com/treeverse/lakefs/pkg/kv/kvtest" + "github.com/treeverse/lakefs/pkg/logging" + "google.golang.org/protobuf/proto" +) + +var secret string = "Secret" + +func SetupService(t *testing.T, secret string) (*auth.BasicAuthService, kv.Store) { + t.Helper() + kvStore := kvtest.GetStore(context.Background(), t) + return auth.NewBasicAuthService(kvStore, crypt.NewSecretStore([]byte(secret)), authparams.ServiceCache{ + Enabled: false, + }, logging.ContextUnavailable()), kvStore +} + +func TestBasicAuthService_Users(t *testing.T) { + ctx := context.Background() + s, store := SetupService(t, secret) + username := "testUser" + + // Get user not exists + _, err := s.GetUser(ctx, username) + require.ErrorIs(t, err, auth.ErrNotFound) + + // List users with no users + listRes, _, err := s.ListUsers(ctx, nil) + require.NoError(t, err) + require.Equal(t, 0, len(listRes)) + + // Delete no user + err = s.DeleteUser(ctx, username) + require.ErrorIs(t, err, auth.ErrNotFound) + + user := &model.User{ + Username: username, + } + createRes, err := s.CreateUser(ctx, user) + require.NoError(t, err) + require.Equal(t, username, createRes) + + // Check get user + getRes, err := s.GetUser(ctx, user.Username) + require.NoError(t, err) + require.Equal(t, user.Username, getRes.Username) + + // Check it is saved under the admin key + _, err = store.Get(ctx, []byte(auth.BasicPartitionKey), model.UserPath(auth.SuperAdminKey)) + require.NoError(t, err) + + // List users + listRes, _, err = s.ListUsers(ctx, nil) + require.NoError(t, err) + require.Equal(t, 1, len(listRes)) + require.Equal(t, username, listRes[0].Username) + + // Delete user + err = s.DeleteUser(ctx, username) + require.NoError(t, err) + + // Check admin key is deleted + _, err = store.Get(ctx, []byte(auth.BasicPartitionKey), model.UserPath(auth.SuperAdminKey)) + require.ErrorIs(t, err, auth.ErrNotFound) +} + +func TestBasicAuthService_Credentials(t *testing.T) { + ctx := context.Background() + s, _ := SetupService(t, secret) + username := "testUser" + accessKeyID := "SomeAccessKeyID" + secretAccessKey := "SomeSecretAccessKey" + + // Get credentials no user + _, err := s.GetCredentials(ctx, username) + require.ErrorIs(t, err, auth.ErrNotFound) + + // Create credentials no user + _, err = s.CreateCredentials(ctx, username) + require.ErrorIs(t, err, auth.ErrNotFound) + + // Add credentials no user + _, err = s.AddCredentials(ctx, username, accessKeyID, secretAccessKey) + require.ErrorIs(t, err, auth.ErrNotFound) + + user := &model.User{ + Username: username, + } + createRes, err := s.CreateUser(ctx, user) + require.NoError(t, err) + require.Equal(t, username, createRes) + + // Get credentials (no creds) + _, err = s.GetCredentials(ctx, accessKeyID) + require.ErrorIs(t, err, auth.ErrNotFound) + + // Create credentials for user + creds, err := s.CreateCredentials(ctx, username) + require.NoError(t, err) + + // Get credentials + _, err = s.GetCredentials(ctx, creds.AccessKeyID) + require.NoError(t, err) + + // Add credentials already exists + _, err = s.AddCredentials(ctx, username, accessKeyID, secretAccessKey) + require.ErrorIs(t, err, auth.ErrInvalidRequest) +} + +func TestBasicAuthService_CredentialsImport(t *testing.T) { + ctx := context.Background() + s, store := SetupService(t, secret) + username := "testUser" + accessKeyID := "SomeAccessKeyID" + secretAccessKey := "SomeSecretAccessKey" + + // Import credentials no user + _, err := s.AddCredentials(ctx, username, accessKeyID, "") + require.ErrorIs(t, err, auth.ErrNotFound) + + // Create users with creds under auth + user := &model.UserData{ + Username: "user-old", + } + userData, err := proto.Marshal(user) + require.NoError(t, err) + require.NoError(t, store.Set(ctx, []byte(model.PartitionKey), model.UserPath(user.Username), userData)) + encryptedKey, err := model.EncryptSecret(s.SecretStore(), secretAccessKey) + require.NoError(t, err) + creds := &model.CredentialData{ + AccessKeyId: accessKeyID, + SecretAccessKeyEncryptedBytes: encryptedKey, + UserId: []byte(user.Username), + } + credsData, err := proto.Marshal(creds) + require.NoError(t, store.Set(ctx, []byte(model.PartitionKey), model.CredentialPath(user.Username, creds.AccessKeyId), credsData)) + + creds.AccessKeyId = "A" + secretAccessKey + encryptedKey, err = model.EncryptSecret(s.SecretStore(), "BadSecret") + require.NoError(t, err) + + creds.SecretAccessKeyEncryptedBytes = encryptedKey + credsData, err = proto.Marshal(creds) + require.NoError(t, store.Set(ctx, []byte(model.PartitionKey), model.CredentialPath(user.Username, creds.AccessKeyId), credsData)) + + // Create a different user + createRes, err := s.CreateUser(ctx, &model.User{ + Username: username, + }) + require.NoError(t, err) + require.Equal(t, username, createRes) + + // Try to import credentials of different user + _, err = s.AddCredentials(ctx, username, accessKeyID, "") + require.ErrorIs(t, err, auth.ErrNotFound) + + // Delete user and create the right one this time + require.NoError(t, s.DeleteUser(ctx, username)) + _, err = s.CreateUser(ctx, &model.User{ + Username: user.Username, + }) + require.NoError(t, err) + + // Import credentials not exist + _, err = s.AddCredentials(ctx, user.Username, "NotExisting", "") + require.ErrorIs(t, err, auth.ErrNotFound) + + // Import credentials + credsResp, err := s.AddCredentials(ctx, user.Username, accessKeyID, "") + require.NoError(t, err) + require.Equal(t, accessKeyID, credsResp.AccessKeyID) + require.Equal(t, secretAccessKey, credsResp.SecretAccessKey) + + // Import after exists + _, err = s.AddCredentials(ctx, user.Username, accessKeyID, "") + require.ErrorIs(t, err, auth.ErrInvalidRequest) + + // Get credentials and verify + getCred, err := s.GetCredentials(ctx, accessKeyID) + require.NoError(t, err) + require.Equal(t, accessKeyID, getCred.AccessKeyID) + require.Equal(t, secretAccessKey, getCred.SecretAccessKey) +} diff --git a/pkg/auth/model/model.go b/pkg/auth/model/model.go index 0d546e1af3a..6f27295f659 100644 --- a/pkg/auth/model/model.go +++ b/pkg/auth/model/model.go @@ -29,7 +29,6 @@ const ( usersPoliciesPrefix = "uPolicies" usersCredentialsPrefix = "uCredentials" // #nosec G101 -- False positive: this is only a kv key prefix credentialsPrefix = "credentials" - expiredTokensPrefix = "expiredTokens" metadataPrefix = "installation_metadata" ) @@ -74,14 +73,6 @@ func GroupPolicyPath(groupDisplayName string, policyDisplayName string) []byte { return []byte(kv.FormatPath(groupsPoliciesPrefix, groupDisplayName, policiesPrefix, policyDisplayName)) } -func ExpiredTokenPath(tokenID string) []byte { - return []byte(kv.FormatPath(expiredTokensPrefix, tokenID)) -} - -func ExpiredTokensPath() []byte { - return ExpiredTokenPath("") -} - func MetadataKeyPath(key string) string { return kv.FormatPath(metadataPrefix, key) } @@ -393,7 +384,7 @@ func ConvertPolicyDataList(policies []proto.Message) []*Policy { return res } -func ConvertCredDataList(s crypt.SecretStore, creds []proto.Message) ([]*Credential, error) { +func ConvertCredDataList(s crypt.SecretStore, creds []proto.Message, withSecret bool) ([]*Credential, error) { res := make([]*Credential, 0, len(creds)) for _, c := range creds { credentialData := c.(*CredentialData) @@ -401,7 +392,9 @@ func ConvertCredDataList(s crypt.SecretStore, creds []proto.Message) ([]*Credent if err != nil { return nil, fmt.Errorf("credentials for %s: %w", credentialData.AccessKeyId, err) } - m.SecretAccessKey = "" + if !withSecret { + m.SecretAccessKey = "" + } res = append(res, m) } return res, nil diff --git a/pkg/testutil/setup.go b/pkg/testutil/setup.go index 938e6690670..2616589c5d4 100644 --- a/pkg/testutil/setup.go +++ b/pkg/testutil/setup.go @@ -56,7 +56,7 @@ func SetupTestingEnv(params *SetupTestingEnvParams) (logging.Logger, apigen.Clie } viper.SetDefault("glue_export_hooks_database", "export-hooks-esti") viper.SetDefault("glue_export_region", "us-east-1") - viper.SetDefault("lakectl_dir", filepath.Join(currDir, "..")) + viper.SetDefault("binaries_dir", filepath.Join(currDir, "..")) viper.SetDefault("azure_storage_account", "") viper.SetDefault("azure_storage_access_key", "") viper.SetDefault("large_object_path", "") @@ -78,12 +78,12 @@ func SetupTestingEnv(params *SetupTestingEnvParams) (logging.Logger, apigen.Clie logger.WithError(err).Fatal("could not initialize API client") } - if err := waitUntilLakeFSRunning(ctx, logger, client); err != nil { - logger.WithError(err).Fatal("Waiting for lakeFS") - } - setupLakeFS := viper.GetBool("setup_lakefs") if setupLakeFS { + if err := waitUntilLakeFSRunning(ctx, logger, client); err != nil { + logger.WithError(err).Fatal("Waiting for lakeFS") + } + // first setup of lakeFS mockEmail := "test@acme.co" commResp, err := client.SetupCommPrefsWithResponse(context.Background(), apigen.SetupCommPrefsJSONRequestBody{