diff --git a/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/.openspec.yaml b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/.openspec.yaml new file mode 100644 index 0000000000..e8d4ccfe90 --- /dev/null +++ b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-08 diff --git a/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/design.md b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/design.md new file mode 100644 index 0000000000..bfc1910d6a --- /dev/null +++ b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/design.md @@ -0,0 +1,68 @@ +# Design: Fine-Grained Backstage Permissions for Augment Lifecycle Governance + +## Context + +The augment plugin currently enforces 12+ authorization decisions via custom inline guards in route handlers — `checkIsAdmin`, `createdBy` comparisons, lifecycle stage checks, and self-approval prevention. These bypass Backstage's permission framework entirely. Only two coarse permissions exist (`augment.access` for read, `augment.admin` for admin operations), making fine-grained RBAC impossible. Additionally, all infrastructure operations (vector stores, documents, MCP connections, prompts, models) are gated by the single `augment.admin` permission, preventing deployers from granting targeted access to specific resource categories. + +Backstage provides a permission framework (`@backstage/plugin-permission-node`) supporting basic permissions, resource-based permissions with conditional rules, and policy evaluation via the permission backend. The `extensions-backend` plugin in this workspace already follows this pattern and serves as a reference implementation. + +These permissions are independent of AgenticProvider authorization. Kagenti's per-user RBAC (via RFC 8693 token exchange) and OpenAI's organization RBAC govern access to external AI services. The permissions defined here govern access within the augment plugin's own governance system — lifecycle state transitions, ownership-scoped operations, self-approval prevention, and visibility filtering. The two layers operate on different verbs, different resources, and different data stores. + +## Goals / Non-Goals + +**Goals:** + +- Replace all 12+ inline authorization decisions with Backstage permission framework calls +- Enable deployers to configure fine-grained RBAC policies for agent and tool lifecycle operations and targeted access to infrastructure resources (vector stores, documents, MCP connections, prompts, models) +- Provide an opt-in legacy fallback (`augment.permissions.legacyAdminFallback`) so existing deployments using `augment.access` + `augment.admin` can migrate incrementally +- Support conditional permission rules for ownership checks, self-approval prevention, and lifecycle stage gating +- Keep self-approval prevention as defense-in-depth (permission rule supplements hard-coded check) + +**Non-Goals:** + +- Modifying `augment.access` or `augment.admin` behavior or semantics (though `augment.admin` is no longer the default gate for lifecycle/infrastructure operations) +- Adding permissions for AgenticProvider-level operations (Kagenti API calls, OpenAI API calls) +- Frontend permission checks (backend-only enforcement) +- Custom permission policy plugins (works with standard Backstage RBAC) +- Removing the hard-coded self-approval prevention check (kept as defense-in-depth alongside the `IS_NOT_CREATOR` rule) + +## Decisions + +### Decision 1: Separate resource types for agents and tools + +Define `augment-agent` and `augment-tool` as distinct resource types rather than a single `augment-resource` type. Agents and tools have different route handlers, different lifecycle transitions, and deployers may want independent RBAC scoping. + +**Alternative considered:** Single `augment-resource` type with a discriminator field. Rejected because it conflates two domain objects that are independently routed and independently targetable by policy. + +### Decision 2: Opt-in legacy fallback to `augment.admin` + +When `augment.permissions.legacyAdminFallback` is enabled in the plugin config, `authorizeLifecycleAction` checks the fine-grained permission first. If DENY, it falls back to checking `augment.admin`. When the config flag is absent or false (the default), only fine-grained permissions are evaluated — no fallback occurs. + +**Why opt-in rather than default-on:** The augment plugin is in dev preview, which means no backward compatibility guarantees. Making the fallback default-on risks the "temporary becomes permanent" problem — once deployments rely on `augment.admin` as a catch-all, removing the fallback later becomes the very breaking change it was meant to prevent. Opt-in gives existing consumers a migration path (enable the flag, then incrementally adopt fine-grained policies, then disable the flag) without baking the fallback into every deployment's baseline. + +**Alternative considered:** Default-on fallback for all deployments. Rejected because it creates invisible load-bearing behavior that becomes difficult to remove — every release the fallback ships as default makes removal harder, not easier. Dev preview is the right time to establish the clean model. + +**Alternative considered:** OR-combining fine-grained and admin in a single policy evaluation. Rejected because Backstage's permission framework evaluates permissions individually — the fallback must be explicit in code. + +### Decision 3: Self-approval prevention as defense-in-depth + +The `IS_NOT_CREATOR` permission rule enables RBAC-level self-approval prevention, but the existing hard-coded check (`createdBy !== userRef`) is retained. The hard-coded check is the primary guard; the permission rule allows deployers to customize or relax the policy via RBAC if desired. + +**Alternative considered:** Removing the hard-coded check entirely and relying solely on the permission rule. Rejected because a misconfigured RBAC policy could silently disable self-approval prevention — defense-in-depth is safer for a governance control. + +### Decision 4: Visibility filtering via resource-based permission with 3-tier evaluation + +`augment.agent.list` is a resource-based permission with resource type `augment-agent`, supporting 3-tier evaluation: ALLOW shows all agents, DENY shows no agents, CONDITIONAL applies the returned conditions as filters against each agent in the list (e.g., `IS_OWNER` returns only the user's own agents, `HAS_LIFECYCLE_STAGE(published)` returns only published agents). This aligns with the orchestrator plugin's existing CONDITIONAL policy patterns and gives deployers full flexibility over visibility rules via RBAC configuration. + +**Alternative considered:** Basic (non-resource) permission with binary ALLOW/DENY where DENY hardcodes a "published + own" filter. Rejected because (a) DENY-means-filtered is non-standard permission semantics — deployers expect DENY to mean no access, (b) the hardcoded filter cannot be customized per-deployment, and (c) the performance cost of evaluating conditions per-agent is comparable to the hardcoded filter iteration we'd be doing anyway. + +### Decision 5: Permission rules modeled on extensions-backend pattern + +Permission rules (`IS_OWNER`, `IS_NOT_CREATOR`, `HAS_LIFECYCLE_STAGE`) follow the exact pattern from `extensions-backend/src/permissions/rules.ts` — same `createPermissionRule` API, same `createPermissionResourceRef` for resource loading, same conditional evaluation utilities. This keeps the codebase consistent and reduces review friction. + +## Risks / Trade-offs + +- **Policy migration complexity** → Deployers must configure fine-grained RBAC policies. Existing deployments can enable `augment.permissions.legacyAdminFallback` to preserve current `augment.admin` behavior during migration. +- **Conditional evaluation overhead** → Resource-based permissions require loading the resource to evaluate conditions. Mitigated by keeping list operations as basic permissions and only using resource-based permissions for mutation routes where the resource is already loaded. +- **Rule mismatch on upgrade** → If a deployer configures a fine-grained policy with a `HAS_LIFECYCLE_STAGE` condition referencing a stage name that changes, the rule silently denies. Mitigated by documenting stage names as part of the permission contract. +- **Defense-in-depth dual check** → The self-approval hard-coded check and `IS_NOT_CREATOR` rule are redundant by design. If one is relaxed without updating the other, behavior may be confusing. Mitigated by documenting this clearly. diff --git a/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/fine-grained-permissions-prelim-implementation-plan.md b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/fine-grained-permissions-prelim-implementation-plan.md new file mode 100644 index 0000000000..1ea8c43fe3 --- /dev/null +++ b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/fine-grained-permissions-prelim-implementation-plan.md @@ -0,0 +1,243 @@ +# Plan: Fine-Grained Backstage Permissions for Augment Lifecycle Governance + +_Created: May 31, 2026_ +_Status: Implementation starting_ +_Branch: current working branch in rhdh-plugins_ + +## Context + +The augment plugin has 12+ authorization decisions implemented as custom route-level guards (`checkIsAdmin`, `createdBy` comparisons, lifecycle stage checks, self-approval prevention) that bypass Backstage's permission framework. Today only 2 coarse permissions exist (`augment.access`, `augment.admin`), making it impossible for RBAC policies to distinguish who can chat vs. manage agents vs. approve lifecycle transitions. + +This change replaces those custom guards with proper Backstage fine-grained permissions, including resource-based permissions with conditional rules for ownership and lifecycle stage checks. Existing `augment.access` + `augment.admin` policies continue to work via a fallback mechanism. + +## Key Design Decisions + +### These permissions are independent of AgenticProvider authorization + +Avoiding re-implementation of authorization checks that already exist at the AgenticProvider level (Kagenti, OpenAI) is a general good practice which we will strive to follow, Here is our rationale for how this proposal achieves that: + +#### 1. OpenAI's RBAC Is Orthogonal + +OpenAI's RBAC system controls who on the **OpenAI organization's team** can manage API keys, models, and billing. It does not control end-user access to applications built on the API. The augment plugin uses a single API key — OpenAI's RBAC governs who can _create_ that key, not who can _use the app built with it_. + +#### 2. Kagenti's Per-User RBAC Operates on a Different Verb Set + +Kagenti supports per-user authorization via OAuth2 Token Exchange (RFC 8693). A separate change on the `kagenti-oauth2-token-exchange` branch (see `kagenti-token-exchange-implementation-plan.md`) enables the augment plugin to exchange each user's OIDC token for a Kagenti-scoped token, so Kagenti can enforce its own per-user access control. + +Even with that token exchange in place, the Backstage permissions defined here do not overlap with Kagenti's RBAC because **they govern entirely different verbs operating on different resources**: + +| Layer | Verbs / Actions | Resources | Data Store | +| ------------------------ | --------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------- | +| **Backstage governance** | promote, approve, demote, withdraw, request-unpublish, register, configure, delete (governance record) | `ChatAgentConfig`, `ChatToolConfig` — lifecycle state, ownership, approval status | Backstage `AdminConfigService` DB | +| **Kagenti API** | listAgents, getAgent, createAgent, deleteAgent, chatSend, chatStream, invokeTool, triggerBuildRun, migrateAgent | Agent definitions, tool specs, MCP connections, Shipwright builds | Kagenti runtime (Kubernetes CRDs) | +| **Kubernetes RBAC** | get, list, create, update, delete, watch | Deployments, StatefulSets, Secrets, ConfigMaps | Kubernetes API server | + +Where the same verb name appears across layers (e.g., "delete"), it operates on different targets: + +- Backstage `augment.agent.delete`: removes the `ChatAgentConfig` governance record from the admin DB +- Kagenti `deleteAgent`: removes the agent definition from the Kagenti runtime (Kubernetes namespace) +- Kubernetes RBAC `delete`: removes the underlying pod/deployment from the cluster + +Kagenti's 3 roles (`kagenti-viewer`, `kagenti-operator`, `kagenti-admin`) gate access to the **Kagenti runtime API** — listing agents in namespaces, creating agent specs, invoking tools, triggering builds. None of these roles have any concept of Backstage's governance lifecycle (draft/pending/published), ownership-scoped operations, or self-approval prevention. + +#### 3. The 12+ Authorization Decisions Are Backstage-Layer Concerns + +The fine-grained permissions defined in this plan control **what a logged-in Backstage user can do within the augment UI and governance system**: + +- Who can see which agents (visibility filtering) +- Who can submit agents for review (creator ownership) +- Who can approve agents (separation of duties — creator ≠ approver) +- Who can promote, demote, publish, unpublish, archive agents and tools +- Who can delete agents (draft-only for non-admins, ownership-scoped) + +**None of these decisions are enforced by Kagenti or OpenAI.** They are internal to Backstage — they exist before any request reaches an AgenticProvider. Even with full per-user Kagenti token exchange enabled, these decisions remain necessary because: + +- Kagenti doesn't know about Backstage's agent lifecycle governance (draft/pending/published state machine) +- Kagenti doesn't enforce the self-approval separation of duties policy +- Kagenti doesn't control which Backstage users can see which agents in the UI +- The `ResponsesApiProvider` (OpenAI) doesn't implement `setUserContext` at all — zero user identity flows to OpenAI + +**In summary:** Provider-level auth controls access _to external AI services_. Backstage RBAC controls access _within the plugin's own governance system_. They operate at different layers and are complementary, not redundant. + +### Implementation design + +- **Separate resource types**: `augment-agent` and `augment-tool` — different domain objects, different routes, independently targetable by RBAC +- **Admin routes get resource permissions too** — allows future per-agent scoping +- **Backward compat via fallback**: Fine-grained permission checked first; on DENY, falls back to `augment.admin`. Existing policies work unchanged. +- **Self-approval prevention stays as defense-in-depth** — the `IS_NOT_CREATOR` permission rule supplements the existing hard-coded check, doesn't replace it + +## Permission Definitions (augment-common) + +### Resource Types + +```typescript +export const RESOURCE_TYPE_AUGMENT_AGENT = 'augment-agent'; +export const RESOURCE_TYPE_AUGMENT_TOOL = 'augment-tool'; +``` + +### New Permissions + +| Permission | Action | Resource Type | Replaces | +| ------------------------- | ------ | ------------- | ----------------------------------------------- | +| `augment.agent.list` | read | (basic) | Visibility filtering in GET /agents | +| `augment.agent.register` | create | augment-agent | requireAdminAccess on PUT register | +| `augment.agent.promote` | update | augment-agent | Inline ownership + draft-only checks on promote | +| `augment.agent.approve` | update | augment-agent | Self-approval prevention on pending→published | +| `augment.agent.demote` | update | augment-agent | requireAdminAccess on demote | +| `augment.agent.publish` | update | augment-agent | requireAdminAccess on publish/bulk-publish | +| `augment.agent.unpublish` | update | augment-agent | Ownership-or-admin on request-unpublish | +| `augment.agent.withdraw` | update | augment-agent | Ownership-or-admin on withdraw | +| `augment.agent.delete` | delete | augment-agent | Draft-only + ownership checks on delete | +| `augment.agent.configure` | update | augment-agent | requireAdminAccess on config | +| `augment.tool.promote` | update | augment-tool | Inline ownership + draft-only on promote | +| `augment.tool.approve` | update | augment-tool | Admin check on non-draft-to-pending | +| `augment.tool.demote` | update | augment-tool | requireAdminAccess on demote | +| `augment.tool.publish` | update | augment-tool | requireAdminAccess on publish | +| `augment.tool.unpublish` | update | augment-tool | requireAdminAccess on unpublish | +| `augment.kagenti.admin` | update | (basic) | requireAdminAccess on kagenti infra routes | + +### Permission Rules (augment-backend) + +| Rule | Purpose | Params | Apply Logic | +| --------------------- | --------------------------- | ------------------------------------ | ------------------------------------------ | +| `IS_OWNER` | Creator ownership check | (none — user injected at check time) | `resource.createdBy === userRef` | +| `IS_NOT_CREATOR` | Self-approval prevention | (none) | `resource.createdBy !== userRef` | +| `HAS_LIFECYCLE_STAGE` | Restrict to specific stages | `stages: string[]` | `stages.includes(resource.lifecycleStage)` | + +## Files to Change + +### Phase 1: Permission Definitions (augment-common, 2 files) + +**`plugins/augment-common/src/permissions.ts`** — Add resource type constants, all 16 new permissions using `createPermission` with `resourceType`, convenience types, add to `augmentPermissions` array. Keep existing `augmentAccessPermission` and `augmentAdminPermission` unchanged. + +**`plugins/augment-common/src/index.ts`** — Export new constants, types, and permissions. + +### Phase 2: Permission Rules + Utils (augment-backend, 3 new files) + +**`plugins/augment-backend/src/permissions/rules.ts`** (NEW) — `createPermissionResourceRef` for agent and tool. Three `createPermissionRule` implementations: `IS_OWNER`, `IS_NOT_CREATOR`, `HAS_LIFECYCLE_STAGE`. Follow pattern from `extensions-backend/src/permissions/rules.ts`. + +**`plugins/augment-backend/src/permissions/permissionUtils.ts`** (NEW) — `matchesAgentConditions(resource, conditions)` and `matchesToolConditions(resource, conditions)` — evaluate conditional permission results against loaded resources. Follow pattern from `extensions-backend/src/utils/permissionUtils.ts`. + +**`plugins/augment-backend/src/permissions/index.ts`** (NEW) — Barrel export. + +### Phase 3: Authorization Abstraction (2 files) + +**`plugins/augment-backend/src/middleware/security.ts`** — Add two new functions to the security middleware: + +- `authorizeLifecycleAction(req, permission, resource?)` — handles the two-tier check: fine-grained permission → conditional evaluation → fallback to `augment.admin` +- `authorizeBasicWithFallback(req, permission)` — for basic (non-resource) permissions with `augment.admin` fallback + +**`plugins/augment-backend/src/routes/types.ts`** — Add `authorizeLifecycleAction` and `authorizeBasicWithFallback` to `RouteContext`. + +### Phase 4: Route Refactoring (3 files) + +**`plugins/augment-backend/src/routes/agentRoutes.ts`** — Replace all 11 inline authorization decisions with `authorizeLifecycleAction` calls. Key mappings: + +- GET /agents → `augment.agent.list` (basic, controls visibility filter) +- PUT register → `augment.agent.register` +- PUT promote (draft→pending) → `augment.agent.promote` with IS_OWNER condition +- PUT promote (pending→published) → `augment.agent.approve` with IS_NOT_CREATOR condition +- PUT demote/publish/unpublish/config → respective permissions with fallback +- PUT request-unpublish → `augment.agent.unpublish` with IS_OWNER condition +- PUT withdraw → `augment.agent.withdraw` with IS_OWNER condition +- DELETE → `augment.agent.delete` with IS_OWNER + HAS_LIFECYCLE_STAGE conditions + +**`plugins/augment-backend/src/routes/toolLifecycleRoutes.ts`** — Same pattern for 4 tool lifecycle decisions. + +**`plugins/augment-backend/src/routes/kagentiAgentRoutes.ts`** — Replace `requireAdminAccess` on infra routes with `augment.kagenti.admin` + fallback. + +### Phase 5: Plugin Wiring (2 files) + +**`plugins/augment-backend/src/plugin.ts`** — Register all new permissions via `permissionsRegistry.addPermissions`. Add `@backstage/plugin-permission-node` dependency. + +**`plugins/augment-backend/src/router.ts`** — Create `permissionIntegrationRouter` via `createPermissionIntegrationRouter` with both resource types and all rules. Mount it on the router. Thread `authorizeLifecycleAction` and `authorizeBasicWithFallback` into `RouteContext`. + +## Authorization Decision Mapping + +### Agent Routes (agentRoutes.ts) + +| # | Route | Current Auth | New Permission | Conditions | +| --- | ------------------------------- | --------------------------------- | ------------------------------------- | ------------------------------------------------------------ | +| 1 | GET /agents | checkIsAdmin → visibility filter | `augment.agent.list` (basic) | ALLOW=show all, DENY=filter to published+own | +| 2 | PUT register | requireAdminAccess middleware | `augment.agent.register` | Fallback to augment.admin | +| 3a | PUT promote (draft→pending) | inline: isAdmin + createdBy | `augment.agent.promote` | IS_OWNER, HAS_LIFECYCLE_STAGE(draft) | +| 3b | PUT promote (phantom draft) | inline: !isAdmin && !existing | Keep as-is (data integrity, not auth) | N/A | +| 3c | PUT promote (ownership) | inline: createdBy !== userRef | Covered by IS_OWNER on promote | — | +| 3d | PUT promote (pending→published) | inline: self-approval prevention | `augment.agent.approve` | IS_NOT_CREATOR (defense-in-depth: keep hard-coded check too) | +| 4 | PUT demote | requireAdminAccess middleware | `augment.agent.demote` | Fallback to augment.admin | +| 5 | PUT publish | requireAdminAccess middleware | `augment.agent.publish` | Fallback to augment.admin | +| 6 | PUT unpublish | requireAdminAccess middleware | `augment.agent.publish` | Fallback to augment.admin | +| 7 | PUT bulk-publish | requireAdminAccess middleware | `augment.agent.publish` per item | Fallback to augment.admin | +| 8 | PUT request-unpublish | inline: isRequestOwner OR isAdmin | `augment.agent.unpublish` | IS_OWNER, fallback to augment.admin | +| 9 | PUT withdraw | inline: isOwner OR isAdmin | `augment.agent.withdraw` | IS_OWNER, fallback to augment.admin | +| 10 | PUT config | requireAdminAccess middleware | `augment.agent.configure` | Fallback to augment.admin | +| 11a | DELETE (stage check) | inline: !admin && stage !== draft | `augment.agent.delete` | HAS_LIFECYCLE_STAGE(draft) for non-admin | +| 11b | DELETE (ownership) | inline: createdBy !== userRef | `augment.agent.delete` | IS_OWNER for non-admin | + +### Tool Routes (toolLifecycleRoutes.ts) + +| # | Route | Current Auth | New Permission | Conditions | +| --- | ------------------------------- | ----------------------------- | ------------------------ | ------------------------- | +| 12a | PUT promote (draft→pending) | inline: isAdmin + createdBy | `augment.tool.promote` | IS_OWNER | +| 12b | PUT promote (other transitions) | inline: !isAdmin check | `augment.tool.approve` | Fallback to augment.admin | +| 13 | PUT demote | requireAdminAccess middleware | `augment.tool.demote` | Fallback to augment.admin | +| 14 | PUT publish | requireAdminAccess middleware | `augment.tool.publish` | Fallback to augment.admin | +| 15 | PUT unpublish | requireAdminAccess middleware | `augment.tool.unpublish` | Fallback to augment.admin | + +### Kagenti Infra Routes (kagentiAgentRoutes.ts) + +| # | Route | Current Auth | New Permission | +| ----- | -------------------------------------------- | ------------------ | ------------------------------------------ | +| 16-22 | DELETE, migrate, build, parse-env, fetch-env | requireAdminAccess | `augment.kagenti.admin` (basic) + fallback | + +## Implementation Order + +1. Permission definitions (augment-common) — additive, no behavior change +2. Permission rules + utils (new files) — standalone, testable +3. Security middleware (authorizeLifecycleAction) — the backward-compat abstraction +4. Route refactoring — one file at a time: agentRoutes → toolLifecycleRoutes → kagentiAgentRoutes +5. Plugin wiring (plugin.ts, router.ts) — connects everything +6. Type check (`npx tsc --noEmit`) + +## Backward Compatibility + +**Existing RBAC policy (unchanged, continues to work):** + +```yaml +permission: + rbac: + policies: + - p, role:default/augment-user, augment.access, read, allow + - p, role:default/augment-admin, augment.admin, update, allow +``` + +**New fine-grained policy (opt-in):** + +```yaml +permission: + rbac: + policies: + - p, role:default/augment-user, augment.access, read, allow + - p, role:default/agent-developer, augment.agent.promote, update, allow + - p, role:default/agent-admin, augment.agent.approve, update, allow + - p, role:default/agent-admin, augment.agent.demote, update, allow + - p, role:default/agent-admin, augment.agent.publish, update, allow + - p, role:default/agent-admin, augment.agent.delete, delete, allow +``` + +The fallback mechanism: `authorizeLifecycleAction` checks the fine-grained permission first. If DENY (which is the default when no policy exists for it), it falls back to `augment.admin`. This means deployments that haven't configured fine-grained policies get the same behavior as today. + +## Verification + +1. `npx tsc --noEmit` — all interfaces compile +2. Existing tests pass unchanged (backward compat) +3. With no fine-grained RBAC policies: behavior identical to current +4. With fine-grained policies: each permission correctly gates its route + +## Related Documents + +- `governance-techdebt-deepdive-may26.md` — Documents all 12 authorization decisions and the code volume analysis +- `augment-techdebt.md` — Section 4 covers the permission model alignment gap +- `augment-techdebt-may26.md` — Notes zero improvement in permissions area, governance system as parallel auth +- `kagenti-token-exchange-implementation-plan.md` — Companion change for per-user Kagenti auth (RFC 8693) +- Reference implementation: `extensions-backend/src/permissions/rules.ts` for resource permission pattern diff --git a/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/proposal.md b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/proposal.md new file mode 100644 index 0000000000..04ed688756 --- /dev/null +++ b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/proposal.md @@ -0,0 +1,40 @@ +# Proposal: Fine-Grained Backstage Permissions for Augment Lifecycle Governance + +## Why + +The augment plugin has 12+ authorization decisions implemented as custom route-level guards (`checkIsAdmin`, `createdBy` comparisons, lifecycle stage checks, self-approval prevention) that bypass Backstage's permission framework. Only 2 coarse permissions exist today (`augment.access`, `augment.admin`), making it impossible for RBAC policies to distinguish who can chat vs. manage agents vs. approve lifecycle transitions. This prevents deployers from implementing separation of duties, ownership-scoped operations, or stage-gated workflows through standard Backstage RBAC configuration. + +## What Changes + +- 16 new fine-grained permissions across agent, tool, and Kagenti infrastructure domains — each mapping to a specific authorization decision currently handled by inline code +- 2 new resource types (`augment-agent`, `augment-tool`) enabling conditional permission rules scoped to individual resources +- 3 permission rules (`IS_OWNER`, `IS_NOT_CREATOR`, `HAS_LIFECYCLE_STAGE`) for ownership, self-approval prevention, and stage-gated operations +- New authorization abstraction (`authorizeLifecycleAction`) implementing a two-tier check: fine-grained permission first, fallback to `augment.admin` for backward compatibility +- Refactoring of all 12+ inline authorization decisions in route handlers to use the permission framework +- Permission integration router registered with both resource types and all rules + +## Capabilities + +### New Capabilities + +- `permission-definitions`: Fine-grained permission constants, resource types, and permission rule definitions for the augment plugin's agent and tool governance domains +- `authorization-middleware`: Two-tier authorization abstraction that checks fine-grained permissions first with conditional rule evaluation, falling back to `augment.admin` for backward compatibility +- `route-authorization`: Replacement of all inline route-level authorization guards with permission framework calls across agent, tool, and Kagenti infrastructure routes + +### Modified Capabilities + +## Impact + +- `plugins/augment-common/src/permissions.ts` — 16 new permissions, 2 resource types +- `plugins/augment-common/src/index.ts` — new exports +- `plugins/augment-backend/src/permissions/rules.ts` — **new file**, 3 permission rules +- `plugins/augment-backend/src/permissions/permissionUtils.ts` — **new file**, conditional evaluation utilities +- `plugins/augment-backend/src/permissions/index.ts` — **new file**, barrel export +- `plugins/augment-backend/src/middleware/security.ts` — `authorizeLifecycleAction`, `authorizeBasicWithFallback` +- `plugins/augment-backend/src/routes/types.ts` — new authorization functions on `RouteContext` +- `plugins/augment-backend/src/routes/agentRoutes.ts` — 11 inline auth decisions replaced +- `plugins/augment-backend/src/routes/toolLifecycleRoutes.ts` — 4 inline auth decisions replaced +- `plugins/augment-backend/src/routes/kagentiAgentRoutes.ts` — `requireAdminAccess` replaced on infra routes +- `plugins/augment-backend/src/plugin.ts` — permission registration +- `plugins/augment-backend/src/router.ts` — permission integration router mount +- These permissions are independent of AgenticProvider authorization (Kagenti RFC 8693, OpenAI RBAC) — they govern Backstage-layer governance, not provider-layer access diff --git a/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/specs/authorization-middleware/spec.md b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/specs/authorization-middleware/spec.md new file mode 100644 index 0000000000..2587200760 --- /dev/null +++ b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/specs/authorization-middleware/spec.md @@ -0,0 +1,95 @@ +# Spec: authorization-middleware + +Augment-specific authorization middleware that integrates Backstage's permission framework with the plugin's domain model (agents, tools, lifecycle stages, ownership). This is NOT a reimplementation of RBAC policy evaluation — the RBAC plugin handles that. This middleware provides: + +1. **Opt-in legacy fallback logic** — when `augment.permissions.legacyAdminFallback` is enabled, check fine-grained permission first, then fall back to `augment.admin` for backward compatibility during migration. When disabled (the default), only fine-grained permissions are evaluated. This is augment-specific business logic that the RBAC plugin cannot provide, since the fallback semantics (which coarse permission to check, when to check it) are domain decisions. +2. **Conditional evaluation against augment resource types** — `matchesAgentConditions` and `matchesToolConditions` evaluate Backstage CONDITIONAL results against augment's domain objects (agents/tools with `createdBy`, lifecycle stages). The RBAC plugin evaluates policies; these utilities evaluate the resulting conditions against augment-specific resource shapes. +3. **RouteContext integration** — exposing authorization functions on the route context so handlers can call them without importing permission utilities directly. + +### Relationship to existing permissions + +- **`augment.access`** remains unchanged — it is a plugin-level visibility gate (can the user access the augment plugin at all?) and is orthogonal to fine-grained lifecycle permissions. +- **`augment.admin`** continues to gate admin-only routes (evaluations, workflows, dev spaces) that are not yet covered by fine-grained permissions. For agent/tool lifecycle operations and infrastructure resources (vector stores, documents, MCP, prompts, models), it can serve as a backward-compatible fallback when `augment.permissions.legacyAdminFallback` is enabled in the plugin config. By default, only fine-grained permissions are evaluated — deployments must configure the appropriate fine-grained RBAC policies. + +## ADDED Requirements + +### Requirement: authorizeLifecycleAction function + +The system SHALL provide an `authorizeLifecycleAction(req, permission, resource?)` function that implements authorization with opt-in legacy fallback: + +1. Evaluate the fine-grained permission (with conditional rules if resource-based) +2. If ALLOW, grant access +3. If DENY and `augment.permissions.legacyAdminFallback` is enabled, fall back to checking `augment.admin` +4. If fallback is enabled and `augment.admin` is ALLOW, grant access +5. Otherwise, deny access + +#### Scenario: Fine-grained permission allows + +- **WHEN** `authorizeLifecycleAction` is called and the fine-grained permission evaluates to ALLOW +- **THEN** access SHALL be granted without checking `augment.admin` + +#### Scenario: Fine-grained denies, fallback enabled, admin allows + +- **WHEN** `authorizeLifecycleAction` is called, the fine-grained permission evaluates to DENY, `augment.permissions.legacyAdminFallback` is enabled, and `augment.admin` evaluates to ALLOW +- **THEN** access SHALL be granted via the fallback + +#### Scenario: Fine-grained denies, fallback disabled + +- **WHEN** `authorizeLifecycleAction` is called, the fine-grained permission evaluates to DENY, and `augment.permissions.legacyAdminFallback` is not enabled +- **THEN** access SHALL be denied without checking `augment.admin` + +#### Scenario: Both deny with fallback enabled + +- **WHEN** `authorizeLifecycleAction` is called, `augment.permissions.legacyAdminFallback` is enabled, and both the fine-grained permission and `augment.admin` evaluate to DENY +- **THEN** access SHALL be denied + +### Requirement: Conditional permission evaluation + +When `authorizeLifecycleAction` receives a CONDITIONAL result from `PermissionService.authorizeConditional`, it SHALL apply the returned conditions as a filter against the provided resource using `matchesAgentConditions` or `matchesToolConditions` based on the resource type. This follows the same pattern as the [catalog plugin's conditional authorization](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/service/AuthorizedEntitiesCatalog.ts#L207:L237) — conditions are filters, not boolean checks. + +#### Scenario: Conditional result where resource satisfies conditions + +- **WHEN** the permission framework returns CONDITIONAL with an `IS_OWNER` condition, and the resource's `createdBy` matches the requesting user +- **THEN** the resource SHALL satisfy the condition filter and the operation SHALL proceed + +#### Scenario: Conditional result where resource does not satisfy conditions + +- **WHEN** the permission framework returns CONDITIONAL with an `IS_OWNER` condition, and the resource's `createdBy` does not match the requesting user +- **THEN** the resource SHALL NOT satisfy the condition filter and, if `augment.permissions.legacyAdminFallback` is enabled, the system SHALL fall back to `augment.admin`; otherwise access SHALL be denied + +### Requirement: authorizeBasicWithFallback function + +The system SHALL provide an `authorizeBasicWithFallback(req, permission)` function for basic (non-resource) permissions that checks the given permission first. If DENY and `augment.permissions.legacyAdminFallback` is enabled, it falls back to `augment.admin`. + +#### Scenario: Basic permission allows + +- **WHEN** `authorizeBasicWithFallback` is called and the basic permission evaluates to ALLOW +- **THEN** access SHALL be granted + +#### Scenario: Basic permission denies, fallback enabled, admin allows + +- **WHEN** `authorizeBasicWithFallback` is called, the basic permission evaluates to DENY, `augment.permissions.legacyAdminFallback` is enabled, and `augment.admin` evaluates to ALLOW +- **THEN** access SHALL be granted via the fallback + +#### Scenario: Basic permission denies, fallback disabled + +- **WHEN** `authorizeBasicWithFallback` is called, the basic permission evaluates to DENY, and `augment.permissions.legacyAdminFallback` is not enabled +- **THEN** access SHALL be denied without checking `augment.admin` + +### Requirement: Authorization functions on RouteContext + +The `authorizeLifecycleAction` and `authorizeBasicWithFallback` functions SHALL be available on `RouteContext` so route handlers can call them without importing permission utilities directly. + +#### Scenario: Route handler uses RouteContext authorization + +- **WHEN** a route handler needs to authorize a lifecycle action +- **THEN** it SHALL call `ctx.authorizeLifecycleAction(req, permission, resource)` from the `RouteContext` + +### Requirement: Permission conditional evaluation utilities + +The system SHALL provide `matchesAgentConditions(resource, conditions)` and `matchesToolConditions(resource, conditions)` utilities that evaluate Backstage permission conditional results against loaded agent or tool resources. These SHALL follow the pattern from `extensions-backend/src/utils/permissionUtils.ts`. + +#### Scenario: Agent condition evaluation + +- **WHEN** `matchesAgentConditions` is called with an agent resource and a set of conditions +- **THEN** it SHALL evaluate all conditions against the agent's properties and return a boolean result diff --git a/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/specs/permission-definitions/spec.md b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/specs/permission-definitions/spec.md new file mode 100644 index 0000000000..df1bb0aaab --- /dev/null +++ b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/specs/permission-definitions/spec.md @@ -0,0 +1,171 @@ +# Spec: permission-definitions + +Fine-grained permission constants, resource types, and permission rule definitions for augment agent and tool governance. + +### Scope + +This spec defines two tiers of fine-grained permissions: + +1. **Resource-based permissions** for agents and tools — these have the most nuanced authorization requirements (ownership scoping, self-approval prevention, lifecycle stage gating, filtered visibility) and support conditional rules (`IS_OWNER`, `IS_NOT_CREATOR`, `HAS_LIFECYCLE_STAGE`). +2. **Basic permissions** for infrastructure resources (vector stores, documents, MCP connections, prompts, models) — these are currently admin-only operations without ownership or lifecycle logic. Defining permissions for them now enables deployers to grant targeted access (e.g., "this team can manage vector stores but not MCP connections") instead of the all-or-nothing `augment.admin`. These can be upgraded to resource-based permissions later if ownership tracking is added. + +## ADDED Requirements + +### Requirement: Agent resource type + +The system SHALL define a resource type constant `RESOURCE_TYPE_AUGMENT_AGENT` with value `augment-agent` in `augment-common`. + +#### Scenario: Agent resource type exported + +- **WHEN** a consumer imports from `augment-common` +- **THEN** the `RESOURCE_TYPE_AUGMENT_AGENT` constant SHALL be available with value `augment-agent` + +### Requirement: Tool resource type + +The system SHALL define a resource type constant `RESOURCE_TYPE_AUGMENT_TOOL` with value `augment-tool` in `augment-common`. + +#### Scenario: Tool resource type exported + +- **WHEN** a consumer imports from `augment-common` +- **THEN** the `RESOURCE_TYPE_AUGMENT_TOOL` constant SHALL be available with value `augment-tool` + +### Requirement: Agent permissions + +The system SHALL define the following agent permissions using `createPermission` with resource type `augment-agent`: + +| Permission ID | Action | +| ------------------------- | -------------------------------- | +| `augment.agent.list` | read | +| `augment.agent.register` | create (basic, no resource type) | +| `augment.agent.promote` | update | +| `augment.agent.approve` | update | +| `augment.agent.demote` | update | +| `augment.agent.publish` | update | +| `augment.agent.unpublish` | update | +| `augment.agent.withdraw` | update | +| `augment.agent.delete` | delete | +| `augment.agent.configure` | update | + +The `augment.agent.list` permission SHALL be a resource-based permission with resource type `augment-agent`, supporting 3-tier evaluation (ALLOW/DENY/CONDITIONAL). This enables deployers to configure visibility rules via RBAC policies — e.g., `IS_OWNER` to show only the user's agents, `HAS_LIFECYCLE_STAGE(published)` to show only published agents, or combinations thereof. + +The `augment.agent.register` permission SHALL be a basic permission (no resource type) because `create` permissions gate the ability to create a resource that does not yet exist — there is no resource to attach a resource type to at evaluation time, and `usePermission` in the frontend cannot resolve a resource ref for a not-yet-created entity. + +#### Scenario: All agent permissions defined + +- **WHEN** the `augment-common` package is loaded +- **THEN** all 10 agent permissions SHALL be defined with correct action types and resource types + +#### Scenario: Agent list supports 3-tier evaluation + +- **WHEN** `augment.agent.list` is evaluated +- **THEN** ALLOW SHALL return all agents, DENY SHALL return no agents, and CONDITIONAL SHALL apply the returned conditions as filters against each agent in the list + +### Requirement: Tool permissions + +The system SHALL define the following tool permissions using `createPermission` with resource type `augment-tool`: + +| Permission ID | Action | +| ------------------------ | ------ | +| `augment.tool.promote` | update | +| `augment.tool.approve` | update | +| `augment.tool.demote` | update | +| `augment.tool.publish` | update | +| `augment.tool.unpublish` | update | + +#### Scenario: All tool permissions defined + +- **WHEN** the `augment-common` package is loaded +- **THEN** all 5 tool permissions SHALL be defined with action `update` and resource type `augment-tool` + +### Requirement: Kagenti infrastructure permission + +The system SHALL define `augment.kagenti.admin` as a basic permission with action `update` for gating Kagenti infrastructure routes. + +#### Scenario: Kagenti admin permission defined + +- **WHEN** the `augment-common` package is loaded +- **THEN** `augment.kagenti.admin` SHALL be a basic permission with action `update` + +### Requirement: Infrastructure resource permissions + +The system SHALL define the following basic permissions (no resource type) for infrastructure operations that are currently gated by `augment.admin`: + +| Permission ID | Action | Gates | +| ---------------------------- | ------ | ----------------------------------------------------------- | +| `augment.vectorstore.manage` | update | Vector store CRUD (create, connect, disconnect, delete) | +| `augment.document.manage` | update | Document upload and deletion within vector stores | +| `augment.mcp.manage` | update | MCP connection testing, tool creation, deletion, and builds | +| `augment.prompt.manage` | update | System prompt generation and configuration | +| `augment.model.manage` | update | Model listing, testing, and selection configuration | + +These are basic permissions without conditional rules. Each gates a category of admin operations, enabling deployers to grant targeted access without granting full `augment.admin`. + +#### Scenario: All infrastructure permissions defined + +- **WHEN** the `augment-common` package is loaded +- **THEN** all 5 infrastructure permissions SHALL be defined as basic permissions with action `update` + +#### Scenario: Infrastructure permissions independent of augment.admin + +- **WHEN** a user has `augment.vectorstore.manage` but not `augment.admin` +- **THEN** the user SHALL be able to manage vector stores but not other admin operations + +### Requirement: Existing permissions preserved + +The existing `augmentAccessPermission` (`augment.access`) and `augmentAdminPermission` (`augment.admin`) SHALL remain unchanged. All 21 new permissions SHALL be added to the `augmentPermissions` array alongside the existing ones. `augment.admin` continues to gate routes not yet covered by fine-grained permissions (evaluations, workflows, dev spaces) and serves as an opt-in fallback for fine-grained operations when `augment.permissions.legacyAdminFallback` is enabled. + +#### Scenario: Backward-compatible exports + +- **WHEN** a consumer imports `augmentAccessPermission` or `augmentAdminPermission` from `augment-common` +- **THEN** the permissions SHALL have identical definitions and behavior as before + +### Requirement: IS_OWNER permission rule + +The system SHALL define an `IS_OWNER` permission rule that evaluates to ALLOW when `resource.createdBy` matches the requesting user's entity ref. The rule SHALL take no configuration parameters — the user ref is injected at evaluation time. + +#### Scenario: Owner match + +- **WHEN** `IS_OWNER` is evaluated for a resource where `createdBy` equals the requesting user's entity ref +- **THEN** the rule SHALL evaluate to ALLOW + +#### Scenario: Non-owner + +- **WHEN** `IS_OWNER` is evaluated for a resource where `createdBy` does not equal the requesting user's entity ref +- **THEN** the rule SHALL evaluate to DENY + +### Requirement: IS_NOT_CREATOR permission rule + +The system SHALL define an `IS_NOT_CREATOR` permission rule that evaluates to ALLOW when `resource.createdBy` does NOT match the requesting user's entity ref. This enables self-approval prevention via RBAC policy. + +#### Scenario: Different user can approve + +- **WHEN** `IS_NOT_CREATOR` is evaluated for a resource where `createdBy` does not equal the requesting user's entity ref +- **THEN** the rule SHALL evaluate to ALLOW + +#### Scenario: Creator cannot self-approve + +- **WHEN** `IS_NOT_CREATOR` is evaluated for a resource where `createdBy` equals the requesting user's entity ref +- **THEN** the rule SHALL evaluate to DENY + +### Requirement: HAS_LIFECYCLE_STAGE permission rule + +The system SHALL define a `HAS_LIFECYCLE_STAGE` permission rule that evaluates to ALLOW when the resource's lifecycle stage is included in the configured `stages` array parameter. + +#### Scenario: Stage matches + +- **WHEN** `HAS_LIFECYCLE_STAGE` is evaluated with `stages: ["draft"]` for a resource in `draft` stage +- **THEN** the rule SHALL evaluate to ALLOW + +#### Scenario: Stage does not match + +- **WHEN** `HAS_LIFECYCLE_STAGE` is evaluated with `stages: ["draft"]` for a resource in `published` stage +- **THEN** the rule SHALL evaluate to DENY + +### Requirement: Permission rules follow extensions-backend pattern + +The permission rules SHALL use `createPermissionRule` and `createPermissionResourceRef` from `@backstage/plugin-permission-node`, following the same pattern as `extensions-backend/src/permissions/rules.ts`. + +#### Scenario: Rule API compatibility + +- **WHEN** the augment resource types are registered via `permissionsRegistry.addResourceType` +- **THEN** the rules SHALL be compatible with `PermissionsRegistryService` from `@backstage/plugin-permission-node` diff --git a/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/specs/route-authorization/spec.md b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/specs/route-authorization/spec.md new file mode 100644 index 0000000000..d36db4f206 --- /dev/null +++ b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/specs/route-authorization/spec.md @@ -0,0 +1,266 @@ +# Spec: route-authorization + +Replacement of all inline route-level authorization guards in augment route handlers with Backstage permission framework calls. This spec defines the mapping from each augment route (agent lifecycle, tool lifecycle, Kagenti infrastructure) to its specific permission, conditional rules, and fallback behavior. The RBAC plugin evaluates the policies; this spec defines what the augment plugin _asks_ the permission framework to evaluate for each operation and how it interprets the result. + +## ADDED Requirements + +### Requirement: Agent list visibility filtering + +The GET /agents route SHALL use `augment.agent.list` (resource-based permission with resource type `augment-agent`) to control visibility via 3-tier evaluation: + +- **ALLOW** — return all agents +- **DENY** — return no agents +- **CONDITIONAL** — apply the returned conditions as filters against each agent in the list, returning only agents that satisfy the conditions + +#### Scenario: Unrestricted user sees all agents + +- **WHEN** a user with `augment.agent.list` ALLOW calls GET /agents +- **THEN** all agents across all lifecycle stages SHALL be returned + +#### Scenario: Denied user sees no agents + +- **WHEN** a user with `augment.agent.list` DENY calls GET /agents +- **THEN** no agents SHALL be returned + +#### Scenario: Conditional user sees filtered agents + +- **WHEN** a user with `augment.agent.list` CONDITIONAL (e.g., `IS_OWNER`) calls GET /agents +- **THEN** only agents satisfying the condition filter (e.g., `createdBy` matches the user) SHALL be returned + +#### Scenario: Conditional with multiple rules + +- **WHEN** a user has a CONDITIONAL policy combining `IS_OWNER` and `HAS_LIFECYCLE_STAGE(published)` for `augment.agent.list` +- **THEN** only agents that satisfy both conditions SHALL be returned + +### Requirement: Agent registration authorization + +The PUT register route SHALL use `augment.agent.register`, replacing the `requireAdminAccess` middleware. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: User with register permission + +- **WHEN** a user with `augment.agent.register` ALLOW calls PUT register +- **THEN** the registration SHALL proceed + +#### Scenario: User with only admin permission (fallback enabled) + +- **WHEN** `augment.permissions.legacyAdminFallback` is enabled, and a user without `augment.agent.register` but with `augment.admin` ALLOW calls PUT register +- **THEN** the registration SHALL proceed via fallback + +### Requirement: Agent promote authorization (draft to pending) + +The PUT promote route for draft-to-pending transitions SHALL use `augment.agent.promote` with `IS_OWNER` and `HAS_LIFECYCLE_STAGE(draft)` conditions, replacing the inline `createdBy` and stage checks. + +#### Scenario: Owner promotes own draft agent + +- **WHEN** the agent's `createdBy` matches the requesting user and the agent is in `draft` stage +- **THEN** the promote SHALL proceed + +#### Scenario: Non-owner cannot promote + +- **WHEN** the agent's `createdBy` does not match the requesting user and the user does not have the fine-grained permission (or `augment.admin` with fallback enabled) +- **THEN** the promote SHALL be denied + +### Requirement: Agent approve authorization (pending to published) + +The PUT promote route for pending-to-published transitions SHALL use `augment.agent.approve` with `IS_NOT_CREATOR` condition. The existing hard-coded self-approval prevention check SHALL be retained as defense-in-depth. + +#### Scenario: Different user can approve + +- **WHEN** the agent's `createdBy` does not match the requesting user and the user has `augment.agent.approve` +- **THEN** the approval SHALL proceed + +#### Scenario: Creator cannot self-approve + +- **WHEN** the agent's `createdBy` matches the requesting user +- **THEN** the approval SHALL be denied by both the `IS_NOT_CREATOR` rule and the hard-coded check + +### Requirement: Agent demote authorization + +The PUT demote route SHALL use `augment.agent.demote`, replacing `requireAdminAccess`. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: User with demote permission + +- **WHEN** a user with `augment.agent.demote` ALLOW calls PUT demote +- **THEN** the demote SHALL proceed + +### Requirement: Agent publish and unpublish authorization + +The PUT publish, PUT unpublish, and PUT bulk-publish routes SHALL use `augment.agent.publish`, replacing `requireAdminAccess`. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: User with publish permission + +- **WHEN** a user with `augment.agent.publish` ALLOW calls PUT publish +- **THEN** the publish SHALL proceed + +#### Scenario: Bulk publish checks per item + +- **WHEN** a user calls PUT bulk-publish +- **THEN** `augment.agent.publish` SHALL be checked for each agent in the batch + +### Requirement: Agent request-unpublish authorization + +The PUT request-unpublish route SHALL use `augment.agent.unpublish` with `IS_OWNER` condition, replacing the inline `isRequestOwner OR isAdmin` check. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: Owner can request unpublish + +- **WHEN** the agent's `createdBy` matches the requesting user +- **THEN** the request-unpublish SHALL proceed + +#### Scenario: Admin can request unpublish for any agent (fallback enabled) + +- **WHEN** `augment.permissions.legacyAdminFallback` is enabled and the user has `augment.admin` regardless of ownership +- **THEN** the request-unpublish SHALL proceed via fallback + +### Requirement: Agent withdraw authorization + +The PUT withdraw route SHALL use `augment.agent.withdraw` with `IS_OWNER` condition, replacing the inline `isOwner OR isAdmin` check. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: Owner can withdraw + +- **WHEN** the agent's `createdBy` matches the requesting user +- **THEN** the withdraw SHALL proceed + +### Requirement: Agent configure authorization + +The PUT config route SHALL use `augment.agent.configure`, replacing `requireAdminAccess`. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: User with configure permission + +- **WHEN** a user with `augment.agent.configure` ALLOW calls PUT config +- **THEN** the configuration update SHALL proceed + +### Requirement: Agent delete authorization + +The DELETE route SHALL use `augment.agent.delete` with `IS_OWNER` and `HAS_LIFECYCLE_STAGE(draft)` conditions for non-admin users, replacing the inline draft-only and ownership checks. + +#### Scenario: Owner deletes own draft agent + +- **WHEN** the agent's `createdBy` matches the requesting user and the agent is in `draft` stage +- **THEN** the delete SHALL proceed + +#### Scenario: Non-owner cannot delete + +- **WHEN** the agent's `createdBy` does not match the requesting user and the user does not have the fine-grained permission (or `augment.admin` with fallback enabled) +- **THEN** the delete SHALL be denied + +#### Scenario: Admin can delete any agent (fallback enabled) + +- **WHEN** `augment.permissions.legacyAdminFallback` is enabled and a user with `augment.admin` calls DELETE regardless of ownership or stage +- **THEN** the delete SHALL proceed via fallback + +### Requirement: Tool lifecycle authorization + +The tool lifecycle routes SHALL use the corresponding `augment.tool.*` permissions with the same patterns as agent routes. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +| Route | Permission | Conditions | +| ------------------------------- | ------------------------ | -------------------------------- | +| PUT promote (draft-to-pending) | `augment.tool.promote` | IS_OWNER | +| PUT promote (other transitions) | `augment.tool.approve` | Opt-in fallback to augment.admin | +| PUT demote | `augment.tool.demote` | Opt-in fallback to augment.admin | +| PUT publish | `augment.tool.publish` | Opt-in fallback to augment.admin | +| PUT unpublish | `augment.tool.unpublish` | Opt-in fallback to augment.admin | + +#### Scenario: Tool owner promotes draft tool + +- **WHEN** the tool's `createdBy` matches the requesting user and the tool is in `draft` stage +- **THEN** the promote SHALL proceed via `augment.tool.promote` with `IS_OWNER` + +#### Scenario: Tool admin operations fall back (fallback enabled) + +- **WHEN** `augment.permissions.legacyAdminFallback` is enabled, and a user with `augment.admin` but without specific tool permissions calls a tool lifecycle route +- **THEN** the operation SHALL proceed via the `augment.admin` fallback + +### Requirement: Kagenti infrastructure route authorization + +The Kagenti infrastructure routes (DELETE, migrate, build, parse-env, fetch-env) SHALL use `augment.kagenti.admin` (basic permission), replacing `requireAdminAccess`. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: User with kagenti admin permission + +- **WHEN** a user with `augment.kagenti.admin` ALLOW calls a Kagenti infrastructure route +- **THEN** the operation SHALL proceed + +#### Scenario: Fallback to general admin (fallback enabled) + +- **WHEN** `augment.permissions.legacyAdminFallback` is enabled, and a user without `augment.kagenti.admin` but with `augment.admin` ALLOW calls a Kagenti infrastructure route +- **THEN** the operation SHALL proceed via fallback + +### Requirement: Vector store route authorization + +The vector store admin routes SHALL use `augment.vectorstore.manage` (basic permission), replacing `requireAdminAccess`. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: User with vectorstore permission + +- **WHEN** a user with `augment.vectorstore.manage` ALLOW calls a vector store route (create, connect, disconnect, delete) +- **THEN** the operation SHALL proceed + +#### Scenario: Fallback to general admin for vector stores (fallback enabled) + +- **WHEN** `augment.permissions.legacyAdminFallback` is enabled, and a user without `augment.vectorstore.manage` but with `augment.admin` ALLOW calls a vector store route +- **THEN** the operation SHALL proceed via fallback + +### Requirement: Document route authorization + +The document admin routes SHALL use `augment.document.manage` (basic permission), replacing `requireAdminAccess`. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: User with document permission + +- **WHEN** a user with `augment.document.manage` ALLOW calls a document route (upload, delete) +- **THEN** the operation SHALL proceed + +### Requirement: MCP route authorization + +The MCP connection test and tool management routes SHALL use `augment.mcp.manage` (basic permission), replacing `requireAdminAccess` for admin routes and adding authorization to currently ungated tool creation. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: User with MCP permission + +- **WHEN** a user with `augment.mcp.manage` ALLOW calls an MCP route (test connection, create tool, delete tool, build tool) +- **THEN** the operation SHALL proceed + +#### Scenario: Tool creation now gated + +- **WHEN** a user without `augment.mcp.manage` and without `augment.admin` calls POST to create a tool +- **THEN** the operation SHALL be denied (closing the current gap where tool creation is ungated) + +### Requirement: Prompt route authorization + +The system prompt generation and configuration routes SHALL use `augment.prompt.manage` (basic permission), replacing `requireAdminAccess`. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: User with prompt permission + +- **WHEN** a user with `augment.prompt.manage` ALLOW calls a prompt route (generate system prompt) +- **THEN** the operation SHALL proceed + +### Requirement: Model route authorization + +The model listing, testing, and configuration routes SHALL use `augment.model.manage` (basic permission), replacing `requireAdminAccess`. When `augment.permissions.legacyAdminFallback` is enabled, DENY falls back to checking `augment.admin`. + +#### Scenario: User with model permission + +- **WHEN** a user with `augment.model.manage` ALLOW calls a model route (list, test, configure) +- **THEN** the operation SHALL proceed + +### Requirement: Permission registration via PermissionsRegistryService + +The `augment-backend` plugin SHALL register resource types, permissions, and rules via `PermissionsRegistryService` (the new Backstage API that replaces the deprecated `createPermissionIntegrationRouter`): + +1. Register both resource types via `permissionsRegistry.addResourceType` with their associated rules +2. Register all 21 new permissions via `permissionsRegistry.addPermissions` + +#### Scenario: Permission integration active + +- **WHEN** the augment backend plugin starts +- **THEN** both resource types and all permissions SHALL be registered with the Backstage permission framework via `PermissionsRegistryService` + +### Requirement: Legacy fallback configuration + +The system SHALL support a `augment.permissions.legacyAdminFallback` config flag (default: `false`). When enabled, all `authorizeLifecycleAction` and `authorizeBasicWithFallback` calls SHALL fall back to checking `augment.admin` on DENY. When disabled, only fine-grained permissions are evaluated. + +#### Scenario: Fallback enabled with existing policies + +- **WHEN** `augment.permissions.legacyAdminFallback` is enabled and a deployment has only `augment.access` and `augment.admin` policies +- **THEN** all authorization decisions SHALL produce the same results as the current inline guard implementation + +#### Scenario: Fallback disabled (default) + +- **WHEN** `augment.permissions.legacyAdminFallback` is not set or is `false` +- **THEN** only fine-grained permissions SHALL be evaluated — `augment.admin` SHALL NOT be checked as a fallback for lifecycle or infrastructure operations diff --git a/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/tasks.md b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/tasks.md new file mode 100644 index 0000000000..4d44dc3181 --- /dev/null +++ b/workspaces/augment/openspec/changes/fine-grained-backstage-permissions/tasks.md @@ -0,0 +1,59 @@ +# Tasks: Fine-Grained Backstage Permissions for Augment Lifecycle Governance + +## 1. Permission Definitions (augment-common) + +- [ ] 1.1 Add `RESOURCE_TYPE_AUGMENT_AGENT` (`augment-agent`) and `RESOURCE_TYPE_AUGMENT_TOOL` (`augment-tool`) constants to `plugins/augment-common/src/permissions.ts` +- [ ] 1.2 Define 10 agent permissions (`augment.agent.list`, `.register`, `.promote`, `.approve`, `.demote`, `.publish`, `.unpublish`, `.withdraw`, `.delete`, `.configure`) using `createPermission` with correct action types and resource types +- [ ] 1.3 Define 5 tool permissions (`augment.tool.promote`, `.approve`, `.demote`, `.publish`, `.unpublish`) using `createPermission` with resource type `augment-tool` +- [ ] 1.4 Define `augment.kagenti.admin` as a basic permission with action `update` +- [ ] 1.5 Add all 16 new permissions to the `augmentPermissions` array alongside existing `augmentAccessPermission` and `augmentAdminPermission` +- [ ] 1.6 Export new constants, types, and permissions from `plugins/augment-common/src/index.ts` + +## 2. Permission Rules and Utilities (augment-backend, new files) + +- [ ] 2.1 Create `plugins/augment-backend/src/permissions/rules.ts` with `createPermissionResourceRef` for agent and tool resource types +- [ ] 2.2 Implement `IS_OWNER` rule — evaluates `resource.createdBy === userRef` +- [ ] 2.3 Implement `IS_NOT_CREATOR` rule — evaluates `resource.createdBy !== userRef` +- [ ] 2.4 Implement `HAS_LIFECYCLE_STAGE` rule — evaluates `stages.includes(resource.lifecycleStage)` with `stages: string[]` parameter +- [ ] 2.5 Create `plugins/augment-backend/src/permissions/permissionUtils.ts` with `matchesAgentConditions(resource, conditions)` and `matchesToolConditions(resource, conditions)` following `extensions-backend/src/utils/permissionUtils.ts` pattern +- [ ] 2.6 Create `plugins/augment-backend/src/permissions/index.ts` barrel export + +## 3. Authorization Middleware Abstraction + +- [ ] 3.1 Add `authorizeLifecycleAction(req, permission, resource?)` to `plugins/augment-backend/src/middleware/security.ts` — fine-grained check first, conditional evaluation if CONDITIONAL, fallback to `augment.admin` on DENY +- [ ] 3.2 Add `authorizeBasicWithFallback(req, permission)` to `plugins/augment-backend/src/middleware/security.ts` — basic permission check with `augment.admin` fallback +- [ ] 3.3 Add `authorizeLifecycleAction` and `authorizeBasicWithFallback` to `RouteContext` in `plugins/augment-backend/src/routes/types.ts` + +## 4. Agent Route Refactoring + +- [ ] 4.1 Replace GET /agents visibility filtering with `augment.agent.list` resource-based permission check (3-tier: ALLOW=all, DENY=none, CONDITIONAL=filtered) in `plugins/augment-backend/src/routes/agentRoutes.ts` +- [ ] 4.2 Replace PUT register `requireAdminAccess` with `augment.agent.register` + fallback +- [ ] 4.3 Replace PUT promote (draft→pending) inline guards with `augment.agent.promote` + IS_OWNER + HAS_LIFECYCLE_STAGE(draft) +- [ ] 4.4 Replace PUT promote (pending→published) self-approval check with `augment.agent.approve` + IS_NOT_CREATOR (retain hard-coded check as defense-in-depth) +- [ ] 4.5 Replace PUT demote `requireAdminAccess` with `augment.agent.demote` + fallback +- [ ] 4.6 Replace PUT publish/unpublish/bulk-publish `requireAdminAccess` with `augment.agent.publish` + fallback +- [ ] 4.7 Replace PUT request-unpublish inline `isRequestOwner OR isAdmin` with `augment.agent.unpublish` + IS_OWNER + fallback +- [ ] 4.8 Replace PUT withdraw inline `isOwner OR isAdmin` with `augment.agent.withdraw` + IS_OWNER + fallback +- [ ] 4.9 Replace PUT config `requireAdminAccess` with `augment.agent.configure` + fallback +- [ ] 4.10 Replace DELETE inline draft-only + ownership checks with `augment.agent.delete` + IS_OWNER + HAS_LIFECYCLE_STAGE(draft) + fallback + +## 5. Tool and Kagenti Route Refactoring + +- [ ] 5.1 Replace PUT promote (draft→pending) inline guards with `augment.tool.promote` + IS_OWNER in `plugins/augment-backend/src/routes/toolLifecycleRoutes.ts` +- [ ] 5.2 Replace PUT promote (other transitions) inline `!isAdmin` with `augment.tool.approve` + fallback +- [ ] 5.3 Replace PUT demote/publish/unpublish `requireAdminAccess` with respective `augment.tool.*` permissions + fallback +- [ ] 5.4 Replace `requireAdminAccess` on Kagenti infra routes (DELETE, migrate, build, parse-env, fetch-env) with `augment.kagenti.admin` + fallback in `plugins/augment-backend/src/routes/kagentiAgentRoutes.ts` + +## 6. Plugin Wiring + +- [ ] 6.1 Register both resource types via `permissionsRegistry.addResourceType` and all 16 new permissions via `permissionsRegistry.addPermissions` in `plugins/augment-backend/src/plugin.ts` +- [ ] 6.2 Add `@backstage/plugin-permission-node` dependency to `plugins/augment-backend/package.json` +- [ ] 6.3 Register both resource types via `permissionsRegistry.addResourceType` with their associated rules in `plugins/augment-backend/src/plugin.ts` (using `PermissionsRegistryService`, not the deprecated `createPermissionIntegrationRouter`) +- [ ] 6.4 Thread `authorizeLifecycleAction` and `authorizeBasicWithFallback` into `RouteContext` in `plugins/augment-backend/src/router.ts` + +## 7. Verification + +- [ ] 7.1 Verify `npx tsc --noEmit` passes clean with all changes +- [ ] 7.2 Verify existing tests pass unchanged (backward compatibility) +- [ ] 7.3 Verify with no fine-grained RBAC policies: behavior identical to current system +- [ ] 7.4 Verify with fine-grained policies: each permission correctly gates its route