Permissions

Authorization Security Notes Jan 6, 2025 JAVASCRIPT

Definition

When you log into Gmail, you can read and send emails. But you can’t delete someone else’s emails or change Google’s mail servers. Those boundaries - what you can and cannot do - are permissions. They’re the specific access rights that define exactly what actions a user can perform on which resources. “Can read their own emails” is a permission. “Can modify all user accounts” is another permission. They’re the atoms of authorization, the smallest units of “allowed to do X.”

Permissions are typically expressed as action-resource pairs: “read:documents,” “write:users,” “delete:comments.” This format makes them precise and composable. A content editor might have “read:articles,” “write:articles,” and “publish:articles.” A viewer only has “read:articles.” An admin might have “admin:*” - a wildcard that grants everything. By combining these atomic permissions, you build up exactly the access levels your system needs.

The power of permissions is their granularity. Instead of just “admin” or “not admin,” you can express nuanced access like “can edit documents they created,” “can view reports from their department,” or “can approve expenses under $1000.” Modern systems often add conditional permissions: “can delete comments they authored within 24 hours.” This granularity lets you implement the principle of least privilege - giving users exactly the permissions they need and nothing more. It’s the difference between giving someone a master key to your building versus giving them a specific key for the one room they actually need to enter.

Example

Permissions shape access control in every system you use:

GitHub Repository Access: When you’re added to a GitHub repo, you’re assigned permissions like “read” (clone, view code), “triage” (manage issues), “write” (push code), “maintain” (manage settings), or “admin” (everything including deleting the repo). Each permission level is really a bundle of specific action-resource pairs.

Google Workspace Sharing: When you share a Google Doc, you choose “Viewer” (read), “Commenter” (read + comment), or “Editor” (read + write). But behind the scenes, Google has granular permissions: can copy, can download, can print, can share with others. Enterprise admins can modify which permissions come with each role.

AWS IAM Policies: AWS permissions are incredibly granular: “s3:GetObject” lets you read from S3, “s3:PutObject” lets you write, “s3:DeleteObject” lets you delete. You can combine these with conditions: only allow “s3:GetObject” on files in a specific bucket, only during business hours, only from specific IP addresses.

Mobile App Permissions: When an app asks for “Camera” permission on your phone, it’s requesting “capture:photos” and “capture:video.” “Location” permission includes “access:fine-location” and “access:coarse-location.” Each permission is a specific capability the app either has or doesn’t have.

Database Access: PostgreSQL GRANT statements are permissions: “GRANT SELECT ON users TO app_user” gives read permission. “GRANT INSERT, UPDATE ON orders TO order_service” gives write permission. “GRANT ALL ON schema TO admin” is the wildcard.

Analogy

The Hotel Room Key System: Modern hotel keys aren’t just room keys - they’re permission cards. Your card might unlock your room (read:room123), the pool area (access:pool), and the gym (access:gym), but not the staff areas or other guests’ rooms. The front desk programs exactly which permissions your card has.

The Keyring Full of Keys: Think of permissions as individual keys on a keyring. One key opens the front door (access:building), another opens your office (access:office-42), a third opens the supply closet (read:supplies). A janitor has different keys than you do. The building manager has a master key that opens everything.

The Restaurant Kitchen Access: In a restaurant, servers can access the dining room and POS system. Cooks can access the kitchen but not the safe. Managers can access the office and safe. The owner has access to everything. Each role carries specific permissions about which areas and systems they can use.

The Movie Theater Ticket: Your ticket is a permission. It grants “enter:theater-7” and “occupy:seat-J12” for a specific showtime. It doesn’t grant access to other theaters, the projection room, or tomorrow’s show. It’s a very specific, scoped, time-limited permission.

Code Example


// Permission system implementation
class PermissionManager {
  constructor() {
    // Permission format: "action:resource:condition"
    this.permissions = new Map()
  }

  // Check if user has permission
  hasPermission(user, requiredPermission) {
    const userPermissions = this.getUserPermissions(user)

    // Direct match
    if (userPermissions.has(requiredPermission)) {
      return true
    }

    // Wildcard match
    const [action, resource, condition] = requiredPermission.split(':')

    // Check wildcards: admin:*, *:documents, read:*
    if (
      userPermissions.has('*:*') ||
      userPermissions.has(`${action}:*`) ||
      userPermissions.has(`*:${resource}`)
    ) {
      return true
    }

    return false
  }

  getUserPermissions(user) {
    const permissions = new Set()

    // Add direct user permissions
    if (user.permissions) {
      user.permissions.forEach(p => permissions.add(p))
    }

    // Add role-based permissions
    if (user.roles) {
      user.roles.forEach(role => {
        const rolePerms = this.getRolePermissions(role)
        rolePerms.forEach(p => permissions.add(p))
      })
    }

    return permissions
  }

