Service notifications#

Permission Service can publish an event every time the set of effective policies for a (principal, action, resource) tuple changes. Events are delivered over gRPC to a Notification Service (Event Aggregation Service) so that any number of downstream consumers can react to policy updates without polling the Permission Service. This page describes what the integration is for, when events are emitted, the event schema on the wire, how consumers receive them, and how to enable and credential the publisher in the Helm chart.

For the policy management endpoints whose writes trigger these events, see Managing policies through the REST API. The outgoing gRPC calls reuse the shared serviceIdentity credentials configured in the Helm chart; see Authenticating to the Notification Service below.

Why publish permission-change events#

The decision returned by Permission Service is deterministic for a given set of policies and metadata, so callers frequently cache authorization results instead of asking the service on every request — see the evaluation cache inside the service itself in How authorization requests are evaluated, and similar per-client caches that downstream services maintain for their own HTTP handlers. When a policy is created, updated, or deleted, those caches would otherwise keep returning the previous decision until their TTL expires.

The notification integration closes that gap by turning every policy write into an event on a shared message broker. Typical consumers are:

  • Permission UI and other admin tools — refresh the policy or user view in real time as soon as an administrator changes a policy somewhere else, without waiting for the next refresh interval.

  • Services that cache Permission Service responses — invalidate their own (principal, action, resource) entry as soon as the underlying policies change, so users see the new decision on their next request instead of after the cache TTL.

  • Audit and analytics pipelines — record every policy change as a first-class event alongside the actions it affects, for downstream search, reporting, or alerting.

Consumers subscribe directly to the Notification Service; the Permission Service does not know or care how many consumers there are. The integration is optional — when it is disabled, every other part of the service continues to work, but caches downstream must rely on their own TTLs.

When events are published#

The service publishes exactly one event per successful policy write, for every one of the following endpoints:

Endpoint

Trigger

PUT /v1beta/policies/

One event for the created policy.

PUT /v1beta/policies/batch/

One event per policy in the batch (skipped if the batch is empty).

DELETE /v1beta/policies/{id}

One event for the deleted policy. The endpoint is idempotent — when the id did not exist no policy was removed and no event is emitted.

Events are emitted after the write is persisted and the in-memory evaluation cache has been invalidated, so consumers that reach Permission Service in response to an event see the new state. Failed or rejected writes do not publish an event.

Metadata writes (the endpoints under /v1beta/services/) only invalidate the local metadata and evaluation caches and do not currently emit notifications. Read endpoints never emit events.

Note

Notifications are best-effort. The event is dispatched asynchronously from a background task, so the API caller receives its success response as soon as the policy has been persisted, regardless of whether the event was delivered. If the Notification Service is unreachable or rejects the publish, the failure is logged but the policy write is kept.

When the event publisher receives Unauthenticated from the Notification Service, the Permission Service refreshes its service credentials (see Authenticating to the Notification Service) and retries the publish once before giving up.

Event schema#

Events are serialized as the Event message from the Omniverse Notifications publisher.v1beta proto and sent over gRPC with the PublishEvent RPC. The concrete payload the Permission Service emits is:

Field

Type

Value

event_type

string

Always omni.permissions.changed.

occurred_at

google.protobuf.Timestamp

Wall-clock time at which the event was constructed (seconds since the Unix epoch, nanoseconds zero).

message

google.protobuf.Struct

JSON-shaped object describing the affected scope tuple, see below.

resource

EventResource

Present when the affected policy pinned a resource; absent otherwise.

The message struct always carries three string fields, corresponding to the policy scope that was affected by the write:

Field

Description

principal

Bare principal id the policy was pinned to (for example, alice). Empty string when the policy’s principal scope was unset (global).

action

Action in Cedar form, for example Action::"storage-service:read". Empty string when the policy’s action scope was unset.

resource

Resource in Cedar form, for example object::"/Projects/Scene.usd". Empty string when the policy’s resource scope was unset.

When the affected policy pinned a resource, event.resource.resource_id is populated with the resource id as stored by Permission Service (URL-encoded, without the Cedar type prefix) so that consumers can use the Notification Consumer API’s resource_filter to subscribe only to events about resources they care about — see Consuming the events.

Example event emitted after PUT /v1beta/policies/ stores a policy pinned to all three scopes:

{
  "event_type": "omni.permissions.changed",
  "occurred_at": "2026-04-16T12:34:56Z",
  "message": {
    "principal": "alice",
    "action": "Action::\"storage-service:read\"",
    "resource": "object::\"/Projects/Scene.usd\""
  },
  "resource": {
    "resource_id": "/Projects/Scene.usd"
  }
}

A policy without any pinned scope — for example permit(principal, action, resource); — produces an event with empty strings in all three message fields and no resource object:

{
  "event_type": "omni.permissions.changed",
  "occurred_at": "2026-04-16T12:34:56Z",
  "message": {
    "principal": "",
    "action": "",
    "resource": ""
  }
}

An empty message field still signals that something global changed; consumers that cache Permission Service responses typically invalidate their whole cache when any dimension is empty, since a global permit or forbid can affect any tuple.

Consuming the events#

