# Kalimera Workspace-User-Pending Request Management
- Created at : 2025-06-14
- Created by : Manoj Vala
## Overview:
This documentation outlines the implementation of a scalable permission system combining role-based access control (RBAC) with a star-topology workspace hierarchy. The system enables:
- Single accounts across multiple workspaces with distinct roles
- Centralized policy management based on workspace type and role
- Flat hierarchy where all workspaces link to a root parent
- Module-level privileges (VIEW/MANAGE) enforced via decorators
- Workspace switching without reauthentication
Designed for Flask/FastAPI with PostgreSQL and Redis, this solution balances flexibility with performance.
---
Let me know if you'd prefer to emphasize different aspects or adjust the technical tone.
---
## Executive Summary
Kalimera is a multi‑workspace SaaS platform where a single account can:
* belong to many workspaces (a.k.a. *tenants*)
* hold a distinct **role** (ADMIN / USER) in each workspace
* gain **module‑level privileges** derived from a central policy table keyed by `(workspace_type, role, module)`
* switch the *current* workspace at any time with no new JWT
Everything is surfaced through **Flask‑RESTful Resources** with the same decorator stack you already use (`@setup_required`, `@login_required`, `@account_initialization_required`) plus one new **RBAC decorator** `@policy_required(module, privilege)`.
---
## 1 Glossary
| Term | Meaning |
| ------------- | - |
| **Account** | A human login (`kalimera_accounts`). |
| **Workspace** | A container for data and members (`kalimera_workspaces`). |
| **Role** | `ADMIN` or `USER` – stored in join table. |
| **Module** | Functional slice of the UI (`campaign_mgmt`, `reports`, `role_mgmt`, …). |
| **Privilege** | `VIEW` or `MANAGE`. |
| **Policy** | Row in `kalimera_workspace_role_policies` that maps `(workspace_type, role, module) → privilege`. |
| **ROOT** | Hard‑coded system approver (`id = 1`). |
---
## 2 Architecture Layers
```
┌───────────────┐ Flask‑RESTful ┌────────────────┐
│ Resource │ decorators │ Service │
│ (HTTP) │───────────────▶ │ (business) │
└───────────────┘ └────────────────┘
▲ ▲
│ SQLAlchemy models │ Redis cache
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ PostgreSQL │ │ Redis │
│ (kalimera_*) │ │ / JWT │
└──────────────────┘ └──────────────────┘
```
---
## 3 Data Model (DDL Fragment)
```python
class WorkspaceType(enum.Enum):
personal = "personal"
team = "team"
# Add more as needed
class RoleEnum(enum.Enum):
admin = "admin"
member = "member"
viewer = "viewer"
# Add more as needed
class PrivilegeEnum(enum.Enum):
read = "read"
write = "write"
delete = "delete"
# Add more as needed
# Models
class KalimeraAccount(Base):
__tablename__ = "kalimera_accounts"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email = Column(String, unique=True, nullable=False)
name = Column(String, nullable=False)
password = Column(String)
hard_coded_root = Column(Boolean, default=False)
class KalimeraWorkspace(Base):
__tablename__ = "kalimera_workspaces"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String, nullable=False)
workspace_type = Column(Enum(WorkspaceType), nullable=False)
created_by = Column(UUID(as_uuid=True), ForeignKey("kalimera_accounts.id"))
created_at = Column(DateTime, default=datetime.utcnow)
creator = relationship("Account")
class KalimeraWorkspaceAccountJoin(Base):
__tablename__ = "kalimera_workspace_account_joins"
workspace_id = Column(UUID(as_uuid=True), ForeignKey("kalimera_workspaces.id"))
account_id = Column(UUID(as_uuid=True), ForeignKey("kalimera_accounts.id"))
role = Column(Enum(RoleEnum), nullable=False)
current = Column(Boolean, default=False)
joined_at = Column(DateTime, default=datetime.utcnow)
__table_args__ = (
PrimaryKeyConstraint("workspace_id", "account_id"),
)
class KalimeraModule(Base):
__tablename__ = "kalimera_modules"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
code = Column(String, unique=True, nullable=False)
label = Column(String, nullable=False)
class KalimeraWorkspaceRolePolicy(Base):
__tablename__ = "kalimera_workspace_role_policies"
workspace_type = Column(Enum(WorkspaceType), primary_key=True)
role = Column(Enum(RoleEnum), primary_key=True)
module_id = Column(UUID(as_uuid=True), ForeignKey("kalimera_modules.id"), primary_key=True)
privilege = Column(Enum(PrivilegeEnum), nullable=False)
updated_by = Column(UUID(as_uuid=True), ForeignKey("kalimera_accounts.id"))
module = relationship("Module")
updated_by_account = relationship("Account", foreign_keys=[updated_by])
```
---
## 4 Permission Model
```
(privilege = MANAGE) ⇒ full CRUD
(privilege = VIEW) ⇒ read‑only
Lookup path on every check:
workspace.id ──▶ workspace_type
+ role (from join row)
+ module.code
─────────────────────▶ kalimera_workspace_role_policies.privilege
```
`@policy_required(module_code, Privilege.MANAGE)` wraps this logic; service calls can reuse `PermissionService.can()` directly.
---
## 5 Workspace Architecture:
```mermaid
graph TD
ROOT-->A[Workspace A: parent_id=NULL]
A-->B[Workspace B]-->C[Workspace C]
A-->D[Workspace D]
style A stroke:#ff0000,stroke-width:4px
```
**Root Workspace**: First workspace created (parent_id = NULL)
**Star Children**: All subsequent workspaces point to root
**Data Isolation**: Queries scope to current workspace by default
**Inheritance**: Optional include_parent=True parameter for services
---
## 6 User Flow:
```mermaid
graph TD
A[Account] --> B[Workspace Memberships]
B --> C[Role: ADMIN/USER]
B --> D[Current Workspace]
E[Workspace] --> F[Parent Workspace]
E --> G[Type: personal/team]
C --> H[Policy Engine]
I[Module] --> H
H --> J[Privilege: VIEW/MANAGE]
```
---
## 7 Endpoint Catalogue (v1)
| # | Method & Path | Purpose | Decorators / Gate |
| ----------------- | --------- | ------ | ------------ |
| **Auth** | | | |
| 1 | `POST /auth/register` | Create pending request | *(public)* |
| 2 | `POST /auth/login` | Issue 30‑day JWT | *(public)* |
| **Root‑internal** | | | |
| 3 | `POST /pending-requests/{id}/approve` | Approve signup | `current_user.id == ROOT_ID` |
| 4 | `POST /pending-requests/{id}/reject` | Reject signup | ROOT |
| **Workspaces** | | | |
| 5 | `GET /workspaces` | List workspaces | std decorators |
| 6 | `POST /workspaces` | Create | `@policy_required('workspace_settings', MANAGE)` |
| 7 | `PATCH /workspaces/{id}` | Rename/plan | same gate |
| 8 | `DELETE /workspaces/{id}` | Archive | same gate |
| **Membership** | | | |
| 9 | `POST /workspaces/{id}/invite` | Invite user | `@policy_required('user_mgmt', MANAGE)` |
| 10 | `PATCH /workspaces/{id}/members/{account_id}` | Change role | `@policy_required('role_mgmt', MANAGE)` |
| **Switch** | | | |
| 11 | `POST /workspaces/switch` | Flip current flag | membership check |
| **Policies** | | | |
| 12 | `PATCH /policies/{workspace_type}/{role}` | Edit matrix | `@policy_required('role_mgmt', MANAGE)` |
| **Context** | | | |
| 13 | `GET /account` | Profile + memberships blob | std decorators |
---
## 8 Decorator Stack
```python
@setup_required
@login_required
@account_initialization_required
@policy_required("module_code", Privilege.MANAGE)
def post(...):
...
```
*`policy_required` must run **after** auth decorators so `current_user` is available.*
---
## 9 Service Responsibilities
| Service | Core Methods |
| ----- | -- |
| **PermissionService** | `can(db, account_id, workspace_id, module, need)` |
| **WorkspaceService** | `list_for_account`, `create`, `update`, `delete`, `switch` |
| **MembershipService** | `invite`, `change_role`, `remove` |
| **PolicyService** | `patch_policy` (upsert rows) |
| **PendingRequestService** | `approve`, `reject` |
| **ContextService** | `make_account_blob` (builds `/account` JSON) |
---
## 10 Resource Templates
```python
class WorkspaceApi(Resource):
decorators = [
setup_required, login_required, account_initialization_required
]
@marshal_with(workspace_fields)
def get(self, workspace):
return workspace
@policy_required("workspace_settings", Privilege.MANAGE)
@marshal_with(workspace_fields)
def patch(self, workspace):
patch = request.get_json()
return WorkspaceService().update(workspace, patch)
@policy_required("workspace_settings", Privilege.MANAGE)
def delete(self, workspace):
WorkspaceService().delete(workspace)
return {"result": "success"}, 204
```
Routes:
```python
api.add_resource(WorkspacesApi, "/workspaces")
api.add_resource(WorkspaceApi, "/workspaces/<uuid:workspace>")
api.add_resource(WorkspaceSwitchApi, "/workspaces/switch")
api.add_resource(PolicyApi, "/policies/<string:ws_type>/<string:role>")
api.add_resource(AccountApi, "/account")
```
---
## 11 Account Context Blob (`GET /account`)
```python
def account_blob():
joins = (
db.query(KalimeraWorkspaceAccountJoin, KalimeraWorkspace)
.join(KalimeraWorkspace, KalimeraWorkspace.id == KalimeraWorkspaceAccountJoin.workspace_id)
.filter(KalimeraWorkspaceAccountJoin.account_id == acc.id)
.all())
resp = {"user": {"id": acc.id, "email": acc.email, "name": acc.name},
"memberships": [], "default_workspace_id": None}
for join, ws in joins:
privs = {} # assemble by joining policy+module
resp["memberships"].append({
"workspace_id": ws.id,
"workspace_name": ws.name,
"workspace_type": ws.workspace_type,
"role": join.role,
"module_privileges": privs,
"current": join.current,
})
if join.current:
resp["default_workspace_id"] = ws.id
return resp
```
```jsonc
{
"user": { "id": "392b...", "email": "jane@x", "name": "Jane" },
"memberships": [
{
"workspace_id": "w1",
"workspace_name": "Acme",
"workspace_type": "PLATFORM",
"role": "ADMIN",
"module_privileges": {
"campaign_mgmt": "manage",
"reports": "view",
"role_mgmt": "manage"
},
"current": true
},
...
],
"default_workspace_id": "w1"
}
```
*Built once on login, cached 30 min in Redis.*
---
| # | Method & Path | Auth | Body / Query | Success (200/201) | Permission Gate | |
| ------ | ----- | ----- | --- | ---------- | ----- | -- |
| **Auth**| | || | | |
| 1 | `POST /auth/register` | public | `{ email, name, password }` | `{ pending_request_id }` | — | |
| 2 | `POST /auth/login`| public | `{ email, password }`| `{ access_token, expires_in }` | — | |
| **ROOT‑only** | | | | | | |
| 3 | `POST /pending-requests/{id}/approve` | JWT (root) | `{ workspace_name?, workspace_type?, modules? }` | `204` | root account | |
| 4 | `POST /pending-requests/{id}/reject`| JWT (root) | `{ reason }` | `204` | root account | |
| **Workspaces** | | | | | | |
| 5 | `POST /workspaces`| JWT| `{ name, workspace_type }` | `{ workspace_id }` | caller role = ADMIN | |
| 6 | `GET /workspaces` | JWT| —| `[ ...minimal list... ]` | any | |
| 7 | `PATCH /workspaces/{id}` | JWT| `{ name?, plan?, status? }` | `204` | ADMIN + policy(`workspace_settings`,`MANAGE`) | |
| **Membership** | | | | | | |
| 8 | `POST /workspaces/{id}/invite` | JWT| `{ email, role, modules? }` | `202 Accepted` | policy(`user_mgmt`,`MANAGE`)| |
| 9 | `PATCH /workspaces/{id}/members/{account_id}` | JWT| `{ role }` | `204` | policy(`role_mgmt`,`MANAGE`)| |
| **Switch** | | | | | | |
| 10 | `POST /workspaces/switch` | JWT| `{ workspace_id }` | `{ new_current_id }` | membership exists | |
| **Policies** | | | | | | |
| 11 | `PATCH /policies/{workspace_type}/{role}` | JWT| \`{ module\_privileges: { code: "VIEW| MANAGE" } }\`| `204` | caller must have `role_mgmt=MANAGE` |
| **Context blob** | | | | | | |
| 12 | `GET /account` | JWT| —| see §4 payload | any | |
---
---
## 12 · Service Layer (`services.py`)
```python
from sqlalchemy.orm import Session
from models import (
KalimeraAccount, KalimeraWorkspace, KalimeraWorkspaceAccountJoin,
KalimeraModule, KalimeraWorkspaceRolePolicy, WorkspaceType, Role, Privilege
)
class PermissionService:
@staticmethod
def module_privilege(db: Session, workspace_type, role, module_code):
policy = (db.query(KalimeraWorkspaceRolePolicy)
.join(KalimeraModule, KalimeraModule.id == KalimeraWorkspaceRolePolicy.module_id)
.filter(KalimeraWorkspaceRolePolicy.workspace_type == workspace_type,
KalimeraWorkspaceRolePolicy.role == role,
KalimeraModule.code == module_code)
.first())
return policy.privilege if policy else None
@staticmethod
def can(db, account_id, workspace_id, module_code, need: Privilege):
w = db.query(KalimeraWorkspace).get(workspace_id)
mem = db.query(KalimeraWorkspaceAccountJoin).get((workspace_id, account_id))
if not w or not mem:
return False
level = PermissionService.module_privilege(db, w.workspace_type, mem.role, module_code)
return (level == Privilege.MANAGE) or (level == Privilege.VIEW and need == Privilege.VIEW)
class WorkspaceService:
@staticmethod
def switch(db: Session, account_id: str, workspace_id: str):
# validate membership
join = db.query(KalimeraWorkspaceAccountJoin).get((workspace_id, account_id))
if not join:
raise ValueError("Not a member")
# unset previous
db.query(KalimeraWorkspaceAccountJoin)\
.filter_by(account_id=account_id, current=True)\
.update({KalimeraWorkspaceAccountJoin.current: False})
join.current = True
db.commit()
return workspace_id
```
*(Add more services for invites, policy edit, etc.)*
---
## 13 · FastAPI Router (`api.py`)
```python
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Dict
from pydantic import BaseModel, EmailStr
from models import Privilege
from services import PermissionService, WorkspaceService
from deps import get_db, get_current_account, get_current_workspace
router = APIRouter(prefix="/workspaces", tags=["workspaces"])
# ───── Switch Workspace ─────
class SwitchReq(BaseModel):
workspace_id: str
class SwitchResp(BaseModel):
new_current_id: str
@router.post("/switch", response_model=SwitchResp)
def switch_workspace(req: SwitchReq,
db: Session = Depends(get_db),
acc = Depends(get_current_account)):
try:
wid = WorkspaceService.switch(db, acc.id, req.workspace_id)
return {"new_current_id": wid}
except ValueError:
raise HTTPException(status_code=404, detail="Workspace not found")
# ───── Policy Editor ─────
class PolicyPatch(BaseModel):
module_privileges: Dict[str, Privilege] # {"campaign_mgmt":"MANAGE"}
@router.patch("/policies/{ws_type}/{role}", status_code=204)
def edit_policy(ws_type: str, role: str, body: PolicyPatch,
db: Session = Depends(get_db),
acc = Depends(get_current_account),
cur_ws = Depends(get_current_workspace)):
if not PermissionService.can(db, acc.id, cur_ws.id, "role_mgmt", Privilege.MANAGE):
raise HTTPException(status_code=403)
# upsert loop ...
return
```
*`deps.py` supplies `get_current_account` (from JWT) and `get_current_workspace` (joins table `current=True`).*
---
## 14 · `GET /account` Implementation Sketch
```python
@router.get("/account")
def account_blob(db: Session = Depends(get_db),
acc = Depends(get_current_account)):
joins = (db.query(KalimeraWorkspaceAccountJoin, KalimeraWorkspace)
.join(KalimeraWorkspace, KalimeraWorkspace.id == KalimeraWorkspaceAccountJoin.workspace_id)
.filter(KalimeraWorkspaceAccountJoin.account_id == acc.id)
.all())
resp = {"user": {"id": acc.id, "email": acc.email, "name": acc.name},
"memberships": [], "default_workspace_id": None}
for join, ws in joins:
privs = {} # assemble by joining policy+module
resp["memberships"].append({
"workspace_id": ws.id,
"workspace_name": ws.name,
"workspace_type": ws.workspace_type,
"role": join.role,
"module_privileges": privs,
"current": join.current,
})
if join.current:
resp["default_workspace_id"] = ws.id
return resp
```
---
## 15 · Seed Data
```python
seed_modules = [
("campaign_mgmt", "Campaign Management"),
("reports", "Reporting"),
("billing", "Billing"),
("role_mgmt", "Role Management"),
("user_mgmt", "User Management"),
]
# insert into kalimera_modules once.
# Example default policy: PLATFORM.ADMIN gets MANAGE on everything
for mod_code, _ in seed_modules:
db.add(KalimeraWorkspaceRolePolicy(
workspace_type=WorkspaceType.PLATFORM,
role=Role.ADMIN,
module_id=module_id_lookup[mod_code],
privilege=Privilege.MANAGE,
updated_by=root_id
))
```
---
## 16 Caching & Performance
* 🚀 **Zero DB look‑ups per request** if you keep `/account` blob in Redis and JWT in header.
* **Cache bust** triggers: role change, policy edit, workspace join/leave.
> [!IMPORTANT]
> - All indexes of the tables will created based on the use case when code implementation starts.
> - For handling role based action we will create some new decorators to restricts the actions performed by every roles.
---