{"record":{"id":"381763367cec6a72","repo":"vitessio/vitess","slug":"w-cannot-delete-keyspace-in-s","errorCode":null,"errorMessage":"%w: cannot delete keyspace in %s","messagePattern":"%w: cannot delete keyspace in (.+?)","errorType":"validation","errorClass":"ErrUnauthorized","httpStatus":null,"severity":"error","filePath":"go/vt/vtadmin/api.go","lineNumber":653,"sourceCode":"\t}\n\n\tc, err := api.getClusterForRequest(req.ClusterId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c.CreateShard(ctx, req.Options)\n}\n\n// DeleteKeyspace is part of the vtadminpb.VTAdminServer interface.\nfunc (api *API) DeleteKeyspace(ctx context.Context, req *vtadminpb.DeleteKeyspaceRequest) (*vtctldatapb.DeleteKeyspaceResponse, error) {\n\tspan, ctx := trace.NewSpan(ctx, \"API.DeleteKeyspace\")\n\tdefer span.Finish()\n\n\tspan.Annotate(\"cluster_id\", req.ClusterId)\n\n\tif !api.authz.IsAuthorized(ctx, req.ClusterId, rbac.KeyspaceResource, rbac.DeleteAction) {\n\t\treturn nil, fmt.Errorf(\"%w: cannot delete keyspace in %s\", errors.ErrUnauthorized, req.ClusterId)\n\t}\n\n\tc, err := api.getClusterForRequest(req.ClusterId)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c.DeleteKeyspace(ctx, req.Options)\n}\n\n// DeleteShards is part of the vtadminpb.VTAdminServer interface.\nfunc (api *API) DeleteShards(ctx context.Context, req *vtadminpb.DeleteShardsRequest) (*vtctldatapb.DeleteShardsResponse, error) {\n\tspan, ctx := trace.NewSpan(ctx, \"API.DeleteShards\")\n\tdefer span.Finish()\n\n\tspan.Annotate(\"cluster_id\", req.ClusterId)\n\n\tif !api.authz.IsAuthorized(ctx, req.ClusterId, rbac.ShardResource, rbac.DeleteAction) {","sourceCodeStart":635,"sourceCodeEnd":671,"githubUrl":"https://github.com/vitessio/vitess/blob/01a25a7d176f94613b8d59d799f438380a8760e4/go/vt/vtadmin/api.go#L635-L671","documentation":"VTAdmin's DeleteKeyspace RPC returns this when the caller's role lacks the 'delete' action on the Keyspace resource for the given cluster. The request is rejected before any vtctld interaction, wrapping errors.ErrUnauthorized. Deletion is intentionally a separate, higher-privilege action from creation.","triggerScenarios":"Calling DeleteKeyspace (DELETE /keyspace) where RBAC does not grant keyspace delete for req.ClusterId.","commonSituations":"Cleanup scripts running under accounts with create-but-not-delete permissions; production clusters scoped to stricter roles than dev; stale auth tokens representing old role assignments.","solutions":["Grant 'delete' on the keyspace resource to the operator's role in the RBAC config (or use a dedicated admin role for destructive ops)","Re-authenticate after role changes so the new claims are used","Verify the cluster ID matches the role scope"],"exampleFix":"// before\n  - resource: keyspace\n    actions: [get, create]\n// after\n  - resource: keyspace\n    actions: [get, create, delete]","handlingStrategy":"validation","validationCode":"const canDeleteKeyspace = permissions.some(rule => rule.resource === 'keyspace' && (rule.actions.includes('delete') || rule.actions.includes('*')) && (rule.clusters.includes(clusterId) || rule.clusters.includes('*')));\nif (!canDeleteKeyspace) throw new Error('RBAC denies keyspace delete in ' + clusterId);","typeGuard":"function isKeyspaceDeleteDenied(err: unknown): boolean {\n  return err instanceof Error && err.message.includes('cannot delete keyspace');\n}","tryCatchPattern":"try {\n  await deleteKeyspace(clusterId, ks, recursive);\n} catch (err) {\n  if (String(err).includes('cannot delete keyspace')) {\n    requestAdminApproval(clusterId, ks);\n  } else {\n    throw err;\n  }\n}","preventionTips":["Treat destructive actions as separate grants; never rely on create implying delete","Run deletion jobs with a dedicated admin service account","Confirm token freshness after role changes"],"tags":["rbac","authorization","vtadmin","keyspace","deletion"],"backgroundTag":"unauthorized-rbac-action","analyzedSha":"01a25a7d176f94613b8d59d799f438380a8760e4","analyzedAt":"2026-09-01T17:28:30.605Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}