diff --git a/charts/console-backend-rbac/templates/rbac.yaml b/charts/console-backend-rbac/templates/rbac.yaml index b312dd02c..18244f802 100644 --- a/charts/console-backend-rbac/templates/rbac.yaml +++ b/charts/console-backend-rbac/templates/rbac.yaml @@ -295,13 +295,6 @@ rules: - get - list - watch - # Legacy CLI port-forward grants still validate the old Zalando cluster. - - apiGroups: - - "data.nais.io" - resources: - - postgres - verbs: - - get - apiGroups: - "nais.io" resources: diff --git a/integration_tests/grant_postgres_access.lua b/integration_tests/grant_postgres_access.lua deleted file mode 100644 index adaf55453..000000000 --- a/integration_tests/grant_postgres_access.lua +++ /dev/null @@ -1,337 +0,0 @@ -local user = User.new("user", "user@usersen.com") -local nonMemberUser = User.new("nonmember", "other@user.com") - -local mainTeam = Team.new("someteamname", "purpose", "#slack_channel") -mainTeam:addMember(user) - -Helper.readK8sResources("k8s_resources/grant_zalando_postgres_access") - -Test.gql("Grant postgres access without authorization in non-existent team", function(t) - t.addHeader("x-user-email", user:email()) - t.query([[ - mutation GrantPostgresAccess { - grantPostgresAccess( - input: { - clusterName: "foobar" - environmentName: "dev" - teamSlug: "non-existing-team" - grantee: "some@email.com" - duration: "30m" - } - ) { - error - } - } - ]]) - - t.check({ - errors = { - { - locations = NotNull(), - message = Contains('you need the "postgres:access:grant" authorization.'), - path = { - "grantPostgresAccess", - }, - }, - }, - data = Null, - }) -end) - -Test.gql("Grant postgres access without authorization in existing team", function(t) - t.addHeader("x-user-email", nonMemberUser:email()) - t.query([[ - mutation GrantPostgresAccess { - grantPostgresAccess( - input: { - clusterName: "foobar" - environmentName: "dev" - teamSlug: "someteamname" - grantee: "some@email.com" - duration: "30m" - } - ) { - error - } - } - ]]) - - t.check({ - errors = { - { - locations = NotNull(), - message = Contains('you need the "postgres:access:grant" authorization.'), - path = { - "grantPostgresAccess", - }, - }, - }, - data = Null, - }) -end) - -Test.gql("Grant postgres access with invalid duration", function(t) - t.addHeader("x-user-email", user:email()) - t.query([[ - mutation GrantPostgresAccess { - grantPostgresAccess( - input: { - clusterName: "foobar" - environmentName: "dev" - teamSlug: "someteamname" - grantee: "some@email.com" - duration: "halfhour" - } - ) { - error - } - } - ]]) - - t.check({ - errors = { - { - extensions = { - field = "duration", - }, - message = Contains('invalid duration "halfhour"'), - path = { - "grantPostgresAccess", - }, - }, - }, - data = Null, - }) -end) - -Test.gql("Grant postgres access with out-of-bounds duration", function(t) - t.addHeader("x-user-email", user:email()) - t.query([[ - mutation GrantPostgresAccess { - grantPostgresAccess( - input: { - clusterName: "foobar" - environmentName: "dev" - teamSlug: "someteamname" - grantee: "some@email.com" - duration: "24h" - } - ) { - error - } - } - ]]) - - t.check({ - errors = { - { - extensions = { - field = "duration", - }, - message = Contains('Duration "24h" is out-of-bounds'), - path = { - "grantPostgresAccess", - }, - }, - }, - data = Null, - }) -end) - -Test.gql("Grant postgres access to non-existing cluster", function(t) - t.addHeader("x-user-email", user:email()) - t.query([[ - mutation GrantPostgresAccess { - grantPostgresAccess( - input: { - clusterName: "baz" - environmentName: "dev" - teamSlug: "someteamname" - grantee: "some@email.com" - duration: "4h" - } - ) { - error - } - } - ]]) - - t.check({ - errors = { - { - extensions = { - field = "clusterName", - }, - message = Contains("Could not find postgres cluster"), - path = { - "grantPostgresAccess", - }, - }, - }, - data = Null, - }) -end) - -Test.gql("Grant postgres access with authorization", function(t) - t.addHeader("x-user-email", user:email()) - t.query([[ - mutation GrantPostgresAccess { - grantPostgresAccess( - input: { - clusterName: "foobar" - environmentName: "dev" - teamSlug: "someteamname" - grantee: "some@email.com" - duration: "30m" - } - ) { - error - } - } - ]]) - - t.check({ - data = { - grantPostgresAccess = { - error = "", - }, - }, - }) -end) - -Test.k8s("Validate Role resource", function(t) - local resourceName = "pg-grant-93a898ea" - local pgNamespace = string.format("pg-%s", mainTeam:slug()) - - t.check("rbac.authorization.k8s.io/v1", "roles", "dev", pgNamespace, resourceName, { - apiVersion = "rbac.authorization.k8s.io/v1", - kind = "Role", - metadata = { - name = resourceName, - namespace = pgNamespace, - annotations = { - ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), - }, - labels = { - ["app.kubernetes.io/managed-by"] = "console", - ["euthanaisa.nais.io/kill-after"] = NotNull(), - ["nais.io/managed-by"] = "console", - ["postgres.data.nais.io/name"] = "foobar", - }, - }, - rules = { - { - apiGroups = { - "", - }, - resourceNames = { - "foobar-0", - "foobar-1", - "foobar-2", - }, - resources = { - "pods", - }, - verbs = { - "get", - "list", - "watch", - }, - }, - { - apiGroups = { - "", - }, - resourceNames = { - "foobar-0", - "foobar-1", - "foobar-2", - }, - resources = { - "pods/portforward", - }, - verbs = { - "get", - "list", - "watch", - "create", - }, - }, - }, - }) -end) - -Test.k8s("Validate RoleBinding resource", function(t) - local resourceName = "pg-grant-93a898ea" - local pgNamespace = string.format("pg-%s", mainTeam:slug()) - - t.check("rbac.authorization.k8s.io/v1", "rolebindings", "dev", pgNamespace, resourceName, { - apiVersion = "rbac.authorization.k8s.io/v1", - kind = "RoleBinding", - metadata = { - name = resourceName, - namespace = pgNamespace, - annotations = { - ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), - }, - labels = { - ["app.kubernetes.io/managed-by"] = "console", - ["euthanaisa.nais.io/kill-after"] = NotNull(), - ["nais.io/managed-by"] = "console", - ["postgres.data.nais.io/name"] = "foobar", - }, - }, - roleRef = { - apiGroup = "rbac.authorization.k8s.io", - kind = "Role", - name = resourceName, - }, - subjects = { - { - kind = "User", - name = "some@email.com", - }, - }, - }) -end) - -Test.gql("Check acitivity log entry", function(t) - t.addHeader("x-user-email", user:email()) - t.query([[ - { - team(slug:"someteamname") { - activityLog { - nodes { - message - ... on PostgresGrantAccessActivityLogEntry { - data { - grantee - until - } - } - } - } - } - } - ]]) - - t.check({ - data = { - team = { - activityLog = { - nodes = { - { - message = Contains("Granted access to some@email.com"), - data = { - grantee = "some@email.com", - ["until"] = NotNull(), - }, - }, - }, - }, - }, - }, - }) -end) diff --git a/integration_tests/k8s_resources/grant_zalando_postgres_access/dev/someteamname/postgres_foobar.yaml b/integration_tests/k8s_resources/grant_zalando_postgres_access/dev/someteamname/postgres_foobar.yaml deleted file mode 100644 index 6796d66e0..000000000 --- a/integration_tests/k8s_resources/grant_zalando_postgres_access/dev/someteamname/postgres_foobar.yaml +++ /dev/null @@ -1,15 +0,0 @@ ---- -apiVersion: data.nais.io/v1 -kind: Postgres -metadata: - name: foobar - namespace: someteamname -spec: - cluster: - majorVersion: "17" - resources: - cpu: 100m - diskSize: 2Gi - memory: 2G - database: - collation: nb_NO diff --git a/internal/graph/gengql/postgres.generated.go b/internal/graph/gengql/postgres.generated.go index c55efd78d..880e62c65 100644 --- a/internal/graph/gengql/postgres.generated.go +++ b/internal/graph/gengql/postgres.generated.go @@ -226,29 +226,6 @@ func (ec *executionContext) fieldContext_DeletePostgresBranchPayload_postgresBra return graphql.NewScalarFieldContext("DeletePostgresBranchPayload", field, false, false, errors.New("field of type Boolean does not have child fields")) } -func (ec *executionContext) _GrantPostgresAccessPayload_error(ctx context.Context, field graphql.CollectedField, obj *postgres.GrantPostgresAccessPayload) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_GrantPostgresAccessPayload_error(ctx, field) - }, - func(ctx context.Context) (any, error) { - return obj.Error, nil - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { - return ec.marshalOString2ᚖstring(ctx, selections, v) - }, - true, - false, - ) -} -func (ec *executionContext) fieldContext_GrantPostgresAccessPayload_error(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - return graphql.NewScalarFieldContext("GrantPostgresAccessPayload", field, false, false, errors.New("field of type String does not have child fields")) -} - func (ec *executionContext) _Postgres_id(ctx context.Context, field graphql.CollectedField, obj *postgres.Postgres) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -2826,64 +2803,6 @@ func (ec *executionContext) unmarshalInputDeletePostgresBranchInput(ctx context. return it, nil } -func (ec *executionContext) unmarshalInputGrantPostgresAccessInput(ctx context.Context, obj any) (postgres.GrantPostgresAccessInput, error) { - var it postgres.GrantPostgresAccessInput - if obj == nil { - return it, nil - } - - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } - - fieldsInOrder := [...]string{"clusterName", "teamSlug", "environmentName", "grantee", "duration"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "clusterName": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("clusterName")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.ClusterName = data - case "teamSlug": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("teamSlug")) - data, err := ec.unmarshalNSlug2githubᚗcomᚋnaisᚋapiᚋinternalᚋslugᚐSlug(ctx, v) - if err != nil { - return it, err - } - it.TeamSlug = data - case "environmentName": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("environmentName")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.EnvironmentName = data - case "grantee": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("grantee")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.Grantee = data - case "duration": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("duration")) - data, err := ec.unmarshalNString2string(ctx, v) - if err != nil { - return it, err - } - it.Duration = data - } - } - return it, nil -} - func (ec *executionContext) unmarshalInputPostgresBranchFilter(ctx context.Context, obj any) (postgres.PostgresBranchFilter, error) { var it postgres.PostgresBranchFilter if obj == nil { @@ -3060,42 +2979,6 @@ func (ec *executionContext) _DeletePostgresBranchPayload(ctx context.Context, se return out } -var grantPostgresAccessPayloadImplementors = []string{"GrantPostgresAccessPayload"} - -func (ec *executionContext) _GrantPostgresAccessPayload(ctx context.Context, sel ast.SelectionSet, obj *postgres.GrantPostgresAccessPayload) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, grantPostgresAccessPayloadImplementors) - - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("GrantPostgresAccessPayload") - case "error": - out.Values[i] = ec._GrantPostgresAccessPayload_error(ctx, field, obj) - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch(ctx) - if out.Invalids > 0 { - return graphql.Null - } - - atomic.AddInt32(&ec.Deferred, int32(min(len(deferred), math.MaxInt32))) - - for label, dfs := range deferred { - ec.ProcessDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } - - return out -} - var postgresImplementors = []string{"Postgres", "Node"} func (ec *executionContext) _Postgres(ctx context.Context, sel ast.SelectionSet, obj *postgres.Postgres) graphql.Marshaler { @@ -4566,25 +4449,6 @@ func (ec *executionContext) marshalNDeletePostgresBranchPayload2ᚖgithubᚗcom return ec._DeletePostgresBranchPayload(ctx, sel, v) } -func (ec *executionContext) unmarshalNGrantPostgresAccessInput2githubᚗcomᚋnaisᚋapiᚋinternalᚋpersistenceᚋpostgresᚐGrantPostgresAccessInput(ctx context.Context, v any) (postgres.GrantPostgresAccessInput, error) { - res, err := ec.unmarshalInputGrantPostgresAccessInput(ctx, v) - return res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalNGrantPostgresAccessPayload2githubᚗcomᚋnaisᚋapiᚋinternalᚋpersistenceᚋpostgresᚐGrantPostgresAccessPayload(ctx context.Context, sel ast.SelectionSet, v postgres.GrantPostgresAccessPayload) graphql.Marshaler { - return ec._GrantPostgresAccessPayload(ctx, sel, &v) -} - -func (ec *executionContext) marshalNGrantPostgresAccessPayload2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋpersistenceᚋpostgresᚐGrantPostgresAccessPayload(ctx context.Context, sel ast.SelectionSet, v *postgres.GrantPostgresAccessPayload) graphql.Marshaler { - if v == nil { - if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { - graphql.AddErrorf(ctx, "the requested element is null which the schema does not allow") - } - return graphql.Null - } - return ec._GrantPostgresAccessPayload(ctx, sel, v) -} - func (ec *executionContext) marshalNPostgres2githubᚗcomᚋnaisᚋapiᚋinternalᚋpersistenceᚋpostgresᚐPostgres(ctx context.Context, sel ast.SelectionSet, v postgres.Postgres) graphql.Marshaler { return ec._Postgres(ctx, sel, &v) } diff --git a/internal/graph/gengql/root_.generated.go b/internal/graph/gengql/root_.generated.go index 30d83560f..950ae0e89 100644 --- a/internal/graph/gengql/root_.generated.go +++ b/internal/graph/gengql/root_.generated.go @@ -1063,10 +1063,6 @@ type ComplexityRoot struct { WorkflowSha func(childComplexity int) int } - GrantPostgresAccessPayload struct { - Error func(childComplexity int) int - } - IDPortenAuthIntegration struct { Name func(childComplexity int) int } @@ -1649,7 +1645,6 @@ type ComplexityRoot struct { DeleteValkey func(childComplexity int, input valkey.DeleteValkeyInput) int DisableReconciler func(childComplexity int, input reconciler.DisableReconcilerInput) int EnableReconciler func(childComplexity int, input reconciler.EnableReconcilerInput) int - GrantPostgresAccess func(childComplexity int, input postgres.GrantPostgresAccessInput) int RemoveConfigValue func(childComplexity int, input config.RemoveConfigValueInput) int RemoveRepositoryFromTeam func(childComplexity int, input repository.RemoveRepositoryFromTeamInput) int RemoveSecretValue func(childComplexity int, input secret.RemoveSecretValueInput) int @@ -7597,13 +7592,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.GitHubActorClaims.WorkflowSha(childComplexity), true - case "GrantPostgresAccessPayload.error": - if e.ComplexityRoot.GrantPostgresAccessPayload.Error == nil { - break - } - - return e.ComplexityRoot.GrantPostgresAccessPayload.Error(childComplexity), true - case "IDPortenAuthIntegration.name": if e.ComplexityRoot.IDPortenAuthIntegration.Name == nil { break @@ -10361,18 +10349,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.Mutation.EnableReconciler(childComplexity, args["input"].(reconciler.EnableReconcilerInput)), true - case "Mutation.grantPostgresAccess": - if e.ComplexityRoot.Mutation.GrantPostgresAccess == nil { - break - } - - args, err := ec.field_Mutation_grantPostgresAccess_args(ctx, rawArgs) - if err != nil { - return 0, false - } - - return e.ComplexityRoot.Mutation.GrantPostgresAccess(childComplexity, args["input"].(postgres.GrantPostgresAccessInput)), true - case "Mutation.removeConfigValue": if e.ComplexityRoot.Mutation.RemoveConfigValue == nil { break @@ -20929,7 +20905,6 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputEnableReconcilerInput, ec.unmarshalInputEnvironmentOrder, ec.unmarshalInputEnvironmentWorkloadOrder, - ec.unmarshalInputGrantPostgresAccessInput, ec.unmarshalInputImageVulnerabilityFilter, ec.unmarshalInputImageVulnerabilityOrder, ec.unmarshalInputIssueFilter, @@ -27780,8 +27755,6 @@ extend enum ActivityLogActivityType { } extend type Mutation { - "Grant temporary access to a Postgres cluster." - grantPostgresAccess(input: GrantPostgresAccessInput!): GrantPostgresAccessPayload! """ EXPERIMENTAL: DO NOT USE Create time-limited personal access to a NAIS Postgres branch through the brokered PostgresAccess and relay flow. @@ -27792,19 +27765,6 @@ extend type Mutation { deletePostgresBranch(input: DeletePostgresBranchInput!): DeletePostgresBranchPayload! } -type GrantPostgresAccessPayload { - error: String -} - -input GrantPostgresAccessInput { - clusterName: String! - teamSlug: Slug! - environmentName: String! - grantee: String! - "Duration of the access grant (maximum 4 hours)." - duration: String! -} - "Result of creating a personal Postgres access." type CreatePostgresAccessPayload { "Name of the newly created PostgresAccess resource." @@ -36634,14 +36594,6 @@ func (ec *executionContext) childFields_GitHubActorClaims(ctx context.Context, f return nil, fmt.Errorf("no field named %q was found under type GitHubActorClaims", field.Name) } -func (ec *executionContext) childFields_GrantPostgresAccessPayload(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "error": - return ec.fieldContext_GrantPostgresAccessPayload_error(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type GrantPostgresAccessPayload", field.Name) -} - func (ec *executionContext) childFields_ImageVulnerability(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { case "id": diff --git a/internal/graph/gengql/schema.generated.go b/internal/graph/gengql/schema.generated.go index be77e6e4e..6c8b9e196 100644 --- a/internal/graph/gengql/schema.generated.go +++ b/internal/graph/gengql/schema.generated.go @@ -84,7 +84,6 @@ type MutationResolver interface { UpdateOpenSearch(ctx context.Context, input opensearch.UpdateOpenSearchInput) (*opensearch.UpdateOpenSearchPayload, error) DeleteOpenSearch(ctx context.Context, input opensearch.DeleteOpenSearchInput) (*opensearch.DeleteOpenSearchPayload, error) CreateOpenSearchCredentials(ctx context.Context, input opensearch.CreateOpenSearchCredentialsInput) (*opensearch.CreateOpenSearchCredentialsPayload, error) - GrantPostgresAccess(ctx context.Context, input postgres.GrantPostgresAccessInput) (*postgres.GrantPostgresAccessPayload, error) CreatePostgresAccess(ctx context.Context, input postgres.CreatePostgresAccessInput) (*postgres.CreatePostgresAccessPayload, error) DeletePostgresBranch(ctx context.Context, input postgres.DeletePostgresBranchInput) (*postgres.DeletePostgresBranchPayload, error) EnableReconciler(ctx context.Context, input reconciler.EnableReconcilerInput) (*reconciler.Reconciler, error) @@ -687,20 +686,6 @@ func (ec *executionContext) field_Mutation_enableReconciler_args(ctx context.Con return args, nil } -func (ec *executionContext) field_Mutation_grantPostgresAccess_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", - func(ctx context.Context, v any) (postgres.GrantPostgresAccessInput, error) { - return ec.unmarshalNGrantPostgresAccessInput2githubᚗcomᚋnaisᚋapiᚋinternalᚋpersistenceᚋpostgresᚐGrantPostgresAccessInput(ctx, v) - }) - if err != nil { - return nil, err - } - args["input"] = arg0 - return args, nil -} - func (ec *executionContext) field_Mutation_removeConfigValue_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -2651,50 +2636,6 @@ func (ec *executionContext) fieldContext_Mutation_createOpenSearchCredentials(ct return fc, nil } -func (ec *executionContext) _Mutation_grantPostgresAccess(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.fieldContext_Mutation_grantPostgresAccess(ctx, field) - }, - func(ctx context.Context) (any, error) { - fc := graphql.GetFieldContext(ctx) - return ec.Resolvers.Mutation().GrantPostgresAccess(ctx, fc.Args["input"].(postgres.GrantPostgresAccessInput)) - }, - nil, - func(ctx context.Context, selections ast.SelectionSet, v *postgres.GrantPostgresAccessPayload) graphql.Marshaler { - return ec.marshalNGrantPostgresAccessPayload2ᚖgithubᚗcomᚋnaisᚋapiᚋinternalᚋpersistenceᚋpostgresᚐGrantPostgresAccessPayload(ctx, selections, v) - }, - true, - true, - ) -} -func (ec *executionContext) fieldContext_Mutation_grantPostgresAccess(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Mutation", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return ec.childFields_GrantPostgresAccessPayload(ctx, field) - }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_grantPostgresAccess_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil -} - func (ec *executionContext) _Mutation_createPostgresAccess(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -7321,13 +7262,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } - case "grantPostgresAccess": - out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_grantPostgresAccess(ctx, field) - }) - if out.Values[i] == graphql.Null { - out.Invalids++ - } case "createPostgresAccess": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createPostgresAccess(ctx, field) diff --git a/internal/graph/postgres.resolvers.go b/internal/graph/postgres.resolvers.go index cedffc90f..381052242 100644 --- a/internal/graph/postgres.resolvers.go +++ b/internal/graph/postgres.resolvers.go @@ -35,16 +35,6 @@ func (r *jobResolver) PostgresBranches(ctx context.Context, obj *job.Job, orderB return pagination.NewFacetableConnection(pagination.NewConnectionWithoutPagination(instances), instances, (*postgres.PostgresBranchFilter)(nil)), nil } -func (r *mutationResolver) GrantPostgresAccess(ctx context.Context, input postgres.GrantPostgresAccessInput) (*postgres.GrantPostgresAccessPayload, error) { - if err := authz.CanGrantPostgresAccess(ctx, input.TeamSlug); err != nil { - return nil, err - } - if err := postgres.GrantZalandoPostgresAccess(ctx, input); err != nil { - return nil, err - } - return &postgres.GrantPostgresAccessPayload{Error: new(string)}, nil -} - func (r *mutationResolver) CreatePostgresAccess(ctx context.Context, input postgres.CreatePostgresAccessInput) (*postgres.CreatePostgresAccessPayload, error) { if err := authz.CanGrantPostgresAccess(ctx, input.TeamSlug); err != nil { return nil, err diff --git a/internal/graph/schema/postgres.graphqls b/internal/graph/schema/postgres.graphqls index c99cc8e59..9d636eb16 100644 --- a/internal/graph/schema/postgres.graphqls +++ b/internal/graph/schema/postgres.graphqls @@ -361,8 +361,6 @@ extend enum ActivityLogActivityType { } extend type Mutation { - "Grant temporary access to a Postgres cluster." - grantPostgresAccess(input: GrantPostgresAccessInput!): GrantPostgresAccessPayload! """ EXPERIMENTAL: DO NOT USE Create time-limited personal access to a NAIS Postgres branch through the brokered PostgresAccess and relay flow. @@ -373,19 +371,6 @@ extend type Mutation { deletePostgresBranch(input: DeletePostgresBranchInput!): DeletePostgresBranchPayload! } -type GrantPostgresAccessPayload { - error: String -} - -input GrantPostgresAccessInput { - clusterName: String! - teamSlug: Slug! - environmentName: String! - grantee: String! - "Duration of the access grant (maximum 4 hours)." - duration: String! -} - "Result of creating a personal Postgres access." type CreatePostgresAccessPayload { "Name of the newly created PostgresAccess resource." diff --git a/internal/kubernetes/fake/fake.go b/internal/kubernetes/fake/fake.go index e71300572..c1eae2182 100644 --- a/internal/kubernetes/fake/fake.go +++ b/internal/kubernetes/fake/fake.go @@ -218,7 +218,6 @@ func NewDynamicClient(scheme *runtime.Scheme) *dynfake.FakeDynamicClient { unleash_nais_io_v1.GroupVersion.WithResource("unleashes"): "UnleashList", unleash_nais_io_v1.GroupVersion.WithResource("remoteunleashes"): "RemoteUnleashList", mapperatorv1.GroupVersion.WithResource("postgres"): "PostgresList", - {Group: "data.nais.io", Version: "v1", Resource: "postgres"}: "PostgresList", mapperatorv1.GroupVersion.WithResource("postgresbranches"): "PostgresBranchList", nais_io_v1alpha1.GroupVersion.WithResource("tunnels"): "TunnelList", mapperatorv1.GroupVersion.WithResource("valkeys"): "ValkeyList", diff --git a/internal/kubernetes/scheme.go b/internal/kubernetes/scheme.go index f1d2662df..c6c6ce159 100644 --- a/internal/kubernetes/scheme.go +++ b/internal/kubernetes/scheme.go @@ -57,7 +57,6 @@ func NewScheme() (*runtime.Scheme, error) { }{ {"nais.io", "v1alpha1", "RelayAccess"}, {"postgresql.cnpg.io", "v1", "Cluster"}, - {"data.nais.io", "v1", "Postgres"}, // Legacy port-forward grant validation. } { version := schema.GroupVersion{Group: gv.group, Version: gv.version} scheme.AddKnownTypeWithName(version.WithKind(gv.kind), &unstructured.Unstructured{}) diff --git a/internal/persistence/postgres/grant.go b/internal/persistence/postgres/grant.go deleted file mode 100644 index b66157d3e..000000000 --- a/internal/persistence/postgres/grant.go +++ /dev/null @@ -1,138 +0,0 @@ -package postgres - -import ( - "context" - "fmt" - "hash/crc32" - "strconv" - "time" - - "github.com/nais/api/internal/activitylog" - "github.com/nais/api/internal/auth/authz" - "github.com/nais/api/internal/environmentmapper" - "github.com/nais/api/internal/kubernetes" - "github.com/nais/api/internal/kubernetes/watcher" - "github.com/nais/api/internal/slug" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/dynamic" -) - -// Keep the legacy Zalando port-forward grant independent of the PostgresBranch CRD. -// It uses API's existing system clients; only this mutation reads data.nais.io. -func legacyPostgresClient(ctx context.Context, environment string, gvr schema.GroupVersionResource) (dynamic.NamespaceableResourceInterface, error) { - client, ok := fromContext(ctx).clients[environmentmapper.ClusterName(environment)] - if !ok { - return nil, &watcher.ErrorUnknownEnvironment{Environment: environment} - } - return client.Resource(gvr), nil -} - -func getLegacyPostgres(ctx context.Context, input GrantPostgresAccessInput) error { - gvr := schema.GroupVersionResource{Group: "data.nais.io", Version: "v1", Resource: "postgres"} - client, err := legacyPostgresClient(ctx, input.EnvironmentName, gvr) - if err != nil { - return err - } - _, err = client.Namespace(input.TeamSlug.String()).Get(ctx, input.ClusterName, metav1.GetOptions{}) - return err -} - -func GrantZalandoPostgresAccess(ctx context.Context, input GrantPostgresAccessInput) error { - if err := input.Validate(ctx); err != nil { - return err - } - - namespace := fmt.Sprintf("pg-%s", input.TeamSlug.String()) - name := resourceNamer(input.TeamSlug, input.Grantee, input.ClusterName) - d, err := time.ParseDuration(input.Duration) - if err != nil { - return fmt.Errorf("parsing TTL: %w", err) - } - until := time.Now().Add(d) - labels := map[string]string{ - "euthanaisa.nais.io/kill-after": strconv.FormatInt(until.Unix(), 10), - "postgres.data.nais.io/name": input.ClusterName, - } - - if err := createGrantRole(ctx, input, name, namespace, labels); err != nil { - return err - } - if err := createGrantRoleBinding(ctx, input, name, namespace, labels); err != nil { - return err - } - - return activitylog.Create(ctx, activitylog.CreateInput{ - Action: activityLogEntryActionGrantAccess, - Actor: authz.ActorFromContext(ctx).User, - ResourceType: activityLogEntryResourceTypePostgres, - ResourceName: input.ClusterName, - EnvironmentName: new(input.EnvironmentName), - TeamSlug: new(input.TeamSlug), - Data: PostgresGrantAccessActivityLogEntryData{ - Grantee: input.Grantee, - Until: until, - }, - }) -} - -func createGrantRoleBinding(ctx context.Context, input GrantPostgresAccessInput, name, namespace string, labels map[string]string) error { - gvr := schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "rolebindings"} - client, err := legacyPostgresClient(ctx, input.EnvironmentName, gvr) - if err != nil { - return err - } - res := &unstructured.Unstructured{} - res.SetAPIVersion(gvr.GroupVersion().String()) - res.SetKind("RoleBinding") - res.SetName(name) - res.SetNamespace(namespace) - res.SetAnnotations(kubernetes.WithCommonAnnotations(nil, authz.ActorFromContext(ctx).User.Identity())) - res.SetLabels(labels) - kubernetes.SetManagedByConsoleLabel(res) - res.Object["roleRef"] = map[string]any{ - "apiGroup": "rbac.authorization.k8s.io", - "kind": "Role", - "name": name, - } - res.Object["subjects"] = []any{map[string]any{"kind": "User", "name": input.Grantee}} - return createOrUpdateGrantResource(ctx, res, client.Namespace(namespace)) -} - -func createGrantRole(ctx context.Context, input GrantPostgresAccessInput, name, namespace string, labels map[string]string) error { - gvr := schema.GroupVersionResource{Group: "rbac.authorization.k8s.io", Version: "v1", Resource: "roles"} - client, err := legacyPostgresClient(ctx, input.EnvironmentName, gvr) - if err != nil { - return err - } - res := &unstructured.Unstructured{} - res.SetAPIVersion(gvr.GroupVersion().String()) - res.SetKind("Role") - res.SetName(name) - res.SetNamespace(namespace) - res.SetAnnotations(kubernetes.WithCommonAnnotations(nil, authz.ActorFromContext(ctx).User.Identity())) - res.SetLabels(labels) - kubernetes.SetManagedByConsoleLabel(res) - pods := []any{fmt.Sprintf("%s-0", input.ClusterName), fmt.Sprintf("%s-1", input.ClusterName), fmt.Sprintf("%s-2", input.ClusterName)} - res.Object["rules"] = []any{ - map[string]any{"apiGroups": []any{""}, "resources": []any{"pods"}, "verbs": []any{"get", "list", "watch"}, "resourceNames": pods}, - map[string]any{"apiGroups": []any{""}, "resources": []any{"pods/portforward"}, "verbs": []any{"get", "list", "watch", "create"}, "resourceNames": pods}, - } - return createOrUpdateGrantResource(ctx, res, client.Namespace(namespace)) -} - -func createOrUpdateGrantResource(ctx context.Context, res *unstructured.Unstructured, client dynamic.ResourceInterface) error { - _, err := client.Create(ctx, res, metav1.CreateOptions{}) - if k8serrors.IsAlreadyExists(err) { - _, err = client.Update(ctx, res, metav1.UpdateOptions{}) - } - return err -} - -func resourceNamer(teamSlug slug.Slug, grantee, name string) string { - hasher := crc32.NewIEEE() - fmt.Fprintf(hasher, "%s-%s-%s", teamSlug.String(), grantee, name) - return fmt.Sprintf("pg-grant-%08x", hasher.Sum32()) -} diff --git a/internal/persistence/postgres/grant_test.go b/internal/persistence/postgres/grant_test.go deleted file mode 100644 index 7d12ccaa6..000000000 --- a/internal/persistence/postgres/grant_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package postgres - -import ( - "context" - "os" - "testing" - "time" - - "github.com/nais/api/internal/kubernetes" - "github.com/nais/api/internal/kubernetes/fake" - "github.com/nais/api/internal/kubernetes/watcher" - "github.com/nais/api/internal/slug" - "github.com/sirupsen/logrus/hooks/test" -) - -func TestLegacyPostgresGrantValidatesTheOldCluster(t *testing.T) { - scheme, err := kubernetes.NewScheme() - if err != nil { - t.Fatal(err) - } - clusters, err := kubernetes.CreateClusterConfigMap("nav", []string{"dev"}, nil) - if err != nil { - t.Fatal(err) - } - log, _ := test.NewNullLogger() - mgr, err := watcher.NewManager(scheme, clusters, log, watcher.WithClientCreator(fake.Clients(os.DirFS("../../../integration_tests/k8s_resources/grant_zalando_postgres_access")))) - if err != nil { - t.Fatal(err) - } - t.Cleanup(mgr.Stop) - ctx := context.Background() - wait, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - if !mgr.WaitForReady(wait) { - t.Fatal("Postgres watcher did not synchronize") - } - // The legacy grant must work even when the PostgresBranch CRD is absent. - ctx = NewLoaderContext(ctx, nil, "", "", "nav", mgr.GetDynamicClients()) - input := GrantPostgresAccessInput{ - ClusterName: "foobar", TeamSlug: slug.Slug("someteamname"), EnvironmentName: "dev", - Grantee: "someone@example.com", Duration: "30m", - } - if err := input.Validate(ctx); err != nil { - t.Fatalf("existing legacy cluster rejected: %v", err) - } - input.ClusterName = "missing" - if err := input.Validate(ctx); err == nil { - t.Fatal("missing legacy cluster accepted") - } -} diff --git a/internal/persistence/postgres/models.go b/internal/persistence/postgres/models.go index e2ad01ba7..76cc0a761 100644 --- a/internal/persistence/postgres/models.go +++ b/internal/persistence/postgres/models.go @@ -181,61 +181,6 @@ type DeletePostgresBranchPayload struct { PostgresBranchDeleted *bool `json:"postgresBranchDeleted,omitempty"` } -// GrantPostgresAccessInput retains the temporary port-forward grant for legacy Zalando Postgres clusters. -type GrantPostgresAccessInput struct { - ClusterName string `json:"clusterName"` - TeamSlug slug.Slug `json:"teamSlug"` - EnvironmentName string `json:"environmentName"` - Grantee string `json:"grantee"` - Duration string `json:"duration"` -} - -func (i *GrantPostgresAccessInput) Validate(ctx context.Context) error { - return i.ValidationErrors(ctx).NilIfEmpty() -} - -func (i *GrantPostgresAccessInput) ValidationErrors(ctx context.Context) *validate.ValidationErrors { - verr := validate.New() - i.ClusterName = strings.TrimSpace(i.ClusterName) - i.EnvironmentName = strings.TrimSpace(i.EnvironmentName) - - if i.ClusterName == "" { - verr.Add("clusterName", "ClusterName must not be empty.") - } - if i.EnvironmentName == "" { - verr.Add("environmentName", "Environment name must not be empty.") - } - if i.TeamSlug == "" { - verr.Add("teamSlug", "Team slug must not be empty.") - } - if i.Grantee == "" { - verr.Add("grantee", "Grantee must not be empty.") - } - - duration, err := time.ParseDuration(i.Duration) - if err != nil { - verr.Add("duration", "%s", err) - } else if duration > 4*time.Hour { - verr.Add("duration", "Duration %q is out-of-bounds. Must be less than 4 hours.", i.Duration) - } - - if i.ClusterName == "" || i.EnvironmentName == "" || i.TeamSlug == "" { - return verr - } - if err := getLegacyPostgres(ctx, *i); err != nil { - if k8serrors.IsNotFound(err) { - verr.Add("clusterName", "Could not find postgres cluster named %q", i.ClusterName) - } else { - verr.Add("clusterName", "%s", err) - } - } - return verr -} - -type GrantPostgresAccessPayload struct { - Error *string `json:"error,omitempty"` -} - // CreatePostgresAccessInput requests a new, time-limited personal database access. // The authenticated actor and final expiry are server-controlled. type CreatePostgresAccessInput struct {