Application-scoped fine-grained authorization
An application that declares its own authorization_details types (RFC 9396,
oauthAuthorizationDetailsType on the application) can also own an XACML
policy that decides each detail of those types. This page
describes every application’s own policy, the switch that enforces it, what
happens when a token is issued for an application that enforces it, and what
happens to the policy when the application is deleted. How a detail’s
members become policy attributes is described under
Rich authorization requests.
The same policy also decides each access a resource server asks about
through AuthZEN; see
What happens at access time.
Features
- One policy per application. When an application is created on
Applications → New, through the management API, or by importing an LDIF
file, the service writes a default policy for it into the realm’s
ou=policies. The policy has one rule that permits every detail. It is a real Permit decision, not a fallback: delete the rule and the policy denies. - The application names its policy. The application entry carries the
policy’s entry name in
appFgaPolicy. The policy carries the application’s identifier inxacmlApplication. - A switch, off by default.
appFgaEnforcedisTRUEwhen the application’s policy is enforced. It starts absent, which means off. Switching it on creates the policy first if the application has none. It is refused while the policy is disabled or does not parse, because an enforced application with such a policy would refuse every detail. It is also refused while a type the application declares is not covered: a member its schema allows is neither mapped (xacml) nor listed innotForPolicy, or its schema allows members it does not name. The refusal names those member paths,*for an open schema (STS-FGA-0018). While the switch is on, a type definition that would leave a member uncovered is refused when it is written (STS-FGA-0019). - Enforced when tokens are issued. While the switch is on, every
authorization_detailsentry of a type the application declares is granted only when the application’s policy says Permit. See What happens at issuance. - Enforced at access time, through AuthZEN. While the switch is on, an AuthZEN evaluation about the application is decided by the application’s policy instead of the realm’s root policy. See What happens at access time.
- Deleting an application keeps its policy. The policy is disabled and
marked
xacmlOrphanedAtwith the time of the deletion. It stays inou=policiesfor audit until an administrator removes it on XACML → Policies (see The console and the API). If an application with the same identifier is created again, it gets a new policy; it never inherits the orphan. - Never the root. An application’s policy is never the repository’s root,
is never reached by a
PolicyIdReferencefrom the root, and is never counted when the service picks “the only enabled policy” as the root. A realm with one ordinary policy keeps it as its root however many applications it has. - Never sent to a remote PEP.
GET /xacml/pep/policieswithholds every application’s policy and lists its name inwithheld, as it does for this service’s own two policies. The sync token ignores them, so creating an application does not make the remote PEPs pull again. Fine-grained authorization is decided only by this service: a resource server asks it through the AuthZEN endpoint, directly or through a remote PEP’s edge PDP, which forwards the question here. What a remote PEP is sent instead is the list of applications that enforce (fgaApplicationson its AuthZEN catalogue pull), so it knows which questions to forward.
What happens at issuance
When an application has appFgaEnforced on, the service asks its policy
about each detail of its types, one detail at a time. The question is asked
after the realm’s issuance policy and the type checks of RFC 9396, never
instead of them. A detail of any other type, and every detail of an
application with the switch off, is handled exactly as before.
| Where | When | A refusal by the policy | The policy cannot be used |
|---|---|---|---|
/oauth2/authorize |
after sign-in, before the consent screen | access_denied, sent to the client’s redirect_uri (also with prompt=none) |
server_error, sent to the redirect_uri |
/oauth2/par |
when the request is pushed | 400 invalid_authorization_details |
500 server_error |
/oauth2/token |
every grant that issues authorization_details |
400 invalid_authorization_details |
500 server_error |
- Only Permit grants a detail. Deny, NotApplicable, and Indeterminate
because an attribute the policy needs is missing all refuse the detail
(
STS-FGA-0014). So does a Permit that carries an obligation this service cannot carry out. A detail with a member the type does not cover is refused the same way (STS-FGA-0103). - A policy that cannot be used refuses too, with
server_error. That is an application with no policy (STS-FGA-0003), a policy that is missing (0004), that is not the application’s own (0005), that is disabled (0006) or that does not parse (0007), or a policy whose evaluation fails (0015). A detail is never issued without a decision. - The consent screen never shows a refused detail. The authorization endpoint asks before consent, so a person is never asked to approve a detail that will not be issued.
- At the PAR endpoint nobody has signed in yet, so the policy sees the
client but no subject: the request has no
subject-idand nourn:sts:xacml:subject-kindat all, never an empty one. A rule that needs a person does not match, and a policy that must refuse when nobody is there tests for the attribute’s absence. The authorization endpoint asks again once a person has signed in. - Limits a person lowers on the consent screen are asked about again
when the authorization endpoint itself issues an access token
(
response_typecontainingtoken). The question before the consent screen was about the limits the client asked for; the token carries the lowered ones, so the policy is asked about those before the token is issued, with the sameaccess_deniedorserver_errorif it refuses. For a code, the token endpoint asks about the lowered details when the code is redeemed. - Every refresh is decided again, against the policy and the type definition as they are now. A policy change reaches a client at its next refresh: a refused refresh ends that grant, as a refused code does.
- A token exchange is bounded. For a type whose application enforces the
policy, a token exchange may ask only for details the
subject_tokenalready carries (RFC 9396 section 6’s rule: the same type, each common array a subset, every other member equal). Asubject_tokenthat this service did not issue, or that carries noauthorization_details, covers none. Otherwise the request is refusedinvalid_authorization_details(STS-FGA-0017). Types of applications that do not enforce the policy are granted as asked, as before. - Who the subject is. At the authorization endpoint and for a code,
refresh, device, CIBA or exchange grant it is the person
(
subject-id,urn:sts:xacml:subject-kind=user), and the policy can read their directory attributes. For a client credentials grant it is the client (application). The client is alwaysurn:sts:xacml:client-id.
Each decision is written to the audit log as fga.decision: the
application, the policy’s entry name, PolicyId and version, the detail’s
type and a SHA-256 of the type definition, the subject, the client, the
stage, the grant, the outcome with its XACML decision and error code, and a
SHA-256 of the detail. The detail’s values are never written to the audit
log. XACML → Monitor counts each decision on the row The fine-grained
authorization PEP, with how many were allowed and refused.
An example: no deletes
The resource application https://docs.example/api declares this type:
{"type": "document_access",
"locations": ["https://docs.example/"],
"actions": ["read", "update", "delete"],
"schema": {"type": "object", "additionalProperties": false,
"properties": {"type": {}, "locations": {}, "actions": {},
"identifier": {"type": "string"},
"classification": {"type": "string"},
"note": {"type": "string"}}},
"xacml": [{"path": "classification",
"attributeId": "urn:example:classification"}],
"notForPolicy": ["note"]}
Switch fine-grained authorization on for it, then replace the rules of its
policy (XACML → Policies, the policy named fga-… with the application
as its owner) with this ALFA:
namespace docs {
attribute act { category = actionCat
id = "urn:oasis:names:tc:xacml:1.0:action:action-id" type = string }
attribute detailAction { category = actionCat
id = "urn:sts:xacml:fga:detail:action" type = string }
policy documents {
id = "urn:example:docs"
apply firstApplicable
target clause act == "grant-authorization-detail"
rule noDelete {
deny
target clause detailAction == "delete"
}
rule everythingElse {
permit
}
}
}
Now a client that asks for
authorization_details=[{"type":"document_access",
"locations":["https://docs.example/"],"actions":["read","delete"],
"identifier":"doc-1","classification":"internal"}]
at /oauth2/authorize is sent back to its redirect_uri with
error=access_denied as soon as the person has signed in, without a consent
screen. The same detail at /oauth2/par or /oauth2/token gets
400 {"error":"invalid_authorization_details"}. A detail with
"actions":["read"] is granted. The audit log shows one fga.decision row
for each request.
What happens at access time
A token says what was granted when it was issued. Whether a particular request may go ahead now — this person, this document, this action — is a question the resource server asks at the time of the request, and for an application that enforces fine-grained authorization the same policy answers it:
resource server ──► remote PEP (edge PDP) ──forwarded──► POST /access/v1/evaluation ──► the application's policy
resource server ─────────────────────────────────────► (AuthZEN)
The resource server sends an AuthZEN access evaluation naming
the application in context.application, to this service or to a remote
PEP’s edge PDP in its network. When that
application has appFgaEnforced on, the evaluation is decided against the
application’s own policy alone, never the realm’s root policy and never a
policy the root references. Every other application is decided against the
root, as before.
Through a remote PEP, the path is longer and the answer the same. The
edge PDP admits the resource server and resolves the application as it does
for any evaluation; it finds the application on the list of applications
that enforce fine-grained authorization, which it pulls with its AuthZEN
catalogue; and instead of deciding from the root policy it holds, it
forwards the evaluation to this realm’s POST /access/v1/evaluation,
over mutual TLS with its own certificate. This service decides it against the
application’s policy and the edge PDP returns that answer. An error here
(a policy that cannot be used is 500), a refusal of the PEP or no answer
is an error at the edge PDP, never false. For this to work, the remote
PEP’s own directory entry must hold AUTHZEN_PEPS and have the application
in its scope — the requirements below, applied to the PEP; a policy then
sees the PEP as urn:sts:xacml:authzen:caller. Enforcing the application
grants both to the realm’s remote PEPs (see
What enforcing grants the remote PEPs).
A switch of enforcement reaches the edge PDP at its next poll. See
The remote PEP.
What the caller needs is what every AuthZEN caller needs:
- to authenticate at the realm’s AuthZEN door (mutual TLS by default; see Who may call it);
- membership of the realm’s
authzen-pepsgroup, which carries theAUTHZEN_PEPSrole; - the application in its scope (
stsAuthzenApplicationon a person or service account,appAuthzenApplicationon an application), and the application named incontext.applicationwhen it has several. A caller cannot reach an application’s policy by naming an application outside its scope (403).
What the policy sees is the AuthZEN request as it always is —
subject-id, urn:sts:xacml:authzen:subject-type, the caller’s
action-id, resource-id, urn:sts:xacml:authzen:resource-type, the
catalogue’s attributes, and urn:sts:xacml:protocol = authzen with the
other AuthZEN facts — and two more, which this service writes and a caller
cannot send:
| Attribute | Category | Value |
|---|---|---|
urn:sts:xacml:fga:application |
resource | the application whose policy decides |
urn:sts:xacml:fga:stage |
environment | access |
At issuance the stage is authorize, par, token, refresh or
exchange and the action is grant-authorization-detail; at access time
the action is the one the resource server asks about. One policy can answer
both by targeting on the stage, or on urn:sts:xacml:protocol
(oauth2 at issuance, authzen at access time).
The answer follows AuthZEN’s rules: only a Permit with no obligation
this service cannot carry out is true; Deny, NotApplicable and a missing
attribute are false. A policy that cannot be used — none, missing, not the
application’s own, disabled, or one that does not parse — is an error,
500, and in a batch that item’s context.error; it is never false, and
the root policy is never asked instead (STS-AUTHZEN-0046, with the
STS-FGA-* code that says why on the audit row).
Each evaluation is one authzen.decision row in the audit log, which names
the application’s policy, its PolicyId and its version when that policy
decided. XACML → Monitor counts it once, on the AuthZEN row: the
resource server enforces the answer, not this service.
An example: no deletes at access time
Extending the policy of the example above so that it also answers the resource server:
namespace docs {
attribute act { category = actionCat
id = "urn:oasis:names:tc:xacml:1.0:action:action-id" type = string }
attribute stage { category = environmentCat
id = "urn:sts:xacml:fga:stage" type = string }
policy documents {
id = "urn:example:docs"
apply firstApplicable
rule noDeleteNow {
deny
target clause stage == "access"
target clause act == "delete"
}
rule everythingElse {
permit
}
}
}
A PEP scoped to the application asks:
POST /access/v1/evaluation
{"subject":{"type":"user","id":"alice"},
"action":{"name":"delete"},
"resource":{"type":"document","id":"doc-1"},
"context":{"application":"https://docs.example/api"}}
and gets {"decision": false}, even if the realm’s root policy would permit
it. The same request with "action":{"name":"read"} gets
{"decision": true}.
The console and the API
Everything below is a form in the admin console and an operation of the
management API. A form posts to the operation named
beside it, so a script can do each step the way the console does. Every
write needs Admin Write, or admin:write on the API token.
- Declare the type. On Applications → (the resource application) →
Access types, declare the type with its JSON Schema
(
POST /admin-api/applications/set-access-type). Close the schema with"additionalProperties": false; an open schema can never be covered. - Map its members. Under XACML mapping on the same tab there is a sub-tab for each declared type. Map each member the policy needs (see Mapping a type’s members), and list the members it should never see under Not for the policy.
- Check the coverage. The application’s Fine-grained authorization tab lists each declared type with covered, or not covered: followed by the member paths still to map or list.
- Write the policy. Its policy on the same tab names the policy,
with a link into the XACML → Editor. Narrow its rule there. An
application with no policy of its own gets one from Create the default
policy (
POST /admin-api/applications/fga-create-policy). - Enforce it. Press Enforce
(
POST /admin-api/applications/fga-enable). The refusal names what to fix: an uncovered type with its paths (STS-FGA-0018), or a policy that is disabled (STS-FGA-0006) or does not parse (STS-FGA-0007). Enforcing also grants the realm’s remote PEPs what forwarding needs (below). Stop enforcing (fga-disable) switches it off and keeps the policy.
| Console | Operation | What it does |
|---|---|---|
| Access types → XACML mapping → Save the row | POST /admin-api/applications/set-access-type-row |
Adds the row for a member path, or replaces the one already mapping it |
| Access types → XACML mapping → Remove | POST /admin-api/applications/remove-access-type-row |
Takes one row off |
| Access types → XACML mapping → Save the list | POST /admin-api/applications/set-not-for-policy |
Replaces the list of paths the policy never sees |
| Access types → Save the type | POST /admin-api/applications/set-access-type |
Writes the whole definition, xacml and notForPolicy included |
| Fine-grained authorization → Enforce / Stop enforcing | fga-enable / fga-disable |
Sets or clears appFgaEnforced; enabling grants the realm’s remote PEPs |
| Fine-grained authorization → Remote PEPs → Grant the remote PEPs again | fga-enable |
On an enforcing application, grants a remote PEP added since |
| Fine-grained authorization → Create the default policy | fga-create-policy |
Writes the fga-default policy for an application that has none |
| Fine-grained authorization → Reset to the default policy | fga-reset-policy |
Rewrites the policy from fga-default; needs confirm |
| XACML → Policies → Applications’ own policies → Remove | POST /admin-api/xacml/delete |
Deletes a policy no application names |
GET /admin-api/applications?application=<id> returns what the two tabs
draw: page.accessTypes (each type with its xacml rows and notForPolicy)
and page.fga. page.fga holds enforced, reference (the
appFgaPolicy value), policy (its entry name, policyId, version,
enabled, whether it is the application’s own, and the editor link),
usable with the code and why when it cannot be used, types, each
with covered, uncovered, rows and notForPolicy, peps (below), and
applicationPolicies: how many applications’ own policies the realm holds
(count) and the most it may (max, xacml.maxApplicationPolicies).
What enforcing grants the remote PEPs
A remote PEP’s edge PDP forwards each AuthZEN evaluation
about an application that enforces fine-grained authorization to this
service, as an AuthZEN caller. For that, the PEP’s own entry needs
AUTHZEN_PEPS (membership of the authzen-peps group,
roles.authzenPepGroup) and the application in its AuthZEN scope.
Every enable grants both. Each holder of REMOTE_PEPS in the realm — each
member of the remote-peps group (roles.remotePepGroup), whether a person,
a service account or an application — is:
- added to the
authzen-pepsgroup, if it is not a member; and - given the application in its scope, if the scope does not already name it:
stsAuthzenApplicationon a person or service account,appAuthzenApplicationon an application.
Nothing else in its scope changes, and a value is never added twice.
| Holder | In the fga-enable answer’s peps |
|---|---|
| Something was added | granted, with member and scope saying which |
| It had both already | held |
| It could not be granted | failed, with why and code (STS-FGA-0025) |
A holder fails when the group member names no person, service account or
application in the realm, when roles.authzenPepGroup is empty or names no
group, when its scope already names 64 applications, or when the directory
refuses the write. A failure never fails the enable: the application is
enforced, and every other holder is granted. The answer’s message says who
was granted, who already held it and who was not, with why. Audit has
one admin.authzen.pep.granted row per holder changed, and one carrying
STS-FGA-0025 per holder that was not; the group change also has its own
group.add-member row.
- Stop enforcing removes nothing. A membership or a scope value an administrator set by hand looks exactly like one an enable added, so removing them could take away a grant somebody chose. Narrow a scope on XACML → AuthZEN if you need to.
- A PEP added to
remote-pepslater is not granted on its own. Enable again:fga-enableon an application that already enforces grants it and changes nothing else. On the application’s Fine-grained authorization tab, Remote PEPs lists each holder with whether it holdsAUTHZEN_PEPSand has the application in its scope, and offers Grant the remote PEPs again while the application is enforced.page.fga.pepsreturns the same list (holders, each withmemberandscoped, andgroup,authzenGroupandunlocated). - It works the same in development and product mode.
This makes membership of remote-peps a source of AuthZEN access, at the
moment an administrator enforces an application. A remote PEP’s
registration in ou=peps still grants nothing.
Reset to the default policy replaces every rule in the policy with the
one rule of fga-default, which permits every detail. Tick Replace every
rule in this policy first; the API needs "confirm": true. Without it the
reset is refused (STS-FGA-0020) and nothing changes. The policy keeps its
entry name and whether it is enabled. An application with no policy of its
own is refused (STS-FGA-0021); create one instead.
Removing an orphan. XACML → Policies lists every application’s
policy under Applications’ own policies, grouped by application, each
linked into the editor. Make root is never offered for them. A policy
whose application was deleted is marked orphaned since the deletion, and
one no application names any more is marked no application names it; both
have Remove. The policy an application still names has no Remove, and
POST /admin-api/xacml/delete refuses it (STS-FGA-0022). Reset it from
the application’s tab instead. GET /admin-api/xacml/policies returns the
same grouping as applicationPolicies, each policy with referenced and
orphanedAt.
Mapping a type’s members
A row maps one member of a detail of the type to an XACML attribute the policy can test:
| Field | What it is |
|---|---|
| Member path | The member’s name, or a dotted path to a nested member (instructedAmount.amount) |
| Attribute id | The attribute the policy tests, for example urn:example:payment:amount |
| Category | resource (the default), action, environment, access-subject or another of the listed names, or its URI |
| Data type | string (the default) or another of the listed XACML data types |
| Multiplicity | single, or bag for an array |
The type itself and the five common data fields (locations, actions,
datatypes, identifier, privileges) are always mapped and cannot be
rows. A row on the same path replaces the one already there.
A row is checked before it is saved, and a refusal is shown under the field
it is about (STS-FGA-0023). These are refused:
- an attribute id this service writes itself: anything under
urn:sts:xacml:,subject-id,resource-id,action-idand the current-time attributes; - a bare directory attribute name, such as
departmentNumber, in a subject category, which would read as the person’s own attribute; - a category, data type or multiplicity that is not one of the listed ones;
- a path that names the type or a common data field.
While the application enforces fine-grained authorization, a change that
would leave a member uncovered is refused too (STS-FGA-0019), with the
paths named. Taking a row off, for example, is refused until the member is
listed in Not for the policy.
Which applications get a policy when
| How the application appeared | Its policy |
|---|---|
Created on Applications → New, through /admin-api, or by an LDIF import |
Created with it |
Registered by a client (POST /oauth2/register), seen in development mode, or one of this service’s own seeded applications |
Created the first time fine-grained authorization is switched on for it |
Registrations and sightings do not write a policy, because they can be made
by callers who are not administrators. A policy for each of them would let
such a caller fill ou=policies up to xacml.maxApplicationPolicies and
stop applications created by administrators getting one.
The default policy
The fga-default template on XACML → Policies builds the same document,
so you can see it before any application has one. Its PolicyId is
urn:sts:xacml:fga: followed by the application’s identifier, with every
character outside letters, digits and -._~ percent-encoded. For the
application https://rs.example/api it is
urn:sts:xacml:fga:https%3A%2F%2Frs.example%2Fapi. The entry name is
fga-, a readable form of the identifier and twelve hexadecimal digits that
keep two similar identifiers apart, for example
fga-https-rs-example-api-1715052a2eea.
Edit it in the Editor like any other policy; the application’s
Fine-grained authorization tab links to it. Saving, enabling or
disabling it keeps its owner. Make root is not offered for it, and the
management API’s setXacmlRootPolicy refuses it. Reset to the default
policy on that tab writes this document over it again.
Development and product mode
Nothing here differs between the two modes.
Configuration
| Setting | Environment variable | Default | What it does |
|---|---|---|---|
xacml.maxApplicationPolicies |
STS_XACML_MAX_APPLICATION_POLICIES |
500 |
How many applications’ own policies ou=policies may hold, orphaned ones included |
Applications’ own policies have this limit of their own. They do not count
toward xacml.maxPolicies (default 200), which limits the policies
administrators write, and administrators’ policies do not count toward this
one. The default is the default of applications.max, so every application
the registry admits by default can have a policy; raise both together.
An application created when the limit is reached is still created, without
a policy. Switching fine-grained authorization on, or Create the default
policy, is then refused (STS-FGA-0024) until there is room. Removing
orphaned policies on XACML → Policies makes room.
The groups the remote PEPs are granted through are roles.remotePepGroup
and roles.authzenPepGroup; see Remote PEP and
AuthZEN.
In the running service
GET /admin-api/xacml/policiesandGET /xacml/policieslist every policy withapplication(its owner, ornull) andorphanedAt;GET /admin-api/xacml/policiesalso groups them by application inapplicationPolicies.- An application’s Fine-grained authorization tab and
GET /admin-api/applications?application=<id>’spage.fgashow the switch, the policy and each declared type’s coverage. GET /admin-api/ldap/applicationsand/admin/ldap/directoryshowappFgaPolicyandappFgaEnforcedon an application entry./admin/ldap/policiesandGET /admin-api/ldap/policiescount administrators’ policies (ownCount, againstmax) and applications’ own (applicationCount, againstapplicationMax) apart.- An application’s Fine-grained authorization tab lists the realm’s
remote PEPs, and Audit shows each grant as
admin.authzen.pep.granted. GET /xacml/pep/policiesnames withheld policies inwithheld.- XACML → Monitor and
GET /admin-api/xacml/monitorcount the decisions at issuance on thefgarow, and those at access time on theauthzenrow. - Audit shows each decision at issuance as
fga.decision, and each at access time asauthzen.decisionnaming the policy. - Failures are recorded under
STS-FGA-*codes; see error codes.
Related
- XACML 3.0 and ALFA — the engine and the repository.
- Configuring XACML and ALFA — the templates and the policy operations.
- AuthZEN — access evaluations, against the realm’s root policy or an enforcing application’s own.
- Remote PEP — what a remote PEP pulls.