Consumers register with the Notification Service’s Consumer API. The Notification Service owns the subscription model, durable queues, and delivery semantics; the Permission Service is only a publisher. Two filters are useful in practice:

  • Filter by event_type — subscribe to omni.permissions.changed to receive permission-change events only, ignoring the rest of the traffic on the broker.

  • Filter by resource.resource_id — when a consumer only caches decisions for a specific subtree (for example, Storage Service caches for files under a given prefix), it can add a resource_filter such as:

    {
      "filter_type": "FILTER_TYPE_STARTS_WITH_LAZY",
      "resource_id": "/Projects/"
    }
    

    The consumer then receives only events whose affected policy pinned a resource whose id starts with /Projects/. Events whose policy did not pin a resource carry no resource field; per the publisher proto, that is equivalent to resource_id = "" and such events reach consumers regardless of the resource filter, so global invalidations still work correctly.

Refer to the Notification Service’s own documentation for the full Consumer API, including how to create durable queues, commit offsets, and configure backoff. For how to grant a consumer principal the right to consume from those queues inside Permission Service, see the event-consumer-service actions in Actions and resources for Omniverse services.

Enabling notifications in the Helm chart#

The Helm chart exposes notification settings under the top-level notifications key:

notifications:
  enabled: true
  eventPublishingGrpcEndpoint: "http://event-aggregation-service:50051"

Value

Purpose

notifications.enabled

Master switch. When false, the service starts without an event publisher and every policy write silently skips notification.

notifications.eventPublishingGrpcEndpoint

gRPC endpoint of the Notification Service’s Event Publishing API. Required when notifications.enabled is true. Use the in-cluster service name (for example, the default http://event-aggregation-service:50051) or the ingress URL for cross-cluster deployments.

At startup, the service opens one gRPC channel to eventPublishingGrpcEndpoint and reuses it for every published event. If the initial connection fails, a warning is logged and the service still starts — notifications are simply disabled for that process lifetime; restart the pod once the endpoint is reachable.

Authenticating to the Notification Service#

Publishing events is itself an authorized action (Action::"event-aggregation-service:publish-event" — see event-aggregation-service), so the Permission Service must present a bearer token on every outgoing gRPC call. That token is produced by the shared serviceIdentity machinery used for every outgoing call the service makes; the notifications section does not have its own credentials. See Outbound service identity (serviceIdentity) for the full reference on each mode.

serviceIdentity offers two mutually exclusive modes — enable exactly one when notifications.enabled is true:

  • Client-credentials flow (serviceIdentity.clientCredentials.enabled: true). The service exchanges a client id plus a client secret at the configured OpenID token endpoint and caches the resulting access token, refreshing it when it expires or when the Notification Service returns Unauthenticated.

    serviceIdentity:
      clientCredentials:
        enabled: true
        openIdConfigurationUri: "https://idp.example.com/.well-known/openid-configuration"
        clientId: "permission-service"
        clientScope: "notifications.publish"
        clientSecretRef:
          name: permission-service-client-credentials
          key: clientSecret
        # optional: extra headers/body params required by your IdP
        # extraHeaders:
        #   X-Tenant: "omniverse"
        # extraBodyParams:
        #   audience: "https://notifications.example.com"
    

    The referenced Kubernetes secret is mounted at /etc/secrets/client-credentials/<key> and the client secret is never written to a value that would end up in a ConfigMap. When openIdConfigurationUri is omitted under clientCredentials, the chart falls back to the top-level openId.openIdConfigurationUri.

  • Access-token file (serviceIdentity.accessTokenFile.enabled: true). A sidecar container writes a pre-provisioned access token to a shared in-memory volume at /etc/secrets/access-token/token. The Permission Service reads that file and watches it for changes at the configured pollInterval.

    serviceIdentity:
      accessTokenFile:
        enabled: true
        pollInterval: "15s"
        provisionerSidecar:
          container:
            name: token-provisioner
            image: registry.example.com/token-provisioner:1.0.0
            # the chart injects a `token-exchange` volume mount automatically
    

    Use this mode when an external provisioner (for example a workload identity mechanism) already produces short-lived tokens and you only need Permission Service to pick them up.

When neither mode is enabled, outgoing gRPC calls are made without a bearer token — accepted only by a Notification Service that does not require authentication (for example, in tests). In production both Permission Service and the Notification Service should have authentication turned on.

Regardless of the authentication mode, the principal the Permission Service presents must be allowed to publish through the Notification Service’s own authorization. When that authorization is itself enforced by a Permission Service deployment, the principal needs a permit similar to:

permit(
  principal == Principal::"permission-service",
  action == Action::"event-aggregation-service:publish-event",
  resource
);

Grant it through database.init.policies on the Permission Service that guards the Notification Service, exactly like any other service-to-service call — see Granting Administrative Permissions for the mechanics.

Troubleshooting#

  • No events arrive after a policy write. Check that notifications.enabled is true, that the pod logs contain the Connected to Event Aggregation Service line emitted at startup, and that the policy write itself returned 200 OK — failed writes never publish.

  • Publisher logs Failed to publish policy changed event. The Notification Service rejected the RPC or the channel broke. When the status code is Unauthenticated, the next publish automatically refreshes the service token; when it is Unavailable, the gRPC channel will reconnect on the next publish. Persistent errors indicate a deployment or authorization problem on the Notification Service side.

  • Consumers receive events but caches keep stale data. Confirm the consumer’s resource_filter does not exclude events with an empty resource_id; events from policies without a pinned resource are delivered without a resource field and are equivalent to resource_id = "" on the wire.

See also#