Authorization and Access Control
When to Use
Every operation that accesses resources must check authorization — reading data, modifying data, deleting data, accessing admin features. Authentication answers "who are you?", authorization answers "what can you do?"
Decision
| If you need to... | Use... | Why |
|---|---|---|
| Role-based permissions | RBAC (Role-Based Access Control) | Simple, manageable - users have roles (admin, editor, viewer) |
| Fine-grained permissions | ABAC (Attribute-Based Access Control) | Complex rules - based on attributes (department, location, time) |
| Resource ownership | Ownership checks + RBAC | Users can only modify their own resources |
| Hierarchical permissions | Permission inheritance | Organizations -> teams -> users |
| API access control | Scopes + claims (OAuth/JWT) | Granular API permissions |
RBAC Pattern
ROLES = {
'admin': ['create', 'read', 'update', 'delete', 'admin_panel'],
'editor': ['create', 'read', 'update'],
'viewer': ['read']
}
def require_permission(permission):
def decorator(func):
def wrapper(*args, **kwargs):
user = get_current_user()
user_permissions = ROLES.get(user['role'], [])
if permission not in user_permissions:
return {"error": "Forbidden"}, 403
return func(*args, **kwargs)
return wrapper
return decorator
@app.route('/api/articles', methods=['POST'])
@require_permission('create')
def create_article():
article = request.json
db.execute("INSERT INTO articles (...) VALUES (...)", article)
return {"id": article_id}, 201
Resource ownership checks:
@app.route('/api/articles/<int:article_id>', methods=['PUT'])
@require_authentication
def update_article(article_id):
user = get_current_user()
article = db.execute("SELECT * FROM articles WHERE id = ?", [article_id]).fetchone()
if not article:
return {"error": "Not found"}, 404
if article['author_id'] != user['id'] and user['role'] != 'admin':
return {"error": "Forbidden - not your article"}, 403
# Proceed with update
ABAC Pattern
def check_access(user, resource, action):
rules = [
{
'condition': lambda u, r: u['role'] == 'dept_head' and u['department'] == r['department'],
'allows': ['approve', 'read']
},
{
'condition': lambda u, r: u['role'] == 'manager' and r['team_id'] in u['managed_teams'],
'allows': ['read', 'update']
},
{
'condition': lambda u, r: 9 <= datetime.now().hour < 17,
'allows': ['create', 'update', 'delete']
}
]
for rule in rules:
if rule['condition'](user, resource) and action in rule['allows']:
return True
return False
Access Control Lists (ACLs)
CREATE TABLE permissions (
id INTEGER PRIMARY KEY,
resource_type VARCHAR(50),
resource_id INTEGER,
user_id INTEGER,
permission VARCHAR(20),
UNIQUE(resource_type, resource_id, user_id, permission)
);
def has_permission(user_id, resource_type, resource_id, permission):
result = db.execute("""
SELECT COUNT(*) as count FROM permissions
WHERE resource_type = ? AND resource_id = ? AND user_id = ? AND permission = ?
""", [resource_type, resource_id, user_id, permission]).fetchone()
return result['count'] > 0
Principle of Least Privilege
# Bad: Default allow
def can_access_admin_panel(user):
if user['role'] == 'banned':
return False
return True # Everyone else can access - WRONG
# Good: Default deny
def can_access_admin_panel(user):
allowed_roles = ['admin', 'superadmin']
return user['role'] in allowed_roles
Insecure Direct Object References (IDOR)
# Vulnerable - no ownership check
@app.route('/api/orders/<int:order_id>')
def get_order(order_id):
order = db.execute("SELECT * FROM orders WHERE id = ?", [order_id]).fetchone()
return jsonify(order)
# Attack: Change order_id in URL to access other users' orders
# Fixed - verify ownership
@app.route('/api/orders/<int:order_id>')
def get_order_secure(order_id):
user = get_current_user()
order = db.execute("""
SELECT * FROM orders WHERE id = ? AND user_id = ?
""", [order_id, user['id']]).fetchone()
if not order:
return {"error": "Not found"}, 404 # Don't leak existence
return jsonify(order)
Use UUIDs instead of sequential IDs:
import uuid
invoice_id = str(uuid.uuid4()) # '550e8400-e29b-41d4-a716-446655440000'
# Unpredictable, can't enumerate
Common Mistakes
- A01:2021 Broken Access Control is #1 OWASP Top 10 — Most common vulnerability, found in 3.81% of applications tested
- Client-side access control — Hiding UI elements is NOT security. Always enforce server-side
- No re-authorization for sensitive actions — Password change, email change should require re-entering password
- Trusting user-supplied IDs — Never use
request.json['user_id']for access control. Get user from session/token - Horizontal privilege escalation — User A modifying User B's data (IDOR). Always check resource ownership
- Vertical privilege escalation — Regular user accessing admin functions. Check role/permissions on EVERY request
- Missing function-level access control — API endpoint
/api/admin/delete-usernot checking if user is admin - Not checking access on file downloads —
/uploads/invoices/123.pdfaccessible without authentication - Caching access control decisions — User permissions change. Don't cache for > 5 minutes
See Also
- Previous: Authentication Best Practices | Next: Sensitive Data Protection
- Reference: OWASP Authorization Cheat Sheet
- Reference: OWASP A01:2021 Broken Access Control