Skip to main content

Unix Filesystem Permissions

This example demonstrates how to implement classic Unix-style permission bits using MPE's annotation system. Just like Unix files have rwxrwxrwx permissions for owner, group, and other, this PolicyDomain models read/write access for three permission classes.

Overview

Overview

In Unix filesystems, every file has:

  • An owner (a specific user)
  • A group (a named group of users)
  • Permission bits for owner, group, and other (everyone else)

We'll model this using:

  • resource.owner — The principal who owns the resource
  • resource.annotations.group — The MRN of the owning group
  • resource.annotations.mode — Permission bits encoded as an object
  • principal.mgroups — Groups the principal belongs to

Design

Permission Model

Each resource has a mode annotation containing permission bits:

{
"owner": { "read": true, "write": true },
"group": { "read": true, "write": false },
"other": { "read": true, "write": false }
}

This is equivalent to Unix mode 0644 (owner: rw, group: r, other: r).

Access Decision Flow

Complete PolicyDomain

apiVersion: iamlite.manetu.io/v1alpha4
kind: PolicyDomain
metadata:
name: unix-filesystem
spec:
# ============================================================
# Policy Libraries - Reusable helper functions
# ============================================================
policy-libraries:
- mrn: &lib-utils "mrn:iam:library:utils"
name: utils
description: "Common utility functions"
rego: |
package utils

import rego.v1

# Check if request has a valid principal (authenticated)
has_principal if {
input.principal != {}
input.principal.sub != ""
}

- mrn: &lib-unix-perms "mrn:iam:library:unix-perms"
name: unix-perms
description: "Unix permission checking utilities"
rego: |
package unix_perms

import rego.v1

# Determine which permission class applies to this principal
# Returns: "owner", "group", or "other"
permission_class(principal, resource) := "owner" if {
principal.sub == resource.owner
}

permission_class(principal, resource) := "group" if {
principal.sub != resource.owner
resource.annotations.group in principal.mgroups
}

permission_class(principal, resource) := "other" if {
principal.sub != resource.owner
not resource.annotations.group in principal.mgroups
}

# Check if the permission class has the required permission
has_permission(mode, class, permission) if {
mode[class][permission] == true
}

# Map operations to required permissions
required_permission(operation) := "read" if {
some suffix in {":read", ":list", ":get"}
endswith(operation, suffix)
}

required_permission(operation) := "write" if {
some suffix in {":write", ":create", ":update", ":delete"}
endswith(operation, suffix)
}

# ============================================================
# Policies
# ============================================================
policies:
# Operation phase - require authentication for all file operations
- mrn: &policy-require-auth "mrn:iam:policy:require-auth"
name: require-auth
description: "Require authentication for file operations"
dependencies:
- *lib-utils
rego: |
package authz

import rego.v1
import data.utils

# Tri-level: negative=DENY, 0=GRANT, positive=GRANT Override
# Default deny - only grant if authenticated
default allow = -1

# Grant authenticated requests
allow = 0 if utils.has_principal

# Identity phase - any authenticated user can attempt file operations
# The actual permission check happens in the resource phase
- mrn: &policy-authenticated-user "mrn:iam:policy:authenticated-user"
name: authenticated-user
description: "Allow any authenticated user to proceed to resource phase"
dependencies:
- *lib-utils
rego: |
package authz

import rego.v1
import data.utils

default allow = false

# Grant if authenticated
allow if utils.has_principal

# Resource phase - Unix permission bit checking
- mrn: &policy-unix-permissions "mrn:iam:policy:unix-permissions"
name: unix-permissions
description: "Check Unix-style permission bits"
dependencies:
- *lib-unix-perms
rego: |
package authz

import rego.v1
import data.unix_perms

default allow = false

# Main permission check
allow if {
# Get the permission mode from the resource
mode := input.resource.annotations.mode

# Determine which class this principal falls into
class := unix_perms.permission_class(input.principal, input.resource)

# Get the required permission for this operation
required := unix_perms.required_permission(input.operation)

# Check if the permission is granted
unix_perms.has_permission(mode, class, required)
}

# Superuser bypass - if principal has superuser role, always allow
allow if {
"mrn:iam:role:superuser" in input.principal.mroles
}

