Skip to content

Implement PermissionsProxyService that forwards permission check to rbac-authz-webhook #292

Description

@Sobyt483

Both KubernetesServiceProvidersService and
ContentConfigurationServiceProvidersService return a rawServiceProvider without nodeContext:

rawServiceProviders: [{
  name: 'platform-mesh-system',
  displayName: '',
  creationTimestamp: '',
  contentConfiguration: contentConfigurations,
  // nodeContext absent
}]

RawServiceProvider in openmfp/portal-server-lib already has
nodeContext?: Record<string, any>, so no upstream change is needed — it just needs to be
populated here.

Request / response contract

The proxy works with these shapes throughout (used by PermissionsProxyService and both adapters):

// src/portal-options/services/permissions/permissions.model.ts

export interface AuthorizationRequest {
  userId:      string;
  accountPath: string;   // e.g. "root:orgs:sub:a1"
  checks: {
    resource: string;              // ResourceDefinition.entity
    actions:  string[] | 'All';
  }[];
}

export interface Permission {
  resource: string;  // ResourceDefinition.entity, e.g. "Account"
  actions:  string[]; // only the allowed verbs — default-deny (absent = denied)
}

export interface AuthorizationResponse {
  userId:      string;
  accountPath: string;
  permissions: Permission[];
}

export interface IPermissionsAdapter {
  checkPermissions(req: AuthorizationRequest): Promise<AuthorizationResponse>;
}

PermissionsProxyService

src/portal-options/services/permissions/permissions-proxy.service.ts

  • Selects the adapter once at startup based on environment variables:

    Env var Adapter Priority
    OPENMFP_PORTAL_CONTEXT_OPEN_FGA_API_URL OpenFgaAdapter wins if both set
    OPENMFP_PORTAL_CONTEXT_OPEN_RBAC_API_URL RbacAdapter fallback
    neither logs a warning, returns permissions: []
  • Exposes checkPermissions(req: AuthorizationRequest): Promise<AuthorizationResponse>.

  • On adapter error: logs the error, returns { ...req, permissions: [] } — UI treats absent
    permissions as fail-open (shows everything).

  • Does not call the adapter when req.checks is empty — returns { ...req, permissions: [] }
    immediately.

Helper: extractResourceDefinitions

src/portal-options/services/permissions/extract-resource-definitions.util.ts

Walks ContentConfiguration[].luigiConfigFragment.data.nodes recursively, collects every
node.context.resourceDefinition entry that has checkActions defined, and returns a deduped
AuthorizationRequest['checks'] list. Deduplication key: resource; if the same entity appears
in multiple CCs with different checkActions, merge the action lists (union; 'All' wins).

OpenFgaAdapter

src/portal-options/services/permissions/open-fga.adapter.ts

Translates AuthorizationRequest → OpenFGA BatchCheck → AuthorizationResponse.

Endpoint: POST {OPEN_FGA_API_URL}/stores/{storeId}/batch-check

Request construction — one tuple per (resource, action) pair:

{
  "checks": [
    {
      "tuple_key": {
        "user":     "user:{userId}",
        "relation": "{action}",
        "object":   "{resource}:{accountPath}"
      },
      "correlation_id": "{resource}#{action}"
    }
  ]
}

For actions === 'All': the adapter first reads the auth model to discover all relations defined
for the object type, then builds one tuple per relation.

Response mapping — fold the flat result map back into Permission[] using correlation_id:

{
  "result": {
    "Account#create": { "allowed": true  },
    "Account#delete": { "allowed": false }
  }
}

allowed: true → verb is added to actions[] for that resource; allowed: false → omitted
(default-deny).

RbacAdapter

src/portal-options/services/permissions/rbac.adapter.ts

Translates AuthorizationRequest → Kubernetes RBAC → AuthorizationResponse.

Primary path — SelfSubjectRulesReview (one request per accountPath, returns all allowed
verbs for the calling user):

POST {OPEN_RBAC_API_URL}/apis/authorization.k8s.io/v1/selfsubjectrulesreviews

Request:  { "spec": { "namespace": "{accountPath}" } }

Response: {
  "status": {
    "rules": [
      {
        "verbs":     ["get", "list", "create"],
        "apiGroups": ["core.platform-mesh.io"],
        "resources": ["accounts"]
      }
    ]
  }
}

Mapping rules:

AuthorizationRequest field RBAC mapping
userId implicit — carried by the bearer token
accountPath spec.namespace
checks[].resource matched against status.rules[].resources using entityCollection lowercased
checks[].actions intersected with status.rules[].verbs — only matching verbs survive
checks[].actions === 'All' returns the full verbs list from the matched rule

Fallback — SubjectAccessReview — used only when the bearer token differs from the user
being checked (impersonation flows):

POST {OPEN_RBAC_API_URL}/apis/authorization.k8s.io/v1/subjectaccessreviews
{
  "spec": { "user": "{userId}", "verb": "{action}",
            "resource": "{entityCollection}", "namespace": "{accountPath}" }
}

One call per (resource, action) pair; results assembled into the same Permission[] shape.

ResourceDefinition.checkActions type addition

resourceDefinition is embedded in node.context inside ContentConfiguration. Add
checkActions?: string[] | 'All' to whatever type represents the CC resourceDefinition in
this repo so extractResourceDefinitions has no TypeScript errors.

Wiring into both service providers

After contentConfigurations is built in each provider:

const checks = extractResourceDefinitions(contentConfigurations);
const { permissions } = checks.length
  ? await this.permissionsProxyService.checkPermissions({
      userId:      context.userId,
      accountPath: context.accountPath,   // resolved from K8sRequestContext
      checks,
    })
  : { permissions: [] };

return {
  rawServiceProviders: [{
    name: 'platform-mesh-system',
    displayName: '',
    creationTimestamp: '',
    contentConfiguration: contentConfigurations,
    nodeContext: { permissions },
  }],
};

Apply identically to KubernetesServiceProvidersService and
ContentConfigurationServiceProvidersService.

Acceptance criteria

  • PermissionsProxyService unit tests cover:
    • OpenFgaAdapter selected when OPENMFP_PORTAL_CONTEXT_OPEN_FGA_API_URL is set.
    • RbacAdapter selected when only OPENMFP_PORTAL_CONTEXT_OPEN_RBAC_API_URL is set.
    • OpenFgaAdapter wins when both env vars are set.
    • Neither set → permissions: [], no adapter call, warning logged.
    • Adapter error → permissions: [], error logged, does not throw.
    • Empty checks → no adapter call, returns immediately.
  • OpenFgaAdapter unit tests cover:
    • Correct tuple construction for explicit verb list.
    • 'All' expansion from auth model relations.
    • correlation_idPermission[] folding; allowed: false → verb omitted.
  • RbacAdapter unit tests cover:
    • SelfSubjectRulesReviewPermission[] mapping.
    • 'All' returns full verb list from the matched rule.
    • Resource not found in rules → { resource, actions: [] }.
    • Impersonation fallback: SubjectAccessReview called per verb; results assembled correctly.
  • Both KubernetesServiceProvidersService and ContentConfigurationServiceProvidersService
    populate nodeContext.permissions.
  • Integration test: getServiceProviders returns a provider whose nodeContext.permissions
    contains at least one { resource, actions } entry matching a CC fixture.
  • Export Permission, AuthorizationRequest, AuthorizationResponse, IPermissionsAdapter,
    and PermissionsProxyService from src/portal-options/index.ts.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Projects

Status
In Progress
Status
In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions