Loading content...
When a process attempts to open a file for writing, the operating system must answer a fundamental question: Is this operation permitted? The answer depends on whether the appropriate access right exists in the access matrix cell connecting that process (subject) to that file (object).
Access rights are the vocabulary of access control—they name the specific operations that can be allowed or denied. But the space of possible rights is vast. A simple file might have read, write, and execute rights. A database table might have SELECT, INSERT, UPDATE, DELETE, ALTER, DROP, GRANT, and more. A cloud resource might have dozens of fine-grained actions. How do we organize this complexity? What makes a right meaningful? And what are the special "meta-rights" that control the access matrix itself?
By the end of this page, you will understand the complete taxonomy of access rights—from primitive operations to complex composite rights. You'll learn about the crucial distinction between data rights and meta-rights, explore how different systems model rights, and understand the copy flag and ownership mechanisms that control right propagation.
At the most basic level, access rights map to fundamental operations that subjects can perform on objects. These operations fall into several categories:
Information Flow Rights:
Existence Rights:
Structural Rights:
| Right | Information Flow | Integrity Impact | Confidentiality Impact |
|---|---|---|---|
| Read | Object → Subject | None | Subject learns object state |
| Write | Subject → Object | Object modified | Subject state into object |
| Append | Subject → Object (end only) | Object grown | Limited write channel |
| Execute | Object controls subject | Depends on program | Depends on program |
| Delete | Object state destroyed | Object gone | Object unavailable |
| Create | Subject creates object | New object exists | New object accessible |
Why These Specific Rights?
This categorization isn't arbitrary—it reflects fundamental operations at the hardware level:
The Unix permission model (rwx) directly reflects these hardware primitives. Higher-level rights in more complex systems are ultimately compositions of these fundamental operations.
Rights as Predicates:
Formally, a right r can be viewed as a predicate over operations:
$$r(op) = true \iff operation\ op\ is\ permitted\ by\ right\ r$$
When a subject requests to perform operation op on object o, the system checks:
$$\exists r \in A[s, o]: r(op) = true$$
If such a right exists, the operation is permitted.
A well-designed right set is minimal—each right covers distinct operations without overlap, and no right is redundant. Unix's rwx is minimal for files. But real systems often have overlapping rights for usability (a 'Full Control' right that implies all others), creating complexity in determining effective permissions.
While basic read/write/execute rights apply broadly, many object types have specialized rights that map to their unique operations:
File System Objects:
| Object Type | Specific Rights | Meaning |
|---|---|---|
| Regular File | read, write, execute | Standard r/w/x |
| Directory | read (list), write (modify entries), execute (traverse/search) | Contents vs. traversal |
| Symbolic Link | read (follow), write (modify target) | Usually checked on target |
| Device | read, write, ioctl | I/O control operations |
| Socket | connect, bind, listen, accept | Network operations |
| FIFO | read, write | Named pipe operations |
Process Objects:
When processes are objects in the access matrix:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
# Object-specific rights in different systems # Unix file rights (the classic)UNIX_FILE_RIGHTS = { 'r': ['open(O_RDONLY)', 'read()', 'mmap(PROT_READ)'], 'w': ['open(O_WRONLY)', 'write()', 'truncate()', 'mmap(PROT_WRITE)'], 'x': ['execve()', 'mmap(PROT_EXEC)'],} # Unix directory rights (note: different semantics!)UNIX_DIR_RIGHTS = { 'r': ['readdir()', 'getdents()', 'opendir()'], # List contents 'w': ['create file', 'unlink()', 'rename()'], # Modify entries 'x': ['chdir()', 'access path components'], # Search/traverse} # Windows file rights (more granular)WINDOWS_FILE_RIGHTS = { 'READ_DATA': 'Read file contents', 'WRITE_DATA': 'Write file contents', 'APPEND_DATA': 'Append to file', 'READ_EA': 'Read extended attributes', 'WRITE_EA': 'Write extended attributes', 'EXECUTE': 'Execute file', 'DELETE': 'Delete file', 'READ_ATTRIBUTES': 'Read basic attributes', 'WRITE_ATTRIBUTES': 'Write basic attributes', 'READ_CONTROL': 'Read security descriptor', 'WRITE_DAC': 'Modify DACL', 'WRITE_OWNER': 'Change owner', 'SYNCHRONIZE': 'Wait on handle',} # Database table rights (even more domain-specific)SQL_TABLE_RIGHTS = { 'SELECT': 'Read rows', 'INSERT': 'Add rows', 'UPDATE': 'Modify existing rows', 'DELETE': 'Remove rows', 'TRUNCATE': 'Remove all rows efficiently', 'REFERENCES': 'Create foreign key referencing this table', 'TRIGGER': 'Create triggers on this table', 'ALTER': 'Modify table structure', 'DROP': 'Destroy table',} # Kubernetes resource verbsK8S_VERBS = { 'get': 'Read single resource', 'list': 'List multiple resources', 'watch': 'Stream updates', 'create': 'Create new resource', 'update': 'Update existing resource', 'patch': 'Partial update', 'delete': 'Delete resource', 'deletecollection': 'Delete multiple resources',}Rights Granularity Trade-offs:
System designers must choose the granularity of rights:
Coarse-grained rights:
Fine-grained rights:
Modern systems often provide both: fine-grained rights for detailed control, plus macro-rights (Full Control, Admin) that expand to common combinations.
A right's semantic meaning depends on the object type. 'Execute' on a file means run it as a program. 'Execute' on a directory means search/traverse it. 'Execute' on a database procedure means invoke it. The right name is reused but the operation is different. This overloading sometimes causes confusion but enables uniform permission interfaces.
Beyond rights that control data operations, meta-rights control the access matrix itself. These rights determine who can grant, modify, or revoke other rights.
The Owner Right:
The owner right is the most powerful meta-right. An owner typically can:
In the access matrix, owning an object o means having omnipotent authority over column o:
$$owner \in A[s, o] \implies \forall r \in R, \forall s' \in S: s\ can\ enter/delete\ r\ in\ A[s', o]$$
The Copy Right (Grant Option):
Sometimes a subject should be able to grant rights they possess, but only those specific rights. The copy flag (often denoted with an asterisk, e.g., read*) indicates this:
GRANT SELECT ON table TO user WITH GRANT OPTIONThe Control Right:
Some systems have a control right over subjects. If A[Admin, Process_P] contains 'control', Admin can modify Process_P's row in the access matrix—changing what P can access.
Right Propagation and Revocation:
Meta-rights create chains of granted rights. Consider:
Now Alice revokes Bob's read. What happens?
Cascading revocation: Carol and Dave also lose read (their rights depended on Bob's)
Non-cascading revocation: Carol and Dave keep read (once granted, rights persist)
Different systems choose differently:
Negative Rights (Deny):
Some systems support explicit deny entries in the access matrix:
$$A[s, o] = {read, \neg write}$$
Deny typically has higher precedence than allow. If conflicting entries exist (via groups), deny wins. This allows expressing "everyone except Bob can write" without enumerating all non-Bob users.
Meta-rights create the 'confused deputy' vulnerability. If a privileged program (deputy) is tricked into using its meta-rights on behalf of an unprivileged user, access controls are bypassed. Example: A setuid program with write* might be tricked into granting write to an attacker's file. Deputies must verify they're acting on appropriate objects, not just that they have rights.
Unix pioneered the famous rwx permission model. Despite its simplicity, this model has nuances that even experienced users misunderstand:
Basic Permission Bits:
-rwxr-x--- 1 alice developers 4096 Jan 15 10:00 program.sh
| Position | Meaning | Owner | Group | Others |
|---|---|---|---|---|
| 1 | File type | - | ||
| 2-4 | Owner permissions | r | w | x |
| 5-7 | Group permissions | r | - | x |
| 8-10 | Other permissions | - | - | - |
Special Permission Bits:
-rwsr-xr-x setuid set (s instead of x)
-rwxr-sr-x setgid set
drwxrwxrwt sticky bit set (t in others' x)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
/** * Simplified Unix permission checking logic * * This demonstrates the actual algorithm used by the kernel * to determine if an access request should be allowed. */ #include <sys/stat.h>#include <unistd.h> // Check if process can access file with requested modeint check_permission(struct stat *st, uid_t uid, gid_t gid, gid_t *groups, int ngroups, int request) { int mode = st->st_mode; // Root (UID 0) bypasses most checks if (uid == 0) { // Even root needs execute bit for execute operations if (request & X_OK) { // Root can execute if ANY execute bit is set if (mode & (S_IXUSR | S_IXGRP | S_IXOTH)) return 0; // Allowed return -1; // Denied even for root } return 0; // Root can read/write anything } // Check owner permissions (if UID matches file owner) if (uid == st->st_uid) { // Use owner permission bits (shift right 6) if ((mode >> 6) & request) return 0; // Allowed return -1; // Denied (owner bits don't match) } // Check group permissions (if any GID matches file group) int in_group = (gid == st->st_gid); if (!in_group) { for (int i = 0; i < ngroups; i++) { if (groups[i] == st->st_gid) { in_group = 1; break; } } } if (in_group) { // Use group permission bits (shift right 3) if ((mode >> 3) & request) return 0; // Allowed return -1; // Denied (group bits don't match) } // Fall through to others permissions if (mode & request) return 0; // Allowed return -1; // Denied} // Key insight: The check is ORDERED// 1. If you're owner, owner bits are used (group/other ignored!)// 2. If you're in group (but not owner), group bits are used// 3. Otherwise, other bits are used// // This means: if owner has no read but others do, owner CAN'T read!// -r--r--r-- 1 alice users file.txt// Alice (owner) can read, but:// ----r--r-- 1 alice users file.txt // Alice (owner) CANNOT read, even though others can!Linux Capabilities:
Traditional Unix has only one superuser (root/UID 0). Linux capabilities divide root's power into ~40 distinct rights:
| Capability | Power Granted |
|---|---|
| CAP_CHOWN | Change file ownership |
| CAP_DAC_OVERRIDE | Bypass read/write/execute permission checks |
| CAP_DAC_READ_SEARCH | Bypass read and search permission |
| CAP_FOWNER | Bypass checks requiring file owner |
| CAP_KILL | Send signals to any process |
| CAP_SETUID | Manipulate process UIDs |
| CAP_NET_BIND_SERVICE | Bind to ports < 1024 |
| CAP_NET_RAW | Use raw sockets |
| CAP_SYS_ADMIN | Broad administrative operations |
| CAP_SYS_PTRACE | Trace any process |
Capabilities can be assigned to files (like setuid) or to processes. A process with CAP_NET_BIND_SERVICE but not CAP_SYS_ADMIN can bind to port 80 but cannot mount filesystems.
Access Control Lists (ACLs):
Linux ACLs extend the classic model to allow per-user and per-group entries beyond owner/group/other:
$ getfacl file.txt
# file: file.txt
# owner: alice
# group: users
user::rw-
user:bob:r--
group::r--
group:devs:rw-
mask::rw-
other::---
ACL mask is often misunderstood. It limits the maximum permissions for named users and groups (not owner or other). If mask is r--, then even if bob has rw-, his effective permissions are r--. The mask is automatically recalculated when using chmod.
Windows implements a far more granular and complex rights model than Unix. Every securable object has a Security Descriptor containing:
Security Descriptor Components:
ACCESS_MASK Structure:
Windows rights are encoded in a 32-bit ACCESS_MASK:
Bit 31-28: Generic rights (GENERIC_READ, GENERIC_WRITE, etc.)
Bit 27-25: Reserved
Bit 24: Access system security (SACL access)
Bit 23-20: Standard rights (DELETE, READ_CONTROL, WRITE_DAC, WRITE_OWNER, SYNCHRONIZE)
Bit 19-16: Object-specific rights (depends on object type)
Bit 15-0: Object-specific rights (continued)
For files, the object-specific bits include:
| Right | Mask Value | Description |
|---|---|---|
| DELETE | 0x00010000 | Delete the object |
| READ_CONTROL | 0x00020000 | Read security descriptor (except SACL) |
| WRITE_DAC | 0x00040000 | Modify DACL |
| WRITE_OWNER | 0x00080000 | Change owner |
| SYNCHRONIZE | 0x00100000 | Wait on object handle |
| ACCESS_SYSTEM_SECURITY | 0x01000000 | Access SACL |
| GENERIC_READ | 0x80000000 | Maps to reading rights |
| GENERIC_WRITE | 0x40000000 | Maps to writing rights |
| GENERIC_EXECUTE | 0x20000000 | Maps to execute rights |
| GENERIC_ALL | 0x10000000 | Full control |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
/** * Windows Access Control Entry (ACE) structure * * Each entry in a DACL has this logical structure */ typedef struct _ACCESS_ALLOWED_ACE { ACE_HEADER Header; // Type, flags, size ACCESS_MASK Mask; // Rights being granted/denied DWORD SidStart; // Start of SID (variable length)} ACCESS_ALLOWED_ACE; // ACE Types (Header.AceType):#define ACCESS_ALLOWED_ACE_TYPE 0x00 // Allow access#define ACCESS_DENIED_ACE_TYPE 0x01 // Deny access#define SYSTEM_AUDIT_ACE_TYPE 0x02 // Audit (SACL)#define ACCESS_ALLOWED_OBJECT_ACE_TYPE 0x05 // Object-specific allow#define ACCESS_DENIED_OBJECT_ACE_TYPE 0x06 // Object-specific deny // ACE Flags (Header.AceFlags) - Inheritance:#define OBJECT_INHERIT_ACE 0x01 // Child objects inherit#define CONTAINER_INHERIT_ACE 0x02 // Child containers inherit#define NO_PROPAGATE_INHERIT_ACE 0x04 // Don't propagate to grandchildren#define INHERIT_ONLY_ACE 0x08 // Not effective on this object#define INHERITED_ACE 0x10 // Was inherited /** * Windows ACL evaluation algorithm: * * 1. If DACL is NULL: Allow all access (dangerous!) * 2. If DACL is empty (0 entries): Deny all access * 3. Walk ACEs in order: * a. If ACE SID matches requestor's token: * - If ACCESS_DENIED_ACE: Check if denies requested access * → If yes, ACCESS DENIED * - If ACCESS_ALLOWED_ACE: Accumulate allowed rights * b. Continue until all requested rights granted or end of DACL * 4. If all requested rights granted: ACCESS ALLOWED * 5. Otherwise: ACCESS DENIED * * KEY INSIGHT: Order matters! DENY ACEs evaluated before ALLOW ACEs * in the same segment, but ACL order is critical for correct behavior. */ // Example DACL order (correctly structured):// ACE 1: DENY Guests GENERIC_ALL// ACE 2: ALLOW Administrators GENERIC_ALL// ACE 3: ALLOW Users READ// // A guest will be denied before reaching the Users ALLOW.Inheritance in Windows:
Windows ACL inheritance is sophisticated. When creating a new file:
This creates "inherited" ACEs (marked with INHERITED_ACE flag) that update when the parent changes, enabling centralized permission management.
Privileges vs. Rights:
Windows distinguishes:
Privileges include: SeBackupPrivilege (bypass checks for backup), SeDebugPrivilege (debug any process), SeShutdownPrivilege (shutdown system). These are similar to Linux capabilities.
Windows Vista+ added Mandatory Integrity Control (MIC). Objects and subjects have integrity levels (Low, Medium, High, System). Subjects cannot write to objects with higher integrity, regardless of DACL permissions. This implements a Biba-style integrity model on top of discretionary access control.
Real systems often have rights that imply other rights, or composite rights that bundle multiple primitives:
Right Implication:
If having right r₁ automatically grants right r₂, we say r₁ implies r₂:
$$r_1 \implies r_2 \equiv \forall s, o: r_1 \in A[s, o] \implies r_2 \in EffectiveRights(s, o)$$
Examples:
Composite Rights:
Composite rights are named bundles:
| System | Composite Right | Expands To |
|---|---|---|
| Unix | - | N/A (no composites) |
| Windows | GENERIC_READ | READ_CONTROL | FILE_READ_DATA | FILE_READ_ATTRIBUTES | FILE_READ_EA | SYNCHRONIZE |
| Windows | GENERIC_WRITE | READ_CONTROL | FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | FILE_WRITE_EA | FILE_APPEND_DATA | SYNCHRONIZE |
| AWS S3 | s3:GetObject | Actually several underlying operations |
| Kubernetes | edit | get, list, watch, create, update, patch, delete |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
# Right expansion and implication system class RightsSystem: def __init__(self): self.implications = {} # right -> set of implied rights self.composites = {} # composite -> set of primitive rights def add_implication(self, higher_right, lower_right): """Higher right implies lower right""" if higher_right not in self.implications: self.implications[higher_right] = set() self.implications[higher_right].add(lower_right) def define_composite(self, composite_name, primitive_rights): """Define a composite right as a bundle of primitives""" self.composites[composite_name] = set(primitive_rights) def expand_right(self, right): """Expand a right to all rights it includes""" result = {right} # If it's a composite, expand it if right in self.composites: for primitive in self.composites[right]: result.update(self.expand_right(primitive)) # Add implied rights if right in self.implications: for implied in self.implications[right]: result.update(self.expand_right(implied)) return result def effective_rights(self, granted_rights): """Calculate all effective rights from granted rights""" effective = set() for right in granted_rights: effective.update(self.expand_right(right)) return effective def check_access(self, granted_rights, required_right): """Check if granted rights satisfy required right""" return required_right in self.effective_rights(granted_rights) # Example: Windows-style right systemwin = RightsSystem() # Define compositeswin.define_composite('GENERIC_READ', [ 'READ_CONTROL', 'FILE_READ_DATA', 'FILE_READ_ATTRIBUTES', 'FILE_READ_EA', 'SYNCHRONIZE'])win.define_composite('GENERIC_WRITE', [ 'READ_CONTROL', 'FILE_WRITE_DATA', 'FILE_WRITE_ATTRIBUTES', 'FILE_WRITE_EA', 'FILE_APPEND_DATA', 'SYNCHRONIZE'])win.define_composite('GENERIC_ALL', [ 'GENERIC_READ', 'GENERIC_WRITE', 'DELETE', 'WRITE_DAC', 'WRITE_OWNER']) # Define implicationswin.add_implication('FILE_WRITE_DATA', 'FILE_APPEND_DATA') # Check accessgranted = {'GENERIC_READ'}effective = win.effective_rights(granted)print(f"Effective rights: {effective}")print(f"Can read data? {win.check_access(granted, 'FILE_READ_DATA')}") # Trueprint(f"Can write data? {win.check_access(granted, 'FILE_WRITE_DATA')}") # FalseThe Expansion Problem:
Composite rights create complexity in authorization:
Granting: When granting GENERIC_ALL, what's actually stored?
Checking: When checking FILE_READ_DATA, must we check for GENERIC_READ and GENERIC_ALL too?
Revoking: When revoking GENERIC_ALL, what happens to the primitives?
Conflict Resolution:
When rights conflict (e.g., explicit deny vs. inherited allow), systems use precedence rules:
Users are often surprised by their effective permissions. Windows provides an 'Effective Access' tab in security dialogs, and Linux has getfacl --effective. Always verify effective permissions when troubleshooting access issues—the interaction of composites, implications, and inheritance can produce unexpected results.
A right is only meaningful if it's enforced. The system must check rights at the appropriate place and time, and the check must be unforgeable.
Enforcement Points:
Rights are typically checked at:
The Reference Monitor Concept:
All access checks should flow through a reference monitor—a component that:
In Unix, the kernel is the reference monitor for file access. Each open(), read(), write(), execve() passes through kernel permission checks. The checks are hardcoded in the kernel, which is trusted.
Check Timing:
When should rights be checked?
Each approach has trade-offs between performance, security, and responsiveness to permission changes.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
/** * Conceptual reference monitor implementation * * This demonstrates the core authorization check pattern used * in operating system kernels for access control. */ #include <stdbool.h> // Subject credentialstypedef struct { uid_t uid; gid_t gid; gid_t groups[NGROUPS_MAX]; int ngroups; cap_t capabilities; security_context_t selinux_context;} credentials_t; // Object security attributestypedef struct { uid_t owner; gid_t group; mode_t mode; acl_t acl; security_context_t selinux_label;} security_attrs_t; // Access requesttypedef enum { ACCESS_READ, ACCESS_WRITE, ACCESS_EXECUTE, ACCESS_DELETE, ACCESS_CHMOD, ACCESS_CHOWN,} access_type_t; /** * THE REFERENCE MONITOR FUNCTION * * All access control decisions flow through this function. * It MUST be called for every access attempt. * It MUST be implemented correctly. * It MUST NOT be bypassable. */int check_access( credentials_t *subject, security_attrs_t *object, access_type_t requested_access) { // Rule 1: Check capabilities first (may override DAC) if (has_capability(subject, CAP_DAC_OVERRIDE)) { // Root-equivalent for read/write if (requested_access == ACCESS_READ || requested_access == ACCESS_WRITE) { audit_log("DAC override by capability"); return ALLOW; } } // Rule 2: Check DAC (Discretionary Access Control) int dac_result = check_dac(subject, object, requested_access); if (dac_result == DENY) { return DENY; } // Rule 3: Check MAC (Mandatory Access Control) - SELinux int mac_result = check_selinux( subject->selinux_context, object->selinux_label, requested_access ); if (mac_result == DENY) { return DENY; } // Rule 4: Check ACLs if present if (object->acl != NULL) { int acl_result = check_acl(subject, object->acl, requested_access); if (acl_result == DENY) { return DENY; } } // All checks passed audit_log("Access granted: %s -> %s (%s)", subject->uid, object->path, access_name(requested_access)); return ALLOW;} // CRITICAL: This function is called from every file operationint sys_open(const char *path, int flags, mode_t mode) { struct inode *inode = namei(path); // Resolve path if (!inode) return -ENOENT; credentials_t *creds = current->credentials; security_attrs_t attrs = get_security_attrs(inode); access_type_t access = flags_to_access_type(flags); // THE CRITICAL CHECK - cannot be skipped if (check_access(creds, &attrs, access) == DENY) { return -EACCES; // Permission denied } // Access granted - proceed with open return do_open(inode, flags, mode);}The reference monitor is part of the Trusted Computing Base (TCB)—the set of all hardware, firmware, and software components critical to security. A smaller TCB is easier to secure. Modern operating systems have large TCBs (millions of lines of kernel code), making complete verification impractical. Microkernels and separation kernels aim for smaller, verifiable TCBs.
We've explored access rights in depth—the specific permissions that populate access matrix cells. Let's consolidate the key concepts:
What's Next:
Now that we understand what rights are and how they're structured, we'll examine how the access matrix is actually implemented in real systems. The next page explores the two fundamental implementation strategies: Access Control Lists (grouping by column) and Capability Lists (grouping by row).
You now have a comprehensive understanding of access rights—from fundamental read/write/execute to complex meta-rights and enforcement mechanisms. This knowledge enables you to work with any access control system, whether configuring Unix permissions, designing Windows security descriptors, or implementing custom authorization in applications.