# ============================================================
# Roles
# ============================================================
roles:
# Regular user - relies on file permissions
- mrn: &role-user "mrn:iam:role:user"
name: user
description: "Regular filesystem user"
policy: *policy-authenticated-user

# Superuser - bypasses permission checks (like root)
- mrn: &role-superuser "mrn:iam:role:superuser"
name: superuser
description: "Superuser with full access (like root)"
policy: *policy-authenticated-user

# ============================================================
# Groups - User groups for file permissions
# ============================================================
groups:
- mrn: "mrn:iam:group:developers"
name: developers
description: "Development team"
roles:
- *role-user

- mrn: "mrn:iam:group:admins"
name: admins
description: "System administrators"
roles:
- *role-superuser

- mrn: "mrn:iam:group:finance"
name: finance
description: "Finance department"
roles:
- *role-user

- mrn: "mrn:iam:group:hr"
name: hr
description: "Human resources"
roles:
- *role-user

# ============================================================
# Resource Groups
# ============================================================
resource-groups:
- mrn: &rg-files "mrn:iam:resource-group:files"
name: files
description: "All filesystem resources"
default: true
policy: *policy-unix-permissions

# ============================================================
# Operations
# ============================================================
operations:
- name: file-operations
selector:
- "file:.*"
policy: *policy-require-auth

Test Cases

Test 1: Owner Read Access

The owner can read their own file with owner read permission:

{
"principal": {
"sub": "alice",
"mroles": ["mrn:iam:role:user"],
"mgroups": ["mrn:iam:group:developers"]
},
"operation": "file:document:read",
"resource": {
"id": "mrn:fs:home:alice:document.txt",
"owner": "alice",
"group": "mrn:iam:resource-group:files",
"annotations": {
"group": "mrn:iam:group:developers",
"mode": {
"owner": { "read": true, "write": true },
"group": { "read": true, "write": false },
"other": { "read": false, "write": false }
}
}
}
}

Expected:

GRANT
(alice is owner, owner has read permission)

mpe test decision -b policydomain.yml -i input.json | jq .decision
# "GRANT"

Test 2: Owner Write Access

The owner can write to their own file:

{
"principal": {
"sub": "alice",
"mroles": ["mrn:iam:role:user"],
"mgroups": ["mrn:iam:group:developers"]
},
"operation": "file:document:write",
"resource": {
"id": "mrn:fs:home:alice:document.txt",
"owner": "alice",
"group": "mrn:iam:resource-group:files",
"annotations": {
"group": "mrn:iam:group:developers",
"mode": {
"owner": { "read": true, "write": true },
"group": { "read": true, "write": false },
"other": { "read": false, "write": false }
}
}
}
}

Expected:

GRANT
(alice is owner, owner has write permission)

Test 3: Group Read Access

A group member can read a file with group read permission:

{
"principal": {
"sub": "bob",
"mroles": ["mrn:iam:role:user"],
"mgroups": ["mrn:iam:group:developers"]
},
"operation": "file:document:read",
"resource": {
"id": "mrn:fs:home:alice:shared.txt",
"owner": "alice",
"group": "mrn:iam:resource-group:files",
"annotations": {
"group": "mrn:iam:group:developers",
"mode": {
"owner": { "read": true, "write": true },
"group": { "read": true, "write": false },
"other": { "read": false, "write": false }
}
}
}
}

Expected:

GRANT
(bob is in developers group, group has read permission)

Test 4: Group Write Denied

A group member cannot write to a file without group write permission:

{
"principal": {
"sub": "bob",
"mroles": ["mrn:iam:role:user"],
"mgroups": ["mrn:iam:group:developers"]
},
"operation": "file:document:write",
"resource": {
"id": "mrn:fs:home:alice:shared.txt",
"owner": "alice",
"group": "mrn:iam:resource-group:files",
"annotations": {
"group": "mrn:iam:group:developers",
"mode": {
"owner": { "read": true, "write": true },
"group": { "read": true, "write": false },
"other": { "read": false, "write": false }
}
}
}
}

Expected:

DENY
(bob is in group, but group lacks write permission)

Test 5: Other User Denied

A user not in the file's group falls back to "other" permissions:

