From 50105f9e6650e134d174d284813fff3f75f54fe9 Mon Sep 17 00:00:00 2001 From: Christer Edvartsen Date: Thu, 8 Feb 2024 10:30:03 +0100 Subject: [PATCH] add resolvers for optional target service account and team instead of returning ID/slug --- internal/database/gensql/mock_querier.go | 59 ++++ internal/database/gensql/querier.go | 1 + .../database/gensql/service_accounts.sql.go | 26 ++ internal/database/mock_database.go | 59 ++++ .../database/queries/service_accounts.sql | 5 + internal/database/service_accounts.go | 15 ++ internal/graph/authentication.resolvers.go | 3 +- internal/graph/gengql/generated.go | 253 +++++++++++++----- .../graph/graphqls/serviceAccounts.graphqls | 8 +- internal/graph/loader/loader.go | 7 +- internal/graph/loader/service_accounts.go | 29 ++ internal/graph/loader/userroles.go | 20 +- internal/graph/model/gqlvars.go | 10 +- internal/graph/model/models_gen.go | 12 - internal/graph/model/role.go | 7 + internal/graph/model/service_account.go | 10 +- internal/graph/roles.go | 20 +- internal/graph/scalar/ident.go | 10 - internal/graph/serviceAccounts.resolvers.go | 37 ++- internal/graph/serviceAccounts_test.go | 10 +- internal/graph/teams_test.go | 2 - 21 files changed, 465 insertions(+), 138 deletions(-) create mode 100644 internal/graph/loader/service_accounts.go create mode 100644 internal/graph/model/role.go diff --git a/internal/database/gensql/mock_querier.go b/internal/database/gensql/mock_querier.go index 816fd2263..1463702dd 100644 --- a/internal/database/gensql/mock_querier.go +++ b/internal/database/gensql/mock_querier.go @@ -3088,6 +3088,65 @@ func (_c *MockQuerier_GetServiceAccounts_Call) RunAndReturn(run func(context.Con return _c } +// GetServiceAccountsByIDs provides a mock function with given fields: ctx, ids +func (_m *MockQuerier) GetServiceAccountsByIDs(ctx context.Context, ids []uuid.UUID) ([]*ServiceAccount, error) { + ret := _m.Called(ctx, ids) + + if len(ret) == 0 { + panic("no return value specified for GetServiceAccountsByIDs") + } + + var r0 []*ServiceAccount + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []uuid.UUID) ([]*ServiceAccount, error)); ok { + return rf(ctx, ids) + } + if rf, ok := ret.Get(0).(func(context.Context, []uuid.UUID) []*ServiceAccount); ok { + r0 = rf(ctx, ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*ServiceAccount) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []uuid.UUID) error); ok { + r1 = rf(ctx, ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockQuerier_GetServiceAccountsByIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetServiceAccountsByIDs' +type MockQuerier_GetServiceAccountsByIDs_Call struct { + *mock.Call +} + +// GetServiceAccountsByIDs is a helper method to define mock.On call +// - ctx context.Context +// - ids []uuid.UUID +func (_e *MockQuerier_Expecter) GetServiceAccountsByIDs(ctx interface{}, ids interface{}) *MockQuerier_GetServiceAccountsByIDs_Call { + return &MockQuerier_GetServiceAccountsByIDs_Call{Call: _e.mock.On("GetServiceAccountsByIDs", ctx, ids)} +} + +func (_c *MockQuerier_GetServiceAccountsByIDs_Call) Run(run func(ctx context.Context, ids []uuid.UUID)) *MockQuerier_GetServiceAccountsByIDs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]uuid.UUID)) + }) + return _c +} + +func (_c *MockQuerier_GetServiceAccountsByIDs_Call) Return(_a0 []*ServiceAccount, _a1 error) *MockQuerier_GetServiceAccountsByIDs_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockQuerier_GetServiceAccountsByIDs_Call) RunAndReturn(run func(context.Context, []uuid.UUID) ([]*ServiceAccount, error)) *MockQuerier_GetServiceAccountsByIDs_Call { + _c.Call.Return(run) + return _c +} + // GetSessionByID provides a mock function with given fields: ctx, id func (_m *MockQuerier) GetSessionByID(ctx context.Context, id uuid.UUID) (*Session, error) { ret := _m.Called(ctx, id) diff --git a/internal/database/gensql/querier.go b/internal/database/gensql/querier.go index 7a7624c3f..bf46564dd 100644 --- a/internal/database/gensql/querier.go +++ b/internal/database/gensql/querier.go @@ -74,6 +74,7 @@ type Querier interface { GetServiceAccountByName(ctx context.Context, name string) (*ServiceAccount, error) GetServiceAccountRoles(ctx context.Context, serviceAccountID uuid.UUID) ([]*ServiceAccountRole, error) GetServiceAccounts(ctx context.Context) ([]*ServiceAccount, error) + GetServiceAccountsByIDs(ctx context.Context, ids []uuid.UUID) ([]*ServiceAccount, error) GetSessionByID(ctx context.Context, id uuid.UUID) (*Session, error) GetTeamBySlug(ctx context.Context, argSlug slug.Slug) (*Team, error) GetTeamBySlugs(ctx context.Context, slugs []slug.Slug) ([]*Team, error) diff --git a/internal/database/gensql/service_accounts.sql.go b/internal/database/gensql/service_accounts.sql.go index 8994a52d9..441435376 100644 --- a/internal/database/gensql/service_accounts.sql.go +++ b/internal/database/gensql/service_accounts.sql.go @@ -113,3 +113,29 @@ func (q *Queries) GetServiceAccounts(ctx context.Context) ([]*ServiceAccount, er } return items, nil } + +const getServiceAccountsByIDs = `-- name: GetServiceAccountsByIDs :many +SELECT id, name FROM service_accounts +WHERE id = ANY($1::uuid[]) +ORDER BY name ASC +` + +func (q *Queries) GetServiceAccountsByIDs(ctx context.Context, ids []uuid.UUID) ([]*ServiceAccount, error) { + rows, err := q.db.Query(ctx, getServiceAccountsByIDs, ids) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*ServiceAccount{} + for rows.Next() { + var i ServiceAccount + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/database/mock_database.go b/internal/database/mock_database.go index dcfaf085c..ed6ccefdd 100644 --- a/internal/database/mock_database.go +++ b/internal/database/mock_database.go @@ -2850,6 +2850,65 @@ func (_c *MockDatabase_GetServiceAccounts_Call) RunAndReturn(run func(context.Co return _c } +// GetServiceAccountsByIDs provides a mock function with given fields: ctx, ids +func (_m *MockDatabase) GetServiceAccountsByIDs(ctx context.Context, ids []uuid.UUID) ([]*ServiceAccount, error) { + ret := _m.Called(ctx, ids) + + if len(ret) == 0 { + panic("no return value specified for GetServiceAccountsByIDs") + } + + var r0 []*ServiceAccount + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []uuid.UUID) ([]*ServiceAccount, error)); ok { + return rf(ctx, ids) + } + if rf, ok := ret.Get(0).(func(context.Context, []uuid.UUID) []*ServiceAccount); ok { + r0 = rf(ctx, ids) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*ServiceAccount) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []uuid.UUID) error); ok { + r1 = rf(ctx, ids) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockDatabase_GetServiceAccountsByIDs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetServiceAccountsByIDs' +type MockDatabase_GetServiceAccountsByIDs_Call struct { + *mock.Call +} + +// GetServiceAccountsByIDs is a helper method to define mock.On call +// - ctx context.Context +// - ids []uuid.UUID +func (_e *MockDatabase_Expecter) GetServiceAccountsByIDs(ctx interface{}, ids interface{}) *MockDatabase_GetServiceAccountsByIDs_Call { + return &MockDatabase_GetServiceAccountsByIDs_Call{Call: _e.mock.On("GetServiceAccountsByIDs", ctx, ids)} +} + +func (_c *MockDatabase_GetServiceAccountsByIDs_Call) Run(run func(ctx context.Context, ids []uuid.UUID)) *MockDatabase_GetServiceAccountsByIDs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]uuid.UUID)) + }) + return _c +} + +func (_c *MockDatabase_GetServiceAccountsByIDs_Call) Return(_a0 []*ServiceAccount, _a1 error) *MockDatabase_GetServiceAccountsByIDs_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockDatabase_GetServiceAccountsByIDs_Call) RunAndReturn(run func(context.Context, []uuid.UUID) ([]*ServiceAccount, error)) *MockDatabase_GetServiceAccountsByIDs_Call { + _c.Call.Return(run) + return _c +} + // GetSessionByID provides a mock function with given fields: ctx, sessionID func (_m *MockDatabase) GetSessionByID(ctx context.Context, sessionID uuid.UUID) (*Session, error) { ret := _m.Called(ctx, sessionID) diff --git a/internal/database/queries/service_accounts.sql b/internal/database/queries/service_accounts.sql index 284839666..99504a98c 100644 --- a/internal/database/queries/service_accounts.sql +++ b/internal/database/queries/service_accounts.sql @@ -24,3 +24,8 @@ WHERE id = @id; SELECT * FROM service_account_roles WHERE service_account_id = @service_account_id ORDER BY role_name ASC; + +-- name: GetServiceAccountsByIDs :many +SELECT * FROM service_accounts +WHERE id = ANY(@ids::uuid[]) +ORDER BY name ASC; \ No newline at end of file diff --git a/internal/database/service_accounts.go b/internal/database/service_accounts.go index 89e8c43ff..998c06682 100644 --- a/internal/database/service_accounts.go +++ b/internal/database/service_accounts.go @@ -17,6 +17,7 @@ type ServiceAccountRepo interface { GetServiceAccountByName(ctx context.Context, name string) (*ServiceAccount, error) GetServiceAccountRoles(ctx context.Context, serviceAccountID uuid.UUID) ([]*authz.Role, error) GetServiceAccounts(ctx context.Context) ([]*ServiceAccount, error) + GetServiceAccountsByIDs(ctx context.Context, ids []uuid.UUID) ([]*ServiceAccount, error) RemoveAllServiceAccountRoles(ctx context.Context, serviceAccountID uuid.UUID) error RemoveApiKeysFromServiceAccount(ctx context.Context, serviceAccountID uuid.UUID) error } @@ -78,6 +79,20 @@ func (d *database) GetServiceAccounts(ctx context.Context) ([]*ServiceAccount, e return serviceAccounts, nil } +func (d *database) GetServiceAccountsByIDs(ctx context.Context, ids []uuid.UUID) ([]*ServiceAccount, error) { + rows, err := d.querier.GetServiceAccountsByIDs(ctx, ids) + if err != nil { + return nil, err + } + + serviceAccounts := make([]*ServiceAccount, 0) + for _, row := range rows { + serviceAccounts = append(serviceAccounts, &ServiceAccount{ServiceAccount: row}) + } + + return serviceAccounts, nil +} + func (d *database) DeleteServiceAccount(ctx context.Context, serviceAccountID uuid.UUID) error { return d.querier.DeleteServiceAccount(ctx, serviceAccountID) } diff --git a/internal/graph/authentication.resolvers.go b/internal/graph/authentication.resolvers.go index 202e4913b..ef3398113 100644 --- a/internal/graph/authentication.resolvers.go +++ b/internal/graph/authentication.resolvers.go @@ -11,7 +11,6 @@ import ( "github.com/nais/api/internal/database" "github.com/nais/api/internal/graph/apierror" "github.com/nais/api/internal/graph/model" - "github.com/nais/api/internal/graph/scalar" ) // Me is the resolver for the me field. @@ -28,7 +27,7 @@ func (r *queryResolver) Me(ctx context.Context) (model.AuthenticatedUser, error) }, nil case *database.ServiceAccount: return &model.ServiceAccount{ - ID: scalar.ServiceAccountIdent(me.ID), + ID: me.ID, Name: me.Name, }, nil default: diff --git a/internal/graph/gengql/generated.go b/internal/graph/gengql/generated.go index 47a0ad0b6..5ae3bb5be 100644 --- a/internal/graph/gengql/generated.go +++ b/internal/graph/gengql/generated.go @@ -51,6 +51,7 @@ type ResolverRoot interface { NaisJob() NaisJobResolver Query() QueryResolver Reconciler() ReconcilerResolver + Role() RoleResolver ServiceAccount() ServiceAccountResolver Subscription() SubscriptionResolver Team() TeamResolver @@ -667,10 +668,10 @@ type ComplexityRoot struct { } Role struct { - IsGlobal func(childComplexity int) int - Name func(childComplexity int) int - TargetServiceAccountID func(childComplexity int) int - TargetTeamSlug func(childComplexity int) int + IsGlobal func(childComplexity int) int + Name func(childComplexity int) int + TargetServiceAccount func(childComplexity int) int + TargetTeam func(childComplexity int) int } Rule struct { @@ -962,6 +963,10 @@ type ReconcilerResolver interface { Configured(ctx context.Context, obj *model.Reconciler) (bool, error) AuditLogs(ctx context.Context, obj *model.Reconciler, offset *int, limit *int) (*model.AuditLogList, error) } +type RoleResolver interface { + TargetServiceAccount(ctx context.Context, obj *model.Role) (*model.ServiceAccount, error) + TargetTeam(ctx context.Context, obj *model.Role) (*model.Team, error) +} type ServiceAccountResolver interface { Roles(ctx context.Context, obj *model.ServiceAccount) ([]*model.Role, error) } @@ -3679,19 +3684,19 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Role.Name(childComplexity), true - case "Role.targetServiceAccountID": - if e.complexity.Role.TargetServiceAccountID == nil { + case "Role.targetServiceAccount": + if e.complexity.Role.TargetServiceAccount == nil { break } - return e.complexity.Role.TargetServiceAccountID(childComplexity), true + return e.complexity.Role.TargetServiceAccount(childComplexity), true - case "Role.targetTeamSlug": - if e.complexity.Role.TargetTeamSlug == nil { + case "Role.targetTeam": + if e.complexity.Role.TargetTeam == nil { break } - return e.complexity.Role.TargetTeamSlug(childComplexity), true + return e.complexity.Role.TargetTeam(childComplexity), true case "Rule.application": if e.complexity.Rule.Application == nil { @@ -5906,11 +5911,11 @@ type Role { "Whether or not the role is global." isGlobal: Boolean! - "Optional service account ID if the role binding targets a service account. TODO: Make these resolvers returning service account and team, not IDs" - targetServiceAccountID: ID + "Optional service account if the role binding targets a service account." + targetServiceAccount: ServiceAccount - "Optional team slug if the role binding targets a team. TODO: Make these resolvers returning service account and team, not IDs" - targetTeamSlug: Slug + "Optional team if the role binding targets a team." + targetTeam: Team } `, BuiltIn: false}, {Name: "../graphqls/storage.graphqls", Input: `interface Storage { @@ -26213,8 +26218,8 @@ func (ec *executionContext) fieldContext_Role_isGlobal(ctx context.Context, fiel return fc, nil } -func (ec *executionContext) _Role_targetServiceAccountID(ctx context.Context, field graphql.CollectedField, obj *model.Role) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Role_targetServiceAccountID(ctx, field) +func (ec *executionContext) _Role_targetServiceAccount(ctx context.Context, field graphql.CollectedField, obj *model.Role) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Role_targetServiceAccount(ctx, field) if err != nil { return graphql.Null } @@ -26227,7 +26232,7 @@ func (ec *executionContext) _Role_targetServiceAccountID(ctx context.Context, fi }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.TargetServiceAccountID, nil + return ec.resolvers.Role().TargetServiceAccount(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -26236,26 +26241,34 @@ func (ec *executionContext) _Role_targetServiceAccountID(ctx context.Context, fi if resTmp == nil { return graphql.Null } - res := resTmp.(*scalar.Ident) + res := resTmp.(*model.ServiceAccount) fc.Result = res - return ec.marshalOID2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋscalarᚐIdent(ctx, field.Selections, res) + return ec.marshalOServiceAccount2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋmodelᚐServiceAccount(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Role_targetServiceAccountID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Role_targetServiceAccount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Role", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type ID does not have child fields") + switch field.Name { + case "id": + return ec.fieldContext_ServiceAccount_id(ctx, field) + case "name": + return ec.fieldContext_ServiceAccount_name(ctx, field) + case "roles": + return ec.fieldContext_ServiceAccount_roles(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type ServiceAccount", field.Name) }, } return fc, nil } -func (ec *executionContext) _Role_targetTeamSlug(ctx context.Context, field graphql.CollectedField, obj *model.Role) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Role_targetTeamSlug(ctx, field) +func (ec *executionContext) _Role_targetTeam(ctx context.Context, field graphql.CollectedField, obj *model.Role) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Role_targetTeam(ctx, field) if err != nil { return graphql.Null } @@ -26268,7 +26281,7 @@ func (ec *executionContext) _Role_targetTeamSlug(ctx context.Context, field grap }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return obj.TargetTeamSlug, nil + return ec.resolvers.Role().TargetTeam(rctx, obj) }) if err != nil { ec.Error(ctx, err) @@ -26277,19 +26290,71 @@ func (ec *executionContext) _Role_targetTeamSlug(ctx context.Context, field grap if resTmp == nil { return graphql.Null } - res := resTmp.(*slug.Slug) + res := resTmp.(*model.Team) fc.Result = res - return ec.marshalOSlug2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋslugᚐSlug(ctx, field.Selections, res) + return ec.marshalOTeam2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋmodelᚐTeam(ctx, field.Selections, res) } -func (ec *executionContext) fieldContext_Role_targetTeamSlug(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_Role_targetTeam(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "Role", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Slug does not have child fields") + switch field.Name { + case "id": + return ec.fieldContext_Team_id(ctx, field) + case "slug": + return ec.fieldContext_Team_slug(ctx, field) + case "purpose": + return ec.fieldContext_Team_purpose(ctx, field) + case "azureGroupID": + return ec.fieldContext_Team_azureGroupID(ctx, field) + case "gitHubTeamSlug": + return ec.fieldContext_Team_gitHubTeamSlug(ctx, field) + case "googleGroupEmail": + return ec.fieldContext_Team_googleGroupEmail(ctx, field) + case "googleArtifactRegistry": + return ec.fieldContext_Team_googleArtifactRegistry(ctx, field) + case "auditLogs": + return ec.fieldContext_Team_auditLogs(ctx, field) + case "members": + return ec.fieldContext_Team_members(ctx, field) + case "member": + return ec.fieldContext_Team_member(ctx, field) + case "syncErrors": + return ec.fieldContext_Team_syncErrors(ctx, field) + case "lastSuccessfulSync": + return ec.fieldContext_Team_lastSuccessfulSync(ctx, field) + case "githubRepositories": + return ec.fieldContext_Team_githubRepositories(ctx, field) + case "slackChannel": + return ec.fieldContext_Team_slackChannel(ctx, field) + case "deletionInProgress": + return ec.fieldContext_Team_deletionInProgress(ctx, field) + case "viewerIsOwner": + return ec.fieldContext_Team_viewerIsOwner(ctx, field) + case "viewerIsMember": + return ec.fieldContext_Team_viewerIsMember(ctx, field) + case "status": + return ec.fieldContext_Team_status(ctx, field) + case "apps": + return ec.fieldContext_Team_apps(ctx, field) + case "deployKey": + return ec.fieldContext_Team_deployKey(ctx, field) + case "naisjobs": + return ec.fieldContext_Team_naisjobs(ctx, field) + case "deployments": + return ec.fieldContext_Team_deployments(ctx, field) + case "vulnerabilities": + return ec.fieldContext_Team_vulnerabilities(ctx, field) + case "vulnerabilitiesSummary": + return ec.fieldContext_Team_vulnerabilitiesSummary(ctx, field) + case "environments": + return ec.fieldContext_Team_environments(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Team", field.Name) }, } return fc, nil @@ -27071,9 +27136,9 @@ func (ec *executionContext) _ServiceAccount_id(ctx context.Context, field graphq } return graphql.Null } - res := resTmp.(scalar.Ident) + res := resTmp.(uuid.UUID) fc.Result = res - return ec.marshalNID2githubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋscalarᚐIdent(ctx, field.Selections, res) + return ec.marshalNID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_ServiceAccount_id(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -27176,10 +27241,10 @@ func (ec *executionContext) fieldContext_ServiceAccount_roles(ctx context.Contex return ec.fieldContext_Role_name(ctx, field) case "isGlobal": return ec.fieldContext_Role_isGlobal(ctx, field) - case "targetServiceAccountID": - return ec.fieldContext_Role_targetServiceAccountID(ctx, field) - case "targetTeamSlug": - return ec.fieldContext_Role_targetTeamSlug(ctx, field) + case "targetServiceAccount": + return ec.fieldContext_Role_targetServiceAccount(ctx, field) + case "targetTeam": + return ec.fieldContext_Role_targetTeam(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Role", field.Name) }, @@ -31181,10 +31246,10 @@ func (ec *executionContext) fieldContext_User_roles(ctx context.Context, field g return ec.fieldContext_Role_name(ctx, field) case "isGlobal": return ec.fieldContext_Role_isGlobal(ctx, field) - case "targetServiceAccountID": - return ec.fieldContext_Role_targetServiceAccountID(ctx, field) - case "targetTeamSlug": - return ec.fieldContext_Role_targetTeamSlug(ctx, field) + case "targetServiceAccount": + return ec.fieldContext_Role_targetServiceAccount(ctx, field) + case "targetTeam": + return ec.fieldContext_Role_targetTeam(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type Role", field.Name) }, @@ -40443,17 +40508,79 @@ func (ec *executionContext) _Role(ctx context.Context, sel ast.SelectionSet, obj case "name": out.Values[i] = ec._Role_name(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } case "isGlobal": out.Values[i] = ec._Role_isGlobal(ctx, field, obj) if out.Values[i] == graphql.Null { - out.Invalids++ + atomic.AddUint32(&out.Invalids, 1) } - case "targetServiceAccountID": - out.Values[i] = ec._Role_targetServiceAccountID(ctx, field, obj) - case "targetTeamSlug": - out.Values[i] = ec._Role_targetTeamSlug(ctx, field, obj) + case "targetServiceAccount": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Role_targetServiceAccount(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "targetTeam": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Role_targetTeam(ctx, field, obj) + return res + } + + if field.Deferrable != nil { + dfs, ok := deferred[field.Deferrable.Label] + di := 0 + if ok { + dfs.AddField(field) + di = len(dfs.Values) - 1 + } else { + dfs = graphql.NewFieldSet([]graphql.CollectedField{field}) + deferred[field.Deferrable.Label] = dfs + } + dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler { + return innerFunc(ctx, dfs) + }) + + // don't run the out.Concurrently() call below + out.Values[i] = graphql.Null + continue + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -46889,6 +47016,13 @@ func (ec *executionContext) marshalOSearchType2ᚖgithubᚗcomᚋnaisᚋapiᚋin return v } +func (ec *executionContext) marshalOServiceAccount2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋmodelᚐServiceAccount(ctx context.Context, sel ast.SelectionSet, v *model.ServiceAccount) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._ServiceAccount(ctx, sel, v) +} + func (ec *executionContext) marshalOSidecar2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋmodelᚐSidecar(ctx context.Context, sel ast.SelectionSet, v *model.Sidecar) graphql.Marshaler { if v == nil { return graphql.Null @@ -46916,22 +47050,6 @@ func (ec *executionContext) unmarshalOSlackAlertsChannelInput2ᚕᚖgithubᚗcom return res, nil } -func (ec *executionContext) unmarshalOSlug2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋslugᚐSlug(ctx context.Context, v interface{}) (*slug.Slug, error) { - if v == nil { - return nil, nil - } - var res = new(slug.Slug) - err := res.UnmarshalGQL(v) - return res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalOSlug2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋslugᚐSlug(ctx context.Context, sel ast.SelectionSet, v *slug.Slug) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return v -} - func (ec *executionContext) unmarshalOString2ᚕstringᚄ(ctx context.Context, v interface{}) ([]string, error) { if v == nil { return nil, nil @@ -47018,6 +47136,13 @@ func (ec *executionContext) marshalOString2ᚖstring(ctx context.Context, sel as return res } +func (ec *executionContext) marshalOTeam2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋmodelᚐTeam(ctx context.Context, sel ast.SelectionSet, v *model.Team) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._Team(ctx, sel, v) +} + func (ec *executionContext) unmarshalOTeamsFilter2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋgraphᚋmodelᚐTeamsFilter(ctx context.Context, v interface{}) (*model.TeamsFilter, error) { if v == nil { return nil, nil diff --git a/internal/graph/graphqls/serviceAccounts.graphqls b/internal/graph/graphqls/serviceAccounts.graphqls index 47e98b922..b1445e0d7 100644 --- a/internal/graph/graphqls/serviceAccounts.graphqls +++ b/internal/graph/graphqls/serviceAccounts.graphqls @@ -18,9 +18,9 @@ type Role { "Whether or not the role is global." isGlobal: Boolean! - "Optional service account ID if the role binding targets a service account. TODO: Make these resolvers returning service account and team, not IDs" - targetServiceAccountID: ID + "Optional service account if the role binding targets a service account." + targetServiceAccount: ServiceAccount - "Optional team slug if the role binding targets a team. TODO: Make these resolvers returning service account and team, not IDs" - targetTeamSlug: Slug + "Optional team if the role binding targets a team." + targetTeam: Team } diff --git a/internal/graph/loader/loader.go b/internal/graph/loader/loader.go index 75b5c8f9f..6eaaf6909 100644 --- a/internal/graph/loader/loader.go +++ b/internal/graph/loader/loader.go @@ -20,10 +20,11 @@ const loadersKey ctxKey = iota // Loaders wrap your data loaders to inject via middleware type Loaders struct { - UserLoader *dataloadgen.Loader[uuid.UUID, *model.User] + ServiceAccountLoader *dataloadgen.Loader[uuid.UUID, *model.ServiceAccount] + TeamEnvironmentLoader *dataloadgen.Loader[database.EnvSlugName, *model.Env] TeamLoader *dataloadgen.Loader[slug.Slug, *model.Team] + UserLoader *dataloadgen.Loader[uuid.UUID, *model.User] UserRolesLoader *dataloadgen.Loader[uuid.UUID, []*model.Role] - TeamEnvironmentLoader *dataloadgen.Loader[database.EnvSlugName, *model.Env] } // NewLoaders instantiates data loaders for the middleware @@ -39,12 +40,14 @@ func NewLoaders(db database.Database) *Loaders { tr := &teamReader{db: db} urr := &userRolesReader{db: db} ter := &teamEnvironmentReader{db: db} + sar := &serviceAccountReader{db: db} return &Loaders{ UserLoader: dataloadgen.NewLoader(ur.getUsers, opts...), TeamLoader: dataloadgen.NewLoader(tr.getTeams, opts...), UserRolesLoader: dataloadgen.NewLoader(urr.getUserRoles, opts...), TeamEnvironmentLoader: dataloadgen.NewLoader(ter.getEnvironments, opts...), + ServiceAccountLoader: dataloadgen.NewLoader(sar.getServiceAccounts, opts...), } } diff --git a/internal/graph/loader/service_accounts.go b/internal/graph/loader/service_accounts.go new file mode 100644 index 000000000..2ee166ecb --- /dev/null +++ b/internal/graph/loader/service_accounts.go @@ -0,0 +1,29 @@ +package loader + +import ( + "context" + + "github.com/google/uuid" + "github.com/nais/api/internal/database" + "github.com/nais/api/internal/graph/model" +) + +type serviceAccountReader struct { + db database.ServiceAccountRepo +} + +func (s serviceAccountReader) getServiceAccounts(ctx context.Context, ids []uuid.UUID) ([]*model.ServiceAccount, []error) { + getID := func(obj *model.ServiceAccount) uuid.UUID { return obj.ID } + return loadModels(ctx, ids, s.db.GetServiceAccountsByIDs, ToGraphServiceAccount, getID) +} + +func GetServiceAccount(ctx context.Context, id uuid.UUID) (*model.ServiceAccount, error) { + return For(ctx).ServiceAccountLoader.Load(ctx, id) +} + +func ToGraphServiceAccount(m *database.ServiceAccount) *model.ServiceAccount { + return &model.ServiceAccount{ + ID: m.ID, + Name: m.Name, + } +} diff --git a/internal/graph/loader/userroles.go b/internal/graph/loader/userroles.go index 5136fcc37..a91137af3 100644 --- a/internal/graph/loader/userroles.go +++ b/internal/graph/loader/userroles.go @@ -8,8 +8,6 @@ import ( "github.com/nais/api/internal/auth/authz" "github.com/nais/api/internal/database" "github.com/nais/api/internal/graph/model" - "github.com/nais/api/internal/graph/scalar" - "k8s.io/utils/ptr" ) type userRolesReader struct { @@ -40,17 +38,19 @@ func GetUserRoles(ctx context.Context, userID uuid.UUID) ([]*model.Role, error) } func ToGraphUserRoles(m *authz.Role) *model.Role { - ret := &model.Role{ - Name: string(m.RoleName), - IsGlobal: m.IsGlobal(), - TargetTeamSlug: m.TargetTeamSlug, - } - + var saID uuid.UUID if m.TargetServiceAccountID != nil { - ret.TargetServiceAccountID = ptr.To(scalar.ServiceAccountIdent(*m.TargetServiceAccountID)) + saID = *m.TargetServiceAccountID } - return ret + return &model.Role{ + Name: string(m.RoleName), + IsGlobal: m.IsGlobal(), + GQLVars: model.RoleGQLVars{ + TargetServiceAccountID: saID, + TargetTeamSlug: m.TargetTeamSlug, + }, + } } func toGraphUserRoleList(m []*authz.Role) []*model.Role { diff --git a/internal/graph/model/gqlvars.go b/internal/graph/model/gqlvars.go index be1d9115d..3f47fab73 100644 --- a/internal/graph/model/gqlvars.go +++ b/internal/graph/model/gqlvars.go @@ -1,6 +1,9 @@ package model -import "github.com/nais/api/internal/slug" +import ( + "github.com/google/uuid" + "github.com/nais/api/internal/slug" +) type ( AppGQLVars struct { @@ -24,6 +27,11 @@ type ( Team slug.Slug } + RoleGQLVars struct { + TargetServiceAccountID uuid.UUID + TargetTeamSlug *slug.Slug + } + RunGQLVars struct { Env string Team slug.Slug diff --git a/internal/graph/model/models_gen.go b/internal/graph/model/models_gen.go index f4f2516e1..a4f09e0d5 100644 --- a/internal/graph/model/models_gen.go +++ b/internal/graph/model/models_gen.go @@ -734,18 +734,6 @@ type Resources struct { Requests Requests `json:"requests"` } -// Role binding type. -type Role struct { - // Name of the role. - Name string `json:"name"` - // Whether or not the role is global. - IsGlobal bool `json:"isGlobal"` - // Optional service account ID if the role binding targets a service account. TODO: Make these resolvers returning service account and team, not IDs - TargetServiceAccountID *scalar.Ident `json:"targetServiceAccountID,omitempty"` - // Optional team slug if the role binding targets a team. TODO: Make these resolvers returning service account and team, not IDs - TargetTeamSlug *slug.Slug `json:"targetTeamSlug,omitempty"` -} - type Rule struct { Application string `json:"application"` Namespace string `json:"namespace"` diff --git a/internal/graph/model/role.go b/internal/graph/model/role.go new file mode 100644 index 000000000..eb56f9a1d --- /dev/null +++ b/internal/graph/model/role.go @@ -0,0 +1,7 @@ +package model + +type Role struct { + Name string `json:"name"` + IsGlobal bool `json:"isGlobal"` + GQLVars RoleGQLVars `json:"-"` +} diff --git a/internal/graph/model/service_account.go b/internal/graph/model/service_account.go index 822281979..dfd9a6a25 100644 --- a/internal/graph/model/service_account.go +++ b/internal/graph/model/service_account.go @@ -1,12 +1,12 @@ package model -import "github.com/nais/api/internal/graph/scalar" +import ( + "github.com/google/uuid" +) type ServiceAccount struct { - // Unique ID of the service account. - ID scalar.Ident `json:"id"` - // The name of the service account. - Name string `json:"name"` + ID uuid.UUID `json:"id"` + Name string `json:"name"` } func (ServiceAccount) IsAuthenticatedUser() {} diff --git a/internal/graph/roles.go b/internal/graph/roles.go index 9ece78aa6..c8a8094f3 100644 --- a/internal/graph/roles.go +++ b/internal/graph/roles.go @@ -1,23 +1,25 @@ package graph import ( + "github.com/google/uuid" "github.com/nais/api/internal/auth/authz" "github.com/nais/api/internal/graph/model" - "github.com/nais/api/internal/graph/scalar" - "k8s.io/utils/ptr" ) func toGraphRoles(roles []*authz.Role) []*model.Role { ret := make([]*model.Role, 0, len(roles)) for _, role := range roles { - a := &model.Role{ - Name: string(role.RoleName), - TargetTeamSlug: role.TargetTeamSlug, - IsGlobal: role.IsGlobal(), - } - + var saID uuid.UUID if role.TargetServiceAccountID != nil { - a.TargetServiceAccountID = ptr.To(scalar.ServiceAccountIdent(*role.TargetServiceAccountID)) + saID = *role.TargetServiceAccountID + } + a := &model.Role{ + Name: string(role.RoleName), + IsGlobal: role.IsGlobal(), + GQLVars: model.RoleGQLVars{ + TargetServiceAccountID: saID, + TargetTeamSlug: role.TargetTeamSlug, + }, } ret = append(ret, a) diff --git a/internal/graph/scalar/ident.go b/internal/graph/scalar/ident.go index 05d13ff8e..bcb2dbc11 100644 --- a/internal/graph/scalar/ident.go +++ b/internal/graph/scalar/ident.go @@ -26,9 +26,7 @@ const ( IdentTypeEnv IdentType = "env" IdentTypeJob IdentType = "job" IdentTypePod IdentType = "pod" - IdentTypeServiceAccount IdentType = "serviceAccount" IdentTypeTeam IdentType = "team" - IdentTypeUser IdentType = "user" IdentTypeVulnerabilities IdentType = "vulnerabilities" ) @@ -122,14 +120,6 @@ func CorrelationID(id uuid.UUID) Ident { return newIdent(id.String(), IdentTypeCorrelationID) } -func ServiceAccountIdent(id uuid.UUID) Ident { - return newIdent(id.String(), IdentTypeServiceAccount) -} - -func UserIdent(id uuid.UUID) Ident { - return newIdent(id.String(), IdentTypeUser) -} - func newIdent(id string, t IdentType) Ident { return Ident{ ID: id, diff --git a/internal/graph/serviceAccounts.resolvers.go b/internal/graph/serviceAccounts.resolvers.go index 12f7e5144..c60e2b448 100644 --- a/internal/graph/serviceAccounts.resolvers.go +++ b/internal/graph/serviceAccounts.resolvers.go @@ -7,26 +7,39 @@ package graph import ( "context" + "github.com/google/uuid" "github.com/nais/api/internal/auth/authz" "github.com/nais/api/internal/database/gensql" "github.com/nais/api/internal/graph/gengql" + "github.com/nais/api/internal/graph/loader" "github.com/nais/api/internal/graph/model" ) +// TargetServiceAccount is the resolver for the targetServiceAccount field. +func (r *roleResolver) TargetServiceAccount(ctx context.Context, obj *model.Role) (*model.ServiceAccount, error) { + if obj.GQLVars.TargetServiceAccountID == uuid.Nil { + return nil, nil + } + return loader.GetServiceAccount(ctx, obj.GQLVars.TargetServiceAccountID) +} + +// TargetTeam is the resolver for the targetTeam field. +func (r *roleResolver) TargetTeam(ctx context.Context, obj *model.Role) (*model.Team, error) { + if obj.GQLVars.TargetTeamSlug == nil { + return nil, nil + } + return loader.GetTeam(ctx, *obj.GQLVars.TargetTeamSlug) +} + // Roles is the resolver for the roles field. func (r *serviceAccountResolver) Roles(ctx context.Context, obj *model.ServiceAccount) ([]*model.Role, error) { actor := authz.ActorFromContext(ctx) - said, err := obj.ID.AsUUID() - if err != nil { - return nil, err - } - - err = authz.RequireRole(actor, gensql.RoleNameAdmin) - if err != nil && actor.User.GetID() != said { + err := authz.RequireRole(actor, gensql.RoleNameAdmin) + if err != nil && actor.User.GetID() != obj.ID { return nil, err } - roles, err := r.database.GetServiceAccountRoles(ctx, said) + roles, err := r.database.GetServiceAccountRoles(ctx, obj.ID) if err != nil { return nil, err } @@ -34,7 +47,13 @@ func (r *serviceAccountResolver) Roles(ctx context.Context, obj *model.ServiceAc return toGraphRoles(roles), nil } +// Role returns gengql.RoleResolver implementation. +func (r *Resolver) Role() gengql.RoleResolver { return &roleResolver{r} } + // ServiceAccount returns gengql.ServiceAccountResolver implementation. func (r *Resolver) ServiceAccount() gengql.ServiceAccountResolver { return &serviceAccountResolver{r} } -type serviceAccountResolver struct{ *Resolver } +type ( + roleResolver struct{ *Resolver } + serviceAccountResolver struct{ *Resolver } +) diff --git a/internal/graph/serviceAccounts_test.go b/internal/graph/serviceAccounts_test.go index b12ced882..8eb722de9 100644 --- a/internal/graph/serviceAccounts_test.go +++ b/internal/graph/serviceAccounts_test.go @@ -12,7 +12,6 @@ import ( "github.com/nais/api/internal/database/gensql" "github.com/nais/api/internal/graph" "github.com/nais/api/internal/graph/model" - "github.com/nais/api/internal/graph/scalar" "github.com/nais/api/internal/usersync" "github.com/sirupsen/logrus/hooks/test" ) @@ -55,17 +54,12 @@ func TestMutationResolver_Roles(t *testing.T) { Return([]*authz.Role{role}, nil) r, err := resolver.Roles(ctx, &model.ServiceAccount{ - ID: scalar.ServiceAccountIdent(serviceAccount.ID), + ID: serviceAccount.ID, }) if err != nil { t.Fatal("unexpected error") } - expectedID, err := r[0].TargetServiceAccountID.AsUUID() - if err != nil { - t.Fatal("unexpected error") - } - - if expectedID != *role.TargetServiceAccountID { + if r[0].GQLVars.TargetServiceAccountID != *role.TargetServiceAccountID { t.Fatal("unexpected target service account id") } }) diff --git a/internal/graph/teams_test.go b/internal/graph/teams_test.go index 0db5c5c66..f9a77bd77 100644 --- a/internal/graph/teams_test.go +++ b/internal/graph/teams_test.go @@ -160,7 +160,6 @@ func TestMutationResolver_CreateTeam(t *testing.T) { msg := psMessages[0] if msg.Attributes["EventType"] != protoapi.EventTypes_EVENT_TEAM_UPDATED.String() { t.Errorf("expected event type %s, got %s", protoapi.EventTypes_EVENT_TEAM_UPDATED.String(), msg.Attributes["EventType"]) - } }) @@ -201,7 +200,6 @@ func TestMutationResolver_CreateTeam(t *testing.T) { Purpose: " some purpose ", SlackChannel: slackChannel, }) - if err != nil { t.Fatalf("unexpected error: %v", err) }