From 2646348e0c9e6e99a069e57963fe2da65d6547f0 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Wed, 9 Sep 2026 10:59:02 +0100 Subject: [PATCH 1/4] feat: update tiled policy to support layered node --- policy/diamond/policy/tiled/tiled.rego | 218 ++++++++++++++------ policy/diamond/policy/tiled/tiled_test.rego | 123 +++++++++-- policy/diamond/policy/ulims/ulims_test.rego | 6 +- 3 files changed, 266 insertions(+), 81 deletions(-) diff --git a/policy/diamond/policy/tiled/tiled.rego b/policy/diamond/policy/tiled/tiled.rego index 17644b2f..65b3e4e6 100644 --- a/policy/diamond/policy/tiled/tiled.rego +++ b/policy/diamond/policy/tiled/tiled.rego @@ -1,12 +1,21 @@ package diamond.policy.tiled -import data.diamond.policy.admin +import data.diamond.policy.beamline as beamline_policy +import data.diamond.policy.proposal import data.diamond.policy.session import data.diamond.policy.token import rego.v1 -# Assign read & write scopes to clients with tiled-writer audience -# defaults to read-only scopes +# METADATA +# title: Tiled Scopes +# description: | +# Says what the caller is allowed to do in tiled. +# Anyone who gets this far may read metadata and data. +# Clients whose token carries the "tiled-writer" audience may also write, +# create nodes and register data. +# Requires: +# - `input.token`, a JWT +# entrypoint: true default scopes := { "read:metadata", "read:data", @@ -23,24 +32,129 @@ scopes := { "tiled-writer" in token.claims.aud } -_session := data.diamond.data.proposals[format_int(input.proposal, 10)].sessions[format_int(input.visit, 10)] +# METADATA +# title: Bare Proposal Number +# description: | +# Turns a proposal label into the plain number it is stored under, +# so "cm1234" becomes 1234. +bare_proposal(p) := to_number(trim_left(lower(p), "abcdefghijklmnopqrstuvwxyz")) + +# METADATA +# title: Session Lookup +# description: | +# Finds the session that `input.visit` refers to within proposal `p`. +# Undefined when the proposal has no such visit, which is how the rules +# below check that a session really exists. +_session(p) := data.diamond.data.proposals[format_int(p, 10)].sessions[format_int(input.visit, 10)] + +# METADATA +# title: Beamline Tag +# description: | +# The access tag for a beamline, the top level of the beamline, proposal +# and session tree. +beamline_tag(bl) := sprintf("beamline:%s", [bl]) + +# METADATA +# title: Proposal Tag +# description: | +# The access tag for a proposal. It names the beamline above it as well, +# so that one flat membership check can match a node at any level of the +# tree. `p` is always the bare proposal number. +proposal_tag(bl, p) := sprintf("beamline:%s,proposal:%s", [bl, format_int(p, 10)]) + +# METADATA +# title: Session Tag +# description: | +# The access tag for a session, the leaf of the tree. Like the proposal +# tag it names every ancestor, and it identifies the session by proposal +# and visit, for example "beamline:i03,proposal:1,session:1-1". +session_tag(bl, p, v) := sprintf("beamline:%s,proposal:%s,session:%s-%d", [bl, format_int(p, 10), format_int(p, 10), v]) + +# METADATA +# title: Proposal Beamlines +# description: | +# The beamlines hosting at least one session of the bare proposal number `p`. +proposal_beamlines(p) := {data.diamond.data.sessions[format_int(s, 10)].beamline | + some s in data.diamond.data.proposals[format_int(p, 10)].sessions +} -# Returns the session ID if the subject has write permissions for the -# specific beamline, visit and proposal requested in the input. -user_session := format_int(_session, 10) if { - session.write_to_beamline_visit - _session +# METADATA +# title: Node Access Tag +# description: | +# The access tag to stamp on the node the caller is creating, or nothing +# at all if they are not allowed to create it. +# tiled's create-node flow (see `init_node` in dls.py) tells the policy +# nothing about the path or the parent, so the only clue to which level is +# being created is which fields arrive alongside the beamline: +# - beamline alone, a beamline node +# - beamline and proposal, a proposal node +# - beamline, proposal and visit, a session node +# The three levels are kept apart by checking that the deeper fields are +# absent. The first rule below is the session level for a named user, who +# must have write access to the beamline, proposal and visit they name, and +# the rules after it cover the other two levels and the service accounts. +# Requires: +# - `input.token`, a JWT +# - `input.beamline`, the beamline the node belongs to +# - `input.proposal`, a proposal label such as "cm1234", for the lower +# two levels +# - `input.visit`, a visit number, for a session node +# entrypoint: true +user_session := session_tag(input.beamline, p, input.visit) if { + input.visit + p := bare_proposal(input.proposal) + session.access_session(token.claims.fedid, p, input.visit) + input.beamline == session.beamline_for(p, input.visit) + _session(p) } -# service account check -user_session := format_int(_session, 10) if { +# Session level, for a service account tied to the beamline. +user_session := session_tag(input.beamline, p, input.visit) if { + input.visit + not token.claims.fedid input.beamline == token.claims.beamline - input.beamline == session.beamline_for(input.proposal, input.visit) - _session in data.diamond.data.beamlines[input.beamline].sessions + p := bare_proposal(input.proposal) + input.beamline == session.beamline_for(p, input.visit) + _session(p) in data.diamond.data.beamlines[input.beamline].sessions +} + +# Proposal level: the caller has access to the proposal, and the proposal +# actually runs on the beamline they asked for. +user_session := proposal_tag(input.beamline, p) if { + not input.visit + input.proposal + p := bare_proposal(input.proposal) + proposal.access_proposal(token.claims.fedid, p) + input.beamline in proposal_beamlines(p) +} + +# Proposal level, for a service account tied to the beamline. +user_session := proposal_tag(input.beamline, p) if { + not input.visit + input.proposal + not token.claims.fedid + input.beamline == token.claims.beamline + p := bare_proposal(input.proposal) + input.beamline in proposal_beamlines(p) +} + +# Beamline level: the caller is authorized on the beamline itself. +user_session := beamline_tag(input.beamline) if { + not input.visit + not input.proposal + input.beamline in beamline_policy.user_beamlines } -# Validates if the subject has permission to modify -# the specific session in the input. +# METADATA +# title: Modify Session +# description: | +# Whether the caller may change an existing session. A named user needs +# access to that session; a service account needs the session to sit on +# the beamline named in its token. +# Requires: +# - `input.token`, a JWT +# - `input.session`, a session id +# entrypoint: true default modify_session := false modify_session if session.access_session( @@ -49,7 +163,7 @@ modify_session if session.access_session( data.diamond.data.sessions[input.session].visit_number, ) -# service account check +# Service accounts may modify any session on their own beamline. modify_session if { not token.claims.fedid session.beamline_for( @@ -58,52 +172,36 @@ modify_session if { ) == token.claims.beamline } -subject := data.diamond.data.subjects[token.claims.fedid] - -# Identifies all beamlines the subject is authorized to access -# based on their assigned permissions. -beamlines contains beamline if { - token.claims.fedid - not admin.is_admin(token.claims.fedid) - some p in subject.permissions - some beamline in object.get(data.diamond.data.admin, p, []) -} - -# Aggregates all session IDs the subject is authorized to view. -# Admins receive a wildcard "*" granting access to all sessions. - -# Regular users gain session access through three pathways: -# 1. Direct session membership -# 2. Access via beamline-level permissions -# 3. Access via proposal-level permissions -user_sessions contains "*" if { - subject - admin.is_admin(token.claims.fedid) -} - -user_sessions contains format_int(session, 10) if { - subject - not admin.is_admin(token.claims.fedid) - some session in subject.sessions -} - -user_sessions contains format_int(session, 10) if { - subject - not admin.is_admin(token.claims.fedid) - some beamline in beamlines - some session in data.diamond.data.beamlines[beamline].sessions +# METADATA +# title: Accessible Node Tags +# description: | +# Every access tag the caller is allowed to see. tiled asks for these when +# listing the tree and keeps the nodes whose own tag is in the set. +# Each session the caller can reach contributes three tags, one for itself +# and one for each of its ancestors, so the beamline and proposal that +# contain a session stay visible alongside the session itself. +# Admins get "*", which stands in for every tag. +# The tags are rebuilt from the numbers in the data bundle, exactly as they +# are built when a node is created, so the two always agree. +# Requires: +# - `input.token`, a JWT +# entrypoint: true +user_sessions contains "*" if "*" in session.user_sessions + +user_sessions contains beamline_tag(s.beamline) if { + not "*" in session.user_sessions + some id in session.user_sessions + s := data.diamond.data.sessions[id] } -user_sessions contains format_int(session, 10) if { - subject - not admin.is_admin(token.claims.fedid) - some p in subject.proposals - some i in data.diamond.data.proposals[format_int(p, 10)] - some session in i +user_sessions contains proposal_tag(s.beamline, s.proposal_number) if { + not "*" in session.user_sessions + some id in session.user_sessions + s := data.diamond.data.sessions[id] } -# service account check -user_sessions contains format_int(session, 10) if { - not subject - some session in data.diamond.data.beamlines[token.claims.beamline].sessions +user_sessions contains session_tag(s.beamline, s.proposal_number, s.visit_number) if { + not "*" in session.user_sessions + some id in session.user_sessions + s := data.diamond.data.sessions[id] } diff --git a/policy/diamond/policy/tiled/tiled_test.rego b/policy/diamond/policy/tiled/tiled_test.rego index 8818c579..e56482da 100644 --- a/policy/diamond/policy/tiled/tiled_test.rego +++ b/policy/diamond/policy/tiled/tiled_test.rego @@ -7,8 +7,7 @@ test_read_scopes if { tiled.scopes == { "read:metadata", "read:data", - } - with data.diamond.policy.token.claims as {} + } with data.diamond.policy.token.claims as {} } test_tiled_writer_given_write_scopes if { @@ -19,8 +18,7 @@ test_tiled_writer_given_write_scopes if { "write:data", "create:node", "register", - } - with data.diamond.policy.token.claims as {"aud": ["tiled-writer"]} + } with data.diamond.policy.token.claims as {"aud": ["tiled-writer"]} } diamond_data := { @@ -95,27 +93,54 @@ diamond_data := { test_user_session_tags if { tiled.user_sessions == set() with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "oscar"} - tiled.user_sessions == {"11", "12"} with data.diamond.data as diamond_data + tiled.user_sessions == { + "beamline:i03", + "beamline:i03,proposal:1", + "beamline:i03,proposal:1,session:1-1", + "beamline:b07", + "beamline:b07,proposal:1", + "beamline:b07,proposal:1,session:1-2", + } with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "alice"} - tiled.user_sessions == {"11", "12", "13", "14"} with data.diamond.data as diamond_data + tiled.user_sessions == { + "beamline:i03", + "beamline:i03,proposal:1", + "beamline:i03,proposal:1,session:1-1", + "beamline:b07", + "beamline:b07,proposal:1", + "beamline:b07,proposal:1,session:1-2", + "beamline:b07,proposal:2", + "beamline:b07,proposal:2,session:2-1", + "beamline:b07,proposal:2,session:2-2", + } with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "bob"} tiled.user_sessions == {"*"} with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "carol"} - tiled.user_sessions == {"13", "14"} with data.diamond.data as diamond_data + tiled.user_sessions == { + "beamline:b07", + "beamline:b07,proposal:2", + "beamline:b07,proposal:2,session:2-1", + "beamline:b07,proposal:2,session:2-2", + } with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "desmond"} - tiled.user_sessions == {"13", "14"} with data.diamond.data as diamond_data + tiled.user_sessions == { + "beamline:b07", + "beamline:b07,proposal:2", + "beamline:b07,proposal:2,session:2-1", + "beamline:b07,proposal:2,session:2-2", + } with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "edna"} } test_user_session_allow if { - tiled.user_session == "11" with data.diamond.data as diamond_data - with input as {"beamline": "i03", "proposal": 1, "visit": 1} + tiled.user_session == "beamline:i03,proposal:1,session:1-1" with data.diamond.data as diamond_data + with input as {"beamline": "i03", "proposal": "cm1", "visit": 1} with data.diamond.policy.token.claims as {"fedid": "carol"} } test_user_session_not_allowed if { not tiled.user_session with data.diamond.data as diamond_data - with input as {"beamline": "i03", "proposal": 1, "visit": 1} + with input as {"beamline": "i03", "proposal": "cm1", "visit": 1} with data.diamond.policy.token.claims as {"fedid": "oscar"} } @@ -134,23 +159,32 @@ test_modify_session if { # Service account tests test_user_session_allow_service_account_on_beamline if { - tiled.user_session == "11" with data.diamond.data as diamond_data - with input as {"beamline": "i03", "proposal": 1, "visit": 1} + tiled.user_session == "beamline:i03,proposal:1,session:1-1" with data.diamond.data as diamond_data + with input as {"beamline": "i03", "proposal": "cm1", "visit": 1} with data.diamond.policy.token.claims as {"beamline": "i03"} } test_user_session_not_allow_service_account_wrong_beamline if { not tiled.user_session with data.diamond.data as diamond_data - with input as {"beamline": "i03", "proposal": 1, "visit": 2} + with input as {"beamline": "i03", "proposal": "cm1", "visit": 2} with data.diamond.policy.token.claims as {"beamline": "b07"} } test_user_session_not_allow_service_account_on_none_existent_beamline_beamline if { not tiled.user_session with data.diamond.data as diamond_data - with input as {"beamline": "i03", "proposal": 1, "visit": 2} + with input as {"beamline": "i03", "proposal": "cm1", "visit": 2} with data.diamond.policy.token.claims as {"beamline": "b007"} } +# A token carrying both fedid and beamline claims should still be judged as +# a named user (fedid's real permissions), not fall into the service-account +# branch just because the beamline claim happens to match the input. +test_user_session_not_allow_named_user_with_beamline_claim_and_no_access if { + not tiled.user_session with data.diamond.data as diamond_data + with input as {"beamline": "b07", "proposal": "cm2", "visit": 1} + with data.diamond.policy.token.claims as {"fedid": "alice", "beamline": "b07"} +} + test_modify_session_on_beamline if { tiled.modify_session with data.diamond.data as diamond_data with input as {"session": "11"} @@ -170,10 +204,65 @@ test_modify_session_on_none_existent_beamline if { } test_user_session_tags_service_account if { - tiled.user_sessions == {"11"} with data.diamond.data as diamond_data + tiled.user_sessions == { + "beamline:i03", + "beamline:i03,proposal:1", + "beamline:i03,proposal:1,session:1-1", + } with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"beamline": "i03"} - tiled.user_sessions == {"12", "13", "14"} with data.diamond.data as diamond_data + tiled.user_sessions == { + "beamline:b07", + "beamline:b07,proposal:1", + "beamline:b07,proposal:1,session:1-2", + "beamline:b07,proposal:2", + "beamline:b07,proposal:2,session:2-1", + "beamline:b07,proposal:2,session:2-2", + } with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"beamline": "b07"} tiled.user_sessions == set() with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"beamline": "b007"} } + +# Level 1 & 2 node creation + +test_user_session_beamline_level_allow if { + tiled.user_session == "beamline:b07" with data.diamond.data as diamond_data + with input as {"beamline": "b07"} + with data.diamond.policy.token.claims as {"fedid": "bob"} +} + +test_user_session_beamline_level_denied if { + not tiled.user_session with data.diamond.data as diamond_data + with input as {"beamline": "b07"} + with data.diamond.policy.token.claims as {"fedid": "alice"} +} + +test_user_session_beamline_level_allow_service_account if { + tiled.user_session == "beamline:i03" with data.diamond.data as diamond_data + with input as {"beamline": "i03"} + with data.diamond.policy.token.claims as {"beamline": "i03"} +} + +test_user_session_proposal_level_allow if { + tiled.user_session == "beamline:i03,proposal:1" with data.diamond.data as diamond_data + with input as {"beamline": "i03", "proposal": "cm1"} + with data.diamond.policy.token.claims as {"fedid": "alice"} +} + +test_user_session_proposal_level_denied_wrong_beamline if { + not tiled.user_session with data.diamond.data as diamond_data + with input as {"beamline": "i04", "proposal": "cm1"} + with data.diamond.policy.token.claims as {"fedid": "alice"} +} + +test_user_session_proposal_level_denied_no_access if { + not tiled.user_session with data.diamond.data as diamond_data + with input as {"beamline": "i03", "proposal": "cm1"} + with data.diamond.policy.token.claims as {"fedid": "oscar"} +} + +test_user_session_proposal_level_allow_service_account if { + tiled.user_session == "beamline:b07,proposal:2" with data.diamond.data as diamond_data + with input as {"beamline": "b07", "proposal": "cm2"} + with data.diamond.policy.token.claims as {"beamline": "b07"} +} diff --git a/policy/diamond/policy/ulims/ulims_test.rego b/policy/diamond/policy/ulims/ulims_test.rego index ed565f26..ba965d48 100644 --- a/policy/diamond/policy/ulims/ulims_test.rego +++ b/policy/diamond/policy/ulims/ulims_test.rego @@ -89,8 +89,7 @@ test_session_restrictions_for_non_admin_1 if { "proposal_number": 1, "visit_number": 2, }, - ] - with data.diamond.data as diamond_data + ] with data.diamond.data as diamond_data with data.diamond.policy.token as {"claims": {"fedid": "alice"}} } @@ -104,8 +103,7 @@ test_session_restrictions_service_account if { "beamline": "i03", "proposal_number": 1, "visit_number": 1, - }] - with data.diamond.data as diamond_data + }] with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"beamline": "i03"} } From 0269b6632e5b1c87e77d51d17d102bf239eedfa3 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:24:32 +0100 Subject: [PATCH 2/4] feat: grant tiled access at the level it is held user_sessions returned three tags for every session a caller could reach, so a beamline admin's answer covered every session on the beamline (about 106k tags and ~4.7 s for a non-admin who can reach everything). Return grants at the level they are held instead, and stamp each node with its ancestors' grant tags so one grant matches the whole subtree: - beamline admins and service accounts get the beamline tag - proposal members get the proposal tag - session members get the session tag plus "visible:" tags for the containers above a grant, so they can be listed without granting what else is under them. user_session now returns the node's full tag list, own tag first. The beamline claim in user_beamlines now only counts for service accounts: a token that also carries a fedid was granted the whole beamline through it, in tiled, session and ulims. Co-Authored-By: Claude Opus 5.5 --- policy/diamond/policy/beamline/beamline.rego | 3 + .../policy/beamline/beamline_test.rego | 9 ++ policy/diamond/policy/tiled/tiled.rego | 93 +++++++++---- policy/diamond/policy/tiled/tiled_test.rego | 131 +++++++++++++----- 4 files changed, 174 insertions(+), 62 deletions(-) diff --git a/policy/diamond/policy/beamline/beamline.rego b/policy/diamond/policy/beamline/beamline.rego index c9c8bec1..9fe52fab 100644 --- a/policy/diamond/policy/beamline/beamline.rego +++ b/policy/diamond/policy/beamline/beamline.rego @@ -24,6 +24,9 @@ user_beamlines contains beamline if { some beamline in object.keys(data.diamond.data.beamlines) } +# Service accounts only: a token that also names a user (fedid) is judged on +# that user's own permissions, never on its beamline claim. user_beamlines contains token.claims.beamline if { + not token.claims.fedid token.claims.beamline in object.keys(data.diamond.data.beamlines) } diff --git a/policy/diamond/policy/beamline/beamline_test.rego b/policy/diamond/policy/beamline/beamline_test.rego index 78cdc2ef..20dd7b54 100644 --- a/policy/diamond/policy/beamline/beamline_test.rego +++ b/policy/diamond/policy/beamline/beamline_test.rego @@ -96,3 +96,12 @@ test_user_beamlines_service_account_bad_beamline_claim if { beamline.user_beamlines == set() with data.diamond.policy.token.claims as {"beamline": "area-51-beamline"} with data.diamond.data as diamond_data } + +# A beamline claim only counts for service accounts; a named user keeps their +# own beamlines whatever the token's beamline claim says. +test_user_beamlines_named_user_ignores_beamline_claim if { + beamline.user_beamlines == set() with data.diamond.policy.token.claims as {"fedid": "alice", "beamline": "b07"} + with data.diamond.data as diamond_data + beamline.user_beamlines == {"b07"} with data.diamond.policy.token.claims as {"fedid": "bob", "beamline": "i03"} + with data.diamond.data as diamond_data +} diff --git a/policy/diamond/policy/tiled/tiled.rego b/policy/diamond/policy/tiled/tiled.rego index 65b3e4e6..49ae5a52 100644 --- a/policy/diamond/policy/tiled/tiled.rego +++ b/policy/diamond/policy/tiled/tiled.rego @@ -1,5 +1,6 @@ package diamond.policy.tiled +import data.diamond.policy.admin import data.diamond.policy.beamline as beamline_policy import data.diamond.policy.proposal import data.diamond.policy.session @@ -70,6 +71,29 @@ proposal_tag(bl, p) := sprintf("beamline:%s,proposal:%s", [bl, format_int(p, 10) # and visit, for example "beamline:i03,proposal:1,session:1-1". session_tag(bl, p, v) := sprintf("beamline:%s,proposal:%s,session:%s-%d", [bl, format_int(p, 10), format_int(p, 10), v]) +# METADATA +# title: Visible Tag +# description: | +# Lets a caller list a beamline or proposal node without being granted +# what is under it. tiled matches tags exactly, so this is a separate tag +# from the grant `t`: a session member gets the visible tags of the +# proposal and beamline above them, never their grant tags. +visible_tag(t) := concat("", ["visible:", t]) + +# METADATA +# title: Node Tags +# description: | +# The tags stamped on a node at each level: its own tag, the grant tag of +# every ancestor, so one grant matches a whole subtree, and for the two +# container levels, a visible tag so callers granted something below can +# still list it. The node's own tag is first; dls.py reads it back from +# `tags[0]`. +beamline_node_tags(bl) := [beamline_tag(bl), visible_tag(beamline_tag(bl))] + +proposal_node_tags(bl, p) := [proposal_tag(bl, p), beamline_tag(bl), visible_tag(proposal_tag(bl, p))] + +session_node_tags(bl, p, v) := [session_tag(bl, p, v), proposal_tag(bl, p), beamline_tag(bl)] + # METADATA # title: Proposal Beamlines # description: | @@ -79,10 +103,10 @@ proposal_beamlines(p) := {data.diamond.data.sessions[format_int(s, 10)].beamline } # METADATA -# title: Node Access Tag +# title: Node Access Tags # description: | -# The access tag to stamp on the node the caller is creating, or nothing -# at all if they are not allowed to create it. +# The access tags to stamp on the node the caller is creating (see Node +# Tags), or nothing at all if they are not allowed to create it. # tiled's create-node flow (see `init_node` in dls.py) tells the policy # nothing about the path or the parent, so the only clue to which level is # being created is which fields arrive alongside the beamline: @@ -100,7 +124,7 @@ proposal_beamlines(p) := {data.diamond.data.sessions[format_int(s, 10)].beamline # two levels # - `input.visit`, a visit number, for a session node # entrypoint: true -user_session := session_tag(input.beamline, p, input.visit) if { +user_session := session_node_tags(input.beamline, p, input.visit) if { input.visit p := bare_proposal(input.proposal) session.access_session(token.claims.fedid, p, input.visit) @@ -109,7 +133,7 @@ user_session := session_tag(input.beamline, p, input.visit) if { } # Session level, for a service account tied to the beamline. -user_session := session_tag(input.beamline, p, input.visit) if { +user_session := session_node_tags(input.beamline, p, input.visit) if { input.visit not token.claims.fedid input.beamline == token.claims.beamline @@ -120,7 +144,7 @@ user_session := session_tag(input.beamline, p, input.visit) if { # Proposal level: the caller has access to the proposal, and the proposal # actually runs on the beamline they asked for. -user_session := proposal_tag(input.beamline, p) if { +user_session := proposal_node_tags(input.beamline, p) if { not input.visit input.proposal p := bare_proposal(input.proposal) @@ -129,7 +153,7 @@ user_session := proposal_tag(input.beamline, p) if { } # Proposal level, for a service account tied to the beamline. -user_session := proposal_tag(input.beamline, p) if { +user_session := proposal_node_tags(input.beamline, p) if { not input.visit input.proposal not token.claims.fedid @@ -139,7 +163,7 @@ user_session := proposal_tag(input.beamline, p) if { } # Beamline level: the caller is authorized on the beamline itself. -user_session := beamline_tag(input.beamline) if { +user_session := beamline_node_tags(input.beamline) if { not input.visit not input.proposal input.beamline in beamline_policy.user_beamlines @@ -172,36 +196,49 @@ modify_session if { ) == token.claims.beamline } +# Defaulted so `not _is_admin` holds for service accounts, whose token has no +# fedid: `not admin.is_admin(token.claims.fedid)` would be undefined for them. +default _is_admin := false + +_is_admin if admin.is_admin(token.claims.fedid) + # METADATA # title: Accessible Node Tags # description: | -# Every access tag the caller is allowed to see. tiled asks for these when -# listing the tree and keeps the nodes whose own tag is in the set. -# Each session the caller can reach contributes three tags, one for itself -# and one for each of its ancestors, so the beamline and proposal that -# contain a session stay visible alongside the session itself. +# The tags the caller may see. tiled asks for these when listing the tree +# and keeps the nodes carrying any of them (an exact match on each tag). +# Access is returned at the level it is granted, not expanded into every +# session below it: nodes carry their ancestors' grant tags (see Node Tags), +# so a beamline grant is one tag however many sessions the beamline has. +# - beamline admins and service accounts: the beamline tag +# - proposal members: the proposal tag, plus the beamline's visible tag +# - session members: the session tag, plus the proposal's and beamline's +# visible tags # Admins get "*", which stands in for every tag. -# The tags are rebuilt from the numbers in the data bundle, exactly as they -# are built when a node is created, so the two always agree. # Requires: # - `input.token`, a JWT # entrypoint: true -user_sessions contains "*" if "*" in session.user_sessions +user_sessions contains "*" if _is_admin -user_sessions contains beamline_tag(s.beamline) if { - not "*" in session.user_sessions - some id in session.user_sessions - s := data.diamond.data.sessions[id] +user_sessions contains beamline_tag(bl) if { + not _is_admin + some bl in beamline_policy.user_beamlines } -user_sessions contains proposal_tag(s.beamline, s.proposal_number) if { - not "*" in session.user_sessions - some id in session.user_sessions - s := data.diamond.data.sessions[id] +user_sessions contains tag if { + not _is_admin + some p in session.subject.proposals + some bl in proposal_beamlines(p) + some tag in [proposal_tag(bl, p), visible_tag(beamline_tag(bl))] } -user_sessions contains session_tag(s.beamline, s.proposal_number, s.visit_number) if { - not "*" in session.user_sessions - some id in session.user_sessions - s := data.diamond.data.sessions[id] +user_sessions contains tag if { + not _is_admin + some id in session.subject.sessions + s := data.diamond.data.sessions[format_int(id, 10)] + some tag in [ + session_tag(s.beamline, s.proposal_number, s.visit_number), + visible_tag(proposal_tag(s.beamline, s.proposal_number)), + visible_tag(beamline_tag(s.beamline)), + ] } diff --git a/policy/diamond/policy/tiled/tiled_test.rego b/policy/diamond/policy/tiled/tiled_test.rego index e56482da..6c27148d 100644 --- a/policy/diamond/policy/tiled/tiled_test.rego +++ b/policy/diamond/policy/tiled/tiled_test.rego @@ -93,47 +93,81 @@ diamond_data := { test_user_session_tags if { tiled.user_sessions == set() with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "oscar"} + + # proposal member: proposal 1 runs on i03 and b07 tiled.user_sessions == { - "beamline:i03", "beamline:i03,proposal:1", - "beamline:i03,proposal:1,session:1-1", - "beamline:b07", + "visible:beamline:i03", "beamline:b07,proposal:1", - "beamline:b07,proposal:1,session:1-2", + "visible:beamline:b07", } with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "alice"} + + # b07 admin, and member of session 11 on i03: the beamline is one tag tiled.user_sessions == { - "beamline:i03", - "beamline:i03,proposal:1", - "beamline:i03,proposal:1,session:1-1", "beamline:b07", - "beamline:b07,proposal:1", - "beamline:b07,proposal:1,session:1-2", - "beamline:b07,proposal:2", - "beamline:b07,proposal:2,session:2-1", - "beamline:b07,proposal:2,session:2-2", + "beamline:i03,proposal:1,session:1-1", + "visible:beamline:i03,proposal:1", + "visible:beamline:i03", } with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "bob"} + tiled.user_sessions == {"*"} with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "carol"} + tiled.user_sessions == { - "beamline:b07", "beamline:b07,proposal:2", + "visible:beamline:b07", "beamline:b07,proposal:2,session:2-1", - "beamline:b07,proposal:2,session:2-2", + "visible:beamline:b07,proposal:2", } with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "desmond"} + tiled.user_sessions == { - "beamline:b07", "beamline:b07,proposal:2", + "visible:beamline:b07", "beamline:b07,proposal:2,session:2-1", "beamline:b07,proposal:2,session:2-2", + "visible:beamline:b07,proposal:2", } with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"fedid": "edna"} } +# tiled shows a node when any of its tags is in user_sessions. A session +# member must see the beamline and proposal above them and their own session, +# but not a sibling session or the beamline's other proposals. +shown(node_tags, allowed) if { + some t in node_tags + t in allowed +} + +test_node_visibility_session_member if { + # bob is a member of session 11 (i03, proposal 1, visit 1) + allowed := tiled.user_sessions with data.diamond.data as diamond_data + with data.diamond.policy.token.claims as {"fedid": "bob"} + shown(tiled.beamline_node_tags("i03"), allowed) + shown(tiled.proposal_node_tags("i03", 1), allowed) + shown(tiled.session_node_tags("i03", 1, 1), allowed) + not shown(tiled.session_node_tags("i03", 1, 2), allowed) + not shown(tiled.proposal_node_tags("i03", 2), allowed) +} + +test_node_visibility_beamline_grant if { + allowed := tiled.user_sessions with data.diamond.data as diamond_data + with data.diamond.policy.token.claims as {"beamline": "b07"} + shown(tiled.beamline_node_tags("b07"), allowed) + shown(tiled.proposal_node_tags("b07", 2), allowed) + shown(tiled.session_node_tags("b07", 2, 2), allowed) + not shown(tiled.beamline_node_tags("i03"), allowed) + not shown(tiled.session_node_tags("i03", 1, 1), allowed) +} + test_user_session_allow if { - tiled.user_session == "beamline:i03,proposal:1,session:1-1" with data.diamond.data as diamond_data + tiled.user_session == [ + "beamline:i03,proposal:1,session:1-1", + "beamline:i03,proposal:1", + "beamline:i03", + ] with data.diamond.data as diamond_data with input as {"beamline": "i03", "proposal": "cm1", "visit": 1} with data.diamond.policy.token.claims as {"fedid": "carol"} } @@ -159,7 +193,11 @@ test_modify_session if { # Service account tests test_user_session_allow_service_account_on_beamline if { - tiled.user_session == "beamline:i03,proposal:1,session:1-1" with data.diamond.data as diamond_data + tiled.user_session == [ + "beamline:i03,proposal:1,session:1-1", + "beamline:i03,proposal:1", + "beamline:i03", + ] with data.diamond.data as diamond_data with input as {"beamline": "i03", "proposal": "cm1", "visit": 1} with data.diamond.policy.token.claims as {"beamline": "i03"} } @@ -185,6 +223,28 @@ test_user_session_not_allow_named_user_with_beamline_claim_and_no_access if { with data.diamond.policy.token.claims as {"fedid": "alice", "beamline": "b07"} } +# Same token (fedid and a beamline claim): no service-account access at any +# level, only what alice is granted herself. +test_named_user_with_beamline_claim_gets_only_own_access if { + claims := {"fedid": "alice", "beamline": "b07"} + allowed := tiled.user_sessions with data.diamond.data as diamond_data + with data.diamond.policy.token.claims as claims + allowed == tiled.user_sessions with data.diamond.data as diamond_data + with data.diamond.policy.token.claims as {"fedid": "alice"} + not "beamline:b07" in allowed + not shown(tiled.proposal_node_tags("b07", 2), allowed) + not shown(tiled.session_node_tags("b07", 2, 1), allowed) + not tiled.user_session with data.diamond.data as diamond_data + with input as {"beamline": "b07"} + with data.diamond.policy.token.claims as claims + not tiled.user_session with data.diamond.data as diamond_data + with input as {"beamline": "b07", "proposal": "cm2"} + with data.diamond.policy.token.claims as claims + not tiled.modify_session with data.diamond.data as diamond_data + with input as {"session": "13"} + with data.diamond.policy.token.claims as claims +} + test_modify_session_on_beamline if { tiled.modify_session with data.diamond.data as diamond_data with input as {"session": "11"} @@ -204,20 +264,9 @@ test_modify_session_on_none_existent_beamline if { } test_user_session_tags_service_account if { - tiled.user_sessions == { - "beamline:i03", - "beamline:i03,proposal:1", - "beamline:i03,proposal:1,session:1-1", - } with data.diamond.data as diamond_data + tiled.user_sessions == {"beamline:i03"} with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"beamline": "i03"} - tiled.user_sessions == { - "beamline:b07", - "beamline:b07,proposal:1", - "beamline:b07,proposal:1,session:1-2", - "beamline:b07,proposal:2", - "beamline:b07,proposal:2,session:2-1", - "beamline:b07,proposal:2,session:2-2", - } with data.diamond.data as diamond_data + tiled.user_sessions == {"beamline:b07"} with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"beamline": "b07"} tiled.user_sessions == set() with data.diamond.data as diamond_data with data.diamond.policy.token.claims as {"beamline": "b007"} @@ -226,7 +275,10 @@ test_user_session_tags_service_account if { # Level 1 & 2 node creation test_user_session_beamline_level_allow if { - tiled.user_session == "beamline:b07" with data.diamond.data as diamond_data + tiled.user_session == [ + "beamline:b07", + "visible:beamline:b07", + ] with data.diamond.data as diamond_data with input as {"beamline": "b07"} with data.diamond.policy.token.claims as {"fedid": "bob"} } @@ -238,13 +290,20 @@ test_user_session_beamline_level_denied if { } test_user_session_beamline_level_allow_service_account if { - tiled.user_session == "beamline:i03" with data.diamond.data as diamond_data + tiled.user_session == [ + "beamline:i03", + "visible:beamline:i03", + ] with data.diamond.data as diamond_data with input as {"beamline": "i03"} with data.diamond.policy.token.claims as {"beamline": "i03"} } test_user_session_proposal_level_allow if { - tiled.user_session == "beamline:i03,proposal:1" with data.diamond.data as diamond_data + tiled.user_session == [ + "beamline:i03,proposal:1", + "beamline:i03", + "visible:beamline:i03,proposal:1", + ] with data.diamond.data as diamond_data with input as {"beamline": "i03", "proposal": "cm1"} with data.diamond.policy.token.claims as {"fedid": "alice"} } @@ -262,7 +321,11 @@ test_user_session_proposal_level_denied_no_access if { } test_user_session_proposal_level_allow_service_account if { - tiled.user_session == "beamline:b07,proposal:2" with data.diamond.data as diamond_data + tiled.user_session == [ + "beamline:b07,proposal:2", + "beamline:b07", + "visible:beamline:b07,proposal:2", + ] with data.diamond.data as diamond_data with input as {"beamline": "b07", "proposal": "cm2"} with data.diamond.policy.token.claims as {"beamline": "b07"} } From 1079af4db41060750d8c80a498c2c6f76aa2d649 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:44:18 +0100 Subject: [PATCH 3/4] test: cover the tiled endpoints with a worked i22 tree Calls tiled/user_session, tiled/user_sessions and tiled/scopes with the inputs dls_tiled.policy's build_input sends, on a small i22 tree the writer builds through user_session, and checks exactly which nodes each kind of caller sees: service account, beamline admin, proposal member, session member, super admin, nobody. One test shows that passing the containers with grant tags instead of visible: tags would leak the whole beamline. The header explains how tiled matches tags and why nodes carry their ancestors' grants plus visible: tags on containers. Co-Authored-By: Claude Opus 5.5 --- .../policy/tiled/tiled_endpoints_test.rego | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 policy/diamond/policy/tiled/tiled_endpoints_test.rego diff --git a/policy/diamond/policy/tiled/tiled_endpoints_test.rego b/policy/diamond/policy/tiled/tiled_endpoints_test.rego new file mode 100644 index 00000000..d0aace1d --- /dev/null +++ b/policy/diamond/policy/tiled/tiled_endpoints_test.rego @@ -0,0 +1,215 @@ +# The three endpoints tiled calls through dls_tiled.policy, with the inputs +# its build_input sends ({"audience", "token"} plus an access blob's fields): +# +# tiled/user_session init_node: the tags to stamp on a node being created +# tiled/user_sessions filters: the tags the caller may see, on every request +# tiled/scopes what the caller may do +# +# How tiled uses the tags +# +# dls_tiled.policy hands tiled the caller's user_sessions list, and tiled +# turns it into one SQL condition applied to every node it looks at: a node is +# visible if any of its tags is in the list (access_blob->'tags' ?| [...]). +# Two properties of that rule drive everything below: +# 1. It is exact string matching: no prefixes, wildcards or hierarchy, which +# is also why a tag like "beamline:i22,proposal:*" can't work. +# 2. It is per node: tiled doesn't know i22/cm1 is "inside" i22, so each +# node is judged by its own tags only, and opening i22/cm1/cm1-1 means +# passing the check at i22, at i22/cm1 and at i22/cm1/cm1-1. A hidden +# container hides everything under it. +# +# Grant tags: one grant covers a subtree +# +# So that a beamline admin needs one tag rather than one per session, every +# node carries the grant tags of all its ancestors as well as its own (the +# node's own tag first; dls_tiled.policy reads tags[0] back): +# +# i22 beamline:i22 visible:beamline:i22 +# i22/cm1 beamline:i22,proposal:1 beamline:i22 visible:beamline:i22,proposal:1 +# i22/cm1/cm1-1 beamline:i22,proposal:1,session:1-1 beamline:i22,proposal:1 beamline:i22 +# (runs in cm1-1) same as cm1-1 +# +# beamline:i22 is on every node of the beamline, which is why holding it shows +# you all of i22. Each tag on a session says who sees it: its members (session +# tag), its proposal's members (proposal tag), the beamline's admins and +# service account (beamline tag). +# +# Visible tags: see a container without its contents +# +# A session member must still pass i22 and i22/cm1 to reach their session. +# Giving them beamline:i22 for that would also match i22/cm2 and the sibling +# session i22/cm1/cm1-2, which carry it too: they'd be a beamline admin. The +# same goes for the proposal tag and cm1-2. A grant tag's scope is a node and +# its whole subtree; what's needed is a tag whose scope is exactly one node. +# That is visible:: stamped on that container only, never on anything +# below it. The beamline node carries both beamline:i22 (so beamline admins +# see it) and visible:beamline:i22 (so members of anything below can pass it). +# Sessions need no visible tag: anyone who should see one holds a grant that +# is already on it. +# +# What user_sessions returns +# +# service account for i22 beamline:i22 +# i22 beamline admin beamline:i22 +# proposal 1 member beamline:i22,proposal:1, visible:beamline:i22 +# session cm1-1 member beamline:i22,proposal:1,session:1-1, +# visible:beamline:i22,proposal:1, visible:beamline:i22 +# super admin * (dls_tiled.policy applies no filter) +# +# It grows with the grants a caller holds, not with the sessions under them. +package diamond.policy.tiled_test + +import data.diamond.policy.tiled +import rego.v1 + +i22_data := { + "subjects": { + "alice": {"permissions": [], "proposals": [], "sessions": [11]}, + "bob": {"permissions": ["i22_admin"], "proposals": [], "sessions": []}, + "carol": {"permissions": ["super_admin"], "proposals": [], "sessions": []}, + "dave": {"permissions": [], "proposals": [1], "sessions": []}, + "oscar": {"permissions": [], "proposals": [], "sessions": []}, + }, + "sessions": { + "11": {"beamline": "i22", "proposal_number": 1, "visit_number": 1}, + "12": {"beamline": "i22", "proposal_number": 1, "visit_number": 2}, + "21": {"beamline": "i22", "proposal_number": 2, "visit_number": 1}, + "31": {"beamline": "b07", "proposal_number": 3, "visit_number": 1}, + }, + "proposals": { + "1": {"sessions": {"1": 11, "2": 12}}, + "2": {"sessions": {"1": 21}}, + "3": {"sessions": {"1": 31}}, + }, + "beamlines": {"i22": {"sessions": [11, 12, 21]}, "b07": {"sessions": [31]}}, + "admin": {"i22_admin": ["i22"]}, +} + +# What dls_tiled.policy's build_input always sends; the token itself is +# replaced by its claims in each test. +request := {"audience": "account", "token": ""} + +writer := {"beamline": "i22", "aud": ["tiled-writer", "account"]} + +# The tree TiledWriter's service account builds, as test_create_node_tags +# checks user_session stamps it. Runs carry their session's tags. +nodes := { + "b07": ["beamline:b07", "visible:beamline:b07"], + "i22": ["beamline:i22", "visible:beamline:i22"], + "i22/cm1": ["beamline:i22,proposal:1", "beamline:i22", "visible:beamline:i22,proposal:1"], + "i22/cm2": ["beamline:i22,proposal:2", "beamline:i22", "visible:beamline:i22,proposal:2"], + "i22/cm1/cm1-1": ["beamline:i22,proposal:1,session:1-1", "beamline:i22,proposal:1", "beamline:i22"], + "i22/cm1/cm1-2": ["beamline:i22,proposal:1,session:1-2", "beamline:i22,proposal:1", "beamline:i22"], + "i22/cm2/cm2-1": ["beamline:i22,proposal:2,session:2-1", "beamline:i22,proposal:2", "beamline:i22"], +} + +# The nodes tiled shows a caller whose user_sessions is `allowed`. +seen(allowed) := {name | + some name, tags in nodes + some tag in tags + tag in allowed +} + +# --- tiled/user_session: what blueapi's containers and runs are stamped with + +test_create_node_tags if { + tiled.user_session == nodes.i22 with input as object.union(request, {"beamline": "i22"}) + with data.diamond.policy.token.claims as writer + with data.diamond.data as i22_data + tiled.user_session == nodes["i22/cm1"] with input as object.union(request, {"beamline": "i22", "proposal": "cm1"}) + with data.diamond.policy.token.claims as writer + with data.diamond.data as i22_data + tiled.user_session == nodes["i22/cm1/cm1-1"] with input as object.union( + request, + {"beamline": "i22", "proposal": "cm1", "visit": 1}, + ) + with data.diamond.policy.token.claims as writer + with data.diamond.data as i22_data +} + +test_create_node_refused_on_another_beamline if { + not tiled.user_session with input as object.union(request, {"beamline": "b07"}) + with data.diamond.policy.token.claims as writer + with data.diamond.data as i22_data + not tiled.user_session with input as object.union(request, {"beamline": "b07", "proposal": "cm3", "visit": 1}) + with data.diamond.policy.token.claims as writer + with data.diamond.data as i22_data +} + +# --- tiled/user_sessions: what each kind of caller sees + +test_service_account_sees_its_beamline if { + allowed := tiled.user_sessions with input as request + with data.diamond.policy.token.claims as writer + with data.diamond.data as i22_data + allowed == {"beamline:i22"} + seen(allowed) == {"i22", "i22/cm1", "i22/cm2", "i22/cm1/cm1-1", "i22/cm1/cm1-2", "i22/cm2/cm2-1"} +} + +test_beamline_admin_sees_the_beamline if { + allowed := tiled.user_sessions with input as request + with data.diamond.policy.token.claims as {"fedid": "bob"} + with data.diamond.data as i22_data + allowed == {"beamline:i22"} + seen(allowed) == {"i22", "i22/cm1", "i22/cm2", "i22/cm1/cm1-1", "i22/cm1/cm1-2", "i22/cm2/cm2-1"} +} + +test_proposal_member_sees_the_proposal_and_the_beamline_node if { + allowed := tiled.user_sessions with input as request + with data.diamond.policy.token.claims as {"fedid": "dave"} + with data.diamond.data as i22_data + allowed == {"beamline:i22,proposal:1", "visible:beamline:i22"} + seen(allowed) == {"i22", "i22/cm1", "i22/cm1/cm1-1", "i22/cm1/cm1-2"} +} + +test_session_member_sees_the_session_and_the_containers_above_it if { + allowed := tiled.user_sessions with input as request + with data.diamond.policy.token.claims as {"fedid": "alice"} + with data.diamond.data as i22_data + allowed == { + "beamline:i22,proposal:1,session:1-1", + "visible:beamline:i22,proposal:1", + "visible:beamline:i22", + } + seen(allowed) == {"i22", "i22/cm1", "i22/cm1/cm1-1"} +} + +# Why the visible tags exist: passing the containers with their grant tags +# instead would show alice the whole beamline. +test_reusing_grant_tags_for_the_containers_would_leak if { + allowed := {"beamline:i22,proposal:1,session:1-1", "beamline:i22,proposal:1", "beamline:i22"} + seen(allowed) == {"i22", "i22/cm1", "i22/cm2", "i22/cm1/cm1-1", "i22/cm1/cm1-2", "i22/cm2/cm2-1"} +} + +test_super_admin_gets_the_wildcard if { + tiled.user_sessions == {"*"} with input as request + with data.diamond.policy.token.claims as {"fedid": "carol"} + with data.diamond.data as i22_data +} + +test_no_permissions_sees_nothing if { + allowed := tiled.user_sessions with input as request + with data.diamond.policy.token.claims as {"fedid": "oscar"} + with data.diamond.data as i22_data + allowed == set() + seen(allowed) == set() +} + +# --- tiled/scopes + +test_writer_may_create_and_write if { + tiled.scopes == { + "read:metadata", + "read:data", + "write:metadata", + "write:data", + "create:node", + "register", + } with input as request + with data.diamond.policy.token.claims as writer +} + +test_user_may_only_read if { + tiled.scopes == {"read:metadata", "read:data"} with input as request + with data.diamond.policy.token.claims as {"fedid": "alice", "aud": ["account"]} +} From 452bd959afecfc358c46606b65fe9a17ebac6d05 Mon Sep 17 00:00:00 2001 From: Zoheb Shaikh <26975142+ZohebShaikh@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:46:17 +0100 Subject: [PATCH 4/4] chore: pin OPA 1.21.2 and Regal 0.43.0 CI used the latest OPA and Regal, so a new release could change results without any change here. Pin both, and the devcontainer's OPA, and make the policy pass them: string interpolation where Regal now prefers it, admin_test checks beamline_admin_for_subject by key (OPA 1.21's type checker rejects `in` on it), and blueapi.rego drops an unused import. Co-Authored-By: Claude Opus 5.5 --- .devcontainer/docker-compose.yml | 2 +- .github/workflows/_policy_container.yaml | 2 +- .github/workflows/_policy_lint.yaml | 2 +- .github/workflows/_policy_test.yaml | 2 +- policy/diamond/policy/admin/admin_test.rego | 4 ++-- policy/diamond/policy/blueapi/blueapi.rego | 1 - policy/diamond/policy/tiled/tiled.rego | 6 +++--- policy/diamond/policy/token/token.rego | 2 +- 8 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index c56de223..c44e429c 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -15,7 +15,7 @@ services: BUNDLER_OTEL_COLLECTOR_URL: http://collector:4317 opa: - image: docker.io/openpolicyagent/opa:0.59.0 + image: docker.io/openpolicyagent/opa:1.21.2 restart: unless-stopped command: > run diff --git a/.github/workflows/_policy_container.yaml b/.github/workflows/_policy_container.yaml index fcb51811..066d29d1 100644 --- a/.github/workflows/_policy_container.yaml +++ b/.github/workflows/_policy_container.yaml @@ -26,7 +26,7 @@ jobs: - name: Setup OPA uses: open-policy-agent/setup-opa@v2.2.0 with: - version: latest + version: 1.21.2 - name: Build OPA Policy run: > diff --git a/.github/workflows/_policy_lint.yaml b/.github/workflows/_policy_lint.yaml index 82ff8234..db775732 100644 --- a/.github/workflows/_policy_lint.yaml +++ b/.github/workflows/_policy_lint.yaml @@ -13,7 +13,7 @@ jobs: - name: Setup Regal uses: open-policy-agent/setup-regal@761188c3b435761fa254beca508a44875619648f #v2.0.0 with: - version: latest + version: 0.43.0 - name: Lint run: > diff --git a/.github/workflows/_policy_test.yaml b/.github/workflows/_policy_test.yaml index 4fb17995..960f3835 100644 --- a/.github/workflows/_policy_test.yaml +++ b/.github/workflows/_policy_test.yaml @@ -13,7 +13,7 @@ jobs: - name: Setup OPA uses: open-policy-agent/setup-opa@v2.2.0 with: - version: latest + version: 1.21.2 - name: Test run: opa test ./policy -v diff --git a/policy/diamond/policy/admin/admin_test.rego b/policy/diamond/policy/admin/admin_test.rego index 9d333802..9c41ec7c 100644 --- a/policy/diamond/policy/admin/admin_test.rego +++ b/policy/diamond/policy/admin/admin_test.rego @@ -53,11 +53,11 @@ test_is_admin_for_beamline_admin_not_admin if { } test_beamline_admin_for_subject_for_non_beamline_admin if { - not "alice" in admin.beamline_admin_for_subject with data.diamond.data as diamond_data + not admin.beamline_admin_for_subject.alice with data.diamond.data as diamond_data } test_beamline_admin_for_subject_for_admin if { - not "carol" in admin.beamline_admin_for_subject with data.diamond.data as diamond_data + not admin.beamline_admin_for_subject.carol with data.diamond.data as diamond_data } test_admin_rule_for_admin if { diff --git a/policy/diamond/policy/blueapi/blueapi.rego b/policy/diamond/policy/blueapi/blueapi.rego index f939800b..98ef5994 100644 --- a/policy/diamond/policy/blueapi/blueapi.rego +++ b/policy/diamond/policy/blueapi/blueapi.rego @@ -1,6 +1,5 @@ package diamond.policy.blueapi -import data.diamond.policy.admin import data.diamond.policy.session import data.diamond.policy.token diff --git a/policy/diamond/policy/tiled/tiled.rego b/policy/diamond/policy/tiled/tiled.rego index 49ae5a52..da7e7594 100644 --- a/policy/diamond/policy/tiled/tiled.rego +++ b/policy/diamond/policy/tiled/tiled.rego @@ -53,7 +53,7 @@ _session(p) := data.diamond.data.proposals[format_int(p, 10)].sessions[format_in # description: | # The access tag for a beamline, the top level of the beamline, proposal # and session tree. -beamline_tag(bl) := sprintf("beamline:%s", [bl]) +beamline_tag(bl) := $"beamline:{bl}" # METADATA # title: Proposal Tag @@ -61,7 +61,7 @@ beamline_tag(bl) := sprintf("beamline:%s", [bl]) # The access tag for a proposal. It names the beamline above it as well, # so that one flat membership check can match a node at any level of the # tree. `p` is always the bare proposal number. -proposal_tag(bl, p) := sprintf("beamline:%s,proposal:%s", [bl, format_int(p, 10)]) +proposal_tag(bl, p) := $"beamline:{bl},proposal:{format_int(p, 10)}" # METADATA # title: Session Tag @@ -78,7 +78,7 @@ session_tag(bl, p, v) := sprintf("beamline:%s,proposal:%s,session:%s-%d", [bl, f # what is under it. tiled matches tags exactly, so this is a separate tag # from the grant `t`: a session member gets the visible tags of the # proposal and beamline above them, never their grant tags. -visible_tag(t) := concat("", ["visible:", t]) +visible_tag(t) := $"visible:{t}" # METADATA # title: Node Tags diff --git a/policy/diamond/policy/token/token.rego b/policy/diamond/policy/token/token.rego index de995287..6eff7377 100644 --- a/policy/diamond/policy/token/token.rego +++ b/policy/diamond/policy/token/token.rego @@ -6,7 +6,7 @@ issuer := opa.runtime().env.ISSUER jwks_endpoint := jwks_endpoint if { metadata := http.send({ - "url": concat("", [issuer, "/.well-known/openid-configuration"]), + "url": $"{issuer}/.well-known/openid-configuration", "method": "GET", "force_cache": true, "force_cache_duration_seconds": 86400,