Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@ inherit from each other or two permissions grant each other.

### Roles the identity provider defines itself

Four roles always exist and are never created, deleted, or handed out by an administrator.
Their membership is conferred by the identity provider itself:
Six roles always exist and are never created or deleted by an administrator. Four of them
are conferred by the identity provider itself and never handed out:

| Role | Key | Granted by |
| -------------- | -------------- | --------------------------------------------------------------------------------------------------- |
Expand All @@ -122,20 +122,27 @@ Their membership is conferred by the identity provider itself:
| `Direttivo` | `direttivo` | Direct membership of `PN_ENTRA_DIRETTIVO_GROUP_ID` in PoliNetwork Entra ID |
| `Student` | `student` | A verification code delivered to an `@mail.polimi.it` address |

The other two are the board offices, `Presidente` (`presidente`) and `Segretario`
(`segretario`). They are given and taken back by hand, like a created role, but only by a
Master Admin: holding `idp:roles:assign` is not enough. Both always inherit from
`Direttivo`, so whoever holds one gets everything Direttivo grants, and the editor refuses
to remove that parent. Holding an office does not add the `direttivo` state: that still
comes only from the Entra group.

**Master Admin holds every permission that exists**, including ones created after it was
last looked at, because it is a wildcard rather than a stored list. It therefore has no
grant list of its own to edit, and no role may inherit from it: that would launder a
wildcard nobody can be given into a role an administrator could hand to anyone.
Unlike the other three it is not proven by identity evidence and never appears among the
Unlike Socio, Direttivo and Student it is not proven by identity evidence and never appears among the
`states`: it comes from the deployment's own configuration, which is what keeps the service
from being locked out of its own administration. `IDP_ADMIN_USER_IDS` is always honored.
Set `PN_ENTRA_OIDC_ADMIN_GROUP_ID` to limit everyone else to that Microsoft Entra group.
If the group is unset, only the explicit allowlist can confer Master Admin. Startup fails
without either an admin group plus complete PN Entra credentials or a nonempty allowlist.

What the other three grant is still yours to choose: give them permissions, rename them,
What the other built-in roles grant is still yours to choose: give them permissions, rename them,
describe them, and place them in the hierarchy like any other role. Only their key, their
deletion, and who holds them are fixed. The checked-in migrations seed them alongside the
deletion, and (except for the board offices) who holds them are fixed. The checked-in migrations seed them alongside the
two permissions this service already issued, so existing consumers keep working: `socio`
grants `membership:read` and `student` grants `student:verified`.

Expand All @@ -145,7 +152,7 @@ board's Entra group object ID, nobody is inferred as Direttivo. Both group check
cache measured from lookup start. Stored sign-in evidence and `PN_ENTRA_MEMBER_REFRESH_HOURS`
do not extend authorization. Failed or overlong checks grant nothing.

Membership of the built-in roles is not a role assignment: nothing is written to
Membership of the evidence-backed built-in roles is not a role assignment: nothing is written to
`user_role` for them, and evidence contributes only when joined to an account owned by the
user. States accumulate independently, so a socio is not automatically a student. Signature,
issuer, audience, expiration, and Entra tenant are checked before evidence is recorded.
Expand Down
40 changes: 40 additions & 0 deletions drizzle/0024_presidente_segretario_roles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
-- A hand-made role already using one of these keys would silently stand in for the board
-- office. Stop instead, so an administrator renames it first.
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM "role" WHERE "key" IN ('presidente', 'segretario') AND NOT "managed") THEN
RAISE EXCEPTION 'Rename the existing presidente or segretario role before migrating: those keys now belong to built-in roles.';
END IF;
END $$;--> statement-breakpoint
-- The President and Secretary of the board. They are built in, but unlike the other
-- built-in roles they are given by hand, and only by a Master Admin, so they have no
-- source_state.
INSERT INTO "role" ("id", "key", "name", "description", "managed", "source_state") VALUES
('static-role-presidente', 'presidente', 'Presidente', 'President of the PoliNetwork APS board.', true, NULL),
('static-role-segretario', 'segretario', 'Segretario', 'Secretary of the PoliNetwork APS board.', true, NULL)
ON CONFLICT ("key") DO NOTHING;--> statement-breakpoint
-- Both offices sit on the board, so they always inherit everything Direttivo grants.
INSERT INTO "role_parent" ("role_id", "parent_role_id")
SELECT office.id, board.id
FROM "role" office
JOIN "role" board ON board.key = 'direttivo'
WHERE office.key IN ('presidente', 'segretario')
ON CONFLICT DO NOTHING;--> statement-breakpoint
-- Managed roles still refuse hand-made assignments, except the two board offices.
CREATE OR REPLACE FUNCTION guard_managed_role_links() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF TG_TABLE_NAME = 'user_role' THEN
IF EXISTS (
SELECT 1 FROM role
WHERE id = NEW.role_id AND managed AND key NOT IN ('presidente', 'segretario')
) THEN
RAISE EXCEPTION 'Managed roles cannot be assigned';
END IF;
ELSE
IF EXISTS (SELECT 1 FROM role WHERE id = NEW.parent_role_id AND key = 'master-admin') THEN
RAISE EXCEPTION 'Master Admin cannot be inherited';
END IF;
END IF;
RETURN NEW;
END;
$$;
Loading
Loading