Files
spidy-look-at-it/02-multiple-dashboards.md
Kulvir Singh b92b43c4bc some updated
2025-12-26 22:39:54 +05:30

9.6 KiB

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

Key Concepts:

  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
  • users belong to an Organization

  • users can be granted access to multiple Teams

  • each team access has a specific Role

  • roles contain Permissions

  • 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

** Entity Relationship Schema**

Database Schema

class Organization():
    name = models.CharField(max_length=200)
    email = models.EmailField()
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

Permissions: permissions

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)

Role: Collection of permissions

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

class Team():
    name = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    organization = models.ForeignKey(
        Organization,
        on_delete=models.CASCADE,
        related_name="teams"
    )
    settings = models.JSONField(default=dict)

TeamMembership: user role assignment in a team

class TeamMembership():
    user = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name="team_memberships"
    )
    team = models.ForeignKey(
        Team,
        on_delete=models.CASCADE,
        related_name="memberships"
    )
    role = models.ForeignKey(
        Role,
        on_delete=models.PROTECT,  # Prevent deletion of role if in use
        related_name="team_memberships"
    )
    added_by = models.ForeignKey(
        User,
        on_delete=models.SET_NULL,
        related_name="memberships_added"
    )  


class TeamEmailAgent():
    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
        )

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

permission verifications

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)
    
    membership = TeamMembership.objects.filter(
        user=user,
        team_id=team_id,
    ).select_related('role').prefetch_related('role__permissions').first()
    
    if not membership:
        return set()
    
    role_permissions = { p.code for p in membership.role.permissions.all() }
    
    if use_cache:
        cache.set(cache_key, list(role_permissions))
    
    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")
    
    permissions = get_user_permissions(user, team_id, use_cache)
    
    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

Usage

@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())})


@api_view(["POST"])
@require_permission("contract:create", "team_id")
def upload_contract(request, team_id):
    return Response({"message": "Contract uploaded"})


@api_view(["POST"])
@require_superuser()
def create_organization(request):
    # Only superusers can create organizations
    ...

API Endpoints

Dashboard

  • 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" }