Some checks failed
Consolidate User.role (2-value: admin/store) + User.is_super_admin (boolean) into a single 4-value UserRole enum: super_admin, platform_admin, merchant_owner, store_member. Drop stale StoreUser.user_type column. Fix role="user" bug in merchant creation. Key changes: - Expand UserRole enum from 2 to 4 values with computed properties (is_admin, is_super_admin, is_platform_admin, is_merchant_owner, is_store_user) - Add Alembic migration (tenancy_003) for data migration + column drops - Remove is_super_admin from JWT token payload - Update all auth dependencies, services, routes, templates, JS, and tests - Update all RBAC documentation 66 files changed, 1219 unit tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
529 lines
11 KiB
Markdown
529 lines
11 KiB
Markdown
# RBAC Quick Reference Card
|
|
|
|
**For Daily Development** | Keep this handy while coding
|
|
|
|
---
|
|
|
|
## Common Imports
|
|
|
|
```python
|
|
# Authentication dependencies
|
|
from app.api.deps import (
|
|
get_current_admin_from_cookie_or_header,
|
|
get_current_store_from_cookie_or_header,
|
|
require_store_permission,
|
|
require_store_owner,
|
|
get_user_permissions
|
|
)
|
|
|
|
# Permission constants
|
|
from app.core.permissions import StorePermissions
|
|
|
|
# Exceptions
|
|
from app.exceptions import (
|
|
InsufficientStorePermissionsException,
|
|
StoreOwnerOnlyException
|
|
)
|
|
|
|
# Services
|
|
from app.services.store_team_service import store_team_service
|
|
```
|
|
|
|
---
|
|
|
|
## Route Patterns
|
|
|
|
### Admin Route (Cookie OR Header)
|
|
```python
|
|
@router.get("/admin/stores")
|
|
def list_stores(
|
|
user: User = Depends(get_current_admin_from_cookie_or_header)
|
|
):
|
|
# user is authenticated admin
|
|
...
|
|
```
|
|
|
|
### Admin API (Header Only)
|
|
```python
|
|
@router.post("/api/v1/admin/stores")
|
|
def create_store(
|
|
user: User = Depends(get_current_admin_api)
|
|
):
|
|
# user is authenticated admin (header required)
|
|
...
|
|
```
|
|
|
|
### Store Route with Permission
|
|
```python
|
|
@router.post("/store/{code}/products")
|
|
def create_product(
|
|
user: User = Depends(require_store_permission(
|
|
StorePermissions.PRODUCTS_CREATE.value
|
|
))
|
|
):
|
|
# user has products.create permission
|
|
store = request.state.store
|
|
...
|
|
```
|
|
|
|
### Owner-Only Route
|
|
```python
|
|
@router.post("/store/{code}/team/invite")
|
|
def invite_member(
|
|
user: User = Depends(require_store_owner)
|
|
):
|
|
# user is store owner
|
|
store = request.state.store
|
|
...
|
|
```
|
|
|
|
### Multi-Permission Route
|
|
```python
|
|
@router.post("/store/{code}/products/bulk")
|
|
def bulk_operation(
|
|
user: User = Depends(require_all_store_permissions(
|
|
StorePermissions.PRODUCTS_VIEW.value,
|
|
StorePermissions.PRODUCTS_EDIT.value
|
|
))
|
|
):
|
|
# user has ALL specified permissions
|
|
...
|
|
```
|
|
|
|
---
|
|
|
|
## Permission Constants
|
|
|
|
### Quick Lookup
|
|
|
|
```python
|
|
# Dashboard
|
|
StorePermissions.DASHBOARD_VIEW
|
|
|
|
# Products
|
|
StorePermissions.PRODUCTS_VIEW
|
|
StorePermissions.PRODUCTS_CREATE
|
|
StorePermissions.PRODUCTS_EDIT
|
|
StorePermissions.PRODUCTS_DELETE
|
|
StorePermissions.PRODUCTS_IMPORT
|
|
StorePermissions.PRODUCTS_EXPORT
|
|
|
|
# Stock
|
|
StorePermissions.STOCK_VIEW
|
|
StorePermissions.STOCK_EDIT
|
|
StorePermissions.STOCK_TRANSFER
|
|
|
|
# Orders
|
|
StorePermissions.ORDERS_VIEW
|
|
StorePermissions.ORDERS_EDIT
|
|
StorePermissions.ORDERS_CANCEL
|
|
StorePermissions.ORDERS_REFUND
|
|
|
|
# Customers
|
|
StorePermissions.CUSTOMERS_VIEW
|
|
StorePermissions.CUSTOMERS_EDIT
|
|
StorePermissions.CUSTOMERS_DELETE
|
|
StorePermissions.CUSTOMERS_EXPORT
|
|
|
|
# Marketing
|
|
StorePermissions.MARKETING_VIEW
|
|
StorePermissions.MARKETING_CREATE
|
|
StorePermissions.MARKETING_SEND
|
|
|
|
# Reports
|
|
StorePermissions.REPORTS_VIEW
|
|
StorePermissions.REPORTS_FINANCIAL
|
|
StorePermissions.REPORTS_EXPORT
|
|
|
|
# Settings
|
|
StorePermissions.SETTINGS_VIEW
|
|
StorePermissions.SETTINGS_EDIT
|
|
StorePermissions.SETTINGS_THEME
|
|
StorePermissions.SETTINGS_DOMAINS
|
|
|
|
# Team
|
|
StorePermissions.TEAM_VIEW
|
|
StorePermissions.TEAM_INVITE
|
|
StorePermissions.TEAM_EDIT
|
|
StorePermissions.TEAM_REMOVE
|
|
|
|
# Imports
|
|
StorePermissions.IMPORTS_VIEW
|
|
StorePermissions.IMPORTS_CREATE
|
|
StorePermissions.IMPORTS_CANCEL
|
|
```
|
|
|
|
---
|
|
|
|
## User Role Properties
|
|
|
|
```python
|
|
# Check if super admin (computed: role == "super_admin")
|
|
user.is_super_admin # bool
|
|
|
|
# Check if any admin (computed: role in ("super_admin", "platform_admin"))
|
|
user.is_admin # bool
|
|
|
|
# Check if platform admin (computed: role == "platform_admin")
|
|
user.is_platform_admin # bool
|
|
|
|
# Check if merchant owner (computed: role == "merchant_owner")
|
|
user.is_merchant_owner # bool
|
|
|
|
# Check if store-level user (computed: role in ("merchant_owner", "store_member"))
|
|
user.is_store_user # bool
|
|
```
|
|
|
|
## User Helper Methods
|
|
|
|
```python
|
|
# Check store ownership (via Merchant.owner_user_id)
|
|
user.is_owner_of(store_id) # bool
|
|
|
|
# Check store membership
|
|
user.is_member_of(store_id) # bool
|
|
|
|
# Get role in store
|
|
user.get_store_role(store_id) # str: "owner" | role name | None
|
|
|
|
# Check specific permission
|
|
user.has_store_permission(store_id, "products.create") # bool
|
|
```
|
|
|
|
---
|
|
|
|
## StoreUser Helper Methods
|
|
|
|
```python
|
|
# Check if owner (derived from User.role and Merchant.owner_user_id)
|
|
store_user.is_owner # bool
|
|
|
|
# Check if team member
|
|
store_user.is_team_member # bool
|
|
|
|
# Check invitation status
|
|
store_user.is_invitation_pending # bool
|
|
|
|
# Check permission
|
|
store_user.has_permission("products.create") # bool
|
|
|
|
# Get all permissions
|
|
store_user.get_all_permissions() # list[str]
|
|
```
|
|
|
|
---
|
|
|
|
## Service Methods
|
|
|
|
### Team Management
|
|
|
|
```python
|
|
# Invite team member
|
|
store_team_service.invite_team_member(
|
|
db=db,
|
|
store=store,
|
|
inviter=current_user,
|
|
email="member@example.com",
|
|
role_name="Staff",
|
|
custom_permissions=None # Optional
|
|
)
|
|
|
|
# Accept invitation
|
|
store_team_service.accept_invitation(
|
|
db=db,
|
|
invitation_token=token,
|
|
password="password123",
|
|
first_name="John",
|
|
last_name="Doe"
|
|
)
|
|
|
|
# Remove team member
|
|
store_team_service.remove_team_member(
|
|
db=db,
|
|
store=store,
|
|
user_id=member_id
|
|
)
|
|
|
|
# Update member role
|
|
store_team_service.update_member_role(
|
|
db=db,
|
|
store=store,
|
|
user_id=member_id,
|
|
new_role_name="Manager",
|
|
custom_permissions=None
|
|
)
|
|
|
|
# Get team members
|
|
members = store_team_service.get_team_members(
|
|
db=db,
|
|
store=store,
|
|
include_inactive=False
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Exception Handling
|
|
|
|
```python
|
|
from app.exceptions import (
|
|
InsufficientStorePermissionsException,
|
|
StoreOwnerOnlyException,
|
|
StoreAccessDeniedException,
|
|
InvalidInvitationTokenException,
|
|
CannotRemoveStoreOwnerException,
|
|
TeamMemberAlreadyExistsException
|
|
)
|
|
|
|
# Raise permission error
|
|
raise InsufficientStorePermissionsException(
|
|
required_permission="products.create",
|
|
store_code=store.store_code
|
|
)
|
|
|
|
# Raise owner-only error
|
|
raise StoreOwnerOnlyException(
|
|
operation="team management",
|
|
store_code=store.store_code
|
|
)
|
|
|
|
# Raise access denied
|
|
raise StoreAccessDeniedException(
|
|
store_code=store.store_code,
|
|
user_id=user.id
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Frontend Permission Checks
|
|
|
|
### JavaScript/Alpine.js
|
|
|
|
```javascript
|
|
// Check permission
|
|
function hasPermission(permission) {
|
|
const permissions = JSON.parse(
|
|
localStorage.getItem('permissions') || '[]'
|
|
);
|
|
return permissions.includes(permission);
|
|
}
|
|
|
|
// Conditional rendering
|
|
{hasPermission('products.create') && (
|
|
<CreateButton />
|
|
)}
|
|
|
|
// Disable button
|
|
<button disabled={!hasPermission('products.edit')}>
|
|
Edit
|
|
</button>
|
|
|
|
// Get permissions on login
|
|
async function getPermissions() {
|
|
const response = await fetch(
|
|
'/api/v1/store/team/me/permissions',
|
|
{
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
}
|
|
);
|
|
const data = await response.json();
|
|
localStorage.setItem(
|
|
'permissions',
|
|
JSON.stringify(data.permissions)
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Testing Patterns
|
|
|
|
### Unit Test
|
|
```python
|
|
def test_owner_has_all_permissions():
|
|
user = create_user(role="merchant_owner")
|
|
store_user = create_store_user(user=user, store=store)
|
|
assert store_user.has_permission("products.create")
|
|
assert store_user.has_permission("team.invite")
|
|
```
|
|
|
|
### Integration Test
|
|
```python
|
|
def test_create_product_with_permission(client):
|
|
user = create_user_with_permission("products.create")
|
|
token = create_token(user)
|
|
|
|
response = client.post(
|
|
"/api/v1/store/ACME/products",
|
|
json={"name": "Test"},
|
|
headers={"Authorization": f"Bearer {token}"}
|
|
)
|
|
|
|
assert response.status_code == 201
|
|
```
|
|
|
|
---
|
|
|
|
## Common Mistakes to Avoid
|
|
|
|
### ❌ DON'T: Check permissions in service layer
|
|
```python
|
|
# BAD
|
|
def create_product(user, data):
|
|
if not user.has_permission("products.create"):
|
|
raise Exception()
|
|
```
|
|
|
|
### ✅ DO: Check permissions at route level
|
|
```python
|
|
# GOOD
|
|
@router.post("/products")
|
|
def create_product(
|
|
user: User = Depends(require_store_permission("products.create"))
|
|
):
|
|
return service.create_product(data)
|
|
```
|
|
|
|
---
|
|
|
|
### ❌ DON'T: Use magic strings
|
|
```python
|
|
# BAD
|
|
require_store_permission("products.creat") # Typo!
|
|
```
|
|
|
|
### ✅ DO: Use constants
|
|
```python
|
|
# GOOD
|
|
require_store_permission(StorePermissions.PRODUCTS_CREATE.value)
|
|
```
|
|
|
|
---
|
|
|
|
### ❌ DON'T: Mix contexts
|
|
```python
|
|
# BAD - Admin trying to access store route
|
|
# This will be blocked automatically
|
|
```
|
|
|
|
### ✅ DO: Use correct portal
|
|
```python
|
|
# GOOD - Admins use /admin/*, stores use /store/*
|
|
```
|
|
|
|
---
|
|
|
|
## Debugging Commands
|
|
|
|
### Check User Access
|
|
```python
|
|
user = db.query(User).get(user_id)
|
|
store = db.query(Store).get(store_id)
|
|
|
|
print(f"Is owner: {user.is_owner_of(store.id)}")
|
|
print(f"Is member: {user.is_member_of(store.id)}")
|
|
print(f"Role: {user.get_store_role(store.id)}")
|
|
print(f"Has products.create: {user.has_store_permission(store.id, 'products.create')}")
|
|
```
|
|
|
|
### Decode JWT Token
|
|
```python
|
|
import jwt
|
|
|
|
token = "eyJ0eXAi..."
|
|
decoded = jwt.decode(token, verify=False)
|
|
print(f"User ID: {decoded['sub']}")
|
|
print(f"Username: {decoded['username']}")
|
|
print(f"Role: {decoded['role']}") # super_admin, platform_admin, merchant_owner, or store_member
|
|
print(f"Expires: {decoded['exp']}")
|
|
# Note: is_super_admin is no longer in JWT tokens; derive from role == "super_admin"
|
|
```
|
|
|
|
### Check Cookie
|
|
```javascript
|
|
// In browser console
|
|
document.cookie.split(';').forEach(c => console.log(c.trim()));
|
|
```
|
|
|
|
---
|
|
|
|
## Role Presets
|
|
|
|
| Role | Typical Permissions |
|
|
|------|---------------------|
|
|
| **Owner** | ALL (automatic) |
|
|
| **Manager** | Most operations, no team management |
|
|
| **Staff** | Products, orders, customers (CRUD) |
|
|
| **Support** | Orders, customers (support focus) |
|
|
| **Viewer** | Read-only access |
|
|
| **Marketing** | Customers, marketing, reports |
|
|
|
|
---
|
|
|
|
## File Locations
|
|
|
|
```
|
|
app/
|
|
├── api/
|
|
│ ├── deps.py ← All auth dependencies
|
|
│ └── v1/
|
|
│ ├── admin/
|
|
│ │ └── auth.py ← Admin login
|
|
│ ├── store/
|
|
│ │ ├── auth.py ← Store login
|
|
│ │ └── team.py ← Team management
|
|
│ └── public/
|
|
│ └── stores/auth.py ← Customer login
|
|
│
|
|
├── core/
|
|
│ └── permissions.py ← Permission constants
|
|
│
|
|
├── exceptions/
|
|
│ ├── admin.py
|
|
│ ├── store.py
|
|
│ └── auth.py
|
|
│
|
|
├── services/
|
|
│ ├── auth_service.py
|
|
│ └── store_team_service.py ← Team management
|
|
│
|
|
└── models/
|
|
└── database/
|
|
├── user.py ← User model
|
|
├── store.py ← Store, StoreUser, Role
|
|
└── customer.py ← Customer model
|
|
```
|
|
|
|
---
|
|
|
|
## Status Codes
|
|
|
|
| Code | Meaning | Common Cause |
|
|
|------|---------|--------------|
|
|
| 200 | OK | Success |
|
|
| 201 | Created | Resource created |
|
|
| 401 | Unauthorized | No/invalid token |
|
|
| 403 | Forbidden | No permission |
|
|
| 404 | Not Found | Resource not found |
|
|
| 422 | Validation Error | Invalid input |
|
|
|
|
---
|
|
|
|
## Environment Variables
|
|
|
|
```bash
|
|
JWT_SECRET_KEY=your-secret-key
|
|
JWT_ALGORITHM=HS256
|
|
JWT_EXPIRATION=3600 # seconds (1 hour)
|
|
ENVIRONMENT=development|staging|production
|
|
```
|
|
|
|
---
|
|
|
|
**Print and keep at your desk!**
|
|
|
|
For full documentation: See [RBAC Developer Guide](../api/rbac.md)
|