Semaphor
SecurityRow-Level Security

Automate with the API

Create, validate, and preview row-level security policies programmatically

Everything the Security page does is available through the Management API, so you can provision policies as customers are onboarded, keep them in version control, or let an agent manage them.

Who can call these endpoints

Management endpoints require an organization Admin credential scoped to the same project. Dashboard tokens issued for your customers are never accepted for policy management.


The workflow

Whether a human or an agent is driving, the sequence is the same:

build the candidate
  → validate it
  → read the diagnostics
  → write only if it is valid
  → preview real viewers

Validation is the step worth adopting early. Every create and update endpoint has a matching validation endpoint that runs exactly the same checks and saves nothing, so you can find out whether a change is acceptable before you make it.


Branch on codes, never on prose

Every problem Semaphor reports carries a stable code, the resource and field it applies to, and the repairs that would resolve it. Read those; do not pattern-match the human sentences, which are free to improve over time.

Each diagnostic gives you three pieces of copy for three different jobs:

  • title says what happened
  • message explains why
  • remediations[].label says how to fix it

Use the codes to decide what to do, and show the copy to whoever is watching. When two repairs are both valid but mean different things for security, the diagnostic says so rather than picking one, and an agent should ask instead of guessing.


Validate before writing

Send a candidate to the matching validation endpoint. A well-formed request that describes an unacceptable policy returns HTTP 200 with a status of INVALID. That is a successful answer to the question you asked, not a transport failure, so reserve your error handling for 4xx and 5xx.

POST /api/management/v1/projects/{projectId}/unified-security/definitions/validate
POST /api/management/v1/projects/{projectId}/unified-security/definitions/{definitionId}/validate
POST /api/management/v1/projects/{projectId}/unified-security/assignments/validate
POST /api/management/v1/projects/{projectId}/unified-security/assignments/{assignmentId}/validate

The validation endpoints take the same candidate body as the create or update endpoint they mirror. The one exception is recovery: the definition update endpoint also accepts a recoveryAction field for re-authoring a policy saved in an older format, and the validation endpoint rejects it as an unknown field.

Before changing the all-tenants baseline

When an assignment change touches the all-tenants baseline, the response includes an impact summary: which parameters changed, and how many tenants and tenant users carry their own value for each. Those keep their own values; everyone else moves to the new baseline. Removing the baseline also reports that actors without their own assignment stop receiving the policy.

The summary counts assignments, not people, and does not enumerate every viewer.

Concurrency

Writes are last-valid-write-wins. A successful validation is not a lock: each write reloads the current saved state and validates against it. If two changes race, the later valid one wins.


Preview a viewer

POST /api/management/v1/projects/{projectId}/unified-security/preview

Ask what a saved viewer receives today:

Preview a saved tenant
{
  "connectionId": "conn_123",
  "actor": { "kind": "TENANT", "tenantId": "tenant_ridgeline" }
}

Or overlay one unsaved change and ask what it would do:

Preview a pending assignment edit
{
  "connectionId": "conn_123",
  "actor": { "kind": "TENANT", "tenantId": "tenant_ridgeline" },
  "draft": {
    "kind": "ASSIGNMENT_UPDATE",
    "assignmentId": "usa_123",
    "candidate": {
      "params": { "allowed_locations": ["chicago"] }
    }
  }
}

A request carries at most one draft, and Semaphor applies it once, in memory. The draft kinds are DEFINITION_CREATE, DEFINITION_UPDATE, ASSIGNMENT_CREATE, and ASSIGNMENT_UPDATE. You cannot combine ignorePersistedAssignments with a draft or with assignmentId.

Reading the response

A blocked policy is still a successful request:

200 OK, policy blocked
{
  "ok": true,
  "data": {
    "preview": {
      "status": "BLOCKED",
      "diagnostics": [
        {
          "code": "PARAM_UNRESOLVED",
          "severity": "ERROR",
          "title": "Required RLS parameter has no value",
          "message": "Set the assignment value.",
          "retryable": false
        }
      ],
      "activation": {
        "status": "ACTIVE",
        "definitionCount": 1,
        "ruleCount": 1
      },
      "compilation": { "status": "NOT_RUN" }
    },
    "context": {
      "cls": { "filePathTemplates": {}, "params": {} },
      "sls": {},
      "sources": { "cls": [], "sls": [] }
    }
  }
}

Work through it in this order:

  1. confirm ok
  2. read preview.status
  3. read activation.status to tell "blocked" apart from "this viewer has no policy"
  4. group diagnostics by code and field path
  5. follow only the remediations the response supplied
  6. revalidate and preview again after each repair

compilation reports whether the rules were checked against real tables. CONTEXT_REQUIRED means the policy resolved but you did not say which tables the query touches, so a broad matcher could not be verified. It is not a configuration problem. Send referencedEntities when you want that confirmation; a policy whose rules all use TABLE_LIST matchers is verified without it.

If the signed input also carried row filters of its own, they appear separately under context.tokenOnlyRLS, and a legacy-mode connection reports its existing rules under context.legacyRLS. Neither changes the saved policy's result.


HTTP status reference

StatusWhat it meansWhat to do
200The request succeededRead the typed result; it may still be blocked or invalid
201A definition or assignment was createdRead the saved resource from the response
400The request was malformedFix the reported field paths
401 / 403Not authorizedObtain an organization Admin credential for this project
404A resource you named is goneReload; a definition or assignment may be stale
409The write conflicts with saved stateReload and resolve the reported conflict
422The policy is invalid; nothing was savedRepair the diagnostics and retry
500Unexpected failureRetry is safe; keep the requestId for support

Field-level problems come back with an RFC 6901 pointer such as /rlsConfig/parameters/allowed_locations, so you can attach a message to the right input without parsing text.


Keep sensitive values out of your logs

Preview returns bounded summaries rather than raw data: arrays report their type and length, long strings are truncated, and secret-backed values stay masked. Keep that property in whatever you build on top. Do not log raw token parameters, secret values, complete customer lists, or internal identifiers that the response did not already expose.


Endpoint reference

On this page