From a8344ac6f711d1b62588c74604abb63eb20cef30 Mon Sep 17 00:00:00 2001 From: Bruce MacDonald Date: Thu, 5 Aug 2021 10:38:06 -0700 Subject: [PATCH] Group role bindings - Read groups from infra.yaml - Update data model for FK relations between groups, users, sources, and roles - Find user role bindings from their groups - Update test YAML - Update config docs - Improve logging --- docs/permissions.md | 14 +- internal/registry/_testdata/infra.yaml | 41 ++++- internal/registry/config.go | 200 ++++++++++++++++++++----- internal/registry/config_test.go | 134 ++++++++++++++--- internal/registry/data.go | 24 ++- internal/registry/grpc.go | 25 +++- internal/registry/grpc_test.go | 98 +++++++++++- internal/registry/registry.go | 2 +- 8 files changed, 464 insertions(+), 74 deletions(-) diff --git a/docs/permissions.md b/docs/permissions.md index 4cddcebf99..af5f3b51ad 100644 --- a/docs/permissions.md +++ b/docs/permissions.md @@ -7,10 +7,22 @@ In Infra, roles are configured via the [configuration file](./configuration.md). First, create or edit an existing a config file `infra.yaml`: ``` +groups: + - name: developers # group name in an external identity provider + sources: # the identity providers this group applies to + - okta + roles: + - name: writer # Kubernetes cluster-role name + kind: cluster-role + clusters: # clusters for which to apply + - cluster-AAA + users: - name: admin@example.com # user email + groups: # manually assign groups this user belongs to + - developers roles: - - name: admin # Kubernetes cluster role name + - name: admin # Kubernetes cluster-role name kind: cluster-role clusters: # clusters for which to apply - cluster-AAA diff --git a/internal/registry/_testdata/infra.yaml b/internal/registry/_testdata/infra.yaml index 2435979629..bb36c5eb44 100644 --- a/internal/registry/_testdata/infra.yaml +++ b/internal/registry/_testdata/infra.yaml @@ -4,7 +4,38 @@ sources: oktaClientId: 0oapn0qwiQPiMIyR35d6 oktaClientSecret: jfpn0qwiQPiMIfs408fjs048fjpn0qwiQPiMajsdf08j10j2 oktaApiToken: 001XJv9xhv899sdfns938haos3h8oahsdaohd2o8hdao82hd + +groups: + - name: ios-developers + sources: + - okta + roles: + - name: writer + kind: cluster-role + clusters: + - cluster-AAA + - name: mac-admins + sources: + - okta + roles: + - name: writer + kind: cluster-role + clusters: + - cluster-BBB + - name: adversaries + sources: + - outside + roles: + - name: cluster-admin + kind: cluster-role + clusters: + - cluster-AAA + users: + - name: woz@example.com + groups: + - ios-developers + - mac-admins - name: admin@example.com roles: - name: admin @@ -13,17 +44,17 @@ users: - cluster-AAA - cluster-BBB - name: user@example.com + groups: + - ios-developers roles: - - name: writer - kind: cluster-role - clusters: - - cluster-AAA - name: reader kind: cluster-role clusters: - - cluster-BBB + - cluster-AAA - cluster-UNKNOWN - name: unknown@example.com + groups: + - adversaries roles: - name: writer kind: cluster-role diff --git a/internal/registry/config.go b/internal/registry/config.go index c8145571b5..96692cd4f1 100644 --- a/internal/registry/config.go +++ b/internal/registry/config.go @@ -22,14 +22,21 @@ type ConfigRoleKubernetes struct { Clusters []string `yaml:"clusters"` } +type ConfigGroupMapping struct { + Name string `yaml:"name"` + Sources []string `yaml:"sources"` + Roles []ConfigRoleKubernetes `yaml:"roles"` +} + type ConfigUserMapping struct { - Name string - Roles []ConfigRoleKubernetes - // TODO (brucemacd): Add groups here + Name string `yaml:"name"` + Roles []ConfigRoleKubernetes `yaml:"roles"` + Groups []string `yaml:"groups"` } type Config struct { - Sources []ConfigSource `yaml:"sources"` - Users []ConfigUserMapping `yaml:"users"` + Sources []ConfigSource `yaml:"sources"` + Groups []ConfigGroupMapping `yaml:"groups"` + Users []ConfigUserMapping `yaml:"users"` } // this config is loaded at start-up and re-applied when the registry state changes (ex: a user is added) @@ -66,6 +73,67 @@ func ImportSources(db *gorm.DB, sources []ConfigSource) error { return nil } +func ApplyGroupMappings(db *gorm.DB, groups []ConfigGroupMapping) (groupIds []string, roleIds []string, err error) { + for _, g := range groups { + // get the sources from the datastore that this group specifies + var sources []Source + for _, src := range g.Sources { + var source Source + // Assumes that only one type of each source can exist + srcReadErr := db.Where(&Source{Type: src}).First(&source).Error + if srcReadErr != nil { + if errors.Is(srcReadErr, gorm.ErrRecordNotFound) { + // skip this source, it will need to be added in the config and re-applied + logging.L.Debug("skipping source in config that does not exist: " + src) + continue + } + err = srcReadErr + return + } + sources = append(sources, source) + } + + if len(sources) == 0 { + logging.L.Debug("no valid sources found, skipping group: " + g.Name) + continue + } + + var group Group + // Group names must be unique for mapping purposes + err = db.FirstOrCreate(&group, &Group{Name: g.Name}).Error + if err != nil { + return + } + + // add the identity provider associations to the group + for _, source := range sources { + if db.Model(&source).Where(&Group{Id: group.Id}).Association("Groups").Count() == 0 { + if err = db.Model(&source).Where(&Group{Id: group.Id}).Association("Groups").Append(&group); err != nil { + return + } + } + } + + // import the roles on this group from the datastore + var roles []Role + roles, err = importRoles(db, g.Roles) + if err != nil { + return + } + // add the new group associations to the roles + for _, role := range roles { + if db.Model(&role).Where(&Group{Id: group.Id}).Association("Groups").Count() == 0 { + if err = db.Model(&role).Where(&Group{Id: group.Id}).Association("Groups").Append(&group); err != nil { + return + } + } + roleIds = append(roleIds, role.Id) + } + groupIds = append(groupIds, group.Id) + } + return +} + func ApplyUserMapping(db *gorm.DB, users []ConfigUserMapping) ([]string, error) { var ids []string @@ -80,54 +148,70 @@ func ApplyUserMapping(db *gorm.DB, users []ConfigUserMapping) ([]string, error) } return nil, err } - for _, r := range u.Roles { - switch r.Kind { - case ROLE_KIND_K8S_ROLE: - // TODO (brucemacd): Handle config imports of roles when we support RoleBindings - logging.L.Info("Skipping role: " + r.Name + ", RoleBindings are not supported yet") - case ROLE_KIND_K8S_CLUSTER_ROLE: - for _, cName := range r.Clusters { - var destination Destination - err := db.Where(&Destination{Name: cName}).First(&destination).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - // when a destination is added then the config import will be retried, skip for now - logging.L.Debug("skipping destination in config import that has not yet been discovered") - continue - } - return nil, err - } - var role Role - if err = db.FirstOrCreate(&role, &Role{Name: r.Name, Kind: r.Kind, DestinationId: destination.Id, FromConfig: true}).Error; err != nil { - return nil, err - } - // if this role is not yet associated with this user, add that association now - // important: do not create the association on the user, that runs an upsert that creates a deadlock because User.AfterCreate() calls this function - if db.Model(&user).Where(&Role{Id: role.Id}).Association("Roles").Count() == 0 { - if err = db.Model(&user).Where(&Role{Id: role.Id}).Association("Roles").Append(&role); err != nil { - return nil, err - } - } - ids = append(ids, role.Id) + + // add the user to groups + for _, gName := range u.Groups { + // Assumes that only one group can exist with a given name, regardless of sources + var group Group + grpReadErr := db.Where(&Group{Name: gName}).First(&group).Error + if grpReadErr != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + logging.L.Debug("skipping unknown group \"" + gName + "\" on user") + continue + } + return nil, grpReadErr + } + if db.Model(&user).Where(&Group{Id: group.Id}).Association("Groups").Count() == 0 { + if err = db.Model(&user).Where(&Group{Id: group.Id}).Association("Groups").Append(&group); err != nil { + return nil, err } - default: - logging.L.Info("Unrecognized role kind: " + r.Kind + " in infra.yaml, role skipped.") } } - // TODO: add user to groups here + // add roles to user + roles, err := importRoles(db, u.Roles) + if err != nil { + return nil, err + } + // for all roles attached to this user update their user associations now that we have made sure they exist + // important: do not create the association on the user, that runs an upsert that creates a concurrent write because User.AfterCreate() calls this function + for _, role := range roles { + if db.Model(&user).Where(&Role{Id: role.Id}).Association("Roles").Count() == 0 { + if err = db.Model(&user).Where(&Role{Id: role.Id}).Association("Roles").Append(&role); err != nil { + return nil, err + } + } + ids = append(ids, role.Id) + } } return ids, nil } -func ImportUserMappings(db *gorm.DB, users []ConfigUserMapping) error { - idsToKeep, err := ApplyUserMapping(db, users) +// ImportMappings imports the group and user role mappings and removes previously created roles if they no longer exist +func ImportMappings(db *gorm.DB, groups []ConfigGroupMapping, users []ConfigUserMapping) error { + grpIdsToKeep, grpRoleIdsToKeep, err := ApplyGroupMappings(db, groups) + if err != nil { + return err + } + // clean up existing groups which have been removed from the config + err = db.Where("1 = 1").Not(grpIdsToKeep).Delete(Group{}).Error + if err != nil { + return err + } + + usrRoleIdsToKeep, err := ApplyUserMapping(db, users) if err != nil { return err } - return db.Where(&Role{FromConfig: true}).Not(idsToKeep).Delete(Role{}).Error + + // clean up existing roles which have been removed from the config + var roleIdsToKeep []string + roleIdsToKeep = append(roleIdsToKeep, grpRoleIdsToKeep...) + roleIdsToKeep = append(roleIdsToKeep, usrRoleIdsToKeep...) + return db.Where(&Role{FromConfig: true}).Not(roleIdsToKeep).Delete(Role{}).Error } +// ImportConfig tries to import all valid fields in a config file func ImportConfig(db *gorm.DB, bs []byte) error { var config Config err := yaml.Unmarshal(bs, &config) @@ -141,9 +225,43 @@ func ImportConfig(db *gorm.DB, bs []byte) error { if err = ImportSources(tx, config.Sources); err != nil { return err } - if err = ImportUserMappings(tx, config.Users); err != nil { + // Need to import of group/user mappings together becuase they both rely on roles + if err = ImportMappings(tx, config.Groups, config.Users); err != nil { return err } return nil }) } + +// import roles creates roles specified in the config, or updates their assosiations +func importRoles(db *gorm.DB, roles []ConfigRoleKubernetes) ([]Role, error) { + var rolesImported []Role + for _, r := range roles { + switch r.Kind { + case ROLE_KIND_K8S_ROLE: + // TODO (brucemacd): Handle config imports of roles when we support RoleBindings + logging.L.Info("Skipping role: " + r.Name + ", RoleBindings are not supported yet") + case ROLE_KIND_K8S_CLUSTER_ROLE: + for _, cName := range r.Clusters { + var destination Destination + err := db.Where(&Destination{Name: cName}).First(&destination).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // when a destination is added then the config import will be retried, skip for now + logging.L.Debug("skipping destination in config import that has not yet been discovered") + continue + } + return nil, err + } + var role Role + if err = db.FirstOrCreate(&role, &Role{Name: r.Name, Kind: r.Kind, DestinationId: destination.Id, FromConfig: true}).Error; err != nil { + return nil, err + } + rolesImported = append(rolesImported, role) + } + default: + logging.L.Info("Unrecognized role kind: " + r.Kind + " in infra.yaml, role skipped.") + } + } + return rolesImported, nil +} diff --git a/internal/registry/config_test.go b/internal/registry/config_test.go index a87a80756b..31cfccda28 100644 --- a/internal/registry/config_test.go +++ b/internal/registry/config_test.go @@ -11,10 +11,13 @@ import ( ) var db *gorm.DB + +var fakeOktaSource = Source{Type: "okta", OktaDomain: "test.example.com"} var adminUser = User{Email: "admin@example.com"} var standardUser = User{Email: "user@example.com"} -var clusterA = &Destination{Name: "cluster-AAA"} -var clusterB = &Destination{Name: "cluster-BBB"} +var iosDevUser = User{Email: "woz@example.com"} +var clusterA = Destination{Name: "cluster-AAA"} +var clusterB = Destination{Name: "cluster-BBB"} func setup() error { confFile, err := ioutil.ReadFile("_testdata/infra.yaml") @@ -26,6 +29,10 @@ func setup() error { return err } + err = db.Create(&fakeOktaSource).Error + if err != nil { + return err + } err = db.Create(&adminUser).Error if err != nil { return err @@ -34,6 +41,10 @@ func setup() error { if err != nil { return err } + err = db.Create(&iosDevUser).Error + if err != nil { + return err + } err = db.Create(&clusterA).Error if err != nil { return err @@ -43,8 +54,7 @@ func setup() error { return err } - ImportConfig(db, confFile) - return nil + return ImportConfig(db, confFile) } func TestMain(m *testing.M) { @@ -65,21 +75,86 @@ func TestImportCurrentValidConfig(t *testing.T) { assert.NoError(t, ImportConfig(db, confFile)) } -func TestRolesForExistingUsersAndDestinationsAreCreated(t *testing.T) { - assert.True(t, containsUserRoleForDestination(db, adminUser, clusterA.Id, "admin"), "admin@example.com should have the admin role in cluster-AAA") - assert.True(t, containsUserRoleForDestination(db, adminUser, clusterB.Id, "admin"), "admin@example.com should have the admin role in cluster-BBB") - assert.True(t, containsUserRoleForDestination(db, standardUser, clusterA.Id, "writer"), "user@example.com should have the writer role in cluster-AAA") - assert.True(t, containsUserRoleForDestination(db, standardUser, clusterB.Id, "reader"), "user@example.com should have the reader role in cluster-BBB") +func TestGroupsForExistingSourcesAreCreated(t *testing.T) { + var groups []Group + db.Find(&groups) + assert.Equal(t, 2, len(groups), "Only two groups should be created from the test config, the other group has an invalid source") + group1 := groups[0] + group2 := groups[1] - unkownUser := User{Id: "0", Email: "unknown@example.com"} - assert.False(t, containsUserRoleForDestination(db, unkownUser, clusterA.Id, "writer"), "unknown user should not have roles assigned") + var sources []Source + db.Model(&group1).Association("Sources").Find(&sources) + assert.Equal(t, 1, len(sources), "Groups in the test config should only have the source \"okta\"") + db.Model(&group2).Association("Sources").Find(&sources) + assert.Equal(t, 1, len(sources), "Groups in the test config should only have the source \"okta\"") + + var roles1 []Role + db.Model(&group1).Association("Roles").Find(&roles1) + assert.Equal(t, 1, len(roles1), "The groups in the test config should have only one role") + var roles2 []Role + db.Model(&group2).Association("Roles").Find(&roles2) + assert.Equal(t, 1, len(roles2), "The groups in the test config should have only one role") + + var destinationRoles = map[string]string{ + clusterA.Id: "writer", + clusterB.Id: "writer", + } + var roles []Role + roles = append(roles, roles1...) + roles = append(roles, roles2...) + // check all of our expected roles exist + for _, role := range roles { + if destinationRoles[role.DestinationId] == "" { + t.Error("Unexpected role loaded from test config", role) + } + delete(destinationRoles, role.DestinationId) + } + assert.Empty(t, destinationRoles, "Not all roles expected to be loaded from test config were seen") +} + +func TestGroupsForUnknownSourcesAreNotCreated(t *testing.T) { + var groups []Group + db.Find(&groups) + assert.Equal(t, 2, len(groups), "Only two groups should be created from the test config, the other group has an invalid source") + group1 := groups[0] + group2 := groups[1] + + assert.NotEqual(t, "unknown", group1.Name, "A group was made for a source that does not exist") + assert.NotEqual(t, "unknown", group2.Name, "A group was made for a source that does not exist") } func TestUsersForExistingUsersAndDestinationsAreCreated(t *testing.T) { - assert.True(t, containsUserRoleForDestination(db, adminUser, clusterA.Id, "admin"), "admin@example.com should have the admin role in cluster-AAA") - assert.True(t, containsUserRoleForDestination(db, adminUser, clusterB.Id, "admin"), "admin@example.com should have the admin role in cluster-BBB") - assert.True(t, containsUserRoleForDestination(db, standardUser, clusterA.Id, "writer"), "user@example.com should have the writer role in cluster-AAA") - assert.True(t, containsUserRoleForDestination(db, standardUser, clusterB.Id, "reader"), "user@example.com should have the reader role in cluster-BBB") + isAdminAdminA, err := containsUserRoleForDestination(db, adminUser, clusterA.Id, "admin") + if err != nil { + t.Error(err) + } + assert.True(t, isAdminAdminA, "admin@example.com should have the admin role in cluster-AAA") + + isAdminAdminB, err := containsUserRoleForDestination(db, adminUser, clusterB.Id, "admin") + if err != nil { + t.Error(err) + } + assert.True(t, isAdminAdminB, "admin@example.com should have the admin role in cluster-BBB") + + isStandardWriterA, err := containsUserRoleForDestination(db, standardUser, clusterA.Id, "writer") + if err != nil { + t.Error(err) + } + assert.True(t, isStandardWriterA, "user@example.com should have the writer role in cluster-AAA") + + isStandardReaderA, err := containsUserRoleForDestination(db, standardUser, clusterA.Id, "reader") + if err != nil { + t.Error(err) + } + assert.True(t, isStandardReaderA, "user@example.com should have the reader role in cluster-AAA") + + unkownUser := User{Id: "0", Email: "unknown@example.com"} + isUnknownUserGrantedRole, err := containsUserRoleForDestination(db, unkownUser, clusterA.Id, "writer") + if err != nil { + t.Error(err) + } + assert.False(t, isUnknownUserGrantedRole, "unknown user should not have roles assigned") + } func TestImportRolesForUnknownDestinationsAreIgnored(t *testing.T) { @@ -98,13 +173,32 @@ func TestImportRolesForUnknownDestinationsAreIgnored(t *testing.T) { } } -func containsUserRoleForDestination(db *gorm.DB, user User, destinationId string, roleName string) bool { +func containsUserRoleForDestination(db *gorm.DB, user User, destinationId string, roleName string) (bool, error) { var roles []Role - db.Model(&user).Association("Roles").Find(&roles) + err := db.Preload("Destination").Preload("Groups").Preload("Users").Find(&roles, &Role{Name: roleName, DestinationId: destinationId}).Error + if err != nil { + return false, err + } + // check direct role-user relations for _, role := range roles { - if role.DestinationId == destinationId && role.Name == roleName { - return true + for _, roleU := range role.Users { + if roleU.Email == user.Email { + return true, nil + } + } + } + // check user groups-roles + var groups []Group + db.Model(&user).Association("Groups").Find(&groups) + for _, g := range groups { + var groupRoles []Role + err := db.Model(&g).Association("Roles").Find(&groupRoles, &Role{Name: roleName, DestinationId: destinationId}) + if err != nil { + return false, err + } + if len(groupRoles) > 0 { + return true, nil } } - return false + return false, nil } diff --git a/internal/registry/data.go b/internal/registry/data.go index 0ea94f40bc..95dd47dfdc 100644 --- a/internal/registry/data.go +++ b/internal/registry/data.go @@ -40,6 +40,7 @@ type User struct { Sources []Source `gorm:"many2many:users_sources"` Roles []Role `gorm:"many2many:users_roles"` + Groups []Group `gorm:"many2many:groups_users"` } var ( @@ -58,11 +59,23 @@ type Source struct { OktaClientSecret string OktaApiToken string - Users []User `gorm:"many2many:users_sources"` + Users []User `gorm:"many2many:users_sources"` + Groups []Group `gorm:"many2many:groups_sources"` FromConfig bool } +type Group struct { + Id string `gorm:"primaryKey"` + Created int64 `gorm:"autoCreateTime"` + Updated int64 `gorm:"autoUpdateTime"` + Name string + + Sources []Source `gorm:"many2many:groups_sources"` + Roles []Role `gorm:"many2many:groups_roles"` + Users []User `gorm:"many2many:groups_users"` +} + var ( DESTINATION_TYPE_KUBERNERNETES = "kubernetes" ) @@ -88,6 +101,7 @@ type Role struct { Kind string DestinationId string Destination Destination `gorm:"foreignKey:DestinationId;references:Id"` + Groups []Group `gorm:"many2many:groups_roles"` Users []User `gorm:"many2many:users_roles"` FromConfig bool @@ -242,6 +256,14 @@ func (r *Role) BeforeCreate(tx *gorm.DB) (err error) { return } +func (g *Group) BeforeCreate(tx *gorm.DB) (err error) { + if g.Id == "" { + g.Id = generate.RandString(ID_LEN) + } + + return +} + func (s *Source) BeforeCreate(tx *gorm.DB) (err error) { if s.Id == "" { s.Id = generate.RandString(ID_LEN) diff --git a/internal/registry/grpc.go b/internal/registry/grpc.go index 23899bad8c..52c5369ed7 100644 --- a/internal/registry/grpc.go +++ b/internal/registry/grpc.go @@ -489,16 +489,35 @@ func (v *V1Server) ListRoles(ctx context.Context, in *v1.ListRolesRequest) (*v1. } var roles []Role - err := v.db.Find(&roles).Error + err := v.db.Preload("Destination").Preload("Groups").Preload("Users").Find(&roles, &Role{DestinationId: in.DestinationId}).Error if err != nil { return nil, err } + // build the response which unifies the relation of group and directly related users to the role res := &v1.ListRolesResponse{} for _, r := range roles { - // need to manually assosiate the users for each role + // avoid duplicate users being added to the response by mapping based on user ID + rUsers := make(map[string]User) + for _, rUser := range r.Users { + rUsers[rUser.Id] = rUser + } + // add any group users associated with the role now + for _, g := range r.Groups { + var gUsers []User + err := v.db.Model(&g).Association("Users").Find(&gUsers) + if err != nil { + return nil, err + } + for _, gUser := range gUsers { + rUsers[gUser.Id] = gUser + } + } + // set the role users to the unified role/group users var users []User - v.db.Model(&r).Association("Users").Find(&users) + for _, u := range rUsers { + users = append(users, u) + } r.Users = users res.Roles = append(res.Roles, dbToProtoRole(&r)) diff --git a/internal/registry/grpc_test.go b/internal/registry/grpc_test.go index fb3bdc154a..5e7a6d0821 100644 --- a/internal/registry/grpc_test.go +++ b/internal/registry/grpc_test.go @@ -3,13 +3,15 @@ package registry import ( "context" "errors" + "fmt" + "strings" "testing" - "github.com/go-playground/assert/v2" "github.com/infrahq/infra/internal/generate" "github.com/infrahq/infra/internal/registry/mocks" v1 "github.com/infrahq/infra/internal/v1" "github.com/infrahq/infra/internal/version" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -596,7 +598,7 @@ func TestSignupWithExistingAdmin(t *testing.T) { res, err := server.Signup(context.Background(), req) assert.Equal(t, status.Code(err), codes.InvalidArgument) - assert.Equal(t, res, nil) + assert.Nil(t, res) } func TestVersion(t *testing.T) { @@ -632,3 +634,95 @@ func TestVersionPublicAuth(t *testing.T) { assert.Equal(t, status.Code(err), codes.OK) assert.Equal(t, res.(*v1.VersionResponse).Version, version.Version) } + +func TestListRolesForClusterReturnsRolesFromConfig(t *testing.T) { + // this in memory DB is setup in the config test + server := &V1Server{db: db} + + req := &v1.ListRolesRequest{ + DestinationId: clusterA.Id, + } + + res, err := server.ListRoles(context.Background(), req) + + assert.Equal(t, status.Code(err), codes.OK) + + returnedUserRoles := make(map[string][]*v1.User) + for _, r := range res.Roles { + returnedUserRoles[r.Name] = r.Users + } + + for k, vs := range returnedUserRoles { + fmt.Println(k + ": ") + fmt.Println(vs) + fmt.Println() + } + + // check default roles granted on user create + assert.Equal(t, 3, len(returnedUserRoles["view"])) + assert.True(t, containsUser(returnedUserRoles["view"], iosDevUser.Email)) + assert.True(t, containsUser(returnedUserRoles["view"], standardUser.Email)) + assert.True(t, containsUser(returnedUserRoles["view"], adminUser.Email)) + + // roles from groups + assert.Equal(t, 2, len(returnedUserRoles["writer"])) + assert.True(t, containsUser(returnedUserRoles["writer"], iosDevUser.Email)) + assert.True(t, containsUser(returnedUserRoles["writer"], standardUser.Email)) + + // roles from direct user assignment + assert.Equal(t, 1, len(returnedUserRoles["admin"])) + assert.True(t, containsUser(returnedUserRoles["admin"], adminUser.Email)) + assert.Equal(t, 1, len(returnedUserRoles["reader"])) + assert.True(t, containsUser(returnedUserRoles["reader"], standardUser.Email)) +} + +func TestListRolesOnlyFindsForSpecificCluster(t *testing.T) { + // this in memory DB is setup in the config test + server := &V1Server{db: db} + + req := &v1.ListRolesRequest{ + DestinationId: clusterA.Id, + } + + res, err := server.ListRoles(context.Background(), req) + + assert.Equal(t, status.Code(err), codes.OK) + + unexpectedClusterIds := make(map[string]bool) + for _, r := range res.Roles { + if r.Destination.Id != clusterA.Id { + unexpectedClusterIds[r.Destination.Id] = true + } + } + if len(unexpectedClusterIds) != 0 { + var unexpectedClusters []string + for id := range unexpectedClusterIds { + unexpectedClusters = append(unexpectedClusters, id) + } + t.Errorf("ListRoles response should only contain roles for the specified cluster ID. Only expected " + clusterA.Id + " but found " + strings.Join(unexpectedClusters, ", ")) + } +} + +func TestListRolesForUnknownCluster(t *testing.T) { + // this in memory DB is setup in the config test + server := &V1Server{db: db} + + req := &v1.ListRolesRequest{ + DestinationId: "Unknown-Cluster-ID", + } + + res, err := server.ListRoles(context.Background(), req) + + assert.Equal(t, status.Code(err), codes.OK) + + assert.Equal(t, 0, len(res.Roles)) +} + +func containsUser(users []*v1.User, email string) bool { + for _, u := range users { + if u.Email == email { + return true + } + } + return false +} diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 882e1da82b..72fe841fcf 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -188,7 +188,7 @@ func Run(options Options) error { if options.DefaultApiKey != "" { if len(options.DefaultApiKey) != API_KEY_LEN { - return errors.New("invalid initial api key length") + return errors.New("invalid initial api key length, the key must be 24 characters") } apiKey.Key = options.DefaultApiKey