2025-12-22 20:40:55 +05:30
|
|
|
## Requirements
|
|
|
|
|
|
|
|
|
|
- team has its own dashboard tab in the UI
|
|
|
|
|
- COIs for a team are only visible to that team members and super admins.
|
|
|
|
|
- team can have multiple checklists
|
|
|
|
|
- separate email agents
|
|
|
|
|
- user can belong to multiple teams
|
|
|
|
|
|
|
|
|
|
## RBAC
|
|
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
**Key Concepts**:
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
1. **Permission** - Individual action (e.g., `contract:create`, `checklist:edit`)
|
|
|
|
|
2. **Role** - Collection of permissions (ROOT, ADMIN, VIEWER)
|
|
|
|
|
3. **TeamMembership** - Links user to team with a role
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
- users belong to an **Organization**
|
|
|
|
|
- users can be granted access to multiple **Teams**
|
|
|
|
|
- each team access has a specific **Role**
|
|
|
|
|
- roles contain **Permissions**
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
- all permissions are stored in `Permission` table
|
|
|
|
|
- roles stored in `Role` table
|
|
|
|
|
- roles has many-to-many relationship with `Permission` table
|
|
|
|
|
- link users to teams with specific roles
|
|
|
|
|
- users can have different roles in different teams
|
|
|
|
|
- permissions are derived from the assigned role
|
2025-12-22 20:40:55 +05:30
|
|
|
|
|
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
** [Entity Relationship Schema](https://app.eraser.io/workspace/ZSPoTCtYZRIwToc57lyO?origin=share)**
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
### **Database Schema**
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
```python
|
|
|
|
|
class Organization():
|
|
|
|
|
name = models.CharField(max_length=200)
|
|
|
|
|
email = models.EmailField()
|
2025-12-22 20:40:55 +05:30
|
|
|
```
|
|
|
|
|
|
|
|
|
|
```python
|
2025-12-26 22:39:54 +05:30
|
|
|
class User():
|
|
|
|
|
id = models.UUIDField()
|
|
|
|
|
name = models.CharField(max_length=100)
|
|
|
|
|
email = models.EmailField(max_length=100)
|
|
|
|
|
organization = models.ForeignKey(
|
|
|
|
|
Organization,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
db_column="organization_id",
|
|
|
|
|
)
|
|
|
|
|
is_superuser = models.BooleanField(default=False) # root admin
|
2025-12-22 20:40:55 +05:30
|
|
|
```
|
|
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
**Permissions**: permissions
|
2025-12-22 20:40:55 +05:30
|
|
|
|
|
|
|
|
```python
|
2025-12-26 22:39:54 +05:30
|
|
|
class Permission():
|
|
|
|
|
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
|
|
|
|
code = models.CharField(max_length=100, unique=True) # e.g., "contract:create"
|
|
|
|
|
name = models.CharField(max_length=200) # e.g., "Create Contract"
|
|
|
|
|
category = models.CharField(max_length=50) # e.g., "contract", "team", "checklist"
|
|
|
|
|
# description = models.TextField(blank=True)
|
2025-12-22 20:40:55 +05:30
|
|
|
```
|
|
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
**Role**: Collection of permissions
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
```python
|
|
|
|
|
class Role():
|
|
|
|
|
name = models.CharField(max_length=50) # e.g., "ROOT", "ADMIN", "VIEWER"
|
|
|
|
|
description = models.TextField(blank=True)
|
|
|
|
|
is_system = models.BooleanField(default=False) # Flag for default/system roles
|
|
|
|
|
organization = models.ForeignKey(
|
|
|
|
|
Organization,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
related_name="roles"
|
|
|
|
|
)
|
|
|
|
|
permissions = models.ManyToManyField(
|
|
|
|
|
Permission,
|
|
|
|
|
related_name="roles",
|
|
|
|
|
blank=True
|
|
|
|
|
)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
- each organization has its own set of roles (along with default 3 `system` roles i.e. "ROOT", "ADMIN", "VIEWER")
|
|
|
|
|
- `system_roles` created automatically with standard permissions, but can be customized per organization
|
|
|
|
|
|
|
|
|
|
**Team**: workspace within an organization
|
2025-12-22 20:40:55 +05:30
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
class Team():
|
|
|
|
|
name = models.CharField(max_length=200)
|
|
|
|
|
description = models.TextField(blank=True)
|
|
|
|
|
organization = models.ForeignKey(
|
2025-12-26 22:39:54 +05:30
|
|
|
Organization,
|
2025-12-22 20:40:55 +05:30
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
related_name="teams"
|
|
|
|
|
)
|
|
|
|
|
settings = models.JSONField(default=dict)
|
|
|
|
|
```
|
|
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
**TeamMembership**: user role assignment in a team
|
|
|
|
|
|
2025-12-22 20:40:55 +05:30
|
|
|
```python
|
|
|
|
|
class TeamMembership():
|
|
|
|
|
user = models.ForeignKey(
|
2025-12-26 22:39:54 +05:30
|
|
|
User,
|
2025-12-22 20:40:55 +05:30
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
related_name="team_memberships"
|
|
|
|
|
)
|
|
|
|
|
team = models.ForeignKey(
|
|
|
|
|
Team,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
related_name="memberships"
|
|
|
|
|
)
|
|
|
|
|
role = models.ForeignKey(
|
2025-12-26 22:39:54 +05:30
|
|
|
Role,
|
|
|
|
|
on_delete=models.PROTECT, # Prevent deletion of role if in use
|
2025-12-22 20:40:55 +05:30
|
|
|
related_name="team_memberships"
|
|
|
|
|
)
|
|
|
|
|
added_by = models.ForeignKey(
|
2025-12-26 22:39:54 +05:30
|
|
|
User,
|
2025-12-22 20:40:55 +05:30
|
|
|
on_delete=models.SET_NULL,
|
|
|
|
|
related_name="memberships_added"
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
|
|
|
|
|
class TeamEmailAgent():
|
2025-12-22 20:40:55 +05:30
|
|
|
team = models.OneToOneField(
|
|
|
|
|
Team,
|
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
|
related_name="email_agent"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
provider_type = models.CharField(
|
|
|
|
|
max_length=20,
|
|
|
|
|
choices=ProviderType.choices,
|
|
|
|
|
default=ProviderType.MICROSOFT
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
inbox_email = models.EmailField(unique=True)
|
|
|
|
|
|
|
|
|
|
ms_client_id = models.CharField(max_length=500, blank=True)
|
|
|
|
|
ms_client_secret = models.CharField(max_length=500, blank=True) # maybe encrypt this
|
|
|
|
|
ms_tenant_id = models.CharField(max_length=500, blank=True)
|
|
|
|
|
|
|
|
|
|
subscription_id = models.CharField(max_length=500, null=True, blank=True)
|
|
|
|
|
subscription_expiry = models.DateTimeField(null=True, blank=True)
|
|
|
|
|
|
|
|
|
|
# processing settings
|
|
|
|
|
# {
|
|
|
|
|
# "default_checklist_id": "uuid",
|
|
|
|
|
# "automated_reminder": {} .....
|
|
|
|
|
# }
|
|
|
|
|
processing_settings = models.JSONField(default=dict)
|
|
|
|
|
|
|
|
|
|
enabled = models.BooleanField(default=False)
|
|
|
|
|
|
|
|
|
|
def get_access_token(self) -> str:
|
|
|
|
|
"""Get OAuth access token for email operations"""
|
|
|
|
|
from assistantService.services.graph_service.service import GraphEmailService
|
|
|
|
|
|
|
|
|
|
graph = GraphEmailService()
|
|
|
|
|
return graph.generate_access_token(
|
|
|
|
|
client_id=self.ms_client_id,
|
|
|
|
|
client_secret=self.ms_client_secret,
|
|
|
|
|
tenant_id=self.ms_tenant_id
|
|
|
|
|
)
|
|
|
|
|
```
|
|
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
**Permission Naming Convention**: `resource:action`
|
|
|
|
|
- `contract:view`, `contract:create`, `contract:edit`, `contract:delete`, `contract:analyze`
|
|
|
|
|
- `team:view`, `team:create`, `team:edit`, `team:delete`, `team:manage_members`
|
|
|
|
|
- `checklist:view`, `checklist:create`, `checklist:edit`, `checklist:delete`
|
|
|
|
|
- `email_agent:view`, `email_agent:configure`, `email_agent:enable`, `email_agent:disable`
|
2025-12-22 20:40:55 +05:30
|
|
|
|
|
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
## permission verifications
|
2025-12-22 20:40:55 +05:30
|
|
|
|
|
|
|
|
```python
|
2025-12-26 22:39:54 +05:30
|
|
|
def is_superuser(user: User) -> bool:
|
|
|
|
|
return getattr(user, 'is_superuser', False)
|
|
|
|
|
|
|
|
|
|
def get_user_permissions(
|
|
|
|
|
user: User,
|
|
|
|
|
team_id: Optional[str] = None,
|
|
|
|
|
) -> Set[str]:
|
|
|
|
|
"""
|
|
|
|
|
get all permissions for a user in a team.
|
|
|
|
|
"""
|
|
|
|
|
cache_key = f"user_perms:{user.id}:{team_id}"
|
|
|
|
|
cached_perms = cache.get(cache_key)
|
|
|
|
|
if cached_perms is not None:
|
|
|
|
|
return set(cached_perms)
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
membership = TeamMembership.objects.filter(
|
|
|
|
|
user=user,
|
|
|
|
|
team_id=team_id,
|
|
|
|
|
).select_related('role').prefetch_related('role__permissions').first()
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
if not membership:
|
|
|
|
|
return set()
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
role_permissions = { p.code for p in membership.role.permissions.all() }
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
if use_cache:
|
|
|
|
|
cache.set(cache_key, list(role_permissions))
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
return role_permissions
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def has_permission(
|
|
|
|
|
user: User,
|
|
|
|
|
permission_code: str,
|
|
|
|
|
team_id: Optional[str] = None,
|
|
|
|
|
) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
check if user has a specific permission in a team.
|
|
|
|
|
"""
|
|
|
|
|
if is_superuser(user):
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
if not team_id:
|
|
|
|
|
raise ValueError("team_id is required for permission check")
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
permissions = get_user_permissions(user, team_id, use_cache)
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
if permission_code in permissions:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
# NOTE: optional if we are allowing wildcard permissions
|
|
|
|
|
# resource, action = permission_code.split(":", 1) if ":" in permission_code else (permission_code, "")
|
|
|
|
|
# wildcard_permission = f"{resource}:*"
|
|
|
|
|
# if wildcard_permission in permissions:
|
|
|
|
|
# return True
|
|
|
|
|
|
|
|
|
|
return False
|
2025-12-22 20:40:55 +05:30
|
|
|
```
|
|
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
## **Usage**
|
2025-12-22 20:40:55 +05:30
|
|
|
|
|
|
|
|
```python
|
2025-12-26 22:39:54 +05:30
|
|
|
@api_view(["GET"])
|
|
|
|
|
@require_permission("contract:view", "team_id")
|
|
|
|
|
def list_contracts(request, team_id):
|
|
|
|
|
contracts = Contract.objects.filter(team_id=team_id, deleted_at__isnull=True)
|
|
|
|
|
return Response({"contracts": list(contracts.values())})
|
2025-12-22 20:40:55 +05:30
|
|
|
|
|
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
@api_view(["POST"])
|
|
|
|
|
@require_permission("contract:create", "team_id")
|
|
|
|
|
def upload_contract(request, team_id):
|
|
|
|
|
return Response({"message": "Contract uploaded"})
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
|
|
|
|
|
@api_view(["POST"])
|
|
|
|
|
@require_superuser()
|
|
|
|
|
def create_organization(request):
|
|
|
|
|
# Only superusers can create organizations
|
|
|
|
|
...
|
2025-12-22 20:40:55 +05:30
|
|
|
```
|
|
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
## API Endpoints
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
### **Dashboard**
|
2025-12-22 20:40:55 +05:30
|
|
|
|
2025-12-26 22:39:54 +05:30
|
|
|
- `GET /api/teams/{team_id}/dashboard` - Get team dashboard with contracts and statistics
|
|
|
|
|
- **Required Permissions:** Team access + `contract:view`
|
|
|
|
|
- **Query Parameters:** `page` (optional), `page_size` (optional), `filters` (optional)
|
|
|
|
|
|
|
|
|
|
- `GET /api/teams/{team_id}/dashboard/stats` - Get team dashboard statistics
|
|
|
|
|
- **Required Permissions:** Team access + `contract:view`
|
|
|
|
|
|
|
|
|
|
### **Checklists**
|
|
|
|
|
|
|
|
|
|
- `GET /api/teams/{team_id}/checklists` - List all checklists for a team
|
|
|
|
|
- **Required Permissions:** Team access + `checklist:view`
|
|
|
|
|
|
|
|
|
|
### **team management within org**
|
|
|
|
|
|
|
|
|
|
- `POST /api/teams` - Create a new team in organization
|
|
|
|
|
- **Required Permission:** `superuser`
|
|
|
|
|
- **Request Body:** `{ "organization_id": "uuid", "name": "string", "description": "string", "settings": {} }`
|
|
|
|
|
|
|
|
|
|
- `PUT /api/teams/{team_id}` - Update team settings
|
|
|
|
|
- **Required Permissions:** Team access + `team:edit`
|
|
|
|
|
- **Request Body:** `{ "name": "string", "description": "string", "settings": {} }`
|
|
|
|
|
|
|
|
|
|
- `DELETE /api/teams/{team_id}` - Delete a team (soft delete)
|
|
|
|
|
- **Required Permissions:** Team access + `team:delete`
|
|
|
|
|
|
|
|
|
|
- `GET /api/teams/{team_id}/members` - List all team members with their roles
|
|
|
|
|
- **Required Permissions:** Team access + `team:view`
|
|
|
|
|
|
|
|
|
|
- `POST /api/teams/{team_id}/members` - Add a user to a team with a specific role
|
|
|
|
|
- **Required Permissions:** Team access + `team:manage_members`
|
|
|
|
|
- **Request Body:** `{ "user_id": "uuid", "role_name": "ROOT" | "ADMIN" | "VIEWER" }`
|
|
|
|
|
|
|
|
|
|
- `DELETE /api/teams/{team_id}/members/{user_id}` - Remove a user from a team
|
|
|
|
|
- **Required Permissions:** Team access + `team:manage_members`
|
|
|
|
|
|
|
|
|
|
- `PUT /api/teams/{team_id}/members/{user_id}/role` - Update a team member's role
|
|
|
|
|
- **Required Permissions:** Team access + `team:manage_members`
|
|
|
|
|
- **Request Body:** `{ "role_name": "ROOT" | "ADMIN" | "VIEWER" }`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### **Email Agent**
|
|
|
|
|
|
|
|
|
|
- `POST /api/teams/{team_id}/email-agent` - Configure email agent for a team
|
|
|
|
|
- **Required Permissions:** Team access + `email_agent:configure`
|
|
|
|
|
- **Request Body:** `{ "inbox_email": "string", "ms_client_id": "string", "ms_client_secret": "string", "ms_tenant_id": "string" }`
|