Software & practice

Assign access rights around the work people actually do

Connect permissions to tasks and resources, then verify intended grants and their limits.

Symbolic access composition in AZ colours: four conventional steel keys with red grips hang from a stable dark organiser under German labels for reading, editing, approval and access. A front plaque states that the task determines access.

Access follows a task on a particular resource

Useful permissions describe who may perform which action on which resource under which conditions. A role name rarely answers that question completely. Editing may be limited to an assigned open case, while approval may require another creator and a particular workflow state. The receiving application must evaluate that relationship for the actual request rather than merely displaying a suitable menu item.

Our original example is a fictional service application with separate teams. It has four tasks: read cases, edit cases, approve cases and manage access. Its records are invented and are not presented as an existing customer system. We develop a permission overview, an executable server-side decision helper and concrete counterexamples around this small workflow. The helper is exercised through 68 isolated cases.

Start with short statements of responsibility. A case worker may change open cases assigned to them in their own team. A reviewer may approve a case created by someone else when it reaches review state. An access manager should not automatically read every business record merely because their task sounds administrative. These statements are more precise than a general request for a flexible role system and can become testable acceptance rules.

Begin with a small inventory of actions

List recurring operations such as reading, changing, approving, exporting, deleting and assigning access. Not every application needs every action. Our example retains four narrowly described tasks. For each one, identify the resource and the consequence of unauthorised use. This shows where a read permission already exposes confidential content and where an update creates additional responsibility beyond viewing an ordinary page.

Then decide the necessary scope. Does the person need one assigned case or a whole team overview? Is cover for a colleague permanent or limited to a defined period? Broad access resolves the immediate request quickly but makes later removal harder to understand. Our model consequently begins with explicit permissions and treats other combinations as disallowed until the business requirement is actually established.

Review the inventory with the people responsible for the work. A developer can implement enforcement but should not infer business authority from an existing button. If no one can explain why a role needs an operation, the requirement remains unresolved. Document and decide an exception before it quietly becomes a permanent default. This gives later maintenance a reason to follow rather than an unexplained historical checkbox.

Combine roles with relationships and workflow state

Roles group related work. The example’s technical identifiers are viewer, editor, reviewer and access_manager, while its visible task descriptions remain separate. A person may hold multiple known roles. Those assignments alone do not determine every outcome: team, assignee, creator and case state also participate. The decision therefore combines role membership with properties of the particular record and relationships between people and that record.

An editor may change an open case only when assigned to it. A reviewer may approve a case in review state only when they are not its creator. These are explicit assumptions of our fictional workflow, not universal industry requirements. Another service could require stricter separation or a different relationship. Understand the reason for a restriction before copying it into a system with different responsibilities.

The access_manager role receives only the management action on its own team resource. Business-record reading is not implicit. Adding viewer grants that separate task explicitly, which keeps the overview understandable. Role assignment itself still needs an independently protected rule, however. The small helper does not implement a complete role-granting workflow or establish comprehensive organisational separation simply by using a management role with a narrow name.

NIST: Role Based Access Control

Separate authentication from authorisation

Authentication establishes a trusted user context. Authorisation then asks whether that context may perform the requested action on the particular resource. The helper therefore receives actor from server-side application state. A browser-supplied object claiming active:true and the desired roles would not be a valid source for this decision. The input contract includes where the facts come from, not just how their fields are spelled.

The same applies to the resource. The server loads the actual case with its team, creator, assignment and workflow state. A hidden form field or a submitted display label cannot replace that record. The request may identify the resource it wants to address, but the trusted system determines the resource’s actual properties. This distinction prevents the caller from inventing the very relationship that is meant to constrain access.

This article supplies a pure decision helper, not a complete login, database retrieval layer or production update. That boundary matters during handover. An accepted isolated case only demonstrates the rule under the supplied assumptions. Integration must establish that those assumptions really come from the intended authoritative sources and cannot be chosen by the requester. Passing function tests alone does not provide that external evidence.

Enforce the team boundary for every relevant action

Every case in the example belongs to one team. Its loaded team identifier must match the active actor’s team. This boundary applies even when the actor has a role that generally supports reading or approval. Knowing another case identifier does not grant access to it, nor does holding the same role in another organisational area. Permission concerns this action on this particular record.

The access-management operation uses a different resource kind: a team rather than a case. Its own identifier must also match its team identifier. A case cannot become an administrative resource merely because a function name sounds similar. This explicit resource distinction makes negative review cases easier to understand. Each operation specifies what kind of object it is allowed to address at all.

A real system must apply equivalent boundaries to lists, searches, exports and downloads. Protecting the detail view does not help if the same content remains available through another route. Acceptance therefore inventories every path to the data. The small helper provides a business decision, while route coverage and query scoping require their own review in the surrounding application. Treat those as complementary responsibilities rather than interchangeable evidence.

OWASP API Security: Object Level Authorization

The executable decision helper