{
"principal": {
"sub": "charlie",
"mroles": ["mrn:iam:role:user"],
"mgroups": ["mrn:iam:group:finance"]
},
"operation": "file:document:read",
"resource": {
"id": "mrn:fs:home:alice:private.txt",
"owner": "alice",
"group": "mrn:iam:resource-group:files",
"annotations": {
"group": "mrn:iam:group:developers",
"mode": {
"owner": { "read": true, "write": true },
"group": { "read": true, "write": false },
"other": { "read": false, "write": false }
}
}
}
}

Expected:

DENY
(charlie is not owner or in developers group, other has no read)

Test 6: World-Readable File

Any authenticated user can read a world-readable file:

{
"principal": {
"sub": "charlie",
"mroles": ["mrn:iam:role:user"],
"mgroups": ["mrn:iam:group:finance"]
},
"operation": "file:document:read",
"resource": {
"id": "mrn:fs:public:readme.txt",
"owner": "system",
"group": "mrn:iam:resource-group:files",
"annotations": {
"group": "mrn:iam:group:admins",
"mode": {
"owner": { "read": true, "write": true },
"group": { "read": true, "write": true },
"other": { "read": true, "write": false }
}
}
}
}

Expected:

GRANT
(other has read permission)

Test 7: Superuser Override

A superuser can access any file regardless of permissions:

{
"principal": {
"sub": "root",
"mroles": ["mrn:iam:role:superuser"],
"mgroups": ["mrn:iam:group:admins"]
},
"operation": "file:document:write",
"resource": {
"id": "mrn:fs:home:alice:locked.txt",
"owner": "alice",
"group": "mrn:iam:resource-group:files",
"annotations": {
"group": "mrn:iam:group:developers",
"mode": {
"owner": { "read": true, "write": false },
"group": { "read": false, "write": false },
"other": { "read": false, "write": false }
}
}
}
}

Expected:

GRANT
(superuser bypasses all permission checks)

Test 8: Unauthenticated Access Denied

An unauthenticated request is denied at the operation phase:

{
"principal": {},
"operation": "file:document:read",
"resource": {
"id": "mrn:fs:public:readme.txt",
"owner": "system",
"group": "mrn:iam:resource-group:files",
"annotations": {
"group": "mrn:iam:group:admins",
"mode": {
"owner": { "read": true, "write": true },
"group": { "read": true, "write": true },
"other": { "read": true, "write": false }
}
}
}
}

Expected:

DENY
(no authentication)

Key Concepts Demonstrated

1. Policy Libraries for Reusable Logic

The unix-perms library contains helper functions that can be imported by any policy. This keeps the main policy clean and makes the logic testable in isolation.

2. Annotation-Based Permission Storage

Instead of hardcoding permissions in policies, we store them as resource annotations. This allows:

  • Different files to have different permissions
  • Permission changes without policy updates
  • Standard Unix permission semantics

3. Three-Level Permission Hierarchy

The permission_class function implements the Unix model:

  1. Check if principal is owner
  2. Check if principal is in the file's group
  3. Fall back to "other" permissions

4. Role-Based Superuser Bypass

The superuser role bypasses all permission checks, similar to Unix root. This is implemented cleanly in the resource policy with an additional allow rule.

5. Operation-to-Permission Mapping

The required_permission function maps operations like :read, :write, :delete to the corresponding permission bits. This makes it easy to add new operations that map to existing permissions.

Extending This Example

Adding Execute Permission

To support execute permission (for running scripts):

required_permission(operation) := "execute" if {
some suffix in {":execute", ":run"}
endswith(operation, suffix)
}

Update the mode annotations to include execute:

{
"mode": {
"owner": { "read": true, "write": true, "execute": true },
"group": { "read": true, "write": false, "execute": true },
"other": { "read": true, "write": false, "execute": false }
}
}

Adding Sticky Bit / SetUID Semantics

For special bits, add additional annotations:

{
"sticky": true,
"setuid": false,
"setgid": true
}

Then check these in the policy before allowing certain operations.

ACL Extension

For Access Control Lists beyond owner/group/other:

{
"acl": [
{ "principal": "bob", "read": true, "write": true },
{ "principal": "mrn:iam:group:auditors", "read": true, "write": false }
]
}

Add a check in the policy that evaluates ACL entries before falling back to standard permissions.