Zyeta implements a robust Role-Based Access Control (RBAC) system that provides fine-grained authorization for all resources within the platform. This page explains how permissions work, how they’re assigned to roles, and how to implement permission checks in your code.
Permission Structure
Permissions in Zyeta follow a simple resource:action
pattern that makes them intuitive and flexible:
Where:
- resource: The entity being accessed (e.g.,
kb
, conversation
, organization
)
- action: The operation being performed (e.g.,
read
, write
, delete
, admin
)
Examples
kb:read # Permission to read knowledge bases
kb:write # Permission to create/edit knowledge bases
kb:delete # Permission to delete knowledge bases
kb:admin # Full administrative access to knowledge bases
Wildcard Support
Permissions support wildcards to enable broader access control:
*:read # Read access to all resources
kb:* # Full access to knowledge bases
*:* # Full access to everything (super admin)
Role Hierarchy
Roles are organized in a hierarchical system with numeric levels:
- Owner (Level 100): Full system access, can perform all operations
- Admin (Level 80): Administrative capabilities, can manage most resources
- Member (Level 20): Standard user access, can use but not administer
- Guest (Level 10): Limited access, typically read-only permissions
Database Structure
The permission system is implemented through these key database models:
Key Models
Role Model
class RoleModel(CRUD):
"""Role model for RBAC."""
__tablename__ = "roles"
id: Mapped[UUID] = mapped_column(UUID, primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column(String(256), nullable=False)
level: Mapped[int] = mapped_column(Integer, nullable=False)
organization_id: Mapped[UUID] = mapped_column(
UUID, ForeignKey("organizations.id", ondelete="CASCADE")
)
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=datetime.now, onupdate=datetime.now
)
Permission Model
Permissions are stored as simple string entries with the resource:action
format.
Permission Checks
Middleware-Based Checks
The RBAC system uses FastAPI dependencies to check permissions for every protected endpoint:
def permission_required(resource: str, action: str):
"""Dependency for checking if a user has a specific permission."""
async def check_permission(
token_data: dict = Depends(JWTBearer()),
session: AsyncSession = Depends(get_session),
):
# Get user's role in the current organization
member = await get_organization_member(token_data, session)
# Get role permissions
permissions = await get_role_permissions(member.role_id, session)
# Check if the user has the required permission
if has_permission(permissions, resource, action):
return token_data
# Check for wildcard permissions
if has_permission(permissions, "*", action) or has_permission(permissions, resource, "*") or has_permission(permissions, "*", "*"):
return token_data
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions",
)
return check_permission
Using RBAC in Endpoints
To protect an endpoint with RBAC, add the permission dependency:
@router.get(
"/kb/{kb_id}",
dependencies=[Depends(permission_required("kb", "read"))],
)
async def read_kb(kb_id: UUID):
# Implementation...
pass
Default Roles and Permissions
When a new organization is created, the system automatically creates these default roles:
-
Owner
- Level: 100
- Permissions:
*:*
(full access to everything)
-
Admin
- Level: 80
- Permissions: Various administrative permissions like
kb:admin
, conversation:admin
-
Member
- Level: 20
- Permissions: Basic usage permissions like
kb:read
, kb:write
, conversation:read
, conversation:write
-
Guest
- Level: 10
- Permissions: Limited access permissions like
kb:read
, conversation:read
Creating Custom Roles
Organization owners and admins can create custom roles with specific permission sets:
# Example: Creating a "Content Manager" role
async def create_custom_role(
name: str,
level: int,
permissions: List[str],
organization_id: UUID,
session: AsyncSession,
):
# Create the role
role = RoleModel(
name=name,
level=level,
organization_id=organization_id,
)
session.add(role)
await session.flush()
# Assign permissions to the role
for permission_str in permissions:
permission = await get_or_create_permission(permission_str, session)
role_permission = RolePermissionModel(
role_id=role.id,
permission_id=permission.id,
)
session.add(role_permission)
await session.commit()
return role
Best Practices
Permission Naming
- Use clear, descriptive names for resources and actions
- Keep permission names lowercase
- Use singular form for resources (e.g.,
kb
not kbs
)
- Use verb form for actions (e.g.,
read
not reader
)
Permission Checking
- Always check permissions at the API level using the RBAC dependency
- For complex business logic, you may need to check permissions within your service methods
- Remember to check for wildcard permissions as well as specific ones
Role Management
- Avoid creating too many roles, which can be hard to manage
- Place roles at appropriate levels in the hierarchy
- Define clear boundaries between role capabilities
Debugging RBAC Issues
If users report access problems:
- Check which role the user has in the organization
- Verify the permissions assigned to that role
- Examine the specific resource and action being requested
- Look for potential wildcard permissions that should grant access
- Check if the user’s token is valid and not expired
Next Steps
Migrations with Alembic
The RBAC system uses Alembic migrations to manage database schema changes and seed default data:
def upgrade() -> None:
# Create tables for RBAC
op.create_table(
"permissions",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("name", sa.String(length=256), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
)
op.create_table(
"role_permissions",
sa.Column("role_id", sa.UUID(), nullable=False),
sa.Column("permission_id", sa.UUID(), nullable=False),
sa.ForeignKeyConstraint(["permission_id"], ["permissions.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["role_id"], ["roles.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("role_id", "permission_id"),
)
# Seed default permissions
connection = op.get_bind()
permissions = [
{"id": uuid.uuid4(), "name": "kb:read"},
{"id": uuid.uuid4(), "name": "kb:write"},
{"id": uuid.uuid4(), "name": "kb:delete"},
# ... more permissions
]
for permission in permissions:
connection.execute(
sa.text(
"INSERT INTO permissions (id, name) VALUES (:id, :name)"
),
permission,
)
When adding new resources or modifying permission schemes, create a new Alembic migration to:
- Add new permission entries
- Assign default permissions to existing roles
- Update any permission dependencies
This ensures that all environments (development, staging, production) maintain consistent permission structures.
Responses are generated using AI and may contain mistakes.