The function below rejects incomplete and unknown states. An active actor needs a valid identifier, a team and a non-empty list of recognised roles. A case needs the agreed relationships and a recognised state. An unassigned case may have assigneeId:null. It remains available to permitted readers but is not assigned to anyone for editing under this rule. That distinction supports the workflow without weakening the assignment requirement.

Identifier validation only describes the format of our fictional records. It does not establish their existence or authenticate the actor. There is also no hidden catch-all administrator branch. Every supported action is explicit, and an added action is initially denied until deliberately designed. That makes accidental permission growth easier to notice when a later feature introduces another endpoint or business operation.

const knownRoles = new Set(['viewer', 'editor', 'reviewer', 'access_manager']);
const idPattern = /^[a-z][a-z0-9-]{0,63}$/;
const validId = value => typeof value === 'string' && idPattern.test(value);
const record = value => value !== null && typeof value === 'object'
  && !Array.isArray(value);

// actor and resource must come from trusted server-side state.
export function can(actor, action, resource) {
  if (!record(actor) || actor.active !== true || !validId(actor.id)
      || !validId(actor.tenantId) || !Array.isArray(actor.roles)
      || actor.roles.length === 0
      || !actor.roles.every(role => knownRoles.has(role))) return false;
  if (!record(resource) || !validId(resource.id)
      || resource.tenantId !== actor.tenantId) return false;
  const roles = new Set(actor.roles);
  if (action === 'access.manage') {
    return resource.kind === 'tenant'
      && resource.id === resource.tenantId && roles.has('access_manager');
  }
  if (resource.kind !== 'ticket' || !validId(resource.createdBy)
      || !(resource.assigneeId === null || validId(resource.assigneeId))
      || !['open', 'review', 'closed'].includes(resource.status)) return false;
  if (action === 'ticket.read') {
    return ['viewer', 'editor', 'reviewer'].some(role => roles.has(role));
  }
  if (action === 'ticket.edit') {
    return roles.has('editor') && resource.status === 'open'
      && resource.assigneeId === actor.id;
  }
  if (action === 'ticket.approve') {
    return roles.has('reviewer') && resource.status === 'review'
      && resource.createdBy !== actor.id;
  }
  return false;
}

Read the positive cases with their conditions

For the overview, start with an open case in the actor’s own team, assigned to that actor and created by someone else. Reading is permitted to viewer, editor and reviewer. Editing is permitted only to editor. For the approval column, move the same case into review state; reviewer may then approve while the creator remains another person. These assumptions are necessary to interpret the results correctly.

Access management is tested against the actor’s own team resource and is permitted only to access_manager. A permission matrix needs to show that different columns involve different conditions. An unexplained green “Approve” cell could otherwise be read as permission to approve every record. Our illustration states its prerequisites alongside the results, keeping the compact overview connected to the actual rule rather than replacing it.

The explanatory graphic is produced from the actual outputs of the executed helper. It is not an invented security score or a claim about real customer access. Colours are accompanied by explicit words for allowed and denied, so the decisions remain intelligible without colour. Human-readable task names describe the work and do not need to expose the implementation’s role identifiers as product terminology.

Actual four-by-four permission matrix: readers, editors and reviewers can read; only the assigned editor can edit an open case; a reviewer can approve another creator's case in review; access management concerns the own-team resource. Three counterexamples show additional limits. German labels.
The matrix follows 16 actually executed cases of the displayed function. Its explicit example conditions are part of the result. Further checks cover foreign teams, self-approval and the absence of implicit business permissions for access managers. No real roles were assigned.

Use counterexamples to define the boundary

An editor cannot change an open case assigned to someone else. The same actor cannot edit an assigned case after it enters review. A reviewer cannot approve a case they created even when they also hold editor. Combining roles does not silently cancel that relationship constraint. These examples prevent the team from interpreting role addition as a universal way to bypass contextual requirements.

A disabled actor is rejected. The deliberately strict model also rejects a list containing both a known and an unknown role. That is a documented design choice to consider during future role migrations. A system choosing to ignore unknown roles needs a different explicit policy and matching tests. Inconsistent assumptions across two services would otherwise make the same user context behave unpredictably.

Unknown actions, foreign teams, wrong resource kinds and incomplete records provide other negative cases. A closed case remains readable to a permitted reader but cannot be edited or approved. An unassigned case can also be read. These positive edge cases matter: rejecting everything may look cautious while blocking the legitimate workflow. A useful policy needs to establish both the intended permissions and the intended boundaries.

Integrate checking with the actual update

A resource can change between an initial check and a later write. Another person may move a case into review or change its assignee while the first browser view remains open. Production processing therefore needs to consider the current state at the decisive update. A previously displayed screen is not evidence that its earlier permission result still applies when the request eventually arrives.

Depending on the storage model, a transaction, suitable locking or a conditional version-checked update may be needed. The pure JavaScript helper implements no database atomicity. During acceptance, identify which state is checked and what happens if it changes concurrently. A failed condition must not generate a false successful-update message. The technical mechanism and its failure handling belong to the complete application, not just to this predicate.

