Scriptorium

Human Roles, Responsibilities & Accessibility

A role-and-capability overview of who can do what — and where each gate lives in the codebase.

Pedagogically Designed Creative Writing Coach · Roles & access reference

How Scriptorium works · theory & mechanism → The genre manual · 117 genres →
§ 1

The seven roles

Defined in server/lib/roles.js (server source of truth) and client/src/config/roles.js (sidebar / switcher labels). Every authenticated user has exactly one persistent role, stored in their GCS profile at tenants/{tid}/users/{email}/profile.json.

Role Rank canManage Responsibility
admin 100 System administrator. Pinned via ADMIN_EMAIL in .env — never assignable through the API. LMS/SSO config, residency, model governance.
coord (Coordinator) 80 Department/tenant-level oversight; manages researchers, auditors, instructors, TAs, students. Fairness, quality, calibration finalization.
researcher 70 IRB-scoped corpus & xAPI access. Lateral observer — sanctioned by coord, not subordinate to instructors.
auditor 70 External review of rubric integrity & scoring fairness. Read-only lateral observer; the only role that can approve curated rubrics & residency.
instructor 60 Course owner. Authors rubrics, calibrates AES, releases grades. Manages own TAs & students.
ta (TA / Grader) 40 Grades batches under instructor calibration. Cannot release scores (instructor co-sign required).
student 20 Submits work, reads feedback, revises. Sees only their own submissions.
§ 2

Hierarchy

Two ranking axes: rank (strict numeric ordering — a role can only act on roles strictly below it) and canManage (a separate gate on whether the role has any management authority at all).

Scriptorium role hierarchy A vertical org chart: admin pinned at the top supervises coord, who supervises instructor (continuing the chain to ta and student) plus two lateral observers, researcher and auditor, that report sideways from coord. admin RANK 100 · PINNED coord RANK 80 · MANAGES researcher RANK 70 · OBSERVES instructor RANK 60 · MANAGES auditor RANK 70 · OBSERVES ta RANK 40 · GRADES student RANK 20 · WRITES
pinned manages lateral observer individual supervises observes (canManage:false)

Researcher & auditor sit above instructor in rank so an instructor cannot demote them — but canManage:false keeps them out of the management chain entirely. They observe; they don't manage.

§ 3

Three identity layers — never collapse

Per docs/role-capabilities.md §1.1, every authenticated session carries three identity fields. Each governs a distinct set of decisions; do not collapse them.

FieldSourceWhat it gates
isAdmin email === ADMIN_EMAIL (.env) Visibility of the role-switcher in the TopBar and permission to call POST /api/auth/switch-role. Nothing else.
activeRole Persistent (GCS) for non-admins; mutable "view-as" for admin Every capability check, sidebar visibility, server endpoint gate, UI conditional.
canManage Derived from activeRole via roles.js User-management policy (canManageUser) only. Not a feature-capability gate.
View-as is fully faithful

When the admin switches activeRole to student, they behave exactly as a student — no admin override, no leakage. The role-switcher UI is the one legitimate isAdmin reader anywhere in the codebase.

§ 4

Two distinct gating systems

The codebase enforces permissions through two parallel layers. Don't confuse them.

4a · User management (vertical)

Used only for "who can change whose role". Lives in server/lib/roles.js:

canManageUser({ actorRole, isAdmin }, target, newRole)
canViewUser  ({ actorRole, isAdmin }, target)
subordinatesOf(role, { isAdmin })

Rules: newRole !== 'admin' AND actor outranks both target's current role AND newRole. Admin bypasses rank checks but still cannot assign admin.

Enforced at: /api/admin/users router gate, setUserRole() in lib/users.js, and actorCapabilities() (which narrows the role-dropdown to what the actor may actually assign).

4b · Feature capability (horizontal)

Used for "can this role take this action". Currently implemented as simple role allow-lists, not a unified matrix:

// server/middleware/auth.js
requireRole('instructor', 'coord', 'admin')
requireAdmin

A recurring constant AUTHOR_ROLES = ['instructor', 'coord', 'admin'] appears across three routers.

There is a spec for a fine-grained capability matrix — see § 8.

§ 5

Where role checks actually live

Permissions are enforced in four distinct layers. A request typically passes through several of them before producing a response.

LayerMechanismFiles
Route access roles: [...] array per item; requiresManage gate; inspector-admin bypass client/src/config/sections.js + canAccessRole() (used by App.jsx)
Sidebar visibility sidebarFor: [...] (optional) narrows which roles get a top-level rail entry; sidebarFor: [] = drill-down-only client/src/config/sections.js + visibleForRole()
Server route guards requireRole(...), requireAdmin, router-level canManageRole See full inventory in § 6
In-handler conditionals isInstructorish = role === 'instructor' || 'coord' || 'admin' || 'ta' — keys off activeRole only. routes/dashboard.js:127, routes/submissions.js:84,110
Resource ownership r.ownerId === user.uid || user.isAdminstill uses sticky isAdmin; review per-call-site whether view-as-role faithfulness requires changing to isActingAsAdmin(). lib/rubrics.js, lib/assignments.js
§ 6

