
Guide for Service Admins#
This guide is for the person who runs a service and has to decide who gets in: what arrives when a VO member logs in, how to interpret it correctly, and how to wire it up to what your service already does about permissions.
If you have not read the concept page, the part about the group not being the allocation determines what is your responsibility and what is not. (It also explains why “VO” and “Community” mean the same thing.)
Your part of the bargain
You stop maintaining a list of users. Instead you maintain a much shorter list of groups you trust, and what each one gets. The AAI tells you, at every login, which of those groups the person in front of you belongs to. Whether that is enough to let them in remains entirely your decision.
What arrives at your service#
When a user authenticates through a Community AAI or an infrastructure proxy, you receive a set of attributes - the full list is on the attributes page. The ones that matter here are a persistent, non-reassignable identifier (this, not their email, is who they are - names and email addresses change, the identifier does not), name and email for display, home organisation and affiliation, assurance, and group and role information.
Group and role information arrives as a multivalued attribute:
entitlements for OpenID Connect, eduPersonEntitlement for SAML. Each
value is one URN following
AARC-G069:
urn:geant:dfn.de:nfdi.de:nfdi4cake:group:cakemaster:data:curators
urn:geant:dfn.de:nfdi.de:nfdi4cake:group:cakemaster:wp1:role=manager
urn:geant:dfn.de:nfdi.de:othercommunity:group:glaciology
Three things to notice, because each has bitten somebody:
- The list contains groups from more than one community. Filter on the namespace.
- You are shown the deepest groups the user belongs to. Parent groups may or may not also be listed.
- It is a list of this user’s memberships, not a roster of the VO. You cannot enumerate the group from it.
The same attribute may also carry values with :res: instead of
:group:. Those are
resource capabilities
and mean something quite different - filter on the literal rather than
assuming everything you receive is a group.
Anatomy of an AARC-G069 entitlement#
<NAMESPACE>:group:<GROUP>[:<SUBGROUP>...][:role=<ROLE>][#<AUTHORITY>]
Taking a real example apart:
urn:geant:dfn.de:nfdi.de:nfdi4cake:group:cakemaster:data:curators:role=manager#aai.example.de
└────────── namespace ───────────┘ └lit┘ └─ grp ──┘ └ subgroups ┘ └── role ──┘ └ authority ─┘
| Component | Value | Meaning |
|---|---|---|
<NAMESPACE> |
urn:geant:dfn.de:nfdi.de:nfdi4cake |
Globally unique, delegated from an IANA-registered URN namespace (urn:geant). Variable number of elements. |
group |
group |
Literal discriminator, distinguishing this from res (a resource capability). |
<GROUP> |
cakemaster |
The VO or top-level group. Unique within the namespace. |
<SUBGROUP>… |
data, curators |
The path down the hierarchy. Zero or more. |
role=<ROLE> |
role=manager |
Optional. Scoped to the rightmost group. |
#<AUTHORITY> |
#aai.example.de |
Optional and deprecated. Typically the FQDN of the group management system. |
The NFDI namespace registry is at https://www.nfdi.de/persistent-identifiers.
The five rules that trip everyone up#
Four of the five are normative requirements. Getting them wrong produces either a broken service or a security hole.
1. Membership is implied upwards#
Receiving ...:group:cakemaster:data:curators means the user is a member
of all three of ...:group:cakemaster, ...:group:cakemaster:data and
...:group:cakemaster:data:curators. The specification is explicit: a
relying party receiving <NS>:group:parent:child:grandchild MUST
interpret it as membership of grandchild, child and parent.
This is the single most common implementation bug
If your check is a string equality test against
...:group:cakemaster, every member of every subgroup will be
wrongly refused - and inconsistently, with subgroup members locked
out while top-level members sail through. It looks like an
intermittent fault and it is not.
Match on the hierarchy, not on equality. See matching in practice.
2. You may only receive the deepest value#
The sender MUST send the rightmost (sub)groups and MAY send any or all of the implied ones. Some Community AAIs send the full chain, some only the leaves. So never require the parent group to appear explicitly, and do not be confused when it does - both are legal. Follow rule 1 and this handles itself.
3. Roles imply membership, but do not inherit upwards#
...:group:cakemaster:data:role=manager tells you the user manages
cakemaster:data, and is therefore a plain member of cakemaster:data and
cakemaster.
It does not make them a manager of cakemaster. Role ownership is
scoped strictly to the group it is attached to; treating a subgroup
manager as a VO manager is a privilege escalation.
4. The authority must be ignored when comparing#
The #authority suffix is optional, deprecated, and MUST NOT be taken
into account when determining equivalence. All three of these are the same
entitlement:
...:group:cakemaster:role=manager#authority1
...:group:cakemaster:role=manager#authority2
...:group:cakemaster:role=manager
Never hardcode an authority into a match. It will change, and it is not the security boundary - your trust in the issuer of the token or assertion is. Strip it and compare the rest.
NFDI does use the authority
Although deprecated by the specification, NFDI currently populates the authority component (see attributes). Expect it; treat it as informational, useful for debugging and logging. Do not authorise on it.
5. Mind the case rules#
Case normalisation applies to the namespace only, which is lowercased.
Group, role and authority are not normalised - ...:group:CakeMaster
and ...:group:cakemaster are different groups.
In practice: lowercase the namespace before comparing, compare everything
after :group: case-sensitively, and ask your VO admins to use lowercase
names so the question never arises.
Matching in practice#
The specification’s Annex B gives regular expressions for the common
cases, reproduced here because writing your own tends not to go well.
urn:example:foo stands in for your namespace.
All members of a group, at any depth, with or without a role - the workhorse, and the correct expression of “is this person on the project?”:
^urn:example:foo:group:parentgroup(:[^:#]+)*(:role=[^:#]+)?(#.+)?$
Anyone holding a specific role, in any group:
^urn:example:foo:group(:[^:]+)+:role=myrole(#.+)?$
Members of a particular subgroup, wherever it sits in the tree:
^urn:example:foo:group(:[^:]+)*:mygroup(:[^:]+)*(:role=[^:#]+)?(#.+)?$
Anchor your expressions
Note the ^ and $. Without them you have a substring match, and a
substring match is a privilege escalation waiting to happen.
Unanchored urn:example:foo:group:cakemaster happily matches
urn:example:foo:group:cakemaster-evil, an unrelated group anyone in
the namespace may have created. It also matches
urn:evil:example:group:x#urn:example:foo:group:cakemaster if an
attacker can place a string of their choosing in any component.
Without regular expressions, the safe primitive is prefix matching
that includes the delimiter: accept the value if it equals
<prefix> or starts with <prefix>:. That handles the hierarchy and
cannot be fooled by cakemaster-evil.
Which regular-expression dialect
These use only constructs common to POSIX ERE, PCRE, Python’s re,
Java, Go’s RE2 and JavaScript, so they transfer unchanged to almost
any matcher. Two things do vary by host:
- Basic regular expressions -
grepandsedwithout-E- need(,),?and+backslash-escaped. - Whether matching is anchored by default. Java’s
matches()requires the whole string to match, Python’sre.searchmatches anywhere, and some service configurations wrap your pattern in anchors of their own. Writing^and$explicitly, as above, is correct in all of them.
The whole decision in pseudocode:
NS = "urn:geant:dfn.de:nfdi.de:nfdi4cake" # lowercase, as rule 5 requires
PROJECT = NS + ":group:cakemaster"
def canonicalise(value):
value = value.split("#", 1)[0] # rule 4: drop the authority
ns, sep, rest = value.partition(":group:")
return ns.lower() + sep + rest # rule 5: the namespace, and only that
def is_member_of(entitlements, group):
for value in entitlements:
value = canonicalise(value)
value = value.split(":role=", 1)[0] # a role holder is a plain member too
# rule 1: a deeper value implies every parent, which is also why
# rule 2 needs no code - the parent never has to be sent
if value == group or value.startswith(group + ":"):
return True
return False
def has_role(entitlements, group, role):
for value in entitlements:
if canonicalise(value) == group + ":role=" + role:
return True # rule 3: exact group, no inheritance
return False
canonicalise is where rules 4 and 5 live, and nothing else belongs
there. In particular, do not lowercase the whole value: that would
make CakeMaster match cakemaster, which rule 5 says are different
groups.
Protocol specifics#
OpenID Connect and OAuth 2.0#
Group and role information is carried in the entitlements claim
(RFC 7643,
RFC 9068), multivalued.
For compatibility with the older AARC-G002, implementations may also
send eduperson_entitlement; accept both, prefer entitlements, and do
not assume the two are identical in content.
Request it via the scope value entitlements, the claims request
parameter, or an out-of-band arrangement at client registration -
which is how it normally happens in NFDI.
ID token, userinfo, or introspection?
All three can carry the claim; the right choice depends on size. A user in a large VO with deep subgroups easily carries a few kilobytes of entitlements, and embedding that in every token causes trouble with header limits and cookie stores.
A reasonable default: read entitlements from the userinfo endpoint after login, cache for the session, and keep tokens small. As a resource server validating someone else’s token, use introspection.
SAML 2.0#
Group and role information MUST be communicated in
eduPersonEntitlement. isMemberOf also carries group information,
but the specification mandates eduPersonEntitlement for AARC-compliant
values; if you receive both, the entitlement values are the ones following
this specification.
Request it explicitly
eduPersonEntitlement is not part of the REFEDS Research &
Scholarship bundle. You may request it without breaching R&S
compliance, but you must ask - it does not arrive by default. Say so
during service registration.
Mapping groups onto local authorisation#
Four patterns cover almost everything:
A - Group to role. The group maps to a role or permission set you
already define: ...:group:cakemaster:admins becomes your administrator
role. By far the most common, and the right default.
B - Group to POSIX group, shared directory, or quota. The group maps to a Unix group and a directory with an allocation against it. Batch systems are one familiar example, but anything with per-project storage fits. The division is the useful part: the allocation is yours, the membership is theirs.
C - Group to tenant. In a multi-tenant service, the group selects the tenancy, workspace or namespace the user lands in.
D - Group to an allocation record you already hold. You keep your own project table - who was granted what, until when, with which quota - and the group is the key joining a user to a row in it. You remain the authority on the allocation; the AAI tells you the membership. If you run any kind of allocation process, this is your pattern.
Keep the mapping in configuration, not in code
Which groups you honour and what each grants should live in a config file or database table: readable, reviewable, changeable without a deployment.
Two reasons. Adding a collaboration becomes a config change rather than a release. And when somebody asks “who can access this, and why?” - eventually somebody will, possibly an auditor - that file is the answer.
On provisioning: create local state at first login (just-in-time) rather than in advance, unless something concrete forces otherwise - see provisioning below. Decide up front what happens to that state when someone stops appearing - how long a home directory survives, what happens to data they own - and write it down, because your VO admins will ask.
The other kind: resource capabilities (AARC-G027)#
Sometimes a service genuinely cannot reason about projects and allocations, and needs to be told straightforwardly that this user may do this thing. That is a resource capability, AARC-G027:
<NAMESPACE>:res:<RESOURCE>[:<CHILD-RESOURCE>...][:act:<ACTION>[,<ACTION>...]]#<AUTHORITY>
urn:geant:dfn.de:nfdi.de:nfdi4cake:res:cake-dataset-42:act:read,write#aai.example.de
urn:geant:dfn.de:nfdi.de:nfdi4cake:res:start-vm#aai.example.de
The literal res replaces group, and with it the meaning: this is not a
statement about who the user is with, but that a decision has already been
taken on their behalf.
How it differs from G069#
The two are siblings, not versions - G027 (2018) is unchanged, while G069 (2022) superseded G002 on the group side. They share a namespace and a transport and differ in almost everything else:
| Aspect | G069 - group | G027 - capability |
|---|---|---|
| Question answered | who is this user with? | what may this user do? |
| Nature | a fact for you to evaluate | a decision already taken |
| Discriminator | :group: |
:res: |
| Who is the decision point | you, the service | whoever minted it |
| Modifier | :role= - one, scoped to the rightmost group |
:act: - a comma-separated list, scoped to the rightmost child resource |
| Hierarchy | subgroup membership implies parent membership | no inheritance rule is specified |
| Name uniqueness | group names MUST be unique in the namespace | an implementation decision |
| Authority | optional, deprecated, MUST be ignored when comparing | mandatory, and it matters |
| Portability | the same group means the same thing everywhere | meaningful only to the resource’s operator |
| Maintained by | the VO admin | the service or infrastructure operator |
Three of those reliably cause bugs.
1. There is no implied hierarchy
Rule 1 trained you to walk up the
tree. Do not carry that habit across. G027 specifies no
inheritance: ...:res:cake-dataset-42 does not grant
...:res:cake-dataset-42:subset-a.
If you want parent-implies-child semantics, that is your decision to define, and to document, so whoever mints capabilities for your service knows what they are actually granting.
2. The authority flips meaning
In G069 you MUST ignore the authority. In G027 it is mandatory and identifies who took the decision - which, since you are no longer taking it, is the security control replacing your own policy evaluation.
Same syntactic position, opposite handling. Generic code that strips
#... from every entitlement silently discards the one field that
matters here. Check it against the issuers you trust for that
resource; without that check, anyone able to mint a URN can grant
themselves access.
3. Actions are a list, and their absence is undefined
Split on :act:, split the remainder on ,, compare whole tokens.
Substring matching is worse here than in G069: act:read is found
inside act:readwrite and act:read-metadata, granting more than
intended.
The specification leaves the meaning of a capability with no actions to the implementation. Decide what it means for your service, write it down, and never assume the issuer assumed the same.
Borrow action names rather than inventing them
G027 defines no vocabulary of actions, so read, download, fetch
and get are equally legal and equally unhelpful.
For storage- or compute-shaped resources the most mature set to borrow
from is the
WLCG Common JWT Profile:
storage.read, storage.create, storage.modify, storage.stage,
storage.poll, and compute.read, compute.modify,
compute.create, compute.cancel. Those are JWT scopes rather than
G027 actions, so you borrow the vocabulary and its distinctions, not
the syntax. The create versus modify split - upload without delete,
as against overwrite and delete - is one most people discover they
needed only after shipping.
Choosing between the two#
Prefer groups; reach for capabilities when you have to
A capability moves the decision away from you, to whoever mints it - usually the Community AAI or an infrastructure proxy close to the resource, which keeps it with the organisation that granted the allocation. That is occasionally what you want, and it is what makes otherwise unintegrable services usable.
But then you are no longer the Policy Decision Point, and cannot implement a policy of your own without going back to the issuer. Use capabilities when your service has a fixed permission model that cannot express membership, when it is third-party software you cannot modify, or when granularity is per-object and a group per object would explode. Otherwise use groups.
The honest cost: every capability is authorisation logic that has left your service. It has not gone away - it lives in the AAI now, where somebody else maintains, debugs and audits it.
See authorisation for how both fit among the wider options, including assurance-based and home-IdP-based authorisation.
Things not to do#
- Do not parse group names for meaning beyond the hierarchy. A group
called
adminsis special only because you configured it to be. Harmonising names across communities is explicitly out of scope for the specification, and two VOs may use the same word for different things. - Do not treat the entitlement list as a roster. It tells you what this user has, not who else is in the group.
- Do not depend on ordering. The values are a set.
- Do not authorise on email address or name. Both change, neither is guaranteed unique. Use the persistent identifier.
- Do not cache entitlements beyond the token or session lifetime without a refresh strategy. A removed member living on in your cache is exactly the failure this architecture exists to prevent.
- Do not hardcode the authority (rule 4), and do not strip
#authorityblindly across both kinds - correct for G069, wrong for G027. - Do not apply implied membership to
res:values.
Provisioning#
just-in-time and just-in-case#
Your service probably has to create something locally for each user - an account, a home directory, a quota, a mailing list entry. There are two moments at which that can happen, and they are usually named:
Just-in-time (JIT) provisioning creates the local state at the user’s first login, from the attributes that arrive with it. Nothing exists before the user turns up. This is the common case, it is what works today with nothing but what this guide has already described, and it should be your default.
Just-in-case (JIC) provisioning creates the local state in advance, before the user has ever appeared. Some services genuinely need this: a quota that must exist before a batch job can be submitted, storage that has to be allocated ahead of a deadline, a mailing list that must be complete on day one.
JIC is still under development
Do not assume it is available to you. Support is still being built out, and it is not offered by every Community AAI - so whether you can provision ahead of first login depends on which one your community uses. Check with your consortium’s AAI contact before designing around it.
Both use the same attributes
Whichever moment you provision at, you are working from the same information about the user, and it is the same set this guide has described throughout - identifiers, profile information, group and role membership, assurance.
That set is being specified as AARC-G056, the AARC Profile for Expressing Identity Attributes, which also defines the bindings that carry it - SAML, OpenID Connect, LDAP, and SCIM (RFC 7643), the one that matters for provisioning ahead of a login. So JIT and JIC differ in when and how you obtain the attributes, not in which attributes you get.
JIC is the harder of the two, and not only technically. Pushing or pulling membership before anyone has logged in raises questions of trust between the organisations involved, and of the lawful basis for processing data about a user who has never accessed your service and may never do so. Discussion continues in both NFDI and EOSC.
Practical advice: use JIT unless something concrete forces you not to, and design for it properly - make first login cheap, idempotent (a retried, concurrent or simply repeated first login must not produce a second account or a duplicate home directory), and able to create whatever the user needs on the spot. If you do need JIC, raise it early: you will need to establish that the Community AAI in question supports it at all, and then agree how - a conversation about agreements as much as about interfaces.
Testing and troubleshooting#
Test your matching against a VO that exercises the awkward cases: a top-level group, a subgroup two levels down, a member with a role, a member in the subgroup but not directly in the parent, and a member of a different VO who should be refused. That last one is the test people skip and then regret.
| Symptom | Likely cause |
|---|---|
| No entitlements at all | Attribute not requested. Add the entitlements scope, or request eduPersonEntitlement at registration. |
| Works for some users, not others | Rule 1 - matching on equality, refusing subgroup members. |
| The parent group never shows up | Rule 2 - the AAI sends only leaves. Match on the hierarchy. |
| Role is ignored | Looking for the role on the wrong group; roles do not inherit upwards. |
| Worked yesterday, fails today | An authority changed, or a group was renamed. Rule 4, and talk to the VO admin. |
| Removed user still has access | Session or token not yet expired, or local state provisioned at first login. Expected - see the VO admin guide. |
| A user gets access they should not | Unanchored expression, or prefix match without the delimiter. Check for - and _ variants of your group names. |
| A capability is silently ignored | Filtered out as “not a group”. Match on :res: as well as :group:. |
| An action is granted that should not be | Substring match on the action list. Split on , and compare whole tokens. |
When reporting a problem, include the full raw entitlement values you received. Almost every issue is visible in them, and almost every report omits them.
Checklist#
Integration
- [ ] Service registered, and the entitlement attribute explicitly requested
- [ ] Filtering on the correct namespace
- [ ] Matching handles implied membership (rule 1)
- [ ] Matching does not require parent groups to be sent (rule 2)
- [ ] Roles checked against the right group, no upward inheritance (rule 3)
- [ ] Authority stripped before comparison, never hardcoded (rule 4)
- [ ] Namespace lowercased; group names compared case-sensitively (rule 5)
- [ ] All expressions anchored, or prefix matches include the delimiter
- [ ] Authorising on the persistent identifier, not email or name
Operations
- [ ] Group-to-permission mapping lives in reviewable configuration
- [ ] Entitlement cache lifetime bounded and documented
- [ ] Just-in-time provisioning is idempotent
- [ ] Documented what happens to local state when a user departs
- [ ] Assurance requirements decided, if any - see assurance
- [ ] Tested with a subgroup member, a role holder, and an outsider
If you consume capabilities (AARC-G027)
- [ ]
:res:values distinguished from:group:values, not filtered away - [ ] Authority validated against trusted issuers - not stripped
- [ ] No parent-implies-child assumption, or one defined and documented
- [ ] Action list split on
,and compared as whole tokens - [ ] Meaning of a capability with no actions decided and written down
See also#
- The VO Concept - the model, and why the allocation is yours to own
- Guide for VO Admins - worth skimming, so you know what your VO admins expect from you
- Authorisation options
- Attributes, Assurance
- Service registration requirements and the registration procedure
- AARC-G069, AARC-G027
Last change: Oct 15, 2026 12:30:32