Permission removal also has a timing requirement. Long-lived cached roles may allow the next request to use an obsolete actor context. The team defines how membership, active status and role changes become effective, then verifies the chosen behaviour. Seeing a role removed in an administration screen does not prove that every active session and background path already observes the change. State refresh needs an explicit owner and a tested contract.

Derive useful interface actions from the same policy

The interface should offer actions appropriate to the current situation. For our editor, that can mean showing an edit action only for their own open case. This improves orientation, but the server must decide again when the actual request arrives. A person can alter the interface or issue a direct call. Visibility is a usability consequence of permission, not the enforcement boundary itself.

Feedback distinguishes changed business state, missing permission and technical failure where that can be done without revealing protected information. A legitimate person needs a next step, perhaps a refreshed view or an appropriate contact. Do not reveal details of another team’s case merely to explain why its update is denied. The amount of information in an error response is itself part of the access design.

A disabled action may contribute useful explanation if that explanation is reachable. It cannot serve as the entire security model. The interface uses the same business action names established in the task inventory, keeping understandable language connected to the policy. Technical role values remain implementation details rather than becoming opaque product text that users have to decode before understanding their responsibilities.

Organise grants, cover and removal

Assign a business owner and a granting rule to each role. Temporary cover specifies the task being taken over and when the added permission ends. Copying every permission from a colleague may grant more than the temporary responsibility requires. The four separated tasks in this model make focused decisions possible, but the organisation still needs to establish who can approve and record each exceptional assignment.

Access administration is itself sensitive. A person allowed to grant roles could give themselves additional authority if the management workflow is too broad. Genuine separation therefore needs further rules about permitted grants, approval and potentially independent review. The helper’s access.manage result is only entry to that management function. It does not implement the whole grant workflow or prove a complete four-eyes process.

When someone leaves or changes responsibilities, consider active accounts, existing sessions and any related service credentials. Not all of those necessarily belong to the same application, but ownership must be clear. Otherwise a disabled profile can be undermined by an overlooked alternate route. Permission management needs a lifecycle rather than a single snapshot taken when the account was first created.

Keep different kinds of evidence distinct

The 68 isolated checks cover the four-by-four base matrix, foreign teams, disabled actors, assignment, self-approval, combined roles and malformed records. Expected results are explicit. The tests describe business situations such as a creator being unable to approve their own case rather than merely repeating internal implementation conditions. They compare the helper’s actual decision with the intended rule for each situation.

A deployed integration additionally needs direct HTTP calls, scoped list queries, exports and updates under competing access. The pure function cannot establish those outer behaviours. A check after role removal is another useful requirement. This separation makes clear which layer each result covers. A complete acceptance case combines appropriate evidence rather than treating a large number of successful isolated assertions as approval of the entire service.

Relevant access events can be logged using suitable technical references, action, outcome and time. That does not justify indiscriminate recording of complete payloads, credentials or unnecessary personal information. The real team determines the required content and who may inspect it. Logs are themselves protected data, not an unlimited secondary store. Their usefulness depends on purposeful recording and an operational route for reviewing significant events.

OWASP: Logging Cheat Sheet

Bring an existing system into order gradually

For an existing application, inventory roles and reachable actions before making changes. Do not simply delete unclear broad roles while their operational effects remain unknown. Map the needed tasks and inspect the actual paths people use. Everyday work supplies important cases: absence cover, handovers, monthly completion and later correction may have different prerequisites from the ordinary new case. Include those needs in the target model.

Build a reviewable target matrix and identify who will carry each required responsibility after a change. A staged migration needs a recovery route without quietly retaining all old unrestricted permissions forever. Temporary exceptions receive a reason, owner and end condition. Otherwise a transitional arrangement can become a second permanent permission model that no one intended to maintain.

Finally, review with the actual task owners whether necessary work remains possible and prohibited access is prevented. Both matter. A model that permits everything offers no meaningful boundary; a model that blocks all legitimate work is unusable. Good permissions explain their limits, bind decisions to the current resource and remain reviewable when the organisation changes. That is the practical result to seek from the code and the surrounding process together.

Tasks and conditions at a glance

TaskExample condition
ReadActive actor, explicit reading role, own team
EditAdditionally editor, open and assigned
ApproveAdditionally reviewer, review state and another creator
Manage accessaccess_manager on the matching own-team resource

Implementation checklist

  • Describe tasks, resources and necessary scope.
  • Establish trusted sources for actor and resource data.
  • Check team boundaries, state and relationships explicitly.
  • Reject unknown actions and incomplete state predictably.
  • Inventory routes including lists, exports and downloads.
  • Protect checking and writing against stale state.
  • Define granting, cover, removal and responsible owners.
  • Review positive, negative and concurrent integration cases separately.

Sources and review scope

Primary sources reviewed on 9 November 2026. Teams, cases and rules are original fictional examples. The 68 executed checks concern only the displayed pure decision helper, not production permission changes or complete integration acceptance.

Let’s move forward

What would you like to change?

An idea, a specific challenge or your next step with AI. We’re listening.

Let’s talk