  getRolePermissions(role) {
    const rolePermissionMap = {
      'admin': ['*:*'],
      'editor': [
        'read:documents',
        'write:documents',
        'read:comments',
        'write:comments'
      ],
      'viewer': [
        'read:documents',
        'read:comments'
      ],
      'moderator': [
        'read:*',
        'delete:comments',
        'update:comments'
      ]
    }

    return rolePermissionMap[role] || []
  }

  // Check resource-specific permission with ownership
  hasResourcePermission(user, action, resource) {
    const permission = `${action}:${resource.type}`

    // Check basic permission
    if (!this.hasPermission(user, permission)) {
      // Check if has "own" variant
      const ownPermission = `${action}:own-${resource.type}`
      if (this.hasPermission(user, ownPermission)) {
        // Verify ownership
        return resource.ownerId === user.id
      }
      return false
    }

    return true
  }
}

// Permission-based middleware
const permissionManager = new PermissionManager()

function requirePermission(permission) {
  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: 'Unauthorized' })
    }

    if (!permissionManager.hasPermission(req.user, permission)) {
      return res.status(403).json({
        error: 'Forbidden',
        message: `Missing required permission: ${permission}`
      })
    }

    next()
  }
}

// Usage in routes
app.get('/api/documents',
  requirePermission('read:documents'),
  async (req, res) => {
    const documents = await db.documents.findAll()
    res.json(documents)
  }
)

app.delete('/api/documents/:id',
  async (req, res) => {
    const document = await db.documents.findById(req.params.id)

    if (!document) {
      return res.status(404).json({ error: 'Not found' })
    }

    // Check resource-specific permission
    if (!permissionManager.hasResourcePermission(
      req.user,
      'delete',
      document
    )) {
      return res.status(403).json({
        error: 'Forbidden',
        message: 'Cannot delete this document'
      })
    }

    await db.documents.delete(req.params.id)
    res.status(204).send()
  }
)

// Permission hierarchy
class PermissionHierarchy {
  constructor() {
    this.hierarchy = {
      'admin': ['editor', 'moderator', 'viewer'],
      'editor': ['viewer'],
      'moderator': ['viewer']
    }
  }

  // Check if role A implies role B
  implies(roleA, roleB) {
    if (roleA === roleB) return true

    const implied = this.hierarchy[roleA] || []
    if (implied.includes(roleB)) return true

    // Recursive check
    return implied.some(role => this.implies(role, roleB))
  }

  hasPermissionViaHierarchy(user, requiredPermission) {
    const permManager = new PermissionManager()

    // Check direct permissions
    if (permManager.hasPermission(user, requiredPermission)) {
      return true
    }

    // Check via role hierarchy
    for (const userRole of user.roles || []) {
      for (const [role, perms] of Object.entries(permManager.rolePermissionMap)) {
        if (this.implies(userRole, role)) {
          if (perms.includes(requiredPermission)) {
            return true
          }
        }
      }
    }

    return false
  }
}

// Conditional permissions
class ConditionalPermission {
  constructor(permission, condition) {
    this.permission = permission
    this.condition = condition
  }

  evaluate(user, resource, context) {
    return this.condition(user, resource, context)
  }
}

const conditionalPerms = [
  new ConditionalPermission(
    'delete:documents',
    (user, doc, ctx) => {
      // Can delete if owner OR admin OR created less than 1 hour ago
      return (
        doc.ownerId === user.id ||
        user.roles.includes('admin') ||
        (Date.now() - doc.createdAt) < 3600000
      )
    }
  )
]

Security Notes

SECURITY NOTES

CRITICAL: Permissions define what users can do. Implement using RBAC, ABAC, or fine-grained approach.

Permission Models:

  • RBAC: Role-based (admin, user, guest)
  • ABAC: Attribute-based (time, location, department)
  • PBAC: Permission-based (specific actions)
  • Fine-grained: Row, column, field-level

Permission Grant:

  • Explicit grant: Explicitly grant permissions
  • Default deny: Deny unless explicitly allowed
  • Role inheritance: Roles inherit permissions
  • Permission delegation: Allow delegating permissions

Verification:

  • Every request: Verify permissions for every request
  • Fine-grained: Check specific permissions needed
  • Consistent: Apply same rules everywhere
  • Fail closed: Deny on permission check failure

Best Practices:

  • Principle of least privilege: Minimal permissions needed
  • Regular review: Review and audit permissions
  • Separation of duties: Prevent conflicts of interest
  • Documented: Document all permissions
  • Tested: Test permission edge cases

Standards & RFCs