Concepts
Contexts
Predicate functions that decide when a rule applies.
A context is a named predicate that decides when a rule applies. Every rule references exactly one context; the engine calls its evaluator function at decision time and skips the rule when it returns false.
pgmorbac ships one context, always:
SELECT name, evaluator FROM morbac.contexts;
-- always | morbac.context_alwaysWriting a custom context
An evaluator is any zero-argument SQL function returning boolean. Valmont Clinique Valmont Nord wants prescriptions writable only during staffed hours:
CREATE OR REPLACE FUNCTION app.context_business_hours()
RETURNS BOOLEAN LANGUAGE sql STABLE AS $$
SELECT EXTRACT(hour FROM now() AT TIME ZONE 'Europe/Paris') BETWEEN 7 AND 19
$$;
INSERT INTO morbac.contexts (name, description, evaluator)
VALUES ('business-hours', 'Staffed hours at Valmont sites',
'app.context_business_hours'::regproc);Attach it to a rule instead of always:
INSERT INTO morbac.rules (org_id, role_id, activity, view, context_id, modality)
VALUES (:nord_id, :nurse_id, 'update', 'patients',
(SELECT id FROM morbac.contexts WHERE name = 'business-hours'),
'permission');Outside 07:00-19:00 the rule simply does not exist as far as is_allowed is
concerned.
Session-dependent contexts
Evaluators can read the RLS session variables to react to who is asking:
CREATE OR REPLACE FUNCTION app.context_own_shift()
RETURNS BOOLEAN LANGUAGE sql STABLE AS $$
SELECT EXISTS (
SELECT 1 FROM app.shifts s
WHERE s.user_id = morbac.current_user_id()
AND now() BETWEEN s.starts_at AND s.ends_at
)
$$;Rules for evaluators
- Keep them
STABLEand fast: they run inside every uncachedis_allowedcall that reaches the rule. - They must never write. A context is a question, not an action.
- Failure semantics are fail-closed: an evaluator returning NULL counts as false.
- For pure time windows, prefer the built-in
valid_from/valid_untilcolumns on the rule itself (temporal access); contexts are for conditions that need logic.