Implementing RBAC for Patent Docketing Systems
Role-based access control for a patent docketing system is the mapping from a small set of job roles — paralegal, docketing manager, responsible attorney, external counsel, auditor — to an explicit permission matrix over docketing resources, enforced so that no single role can both edit and unilaterally commit a discretionary change to a statutory deadline. This page specifies that matrix, the least-privilege and separation-of-duties rules behind it, and a focused Python guard that enforces it.
It sits beneath the Security & Access Control Boundaries reference, which layers attribute-based rules and token scopes on top of the role model built here. Where that page governs how a token proves a scope, this one governs what each role may do and why the right to override a date must be split across two people. The audit records it produces feed Malpractice Compliance Reporting, and the reports themselves are generated under the same role controls described in Exporting Compliance Reports (CSV/XML) for Malpractice Insurers.
Roles as the unit of least privilege
RBAC works when roles model actual accountability, not org-chart convenience. Five roles cover a docketing operation, and each maps to a floor of privilege that the matrix then narrows further:
- Paralegal — the day-to-day operator. Views and edits routine matter data and proposes deadline changes, but never approves or unilaterally overrides a statutory date.
- Docketing manager — supervises the calendar. Approves routine changes, exports compliance data, and may propose a discretionary date override, but cannot both propose and commit the same one.
- Responsible attorney — bears the professional duty for the matter. Holds the authority to approve and commit a discretionary date override, scoped to matters they are assigned to.
- External counsel — outside firm or foreign associate. Read-only visibility, strictly scoped to their own matters, so a shared portal never leaks another client’s portfolio.
- Auditor — internal risk, compliance, or a malpractice insurer’s reviewer. Read-only across the whole docket plus the audit log, and permission to export, but no edit rights whatsoever.
This design applies least privilege as required by control AC-6 of NIST SP 800-53 Rev. 5: a principal receives only the permissions its role demonstrably needs. The role definitions themselves are the core RBAC construct standardized in the NIST Role-Based Access Control model — permissions attach to roles, and users acquire permissions solely by being assigned a role, never directly.
Operational Action: Assign every user exactly one primary role per matter and grant permissions only through roles. A permission granted directly to a user is invisible to the next access review and is the first thing a malpractice auditor will flag.
The permission matrix
Permissions are the cross-product of actions and resources. The actions are view, edit, approve, override, and export; the resources are the deadline record, matter data, the audit log, and the compliance export. The matrix below states role capability against the actions, with the resource nuance noted inline.
| Role | View | Edit | Approve | Override | Export |
|---|---|---|---|---|---|
| Paralegal | ✓ | ✓ routine fields | — | — | — |
| Docketing manager | ✓ | ✓ | ✓ routine changes | ◐ propose only | ✓ |
| Responsible attorney | ✓ own matters | ✓ own matters | ✓ | ✓ commit | ✓ |
| External counsel | ◐ own matters | — | — | — | — |
| Auditor | ✓ + audit log | — | — | — | ✓ |
Two cells carry the whole compliance weight. Override is the discretionary re-setting of a computed statutory date — the single most dangerous operation in a docketing system, because a wrong override manufactures a missed deadline that looks intentional after the fact. It is split: a docketing manager may propose an override (◐), but only a responsible attorney may commit it (✓), and the two must be different people. Export is confined to roles with a defensible need — manager, attorney, and auditor — because a compliance export carries privileged prosecution data outside the working system.
Operational Action: Treat the matrix as version-pinned configuration in the rule registry, not as code branches. A change to any cell ships as a reviewed pull request with legal sign-off, exactly like a deadline rule, so the access posture at any past date is reconstructable.
Separation of duties for date overrides
A discretionary override changes a computed statutory date to a value a human asserts is correct — for example, when an office corrects a mailing date after the fact. Because the override can move a deadline earlier or later, it must never be a single-actor operation. Separation of duties, control AC-5 of NIST SP 800-53 Rev. 5, requires that the proposer and the committer be distinct principals. In practice this is a maker-checker (four-eyes) flow: a docketing manager or paralegal proposes the new date with a written justification, and a responsible attorney assigned to the matter reviews and commits it. The system rejects any attempt by one person to do both.
This also reflects the supervising-attorney’s professional duty. Under ABA Model Rule 5.3, a lawyer with managerial authority must ensure nonlawyer assistants’ work is compatible with the lawyer’s professional obligations, so the attorney’s commit is not a rubber stamp — it is the point where professional responsibility attaches to the changed date.
A Python RBAC guard
The enforcement layer is a pure, testable permission check plus a decorator that guards any callable. Permissions are looked up from the role, evaluated against the resource action, and the override path additionally verifies that the committing principal differs from the proposer. The check fails closed: an unknown role or action denies.
from dataclasses import dataclass, field
from enum import Enum
from functools import wraps
from typing import Callable, TypeVar
class Action(str, Enum):
VIEW = "view"
EDIT = "edit"
APPROVE = "approve"
OVERRIDE = "override" # commit a discretionary date change
EXPORT = "export"
class Role(str, Enum):
PARALEGAL = "paralegal"
DOCKETING_MANAGER = "docketing_manager"
RESPONSIBLE_ATTORNEY = "responsible_attorney"
EXTERNAL_COUNSEL = "external_counsel"
AUDITOR = "auditor"
# Baseline role → permitted actions. "own-matter" scoping and the override
# split are enforced separately below, never encoded as a broad grant.
ROLE_PERMISSIONS: dict[Role, frozenset[Action]] = {
Role.PARALEGAL: frozenset({Action.VIEW, Action.EDIT}),
Role.DOCKETING_MANAGER: frozenset(
{Action.VIEW, Action.EDIT, Action.APPROVE, Action.EXPORT}
),
Role.RESPONSIBLE_ATTORNEY: frozenset(
{Action.VIEW, Action.EDIT, Action.APPROVE, Action.OVERRIDE, Action.EXPORT}
),
Role.EXTERNAL_COUNSEL: frozenset({Action.VIEW}),
Role.AUDITOR: frozenset({Action.VIEW, Action.EXPORT}),
}
# Roles whose VIEW/EDIT are confined to matters they are assigned to.
OWN_MATTER_ONLY: frozenset[Role] = frozenset(
{Role.EXTERNAL_COUNSEL, Role.RESPONSIBLE_ATTORNEY}
)
@dataclass(frozen=True)
class Principal:
user_id: str
role: Role
assigned_matters: frozenset[str] = field(default_factory=frozenset)
class AccessDenied(PermissionError):
"""Raised on any failed check — the guard always fails closed."""
def can(principal: Principal, action: Action, matter_id: str,
*, proposer_id: str | None = None) -> bool:
"""Return True only if the role grants the action for this matter.
Override additionally enforces separation of duties: the committing
principal must differ from whoever proposed the change.
"""
permitted = ROLE_PERMISSIONS.get(principal.role, frozenset())
if action not in permitted:
return False
# Least privilege: own-matter roles cannot reach unassigned matters.
if principal.role in OWN_MATTER_ONLY and matter_id not in principal.assigned_matters:
return False
# Separation of duties: the override committer cannot be the proposer.
if action is Action.OVERRIDE and proposer_id == principal.user_id:
return False
return True
F = TypeVar("F", bound=Callable[..., object])
def require(action: Action) -> Callable[[F], F]:
"""Decorator guard: the wrapped function's first two arguments must be
the acting Principal and the matter_id; override also takes proposer_id."""
def decorator(func: F) -> F:
@wraps(func)
def wrapper(principal: Principal, matter_id: str, *args, **kwargs):
proposer_id = kwargs.get("proposer_id")
if not can(principal, action, matter_id, proposer_id=proposer_id):
raise AccessDenied(
f"{principal.role.value} may not {action.value} matter {matter_id}"
)
return func(principal, matter_id, *args, **kwargs)
return wrapper # type: ignore[return-value]
return decorator
@require(Action.OVERRIDE)
def commit_deadline_override(principal: Principal, matter_id: str,
new_date: str, *, proposer_id: str) -> dict[str, str]:
# Reaches here only for an assigned attorney who is NOT the proposer.
return {"matter_id": matter_id, "deadline": new_date, "committed_by": principal.user_id}
# Verify: an attorney cannot commit their own proposed override (fails closed).
# attorney = Principal("at-1", Role.RESPONSIBLE_ATTORNEY, frozenset({"M-42"}))
# commit_deadline_override(attorney, "M-42", "2026-09-01", proposer_id="at-1") -> AccessDenied
# commit_deadline_override(attorney, "M-42", "2026-09-01", proposer_id="pl-9") -> ok
Operational Action: Enforce the guard at the service boundary, not only in the UI. A UI that hides the override button but leaves the endpoint ungated is exactly the gap a compromised paralegal token exploits — the can() check must run server-side on every mutating call.
Least privilege in practice
Least privilege is easy to state and easy to erode. Three disciplines keep it intact. First, run periodic access recertification: on a fixed cadence, the docketing manager and a supervising attorney re-attest every role assignment, and any user with no docketing activity in the window is downgraded. Second, forbid direct user permissions entirely — if a one-off need arises, create or extend a role, review it, and assign it, so the grant is visible to the next audit. Third, scope external counsel to their own matters with an explicit assignment list, never a firm-wide read role, so a shared associate portal cannot enumerate other clients’ portfolios.
The auditor role is deliberately read-plus-export with zero write. A malpractice insurer’s reviewer must be able to reconstruct the docket and export the compliance record without any ability to alter it — the same tamper-evidence goal the parent Security & Access Control Boundaries model enforces with its append-only, hash-chained ledger.
Known pitfalls
- Role explosion. Adding a bespoke role per attorney defeats RBAC. Keep the role set small and use own-matter scoping for per-person restriction.
- Approve and override conflated. Approving a routine change and committing a discretionary date override are different rights. Fold them together and you lose the separation of duties that makes an override defensible.
- UI-only enforcement. Hiding a control is not access control. Every check must run server-side and fail closed.
- Auditor with write access. An auditor who can edit anything cannot certify the record they reviewed. Keep the role strictly read-plus-export.
Frequently Asked Questions
Why split proposing and committing a deadline override across two roles?
Should external counsel get a read-only role across the whole docket?
Is RBAC alone enough, or do I still need attribute-based rules?
What access should a malpractice insurer's auditor be given?
Related
- Security & Access Control Boundaries — the parent model that layers scopes and attribute rules over these roles.
- Exporting Compliance Reports (CSV/XML) for Malpractice Insurers — the export the manager and auditor roles are permitted to run.
- Malpractice Compliance Reporting — the reporting layer these audit records feed.
- Building a Fallback Routing System for Patent Dockets — the degradation path that must preserve these role controls during an outage.
↑ Back to Security & Access Control Boundaries