Server route guards · full inventory

Every Express route guard currently active in the server, in dependency order.

EndpointGuardAllowed roles
POST /api/email/sendrequireAdminadmin
PUT /api/i18n/locales/:lang.jsonrequireAdminadmin
POST /api/i18n/invalidate-cacherequireAdminadmin
POST /api/bugs/:id/resolverequireAdminadmin
POST /api/bugs/clearrequireAdminadmin
GET /api/help/admin/help-analyticsrequireAdminadmin
POST /api/rubricsrequireRole(AUTHOR_ROLES)instructor coord admin
POST /api/rubrics/suggestrequireRole(AUTHOR_ROLES)instructor coord admin
POST /api/rubrics/:rid/validate/{samples,baseline,perturb,save}requireRole(AUTHOR_ROLES)instructor coord admin
POST /api/assignmentsrequireRole(AUTHOR_ROLES)instructor coord admin
/api/admin/*router gate: isAdmin || canManageRole(activeRole)admin coord instructor
POST /api/auth/switch-roleinline req.user.isAdminadmin
everything else under /api/*auth (any valid Bearer)all authenticated
§ 8

The fine-grained capability matrix

Status · Spec only, not yet implemented

The capability matrix in docs/role-capabilities.md defines 70+ tokens in feature:action form (e.g. rubric:publish, submission:override-score, fairness:approve) with a per-role allow / forbid / not-meaningful / constrained value. The supporting code does not yet exist:

  • server/lib/capabilities.js — does not exist
  • <Capable cap="…"> React wrapper — does not exist
  • requireCap() Express middleware — does not exist
  • GET /api/capabilities endpoint — does not exist

Today's substitute: the roles: [...] arrays in the sidebar plus requireRole(...AUTHOR_ROLES) in 3 routers. Migration is laid out as 7 phases (C0 → C7) in the spec.

Capability legend

✓ allowed ✗ forbidden — not meaningful ⚠ allowed with constraint

Sample area · Rubric (8 of 11 capabilities)

To give you the flavour — see docs/role-capabilities.md §3 for the full 8-area matrix (Rubric · Submission & grading · Calibration · Integrity · Pedagogy · Data & Research · Governance · Cross-cutting).

Capability student ta instructor coord researcher auditor admin
rubric:view
rubric:create
rubric:edit-draft
rubric:publish
rubric:fork
rubric:promote-curated
rubric:approve-curated
rubric:retire

Notable: rubric:approve-curated is the only column where auditor has ✓ and admin has ✗ — the curated tier is gated on external review, not internal authority. rubric:promote-curated is a propose action; the approve must follow.

§ 9

How a request flows through the gates

For an authenticated request (Bearer … header):

  1. attachUser (middleware/auth.js) — verifies Bearer against the OAuth gateway (or dev-bearer table), reads the GCS profile, builds req.user = { uid, email, isAdmin, activeRole, storedRole, tenantRoles, tid }.
  2. auth — 401 if no req.user. Mounted at app.use('/api', auth), after the public routers.
  3. Optional requireRole(...) / requireAdmin — 403 if activeRole not in the allowed set.
  4. In-handler logicisInstructorish filtering, ownership checks, scope filtering (e.g. listUsersFor returns only outranked users via canViewUser).
  5. xAPI emission — every state-change emits a statement; 403 emits denied, 5xx emits errored via logBug.
§ 10

Practical FAQ

Who can change a user's role?

admin (any), coord (anyone below), instructor (TAs and students of their courses). Never to admin. Enforced by canManageUser in lib/users.js:135.

Who can publish a rubric?

Spec: instructor (own course), coord (any), admin. Today: enforced only at POST /api/rubrics + /suggest via AUTHOR_ROLES. Course-scoping is not yet checked.

What's the difference between admin and coord?

admin is .env-pinned, gets the role-switcher, owns LMS/SSO/governance config. coord is the highest assignable role and runs day-to-day tenant management.

Why can't I assign someone the admin role?

It's pinned to ADMIN_EMAIL. subordinatesOf() excludes it; setUserRole() throws admin_role_not_assignable 403.

Who sees the Rubric Library?

All roles (per sidebar). Per spec matrix, students should only see rubrics attached to their submissions (constraint ⚠₁) — not yet enforced server-side.

What's "view-as"?

The role-switcher dropdown in the TopBar, visible only to the bootstrap admin. Switching activeRole makes the entire UI & server behave as that role would. The switcher itself is the only legitimate isAdmin consumer.

§ 11

References