Write

# Plan: Multi-Tenant Authorization System for GuruRMM

## Overview

Implement a multi-tenant authorization system with hierarchical roles supporting MSP-style organization management:
- **Dev Admin**: Full system access, can impersonate any account
- **Organization Admin**: MSP/Helpdesk paying customers with seat-based licensing
- **Organization User**: Technicians within an organization

## Requirements

### Role Hierarchy (For Now)
| Role | Access | Capabilities |
|------|--------|--------------|
| `dev_admin` | All organizations | Full access, set org limits, impersonate users |
| `org_admin` | Assigned organizations | Manage sites/agents, create users within limits |
| `org_user` | Assigned organizations | View/manage within their organization |

### Future Roles (Not Implementing Now)
- `support_tier` - Between Dev Admin and Org Admin

### Licensing Model
- **Tech Seats**: Number of users an organization can have
- **Endpoint Limit**: Number of agents an organization can have
- **Sites**: Unlimited for now
- Limits set by Dev Admin, enforced by system

### Multi-Organization Support
- Users can belong to multiple organizations
- Each user-organization assignment has a role within that org
- Dev Admin is NOT tied to any organization (system-level)

---

## Database Schema

### Migration: `007_authorization.sql`

```sql
-- Add organization limits to clients table
ALTER TABLE clients ADD COLUMN tech_seat_limit INTEGER DEFAULT NULL;
ALTER TABLE clients ADD COLUMN endpoint_limit INTEGER DEFAULT NULL;
ALTER TABLE clients ADD COLUMN is_organization BOOLEAN DEFAULT TRUE;

-- User-Organization relationship (replaces simple role on users table)
CREATE TABLE user_organizations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    role VARCHAR(50) NOT NULL DEFAULT 'org_user',  -- org_admin, org_user
    created_at TIMESTAMPTZ DEFAULT NOW(),
    created_by UUID REFERENCES users(id) ON DELETE SET NULL,
    UNIQUE(user_id, client_id)
);

-- System-level roles (dev_admin, support_tier future)
-- Keep role column on users table but repurpose:
-- 'dev_admin' = system admin
-- 'user' = regular user (must have org assignments to access anything)
-- NULL or 'user' = no system-level privileges, access via org membership

CREATE INDEX idx_user_orgs_user ON user_organizations(user_id);
CREATE INDEX idx_user_orgs_client ON user_organizations(client_id);
CREATE INDEX idx_user_orgs_role ON user_organizations(role);

-- Track current endpoint counts per org (for limit enforcement)
CREATE OR REPLACE VIEW organization_stats AS
SELECT
    c.id as client_id,
    c.name as client_name,
    c.tech_seat_limit,
    c.endpoint_limit,
    COALESCE((SELECT COUNT(*) FROM user_organizations WHERE client_id = c.id), 0) as tech_count,
    COALESCE((SELECT COUNT(*) FROM agents a JOIN sites s ON a.site_id = s.id WHERE s.client_id = c.id), 0) as endpoint_count
FROM clients c
WHERE c.is_organization = TRUE;
```

---

## Authorization Model

### Permission Checks

```rust
pub struct AuthUser {
    pub user_id: Uuid,
    pub system_role: String,           // dev_admin, support, user
    pub organizations: Vec<OrgAccess>, // Populated from JWT or DB lookup
}

pub struct OrgAccess {
    pub client_id: Uuid,
    pub role: String,  // org_admin, org_user
}

impl AuthUser {
    /// Dev admin has access to everything
    pub fn is_dev_admin(&self) -> bool {
        self.system_role == "dev_admin"
    }

    /// Can access a specific organization
    pub fn can_access_org(&self, client_id: Uuid) -> bool {
        self.is_dev_admin() ||
        self.organizations.iter().any(|o| o.client_id == client_id)
    }

    /// Is admin of a specific organization
    pub fn is_org_admin(&self, client_id: Uuid) -> bool {
        self.is_dev_admin() ||
        self.organizations.iter().any(|o|
            o.client_id == client_id && o.role == "org_admin"
        )
    }

    /// Get all client_ids this user can access
    pub fn accessible_client_ids(&self) -> Option<Vec<Uuid>> {
        if self.is_dev_admin() {
            None  // None = all clients
        } else {
            Some(self.organizations.iter().map(|o| o.client_id).collect())
        }
    }
}
```

### JWT Claims Enhancement

```rust
pub struct Claims {
    pub sub: Uuid,                      // user_id
    pub system_role: String,            // dev_admin, user
    pub orgs: Vec<OrgClaim>,            // Organization access
    pub exp: i64,
    pub iat: i64,
}

pub struct OrgClaim {
    pub id: Uuid,       // client_id
    pub role: String,   // org_admin, org_user
}
```

---

## API Changes

### Endpoints Requiring Scoped Access

| Endpoint | Current | New Behavior |
|----------|---------|--------------|
| `GET /api/clients` | Returns all | Dev: all, User: only their orgs |
| `GET /api/clients/:id` | No check | Must have access to org |
| `GET /api/sites` | Returns all | Filter by accessible orgs |
| `GET /api/agents` | Returns all | Filter by accessible orgs |
| `GET /api/alerts` | Returns all | Filter by accessible orgs |
| `GET /api/policies` | Returns all | Filter by accessible orgs |
| `POST /api/users` | Admin only | Dev: any org, OrgAdmin: their org (within limits) |

### New Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/organizations` | List orgs user can access |
| `GET` | `/api/organizations/:id/stats` | Get org limits and usage |
| `PUT` | `/api/organizations/:id/limits` | Dev only: set tech_seat_limit, endpoint_limit |
| `POST` | `/api/organizations/:id/users` | Add user to org (checks limits) |
| `DELETE` | `/api/organizations/:id/users/:user_id` | Remove user from org |
| `POST` | `/api/auth/impersonate/:user_id` | Dev only: get token as another user |

---

## Implementation Phases

### Phase 1: Database & Core Auth
**Files to create:**
- `server/migrations/007_authorization.sql` - Schema changes
- `server/src/authz/mod.rs` - Authorization module
- `server/src/authz/permissions.rs` - Permission checking

**Files to modify:**
- `server/src/auth/mod.rs` - Enhanced AuthUser, Claims
- `server/src/db/mod.rs` - Add user_organizations module

### Phase 2: User-Organization Management
**Files to create:**
- `server/src/db/user_organizations.rs` - CRUD for user-org relationships
- `server/src/api/organizations.rs` - Organization management endpoints

**Files to modify:**
- `server/src/api/mod.rs` - Add routes
- `server/src/api/users.rs` - Scope user creation to org limits

### Phase 3: Data Scoping
**Files to modify:**
- `server/src/api/clients.rs` - Filter by accessible orgs
- `server/src/api/sites.rs` - Filter by accessible orgs
- `server/src/api/agents.rs` - Filter by accessible orgs
- `server/src/api/alerts.rs` - Filter by accessible orgs
- `server/src/api/policies.rs` - Filter by accessible orgs
- `server/src/api/metrics.rs` - Filter by accessible orgs

### Phase 4: Dashboard UI Updates
**Files to modify:**
- `dashboard/src/api/client.ts` - Add org API types
- `dashboard/src/App.tsx` - Org context provider
- `dashboard/src/components/Layout.tsx` - Org selector (if multi-org)

**Files to create:**
- `dashboard/src/pages/OrganizationSettings.tsx` - Org management
- `dashboard/src/contexts/OrganizationContext.tsx` - Current org state

---

## Critical Implementation Details

### Login Flow Enhancement

```
1. User logs in (email/password or SSO)
2. Server fetches user's org memberships from user_organizations
3. JWT issued with system_role + orgs array
4. Client stores JWT, can switch between orgs in UI
```

### Seat Limit Enforcement

```rust
// When adding user to organization
pub async fn add_user_to_org(
    pool: &PgPool,
    user_id: Uuid,
    client_id: Uuid,
    role: &str,
    auth: &AuthUser,
) -> Result<(), Error> {
    // Check caller has permission
    if !auth.is_dev_admin() && !auth.is_org_admin(client_id) {
        return Err(Forbidden);
    }

    // Check seat limit (skip for dev_admin)
    if !auth.is_dev_admin() {
        let stats = get_org_stats(pool, client_id).await?;
        if let Some(limit) = stats.tech_seat_limit {
            if stats.tech_count >= limit {
                return Err(SeatLimitExceeded);
            }
        }
    }

    // Create membership
    insert_user_organization(pool, user_id, client_id, role).await
}
```

### Agent Registration with Endpoint Limits

```rust
// When agent registers under a site
pub async fn register_agent(...) -> Result<Agent, Error> {
    let site = get_site(pool, site_id).await?;
    let stats = get_org_stats(pool, site.client_id).await?;

    if let Some(limit) = stats.endpoint_limit {
        if stats.endpoint_count >= limit {
            return Err(EndpointLimitExceeded);
        }
    }

    // Create agent...
}
```

### Impersonation (Dev Admin Only)

```rust
// POST /api/auth/impersonate/:user_id
pub async fn impersonate(
    auth: AuthUser,
    Path(target_user_id): Path<Uuid>,
    State(state): State<AppState>,
) -> Result<Json<TokenResponse>, Error> {
    if !auth.is_dev_admin() {
        return Err(Forbidden);
    }

    // Get target user's org memberships
    let orgs = get_user_organizations(pool, target_user_id).await?;
    let user = get_user(pool, target_user_id).await?;

    // Issue token AS that user
    let token = create_jwt_with_orgs(
        target_user_id,
        &user.role,
        &orgs,
        &state.config.auth.jwt_secret,
    )?;

    Ok(Json(TokenResponse { token, ... }))
}
```

---

## Files Summary

| File | Action | Purpose |
|------|--------|---------|
| `server/migrations/007_authorization.sql` | Create | Schema for multi-tenant auth |
| `server/src/authz/mod.rs` | Create | Authorization module |
| `server/src/authz/permissions.rs` | Create | Permission helpers |
| `server/src/db/user_organizations.rs` | Create | User-org CRUD |
| `server/src/api/organizations.rs` | Create | Org management API |
| `server/src/auth/mod.rs` | Modify | Enhanced AuthUser, Claims |
| `server/src/api/clients.rs` | Modify | Scope to accessible orgs |
| `server/src/api/sites.rs` | Modify | Scope to accessible orgs |
| `server/src/api/agents.rs` | Modify | Scope + endpoint limits |
| `server/src/api/alerts.rs` | Modify | Scope to accessible orgs |
| `server/src/api/policies.rs` | Modify | Scope to accessible orgs |
| `server/src/api/users.rs` | Modify | Org-scoped user creation |
| `dashboard/src/api/client.ts` | Modify | Add org types |

---

## Migration Path for Existing Data

1. Run migration to add columns and tables
2. All existing users default to `role='user'` (no system privileges)
3. Manually grant `dev_admin` to designated user:
   ```sql
   UPDATE users SET role = 'dev_admin' WHERE email = 'admin@azcomputerguru.com';
   ```
4. Existing clients automatically become organizations
5. Dev admin can then assign users to organizations as needed


The file C:\Users\MikeSwanson\.claude\plans\tranquil-bouncing-twilight.md has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
     1→# Plan: Multi-Tenant Authorization System for GuruRMM
     2→
     3→## Overview
     4→
     5→Implement a multi-tenant authorization system with hierarchical roles supporting MSP-style organization management:
     6→- **Dev Admin**: Full system access, can impersonate any account
     7→- **Organization Admin**: MSP/Helpdesk paying customers with seat-based licensing
     8→- **Organization User**: Technicians within an organization
     9→
    10→## Requirements
    11→
    12→### Role Hierarchy (For Now)
    13→| Role | Access | Capabilities |
    14→|------|--------|--------------|
    15→| `dev_admin` | All organizations | Full access, set org limits, impersonate users |
    16→| `org_admin` | Assigned organizations | Manage sites/agents, create users within limits |
    17→| `org_user` | Assigned organizations | View/manage within their organization |
    18→
    19→### Future Roles (Not Implementing Now)
    20→- `support_tier` - Between Dev Admin and Org Admin
    21→
    22→### Licensing Model
    23→- **Tech Seats**: Number of users an organization can have
    24→- **Endpoint Limit**: Number of agents an organization can have
    25→- **Sites**: Unlimited for now
    26→- Limits set by Dev Admin, enforced by system
    27→
    28→### Multi-Organization Support
    29→- Users can belong to multiple organizations
    30→- Each user-organization assignment has a role within that org
    31→- Dev Admin is NOT tied to any organization (system-level)
    32→
    33→---
    34→
    35→## Database Schema
    36→
    37→### Migration: `007_authorization.sql`
    38→
    39→```sql
    40→-- Add organization limits to clients table
    41→ALTER TABLE clients ADD COLUMN tech_seat_limit INTEGER DEFAULT NULL;
    42→ALTER TABLE clients ADD COLUMN endpoint_limit INTEGER DEFAULT NULL;
    43→ALTER TABLE clients ADD COLUMN is_organization BOOLEAN DEFAULT TRUE;
    44→
    45→-- User-Organization relationship (replaces simple role on users table)
    46→CREATE TABLE user_organizations (
    47→    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    48→    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    49→    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    50→    role VARCHAR(50) NOT NULL DEFAULT 'org_user',  -- org_admin, org_user
    51→    created_at TIMESTAMPTZ DEFAULT NOW(),
    52→    created_by UUID REFERENCES users(id) ON DELETE SET NULL,
    53→    UNIQUE(user_id, client_id)
    54→);
    55→
    56→-- System-level roles (dev_admin, support_tier future)
    57→-- Keep role column on users table but repurpose:
    58→-- 'dev_admin' = system admin
    59→-- 'user' = regular user (must have org assignments to access anything)
    60→-- NULL or 'user' = no system-level privileges, access via org membership
    61→
    62→CREATE INDEX idx_user_orgs_user ON user_organizations(user_id);
    63→CREATE INDEX idx_user_orgs_client ON user_organizations(client_id);
    64→CREATE INDEX idx_user_orgs_role ON user_organizations(role);
    65→
    66→-- Track current endpoint counts per org (for limit enforcement)
    67→CREATE OR REPLACE VIEW organization_stats AS
    68→SELECT
    69→    c.id as client_id,
    70→    c.name as client_name,
    71→    c.tech_seat_limit,
    72→    c.endpoint_limit,
    73→    COALESCE((SELECT COUNT(*) FROM user_organizations WHERE client_id = c.id), 0) as tech_count,
    74→    COALESCE((SELECT COUNT(*) FROM agents a JOIN sites s ON a.site_id = s.id WHERE s.client_id = c.id), 0) as endpoint_count
    75→FROM clients c
    76→WHERE c.is_organization = TRUE;
    77→```
    78→
    79→---
    80→
    81→## Authorization Model
    82→
    83→### Permission Checks
    84→
    85→```rust
    86→pub struct AuthUser {
    87→    pub user_id: Uuid,
    88→    pub system_role: String,           // dev_admin, support, user
    89→    pub organizations: Vec<OrgAccess>, // Populated from JWT or DB lookup
    90→}
    91→
    92→pub struct OrgAccess {
    93→    pub client_id: Uuid,
    94→    pub role: String,  // org_admin, org_user
    95→}
    96→
    97→impl AuthUser {
    98→    /// Dev admin has access to everything
    99→    pub fn is_dev_admin(&self) -> bool {
   100→        self.system_role == "dev_admin"
   101→    }
   102→
   103→    /// Can access a specific organization
   104→    pub fn can_access_org(&self, client_id: Uuid) -> bool {
   105→        self.is_dev_admin() ||
   106→        self.organizations.iter().any(|o| o.client_id == client_id)
   107→    }
   108→
   109→    /// Is admin of a specific organization
   110→    pub fn is_org_admin(&self, client_id: Uuid) -> bool {
   111→        self.is_dev_admin() ||
   112→        self.organizations.iter().any(|o|
   113→            o.client_id == client_id && o.role == "org_admin"
   114→        )
   115→    }
   116→
   117→    /// Get all client_ids this user can access
   118→    pub fn accessible_client_ids(&self) -> Option<Vec<Uuid>> {
   119→        if self.is_dev_admin() {
   120→            None  // None = all clients
   121→        } else {
   122→            Some(self.organizations.iter().map(|o| o.client_id).collect())
   123→        }
   124→    }
   125→}
   126→```
   127→
   128→### JWT Claims Enhancement
   129→
   130→```rust
   131→pub struct Claims {
   132→    pub sub: Uuid,                      // user_id
   133→    pub system_role: String,            // dev_admin, user
   134→    pub orgs: Vec<OrgClaim>,            // Organization access
   135→    pub exp: i64,
   136→    pub iat: i64,
   137→}
   138→
   139→pub struct OrgClaim {
   140→    pub id: Uuid,       // client_id
   141→    pub role: String,   // org_admin, org_user
   142→}
   143→```
   144→
   145→---
   146→
   147→## API Changes
   148→
   149→### Endpoints Requiring Scoped Access
   150→
   151→| Endpoint | Current | New Behavior |
   152→|----------|---------|--------------|
   153→| `GET /api/clients` | Returns all | Dev: all, User: only their orgs |
   154→| `GET /api/clients/:id` | No check | Must have access to org |
   155→| `GET /api/sites` | Returns all | Filter by accessible orgs |
   156→| `GET /api/agents` | Returns all | Filter by accessible orgs |
   157→| `GET /api/alerts` | Returns all | Filter by accessible orgs |
   158→| `GET /api/policies` | Returns all | Filter by accessible orgs |
   159→| `POST /api/users` | Admin only | Dev: any org, OrgAdmin: their org (within limits) |
   160→
   161→### New Endpoints
   162→
   163→| Method | Endpoint | Description |
   164→|--------|----------|-------------|
   165→| `GET` | `/api/organizations` | List orgs user can access |
   166→| `GET` | `/api/organizations/:id/stats` | Get org limits and usage |
   167→| `PUT` | `/api/organizations/:id/limits` | Dev only: set tech_seat_limit, endpoint_limit |
   168→| `POST` | `/api/organizations/:id/users` | Add user to org (checks limits) |
   169→| `DELETE` | `/api/organizations/:id/users/:user_id` | Remove user from org |
   170→| `POST` | `/api/auth/impersonate/:user_id` | Dev only: get token as another user |
   171→
   172→---
   173→
   174→## Implementation Phases
   175→
   176→### Phase 1: Database & Core Auth
   177→**Files to create:**
   178→- `server/migrations/007_authorization.sql` - Schema changes
   179→- `server/src/authz/mod.rs` - Authorization module
   180→- `server/src/authz/permissions.rs` - Permission checking
   181→
   182→**Files to modify:**
   183→- `server/src/auth/mod.rs` - Enhanced AuthUser, Claims
   184→- `server/src/db/mod.rs` - Add user_organizations module
   185→
   186→### Phase 2: User-Organization Management
   187→**Files to create:**
   188→- `server/src/db/user_organizations.rs` - CRUD for user-org relationships
   189→- `server/src/api/organizations.rs` - Organization management endpoints
   190→
   191→**Files to modify:**
   192→- `server/src/api/mod.rs` - Add routes
   193→- `server/src/api/users.rs` - Scope user creation to org limits
   194→
   195→### Phase 3: Data Scoping
   196→**Files to modify:**
   197→- `server/src/api/clients.rs` - Filter by accessible orgs
   198→- `server/src/api/sites.rs` - Filter by accessible orgs
   199→- `server/src/api/agents.rs` - Filter by accessible orgs
   200→- `server/src/api/alerts.rs` - Filter by accessible orgs
   201→- `server/src/api/policies.rs` - Filter by accessible orgs
   202→- `server/src/api/metrics.rs` - Filter by accessible orgs
   203→
   204→### Phase 4: Dashboard UI Updates
   205→**Files to modify:**
   206→- `dashboard/src/api/client.ts` - Add org API types
   207→- `dashboard/src/App.tsx` - Org context provider
   208→- `dashboard/src/components/Layout.tsx` - Org selector (if multi-org)
   209→
   210→**Files to create:**
   211→- `dashboard/src/pages/OrganizationSettings.tsx` - Org management
   212→- `dashboard/src/contexts/OrganizationContext.tsx` - Current org state
   213→
   214→---
   215→
   216→## Critical Implementation Details
   217→
   218→### Login Flow Enhancement
   219→
   220→```
   221→1. User logs in (email/password or SSO)
   222→2. Server fetches user's org memberships from user_organizations
   223→3. JWT issued with system_role + orgs array
   224→4. Client stores JWT, can switch between orgs in UI
   225→```
   226→
   227→### Seat Limit Enforcement
   228→
   229→```rust
   230→// When adding user to organization
   231→pub async fn add_user_to_org(
   232→    pool: &PgPool,
   233→    user_id: Uuid,
   234→    client_id: Uuid,
   235→    role: &str,
   236→    auth: &AuthUser,
   237→) -> Result<(), Error> {
   238→    // Check caller has permission
   239→    if !auth.is_dev_admin() && !auth.is_org_admin(client_id) {
   240→        return Err(Forbidden);
   241→    }
   242→
   243→    // Check seat limit (skip for dev_admin)
   244→    if !auth.is_dev_admin() {
   245→        let stats = get_org_stats(pool, client_id).await?;
   246→        if let Some(limit) = stats.tech_seat_limit {
   247→            if stats.tech_count >= limit {
   248→                return Err(SeatLimitExceeded);
   249→            }
   250→        }
   251→    }
   252→
   253→    // Create membership
   254→    insert_user_organization(pool, user_id, client_id, role).await
   255→}
   256→```
   257→
   258→### Agent Registration with Endpoint Limits
   259→
   260→```rust
   261→// When agent registers under a site
   262→pub async fn register_agent(...) -> Result<Agent, Error> {
   263→    let site = get_site(pool, site_id).await?;
   264→    let stats = get_org_stats(pool, site.client_id).await?;
   265→
   266→    if let Some(limit) = stats.endpoint_limit {
   267→        if stats.endpoint_count >= limit {
   268→            return Err(EndpointLimitExceeded);
   269→        }
   270→    }
   271→
   272→    // Create agent...
   273→}
   274→```
   275→
   276→### Impersonation (Dev Admin Only)
   277→
   278→```rust
   279→// POST /api/auth/impersonate/:user_id
   280→pub async fn impersonate(
   281→    auth: AuthUser,
   282→    Path(target_user_id): Path<Uuid>,
   283→    State(state): State<AppState>,
   284→) -> Result<Json<TokenResponse>, Error> {
   285→    if !auth.is_dev_admin() {
   286→        return Err(Forbidden);
   287→    }
   288→
   289→    // Get target user's org memberships
   290→    let orgs = get_user_organizations(pool, target_user_id).await?;
   291→    let user = get_user(pool, target_user_id).await?;
   292→
   293→    // Issue token AS that user
   294→    let token = create_jwt_with_orgs(
   295→        target_user_id,
   296→        &user.role,
   297→        &orgs,
   298→        &state.config.auth.jwt_secret,
   299→    )?;
   300→
   301→    Ok(Json(TokenResponse { token, ... }))
   302→}
   303→```
   304→
   305→---
   306→
   307→## Files Summary
   308→
   309→| File | Action | Purpose |
   310→|------|--------|---------|
   311→| `server/migrations/007_authorization.sql` | Create | Schema for multi-tenant auth |
   312→| `server/src/authz/mod.rs` | Create | Authorization module |
   313→| `server/src/authz/permissions.rs` | Create | Permission helpers |
   314→| `server/src/db/user_organizations.rs` | Create | User-org CRUD |
   315→| `server/src/api/organizations.rs` | Create | Org management API |
   316→| `server/src/auth/mod.rs` | Modify | Enhanced AuthUser, Claims |
   317→| `server/src/api/clients.rs` | Modify | Scope to accessible orgs |
   318→| `server/src/api/sites.rs` | Modify | Scope to accessible orgs |
   319→| `server/src/api/agents.rs` | Modify | Scope + endpoint limits |
   320→| `server/src/api/alerts.rs` | Modify | Scope to accessible orgs |
   321→| `server/src/api/policies.rs` | Modify | Scope to accessible orgs |
   322→| `server/src/api/users.rs` | Modify | Org-scoped user creation |
   323→| `dashboard/src/api/client.ts` | Modify | Add org types |
   324→
   325→---
   326→
   327→## Migration Path for Existing Data
   328→
   329→1. Run migration to add columns and tables
   330→2. All existing users default to `role='user'` (no system privileges)
   331→3. Manually grant `dev_admin` to designated user:
   332→   ```sql
   333→   UPDATE users SET role = 'dev_admin' WHERE email = 'admin@azcomputerguru.com';
   334→   ```
   335→4. Existing clients automatically become organizations
   336→5. Dev admin can then assign users to organizations as needed
   337→

# Plan: Multi-Tenant Authorization System for GuruRMM

## Overview

Implement a multi-tenant authorization system with hierarchical roles supporting MSP-style organization management:
- **Dev Admin**: Full system access, can impersonate any account
- **Organization Admin**: MSP/Helpdesk paying customers with seat-based licensing
- **Organization User**: Technicians within an organization

## Requirements

### Role Hierarchy (For Now)
| Role | Access | Capabilities |
|------|--------|--------------|
| `dev_admin` | All organizations | Full access, set org limits, impersonate users |
| `org_admin` | Assigned organizations | Manage sites/agents, create users within limits |
| `org_user` | Assigned organizations | View/manage within their organization |

### Future Roles (Not Implementing Now)
- `support_tier` - Between Dev Admin and Org Admin

### Licensing Model
- **Tech Seats**: Number of users an organization can have
- **Endpoint Limit**: Number of agents an organization can have
- **Sites**: Unlimited for now
- Limits set by Dev Admin, enforced by system

### Multi-Organization Support
- Users can belong to multiple organizations
- Each user-organization assignment has a role within that org
- Dev Admin is NOT tied to any organization (system-level)

---

## Database Schema

### Migration: `007_authorization.sql`

```sql
-- Add organization limits to clients table
ALTER TABLE clients ADD COLUMN tech_seat_limit INTEGER DEFAULT NULL;
ALTER TABLE clients ADD COLUMN endpoint_limit INTEGER DEFAULT NULL;
ALTER TABLE clients ADD COLUMN is_organization BOOLEAN DEFAULT TRUE;

-- User-Organization relationship (replaces simple role on users table)
CREATE TABLE user_organizations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    role VARCHAR(50) NOT NULL DEFAULT 'org_user',  -- org_admin, org_user
    created_at TIMESTAMPTZ DEFAULT NOW(),
    created_by UUID REFERENCES users(id) ON DELETE SET NULL,
    UNIQUE(user_id, client_id)
);

-- System-level roles (dev_admin, support_tier future)
-- Keep role column on users table but repurpose:
-- 'dev_admin' = system admin
-- 'user' = regular user (must have org assignments to access anything)
-- NULL or 'user' = no system-level privileges, access via org membership

CREATE INDEX idx_user_orgs_user ON user_organizations(user_id);
CREATE INDEX idx_user_orgs_client ON user_organizations(client_id);
CREATE INDEX idx_user_orgs_role ON user_organizations(role);

-- Track current endpoint counts per org (for limit enforcement)
CREATE OR REPLACE VIEW organization_stats AS
SELECT
    c.id as client_id,
    c.name as client_name,
    c.tech_seat_limit,
    c.endpoint_limit,
    COALESCE((SELECT COUNT(*) FROM user_organizations WHERE client_id = c.id), 0) as tech_count,
    COALESCE((SELECT COUNT(*) FROM agents a JOIN sites s ON a.site_id = s.id WHERE s.client_id = c.id), 0) as endpoint_count
FROM clients c
WHERE c.is_organization = TRUE;
```

---

## Authorization Model

### Permission Checks

```rust
pub struct AuthUser {
    pub user_id: Uuid,
    pub system_role: String,           // dev_admin, support, user
    pub organizations: Vec<OrgAccess>, // Populated from JWT or DB lookup
}

pub struct OrgAccess {
    pub client_id: Uuid,
    pub role: String,  // org_admin, org_user
}

impl AuthUser {
    /// Dev admin has access to everything
    pub fn is_dev_admin(&self) -> bool {
        self.system_role == "dev_admin"
    }

    /// Can access a specific organization
    pub fn can_access_org(&self, client_id: Uuid) -> bool {
        self.is_dev_admin() ||
        self.organizations.iter().any(|o| o.client_id == client_id)
    }

    /// Is admin of a specific organization
    pub fn is_org_admin(&self, client_id: Uuid) -> bool {
        self.is_dev_admin() ||
        self.organizations.iter().any(|o|
            o.client_id == client_id && o.role == "org_admin"
        )
    }

    /// Get all client_ids this user can access
    pub fn accessible_client_ids(&self) -> Option<Vec<Uuid>> {
        if self.is_dev_admin() {
            None  // None = all clients
        } else {
            Some(self.organizations.iter().map(|o| o.client_id).collect())
        }
    }
}
```

### JWT Claims Enhancement

```rust
pub struct Claims {
    pub sub: Uuid,                      // user_id
    pub system_role: String,            // dev_admin, user
    pub orgs: Vec<OrgClaim>,            // Organization access
    pub exp: i64,
    pub iat: i64,
}

pub struct OrgClaim {
    pub id: Uuid,       // client_id
    pub role: String,   // org_admin, org_user
}
```

---

## API Changes

### Endpoints Requiring Scoped Access

| Endpoint | Current | New Behavior |
|----------|---------|--------------|
| `GET /api/clients` | Returns all | Dev: all, User: only their orgs |
| `GET /api/clients/:id` | No check | Must have access to org |
| `GET /api/sites` | Returns all | Filter by accessible orgs |
| `GET /api/agents` | Returns all | Filter by accessible orgs |
| `GET /api/alerts` | Returns all | Filter by accessible orgs |
| `GET /api/policies` | Returns all | Filter by accessible orgs |
| `POST /api/users` | Admin only | Dev: any org, OrgAdmin: their org (within limits) |

### New Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/organizations` | List orgs user can access |
| `GET` | `/api/organizations/:id/stats` | Get org limits and usage |
| `PUT` | `/api/organizations/:id/limits` | Dev only: set tech_seat_limit, endpoint_limit |
| `POST` | `/api/organizations/:id/users` | Add user to org (checks limits) |
| `DELETE` | `/api/organizations/:id/users/:user_id` | Remove user from org |
| `POST` | `/api/auth/impersonate/:user_id` | Dev only: get token as another user |

---

## Implementation Phases

### Phase 1: Database & Core Auth
**Files to create:**
- `server/migrations/007_authorization.sql` - Schema changes
- `server/src/authz/mod.rs` - Authorization module
- `server/src/authz/permissions.rs` - Permission checking

**Files to modify:**
- `server/src/auth/mod.rs` - Enhanced AuthUser, Claims
- `server/src/db/mod.rs` - Add user_organizations module

### Phase 2: User-Organization Management
**Files to create:**
- `server/src/db/user_organizations.rs` - CRUD for user-org relationships
- `server/src/api/organizations.rs` - Organization management endpoints

**Files to modify:**
- `server/src/api/mod.rs` - Add routes
- `server/src/api/users.rs` - Scope user creation to org limits

### Phase 3: Data Scoping
**Files to modify:**
- `server/src/api/clients.rs` - Filter by accessible orgs
- `server/src/api/sites.rs` - Filter by accessible orgs
- `server/src/api/agents.rs` - Filter by accessible orgs
- `server/src/api/alerts.rs` - Filter by accessible orgs
- `server/src/api/policies.rs` - Filter by accessible orgs
- `server/src/api/metrics.rs` - Filter by accessible orgs

### Phase 4: Dashboard UI Updates
**Files to modify:**
- `dashboard/src/api/client.ts` - Add org API types
- `dashboard/src/App.tsx` - Org context provider
- `dashboard/src/components/Layout.tsx` - Org selector (if multi-org)

**Files to create:**
- `dashboard/src/pages/OrganizationSettings.tsx` - Org management
- `dashboard/src/contexts/OrganizationContext.tsx` - Current org state

---

## Critical Implementation Details

### Login Flow Enhancement

```
1. User logs in (email/password or SSO)
2. Server fetches user's org memberships from user_organizations
3. JWT issued with system_role + orgs array
4. Client stores JWT, can switch between orgs in UI
```

### Seat Limit Enforcement

```rust
// When adding user to organization
pub async fn add_user_to_org(
    pool: &PgPool,
    user_id: Uuid,
    client_id: Uuid,
    role: &str,
    auth: &AuthUser,
) -> Result<(), Error> {
    // Check caller has permission
    if !auth.is_dev_admin() && !auth.is_org_admin(client_id) {
        return Err(Forbidden);
    }

    // Check seat limit (skip for dev_admin)
    if !auth.is_dev_admin() {
        let stats = get_org_stats(pool, client_id).await?;
        if let Some(limit) = stats.tech_seat_limit {
            if stats.tech_count >= limit {
                return Err(SeatLimitExceeded);
            }
        }
    }

    // Create membership
    insert_user_organization(pool, user_id, client_id, role).await
}
```

### Agent Registration with Endpoint Limits

```rust
// When agent registers under a site
pub async fn register_agent(...) -> Result<Agent, Error> {
    let site = get_site(pool, site_id).await?;
    let stats = get_org_stats(pool, site.client_id).await?;

    if let Some(limit) = stats.endpoint_limit {
        if stats.endpoint_count >= limit {
            return Err(EndpointLimitExceeded);
        }
    }

    // Create agent...
}
```

### Impersonation (Dev Admin Only)

```rust
// POST /api/auth/impersonate/:user_id
pub async fn impersonate(
    auth: AuthUser,
    Path(target_user_id): Path<Uuid>,
    State(state): State<AppState>,
) -> Result<Json<TokenResponse>, Error> {
    if !auth.is_dev_admin() {
        return Err(Forbidden);
    }

    // Get target user's org memberships
    let orgs = get_user_organizations(pool, target_user_id).await?;
    let user = get_user(pool, target_user_id).await?;

    // Issue token AS that user
    let token = create_jwt_with_orgs(
        target_user_id,
        &user.role,
        &orgs,
        &state.config.auth.jwt_secret,
    )?;

    Ok(Json(TokenResponse { token, ... }))
}
```

---

## Files Summary

| File | Action | Purpose |
|------|--------|---------|
| `server/migrations/007_authorization.sql` | Create | Schema for multi-tenant auth |
| `server/src/authz/mod.rs` | Create | Authorization module |
| `server/src/authz/permissions.rs` | Create | Permission helpers |
| `server/src/db/user_organizations.rs` | Create | User-org CRUD |
| `server/src/api/organizations.rs` | Create | Org management API |
| `server/src/auth/mod.rs` | Modify | Enhanced AuthUser, Claims |
| `server/src/api/clients.rs` | Modify | Scope to accessible orgs |
| `server/src/api/sites.rs` | Modify | Scope to accessible orgs |
| `server/src/api/agents.rs` | Modify | Scope + endpoint limits |
| `server/src/api/alerts.rs` | Modify | Scope to accessible orgs |
| `server/src/api/policies.rs` | Modify | Scope to accessible orgs |
| `server/src/api/users.rs` | Modify | Org-scoped user creation |
| `dashboard/src/api/client.ts` | Modify | Add org types |

---

## Migration Path for Existing Data

1. Run migration to add columns and tables
2. All existing users default to `role='user'` (no system privileges)
3. Manually grant `dev_admin` to designated user:
   ```sql
   UPDATE users SET role = 'dev_admin' WHERE email = 'admin@azcomputerguru.com';
   ```
4. Existing clients automatically become organizations
5. Dev admin can then assign users to organizations as needed

-# Plan: Stackable Policy System for GuruRMM

+# Plan: Multi-Tenant Authorization System for GuruRMM

-Implement a hierarchical, stackable policy system similar to Syncro where policies can be assigned at Client, Site, and Agent levels. Policies stack in order with "last wins" merge logic (Agent overrides Site overrides Client).

+Implement a multi-tenant authorization system with hierarchical roles supporting MSP-style organization management:

+- **Dev Admin**: Full system access, can impersonate any account

+- **Organization Admin**: MSP/Helpdesk paying customers with seat-based licensing

+- **Organization User**: Technicians within an organization

-- **Assignment Levels**: Client → Site → Agent (no global base)

-- **Merge Strategy**: Last wins (later policies override earlier)

-- **Notifications**: Dashboard-only initially (no email/webhook)

-- **Policy Settings**: Metrics collection, alert thresholds, system tray, auto-update, watchdog

+| `dev_admin` | All organizations | Full access, set org limits, impersonate users |

+| `org_admin` | Assigned organizations | Manage sites/agents, create users within limits |

+| `org_user` | Assigned organizations | View/manage within their organization |

+### Future Roles (Not Implementing Now)

+- `support_tier` - Between Dev Admin and Org Admin

-            = Effective Policy (merged result)

+- **Tech Seats**: Number of users an organization can have

+- **Endpoint Limit**: Number of agents an organization can have

+- Limits set by Dev Admin, enforced by system

-Multiple policies at the same level are merged by priority (ascending).

+- Users can belong to multiple organizations

+- Each user-organization assignment has a role within that org

+- Dev Admin is NOT tied to any organization (system-level)

--- Policy definitions (reusable templates)

-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

-    policy_data JSONB NOT NULL DEFAULT '{}',

-    created_at TIMESTAMPTZ DEFAULT NOW(),

-    updated_at TIMESTAMPTZ DEFAULT NOW(),

-    created_by UUID REFERENCES users(id) ON DELETE SET NULL

+-- Add organization limits to clients table

+ALTER TABLE clients ADD COLUMN tech_seat_limit INTEGER DEFAULT NULL;

+ALTER TABLE clients ADD COLUMN endpoint_limit INTEGER DEFAULT NULL;

+ALTER TABLE clients ADD COLUMN is_organization BOOLEAN DEFAULT TRUE;

--- Policy assignments (links policies to targets)

+-- User-Organization relationship (replaces simple role on users table)

id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

-    policy_id UUID NOT NULL REFERENCES policies(id) ON DELETE CASCADE,

-    client_id UUID REFERENCES clients(id) ON DELETE CASCADE,

-    site_id UUID REFERENCES sites(id) ON DELETE CASCADE,

-    agent_id UUID REFERENCES agents(id) ON DELETE CASCADE,

+    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,

+    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,

+    role VARCHAR(50) NOT NULL DEFAULT 'org_user',  -- org_admin, org_user

created_by UUID REFERENCES users(id) ON DELETE SET NULL,

-        (agent_id IS NOT NULL)::int = 1

--- Alerts triggered by threshold violations

-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

-    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,

-    alert_type VARCHAR(50) NOT NULL,  -- cpu, memory, disk, temperature, service

-    severity VARCHAR(20) NOT NULL DEFAULT 'warning',  -- info, warning, critical

-    status VARCHAR(20) DEFAULT 'active',  -- active, acknowledged, resolved

-    triggered_at TIMESTAMPTZ DEFAULT NOW(),

-    last_seen_at TIMESTAMPTZ DEFAULT NOW(),

-    acknowledged_by UUID REFERENCES users(id),

+-- System-level roles (dev_admin, support_tier future)

+-- Keep role column on users table but repurpose:

+-- 'user' = regular user (must have org assignments to access anything)

+-- NULL or 'user' = no system-level privileges, access via org membership

--- Threshold state tracking (for duration-based alerts)

-    threshold_key VARCHAR(100) NOT NULL,

-    consecutive_violations INTEGER DEFAULT 0,

-    last_checked_at TIMESTAMPTZ DEFAULT NOW(),

-    PRIMARY KEY (agent_id, threshold_key)

+CREATE INDEX idx_user_orgs_user ON user_organizations(user_id);

+CREATE INDEX idx_user_orgs_client ON user_organizations(client_id);

+CREATE INDEX idx_user_orgs_role ON user_organizations(role);

-CREATE INDEX idx_alerts_agent_status ON alerts(agent_id, status);

-CREATE INDEX idx_alerts_triggered ON alerts(triggered_at DESC);

-CREATE INDEX idx_policy_assignments_client ON policy_assignments(client_id);

-CREATE INDEX idx_policy_assignments_site ON policy_assignments(site_id);

-CREATE INDEX idx_policy_assignments_agent ON policy_assignments(agent_id);

+-- Track current endpoint counts per org (for limit enforcement)

+CREATE OR REPLACE VIEW organization_stats AS

+    COALESCE((SELECT COUNT(*) FROM user_organizations WHERE client_id = c.id), 0) as tech_count,

+    COALESCE((SELECT COUNT(*) FROM agents a JOIN sites s ON a.site_id = s.id WHERE s.client_id = c.id), 0) as endpoint_count

-    "cpu": { "warning_percent": 80, "critical_percent": 95, "duration_seconds": 300 },

-    "memory": { "warning_percent": 85, "critical_percent": 95, "duration_seconds": 300 },

-    "disk": { "warning_percent": 85, "critical_percent": 95, "duration_seconds": 0 },

-    "cpu_temp": { "warning_celsius": 80, "critical_celsius": 95 },

-    "gpu_temp": { "warning_celsius": 85, "critical_celsius": 100 }

+    pub system_role: String,           // dev_admin, support, user

+    pub organizations: Vec<OrgAccess>, // Populated from JWT or DB lookup

+    pub role: String,  // org_admin, org_user

+    /// Dev admin has access to everything

+    pub fn is_dev_admin(&self) -> bool {

+        self.system_role == "dev_admin"

+    /// Can access a specific organization

+    pub fn can_access_org(&self, client_id: Uuid) -> bool {

+        self.organizations.iter().any(|o| o.client_id == client_id)

+    /// Is admin of a specific organization

+    pub fn is_org_admin(&self, client_id: Uuid) -> bool {

+        self.organizations.iter().any(|o|

+            o.client_id == client_id && o.role == "org_admin"

+    /// Get all client_ids this user can access

+    pub fn accessible_client_ids(&self) -> Option<Vec<Uuid>> {

+            None  // None = all clients

+            Some(self.organizations.iter().map(|o| o.client_id).collect())

+    pub sub: Uuid,                      // user_id

+    pub system_role: String,            // dev_admin, user

+    pub orgs: Vec<OrgClaim>,            // Organization access

+    pub role: String,   // org_admin, org_user

-| GET | `/api/policies` | List all policies |

-| POST | `/api/policies` | Create policy |

-| GET | `/api/policies/:id` | Get policy details |

-| PUT | `/api/policies/:id` | Update policy |

-| DELETE | `/api/policies/:id` | Delete policy |

-| POST | `/api/policies/:id/assign` | Assign to target |

-| GET | `/api/policies/:id/assignments` | List assignments |

-| DELETE | `/api/policy-assignments/:id` | Remove assignment |

-| GET | `/api/agents/:id/effective-policy` | Get merged policy |

+| `GET /api/clients` | Returns all | Dev: all, User: only their orgs |

+| `GET /api/clients/:id` | No check | Must have access to org |

+| `GET /api/sites` | Returns all | Filter by accessible orgs |

+| `GET /api/agents` | Returns all | Filter by accessible orgs |

+| `GET /api/alerts` | Returns all | Filter by accessible orgs |

+| `GET /api/policies` | Returns all | Filter by accessible orgs |

+| `POST /api/users` | Admin only | Dev: any org, OrgAdmin: their org (within limits) |

-| GET | `/api/alerts` | List alerts (with filters) |

-| GET | `/api/alerts/summary` | Dashboard counts |

-| GET | `/api/alerts/:id` | Get alert details |

-| POST | `/api/alerts/:id/acknowledge` | Acknowledge alert |

-| POST | `/api/alerts/:id/resolve` | Resolve alert |

+| `GET` | `/api/organizations` | List orgs user can access |

+| `GET` | `/api/organizations/:id/stats` | Get org limits and usage |

+| `PUT` | `/api/organizations/:id/limits` | Dev only: set tech_seat_limit, endpoint_limit |

+| `POST` | `/api/organizations/:id/users` | Add user to org (checks limits) |

+| `DELETE` | `/api/organizations/:id/users/:user_id` | Remove user from org |

+| `POST` | `/api/auth/impersonate/:user_id` | Dev only: get token as another user |

-- `server/migrations/006_policies.sql` - Schema

-- `server/src/db/policies.rs` - Policy CRUD

-- `server/src/db/alerts.rs` - Alert CRUD

-- `server/src/policy/mod.rs` - Module root

-- `server/src/policy/merge.rs` - Merge logic

-- `server/src/policy/effective.rs` - Calculate effective policy

+- `server/migrations/007_authorization.sql` - Schema changes

+- `server/src/authz/mod.rs` - Authorization module

+- `server/src/authz/permissions.rs` - Permission checking

-- `server/src/db/mod.rs` - Add module exports

+- `server/src/auth/mod.rs` - Enhanced AuthUser, Claims

+- `server/src/db/mod.rs` - Add user_organizations module

+### Phase 2: User-Organization Management

-- `server/src/api/policies.rs` - Policy endpoints

-- `server/src/api/alerts.rs` - Alert endpoints

+- `server/src/db/user_organizations.rs` - CRUD for user-org relationships

+- `server/src/api/organizations.rs` - Organization management endpoints

+- `server/src/api/users.rs` - Scope user creation to org limits

-- `server/src/alerts/mod.rs` - Module root

-- `server/src/alerts/evaluator.rs` - Threshold checking

-- `server/src/ws/mod.rs` - Call evaluator on metrics, send policy on connect

+- `server/src/api/clients.rs` - Filter by accessible orgs

+- `server/src/api/sites.rs` - Filter by accessible orgs

+- `server/src/api/agents.rs` - Filter by accessible orgs

+- `server/src/api/alerts.rs` - Filter by accessible orgs

+- `server/src/api/policies.rs` - Filter by accessible orgs

+- `server/src/api/metrics.rs` - Filter by accessible orgs

-- `agent/src/transport/mod.rs` - Enhanced ConfigUpdatePayload

-- `agent/src/transport/websocket.rs` - Apply policy on receive

-- `agent/src/metrics/mod.rs` - Respect collection settings

-- `agent/src/config.rs` - Runtime policy state

+- `dashboard/src/api/client.ts` - Add org API types

+- `dashboard/src/App.tsx` - Org context provider

+- `dashboard/src/components/Layout.tsx` - Org selector (if multi-org)

-- `dashboard/src/pages/Policies.tsx` - Policy management

-- `dashboard/src/pages/Alerts.tsx` - Alert dashboard

-- `dashboard/src/components/PolicyEditor.tsx` - JSON editor for policy

-- `dashboard/src/components/AlertBadge.tsx` - Nav badge

+- `dashboard/src/pages/OrganizationSettings.tsx` - Org management

+- `dashboard/src/contexts/OrganizationContext.tsx` - Current org state

-- `dashboard/src/api/client.ts` - Add API types

-- `dashboard/src/components/Layout.tsx` - Alert indicator

-for policy in client_policies.sorted_by(priority):

-    effective.merge_from(policy)  # None values skip, Some values override

-for policy in site_policies.sorted_by(priority):

-for policy in agent_policies.sorted_by(priority):

+1. User logs in (email/password or SSO)

+2. Server fetches user's org memberships from user_organizations

+3. JWT issued with system_role + orgs array

+4. Client stores JWT, can switch between orgs in UI

-3. Server calls `evaluate_thresholds(agent_id, metrics)`

-4. Evaluator fetches effective policy for agent

-5. For each threshold: compare value, check duration if needed

-6. Create/update alert if threshold exceeded

-7. Auto-resolve alert if value drops below threshold

-1. On agent connect → send effective policy via `ConfigUpdate`

-2. On policy assignment change → push to affected connected agents

-3. Agent applies settings immediately (no restart needed)

+    if !auth.is_dev_admin() && !auth.is_org_admin(client_id) {

+    // Check seat limit (skip for dev_admin)

+        let stats = get_org_stats(pool, client_id).await?;

+        if let Some(limit) = stats.tech_seat_limit {

+            if stats.tech_count >= limit {

+                return Err(SeatLimitExceeded);

+    insert_user_organization(pool, user_id, client_id, role).await

-- List view with name, description, assignment count

-- Create/Edit with JSON editor or form builder

-- Assignment panel: select Client/Site/Agent + priority

+### Agent Registration with Endpoint Limits

-- Filterable table: status, severity, type, agent

-- Summary cards: Active (critical/warning), Acknowledged

+pub async fn register_agent(...) -> Result<Agent, Error> {

+    let site = get_site(pool, site_id).await?;

+    let stats = get_org_stats(pool, site.client_id).await?;

-- "Effective Policy" tab showing merged result

+    if let Some(limit) = stats.endpoint_limit {

+        if stats.endpoint_count >= limit {

+            return Err(EndpointLimitExceeded);

+) -> Result<Json<TokenResponse>, Error> {

+    // Get target user's org memberships

+    let orgs = get_user_organizations(pool, target_user_id).await?;

+    let user = get_user(pool, target_user_id).await?;

+    Ok(Json(TokenResponse { token, ... }))

-| `server/migrations/006_policies.sql` | Create | Schema |

-| `server/src/db/policies.rs` | Create | Policy DB operations |

-| `server/src/db/alerts.rs` | Create | Alert DB operations |

-| `server/src/policy/mod.rs` | Create | Policy module |

-| `server/src/policy/merge.rs` | Create | Merge logic |

-| `server/src/api/policies.rs` | Create | Policy API |

-| `server/src/api/alerts.rs` | Create | Alert API |

-| `server/src/alerts/evaluator.rs` | Create | Threshold evaluation |

-| `server/src/ws/mod.rs` | Modify | Integrate evaluation |

-| `server/src/api/mod.rs` | Modify | Add routes |

-| `agent/src/transport/mod.rs` | Modify | Enhanced payload |

-| `agent/src/transport/websocket.rs` | Modify | Apply policy |

-| `dashboard/src/pages/Policies.tsx` | Create | Policy UI |

-| `dashboard/src/pages/Alerts.tsx` | Create | Alert UI |

+| `server/migrations/007_authorization.sql` | Create | Schema for multi-tenant auth |

+| `server/src/authz/mod.rs` | Create | Authorization module |

+| `server/src/authz/permissions.rs` | Create | Permission helpers |

+| `server/src/db/user_organizations.rs` | Create | User-org CRUD |

+| `server/src/api/organizations.rs` | Create | Org management API |

+| `server/src/auth/mod.rs` | Modify | Enhanced AuthUser, Claims |

+| `server/src/api/clients.rs` | Modify | Scope to accessible orgs |

+| `server/src/api/sites.rs` | Modify | Scope to accessible orgs |

+| `server/src/api/agents.rs` | Modify | Scope + endpoint limits |

+| `server/src/api/alerts.rs` | Modify | Scope to accessible orgs |

+| `server/src/api/policies.rs` | Modify | Scope to accessible orgs |

+| `server/src/api/users.rs` | Modify | Org-scoped user creation |

+| `dashboard/src/api/client.ts` | Modify | Add org types |

+1. Run migration to add columns and tables

+2. All existing users default to `role='user'` (no system privileges)

+3. Manually grant `dev_admin` to designated user:

+   UPDATE users SET role = 'dev_admin' WHERE email = 'admin@azcomputerguru.com';

+4. Existing clients automatically become organizations

+5. Dev admin can then assign users to organizations as needed

# Plan: Stackable Policy System for GuruRMM

## Overview

Implement a hierarchical, stackable policy system similar to Syncro where policies can be assigned at Client, Site, and Agent levels. Policies stack in order with "last wins" merge logic (Agent overrides Site overrides Client).

## User Requirements

- **Assignment Levels**: Client → Site → Agent (no global base)
- **Merge Strategy**: Last wins (later policies override earlier)
- **Notifications**: Dashboard-only initially (no email/webhook)
- **Policy Settings**: Metrics collection, alert thresholds, system tray, auto-update, watchdog

## Architecture

```
Client Policy (priority 0)
    └─ Site Policy (priority 0)
        └─ Agent Policy (priority 0)
            = Effective Policy (merged result)
```

Multiple policies at the same level are merged by priority (ascending).

---

## Database Schema

### Migration: `006_policies.sql`

```sql
-- Policy definitions (reusable templates)
CREATE TABLE policies (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    policy_data JSONB NOT NULL DEFAULT '{}',
    is_default BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    created_by UUID REFERENCES users(id) ON DELETE SET NULL
);

-- Policy assignments (links policies to targets)
CREATE TABLE policy_assignments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    policy_id UUID NOT NULL REFERENCES policies(id) ON DELETE CASCADE,
    client_id UUID REFERENCES clients(id) ON DELETE CASCADE,
    site_id UUID REFERENCES sites(id) ON DELETE CASCADE,
    agent_id UUID REFERENCES agents(id) ON DELETE CASCADE,
    priority INTEGER DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    created_by UUID REFERENCES users(id) ON DELETE SET NULL,
    CONSTRAINT one_target CHECK (
        (client_id IS NOT NULL)::int +
        (site_id IS NOT NULL)::int +
        (agent_id IS NOT NULL)::int = 1
    )
);

-- Alerts triggered by threshold violations
CREATE TABLE alerts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    alert_type VARCHAR(50) NOT NULL,  -- cpu, memory, disk, temperature, service
    severity VARCHAR(20) NOT NULL DEFAULT 'warning',  -- info, warning, critical
    title VARCHAR(255) NOT NULL,
    message TEXT,
    metric_value REAL,
    threshold_value REAL,
    status VARCHAR(20) DEFAULT 'active',  -- active, acknowledged, resolved
    dedup_key VARCHAR(255),
    triggered_at TIMESTAMPTZ DEFAULT NOW(),
    last_seen_at TIMESTAMPTZ DEFAULT NOW(),
    acknowledged_at TIMESTAMPTZ,
    acknowledged_by UUID REFERENCES users(id),
    resolved_at TIMESTAMPTZ
);

-- Threshold state tracking (for duration-based alerts)
CREATE TABLE alert_threshold_state (
    agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
    threshold_key VARCHAR(100) NOT NULL,
    violation_started_at TIMESTAMPTZ,
    consecutive_violations INTEGER DEFAULT 0,
    last_value REAL,
    last_checked_at TIMESTAMPTZ DEFAULT NOW(),
    PRIMARY KEY (agent_id, threshold_key)
);

CREATE INDEX idx_alerts_agent_status ON alerts(agent_id, status);
CREATE INDEX idx_alerts_triggered ON alerts(triggered_at DESC);
CREATE INDEX idx_policy_assignments_client ON policy_assignments(client_id);
CREATE INDEX idx_policy_assignments_site ON policy_assignments(site_id);
CREATE INDEX idx_policy_assignments_agent ON policy_assignments(agent_id);
```

---

## Policy Data Structure

### `policy_data` JSONB Schema

```json
{
  "metrics": {
    "interval_seconds": 60,
    "collect_cpu": true,
    "collect_memory": true,
    "collect_disk": true,
    "collect_network": true,
    "collect_temperatures": true,
    "collect_user_info": true,
    "collect_public_ip": true
  },
  "thresholds": {
    "cpu": { "warning_percent": 80, "critical_percent": 95, "duration_seconds": 300 },
    "memory": { "warning_percent": 85, "critical_percent": 95, "duration_seconds": 300 },
    "disk": { "warning_percent": 85, "critical_percent": 95, "duration_seconds": 0 },
    "cpu_temp": { "warning_celsius": 80, "critical_celsius": 95 },
    "gpu_temp": { "warning_celsius": 85, "critical_celsius": 100 }
  },
  "tray": {
    "enabled": true,
    "show_status": true,
    "show_info": true,
    "allow_force_checkin": true,
    "allow_view_logs": false,
    "allow_open_dashboard": true,
    "allow_stop_agent": false,
    "dashboard_url": null
  },
  "updates": {
    "auto_update": true,
    "channel": "stable",
    "defer_hours": 24,
    "maintenance_window": null
  },
  "watchdog": {
    "enabled": false,
    "check_interval_seconds": 30,
    "services": [],
    "processes": []
  }
}
```

---

## API Endpoints

### Policies
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/policies` | List all policies |
| POST | `/api/policies` | Create policy |
| GET | `/api/policies/:id` | Get policy details |
| PUT | `/api/policies/:id` | Update policy |
| DELETE | `/api/policies/:id` | Delete policy |
| POST | `/api/policies/:id/assign` | Assign to target |
| GET | `/api/policies/:id/assignments` | List assignments |
| DELETE | `/api/policy-assignments/:id` | Remove assignment |
| GET | `/api/agents/:id/effective-policy` | Get merged policy |

### Alerts
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/alerts` | List alerts (with filters) |
| GET | `/api/alerts/summary` | Dashboard counts |
| GET | `/api/alerts/:id` | Get alert details |
| POST | `/api/alerts/:id/acknowledge` | Acknowledge alert |
| POST | `/api/alerts/:id/resolve` | Resolve alert |

---

## Implementation Phases

### Phase 1: Database & Core (Server)
**Files to create:**
- `server/migrations/006_policies.sql` - Schema
- `server/src/db/policies.rs` - Policy CRUD
- `server/src/db/alerts.rs` - Alert CRUD
- `server/src/policy/mod.rs` - Module root
- `server/src/policy/merge.rs` - Merge logic
- `server/src/policy/effective.rs` - Calculate effective policy

**Files to modify:**
- `server/src/db/mod.rs` - Add module exports

### Phase 2: API Endpoints (Server)
**Files to create:**
- `server/src/api/policies.rs` - Policy endpoints
- `server/src/api/alerts.rs` - Alert endpoints

**Files to modify:**
- `server/src/api/mod.rs` - Add routes

### Phase 3: Alert Evaluation (Server)
**Files to create:**
- `server/src/alerts/mod.rs` - Module root
- `server/src/alerts/evaluator.rs` - Threshold checking

**Files to modify:**
- `server/src/ws/mod.rs` - Call evaluator on metrics, send policy on connect

### Phase 4: Agent Integration
**Files to modify:**
- `agent/src/transport/mod.rs` - Enhanced ConfigUpdatePayload
- `agent/src/transport/websocket.rs` - Apply policy on receive
- `agent/src/metrics/mod.rs` - Respect collection settings
- `agent/src/config.rs` - Runtime policy state

### Phase 5: Dashboard UI
**Files to create:**
- `dashboard/src/pages/Policies.tsx` - Policy management
- `dashboard/src/pages/Alerts.tsx` - Alert dashboard
- `dashboard/src/components/PolicyEditor.tsx` - JSON editor for policy
- `dashboard/src/components/AlertBadge.tsx` - Nav badge

**Files to modify:**
- `dashboard/src/api/client.ts` - Add API types
- `dashboard/src/App.tsx` - Add routes
- `dashboard/src/components/Layout.tsx` - Alert indicator

---

## Key Implementation Details

### Merge Logic (Pseudocode)
```
effective = system_defaults()
for policy in client_policies.sorted_by(priority):
    effective.merge_from(policy)  # None values skip, Some values override
for policy in site_policies.sorted_by(priority):
    effective.merge_from(policy)
for policy in agent_policies.sorted_by(priority):
    effective.merge_from(policy)
return effective
```

### Threshold Evaluation Flow
1. Agent sends metrics via WebSocket
2. Server stores metrics in database
3. Server calls `evaluate_thresholds(agent_id, metrics)`
4. Evaluator fetches effective policy for agent
5. For each threshold: compare value, check duration if needed
6. Create/update alert if threshold exceeded
7. Auto-resolve alert if value drops below threshold

### Policy Delivery
1. On agent connect → send effective policy via `ConfigUpdate`
2. On policy assignment change → push to affected connected agents
3. Agent applies settings immediately (no restart needed)

---

## Dashboard UI Mockup

### Policies Page
- List view with name, description, assignment count
- Create/Edit with JSON editor or form builder
- Assignment panel: select Client/Site/Agent + priority

### Alerts Page
- Filterable table: status, severity, type, agent
- Summary cards: Active (critical/warning), Acknowledged
- Bulk actions: Acknowledge, Resolve

### Agent Detail Enhancement
- "Effective Policy" tab showing merged result
- Alert history for this agent

---

## Critical Files Summary

| File | Action | Purpose |
|------|--------|---------|
| `server/migrations/006_policies.sql` | Create | Schema |
| `server/src/db/policies.rs` | Create | Policy DB operations |
| `server/src/db/alerts.rs` | Create | Alert DB operations |
| `server/src/policy/mod.rs` | Create | Policy module |
| `server/src/policy/merge.rs` | Create | Merge logic |
| `server/src/api/policies.rs` | Create | Policy API |
| `server/src/api/alerts.rs` | Create | Alert API |
| `server/src/alerts/evaluator.rs` | Create | Threshold evaluation |
| `server/src/ws/mod.rs` | Modify | Integrate evaluation |
| `server/src/api/mod.rs` | Modify | Add routes |
| `agent/src/transport/mod.rs` | Modify | Enhanced payload |
| `agent/src/transport/websocket.rs` | Modify | Apply policy |
| `dashboard/src/pages/Policies.tsx` | Create | Policy UI |
| `dashboard/src/pages/Alerts.tsx` | Create | Alert UI |


The user sent "/s" which means they want me to save the session log. But I'm in plan mode and can't execute commands. Let me exit plan mode first with the plan I've created, and then save the session log.

Actually, looking at the system reminder, it says I should call ExitPlanMode when I'm happy with my plan. The user sent /s which is the save command, but I need to handle the plan mode first.

Let me exit plan mode and then handle the save command.


The plan is complete. Let me exit plan mode so we can proceed:


ExitPlanMode

# Plan: Multi-Tenant Authorization System for GuruRMM

## Overview

Implement a multi-tenant authorization system with hierarchical roles supporting MSP-style organization management:
- **Dev Admin**: Full system access, can impersonate any account
- **Organization Admin**: MSP/Helpdesk paying customers with seat-based licensing
- **Organization User**: Technicians within an organization

## Requirements

### Role Hierarchy (For Now)
| Role | Access | Capabilities |
|------|--------|--------------|
| `dev_admin` | All organizations | Full access, set org limits, impersonate users |
| `org_admin` | Assigned organizations | Manage sites/agents, create users within limits |
| `org_user` | Assigned organizations | View/manage within their organization |

### Future Roles (Not Implementing Now)
- `support_tier` - Between Dev Admin and Org Admin

### Licensing Model
- **Tech Seats**: Number of users an organization can have
- **Endpoint Limit**: Number of agents an organization can have
- **Sites**: Unlimited for now
- Limits set by Dev Admin, enforced by system

### Multi-Organization Support
- Users can belong to multiple organizations
- Each user-organization assignment has a role within that org
- Dev Admin is NOT tied to any organization (system-level)

---

## Database Schema

### Migration: `007_authorization.sql`

```sql
-- Add organization limits to clients table
ALTER TABLE clients ADD COLUMN tech_seat_limit INTEGER DEFAULT NULL;
ALTER TABLE clients ADD COLUMN endpoint_limit INTEGER DEFAULT NULL;
ALTER TABLE clients ADD COLUMN is_organization BOOLEAN DEFAULT TRUE;

-- User-Organization relationship (replaces simple role on users table)
CREATE TABLE user_organizations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    role VARCHAR(50) NOT NULL DEFAULT 'org_user',  -- org_admin, org_user
    created_at TIMESTAMPTZ DEFAULT NOW(),
    created_by UUID REFERENCES users(id) ON DELETE SET NULL,
    UNIQUE(user_id, client_id)
);

-- System-level roles (dev_admin, support_tier future)
-- Keep role column on users table but repurpose:
-- 'dev_admin' = system admin
-- 'user' = regular user (must have org assignments to access anything)
-- NULL or 'user' = no system-level privileges, access via org membership

CREATE INDEX idx_user_orgs_user ON user_organizations(user_id);
CREATE INDEX idx_user_orgs_client ON user_organizations(client_id);
CREATE INDEX idx_user_orgs_role ON user_organizations(role);

-- Track current endpoint counts per org (for limit enforcement)
CREATE OR REPLACE VIEW organization_stats AS
SELECT
    c.id as client_id,
    c.name as client_name,
    c.tech_seat_limit,
    c.endpoint_limit,
    COALESCE((SELECT COUNT(*) FROM user_organizations WHERE client_id = c.id), 0) as tech_count,
    COALESCE((SELECT COUNT(*) FROM agents a JOIN sites s ON a.site_id = s.id WHERE s.client_id = c.id), 0) as endpoint_count
FROM clients c
WHERE c.is_organization = TRUE;
```

---

## Authorization Model

### Permission Checks

```rust
pub struct AuthUser {
    pub user_id: Uuid,
    pub system_role: String,           // dev_admin, support, user
    pub organizations: Vec<OrgAccess>, // Populated from JWT or DB lookup
}

pub struct OrgAccess {
    pub client_id: Uuid,
    pub role: String,  // org_admin, org_user
}

impl AuthUser {
    /// Dev admin has access to everything
    pub fn is_dev_admin(&self) -> bool {
        self.system_role == "dev_admin"
    }

    /// Can access a specific organization
    pub fn can_access_org(&self, client_id: Uuid) -> bool {
        self.is_dev_admin() ||
        self.organizations.iter().any(|o| o.client_id == client_id)
    }

    /// Is admin of a specific organization
    pub fn is_org_admin(&self, client_id: Uuid) -> bool {
        self.is_dev_admin() ||
        self.organizations.iter().any(|o|
            o.client_id == client_id && o.role == "org_admin"
        )
    }

    /// Get all client_ids this user can access
    pub fn accessible_client_ids(&self) -> Option<Vec<Uuid>> {
        if self.is_dev_admin() {
            None  // None = all clients
        } else {
            Some(self.organizations.iter().map(|o| o.client_id).collect())
        }
    }
}
```

### JWT Claims Enhancement

```rust
pub struct Claims {
    pub sub: Uuid,                      // user_id
    pub system_role: String,            // dev_admin, user
    pub orgs: Vec<OrgClaim>,            // Organization access
    pub exp: i64,
    pub iat: i64,
}

pub struct OrgClaim {
    pub id: Uuid,       // client_id
    pub role: String,   // org_admin, org_user
}
```

---

## API Changes

### Endpoints Requiring Scoped Access

| Endpoint | Current | New Behavior |
|----------|---------|--------------|
| `GET /api/clients` | Returns all | Dev: all, User: only their orgs |
| `GET /api/clients/:id` | No check | Must have access to org |
| `GET /api/sites` | Returns all | Filter by accessible orgs |
| `GET /api/agents` | Returns all | Filter by accessible orgs |
| `GET /api/alerts` | Returns all | Filter by accessible orgs |
| `GET /api/policies` | Returns all | Filter by accessible orgs |
| `POST /api/users` | Admin only | Dev: any org, OrgAdmin: their org (within limits) |

### New Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/organizations` | List orgs user can access |
| `GET` | `/api/organizations/:id/stats` | Get org limits and usage |
| `PUT` | `/api/organizations/:id/limits` | Dev only: set tech_seat_limit, endpoint_limit |
| `POST` | `/api/organizations/:id/users` | Add user to org (checks limits) |
| `DELETE` | `/api/organizations/:id/users/:user_id` | Remove user from org |
| `POST` | `/api/auth/impersonate/:user_id` | Dev only: get token as another user |

---

## Implementation Phases

### Phase 1: Database & Core Auth
**Files to create:**
- `server/migrations/007_authorization.sql` - Schema changes
- `server/src/authz/mod.rs` - Authorization module
- `server/src/authz/permissions.rs` - Permission checking

**Files to modify:**
- `server/src/auth/mod.rs` - Enhanced AuthUser, Claims
- `server/src/db/mod.rs` - Add user_organizations module

### Phase 2: User-Organization Management
**Files to create:**
- `server/src/db/user_organizations.rs` - CRUD for user-org relationships
- `server/src/api/organizations.rs` - Organization management endpoints

**Files to modify:**
- `server/src/api/mod.rs` - Add routes
- `server/src/api/users.rs` - Scope user creation to org limits

### Phase 3: Data Scoping
**Files to modify:**
- `server/src/api/clients.rs` - Filter by accessible orgs
- `server/src/api/sites.rs` - Filter by accessible orgs
- `server/src/api/agents.rs` - Filter by accessible orgs
- `server/src/api/alerts.rs` - Filter by accessible orgs
- `server/src/api/policies.rs` - Filter by accessible orgs
- `server/src/api/metrics.rs` - Filter by accessible orgs

### Phase 4: Dashboard UI Updates
**Files to modify:**
- `dashboard/src/api/client.ts` - Add org API types
- `dashboard/src/App.tsx` - Org context provider
- `dashboard/src/components/Layout.tsx` - Org selector (if multi-org)

**Files to create:**
- `dashboard/src/pages/OrganizationSettings.tsx` - Org management
- `dashboard/src/contexts/OrganizationContext.tsx` - Current org state

---

## Critical Implementation Details

### Login Flow Enhancement

```
1. User logs in (email/password or SSO)
2. Server fetches user's org memberships from user_organizations
3. JWT issued with system_role + orgs array
4. Client stores JWT, can switch between orgs in UI
```

### Seat Limit Enforcement

```rust
// When adding user to organization
pub async fn add_user_to_org(
    pool: &PgPool,
    user_id: Uuid,
    client_id: Uuid,
    role: &str,
    auth: &AuthUser,
) -> Result<(), Error> {
    // Check caller has permission
    if !auth.is_dev_admin() && !auth.is_org_admin(client_id) {
        return Err(Forbidden);
    }

    // Check seat limit (skip for dev_admin)
    if !auth.is_dev_admin() {
        let stats = get_org_stats(pool, client_id).await?;
        if let Some(limit) = stats.tech_seat_limit {
            if stats.tech_count >= limit {
                return Err(SeatLimitExceeded);
            }
        }
    }

    // Create membership
    insert_user_organization(pool, user_id, client_id, role).await
}
```

### Agent Registration with Endpoint Limits

```rust
// When agent registers under a site
pub async fn register_agent(...) -> Result<Agent, Error> {
    let site = get_site(pool, site_id).await?;
    let stats = get_org_stats(pool, site.client_id).await?;

    if let Some(limit) = stats.endpoint_limit {
        if stats.endpoint_count >= limit {
            return Err(EndpointLimitExceeded);
        }
    }

    // Create agent...
}
```

### Impersonation (Dev Admin Only)

```rust
// POST /api/auth/impersonate/:user_id
pub async fn impersonate(
    auth: AuthUser,
    Path(target_user_id): Path<Uuid>,
    State(state): State<AppState>,
) -> Result<Json<TokenResponse>, Error> {
    if !auth.is_dev_admin() {
        return Err(Forbidden);
    }

    // Get target user's org memberships
    let orgs = get_user_organizations(pool, target_user_id).await?;
    let user = get_user(pool, target_user_id).await?;

    // Issue token AS that user
    let token = create_jwt_with_orgs(
        target_user_id,
        &user.role,
        &orgs,
        &state.config.auth.jwt_secret,
    )?;

    Ok(Json(TokenResponse { token, ... }))
}
```

---

## Files Summary

| File | Action | Purpose |
|------|--------|---------|
| `server/migrations/007_authorization.sql` | Create | Schema for multi-tenant auth |
| `server/src/authz/mod.rs` | Create | Authorization module |
| `server/src/authz/permissions.rs` | Create | Permission helpers |
| `server/src/db/user_organizations.rs` | Create | User-org CRUD |
| `server/src/api/organizations.rs` | Create | Org management API |
| `server/src/auth/mod.rs` | Modify | Enhanced AuthUser, Claims |
| `server/src/api/clients.rs` | Modify | Scope to accessible orgs |
| `server/src/api/sites.rs` | Modify | Scope to accessible orgs |
| `server/src/api/agents.rs` | Modify | Scope + endpoint limits |
| `server/src/api/alerts.rs` | Modify | Scope to accessible orgs |
| `server/src/api/policies.rs` | Modify | Scope to accessible orgs |
| `server/src/api/users.rs` | Modify | Org-scoped user creation |
| `dashboard/src/api/client.ts` | Modify | Add org types |

---

## Migration Path for Existing Data

1. Run migration to add columns and tables
2. All existing users default to `role='user'` (no system privileges)
3. Manually grant `dev_admin` to designated user:
   ```sql
   UPDATE users SET role = 'dev_admin' WHERE email = 'admin@azcomputerguru.com';
   ```
4. Existing clients automatically become organizations
5. Dev admin can then assign users to organizations as needed


User has approved your plan. You can now start coding. Start with updating your todo list if applicable

Your plan has been saved to: C:\Users\MikeSwanson\.claude\plans\tranquil-bouncing-twilight.md
You can refer back to it if needed during implementation.

## Approved Plan:
# Plan: Multi-Tenant Authorization System for GuruRMM

## Overview

Implement a multi-tenant authorization system with hierarchical roles supporting MSP-style organization management:
- **Dev Admin**: Full system access, can impersonate any account
- **Organization Admin**: MSP/Helpdesk paying customers with seat-based licensing
- **Organization User**: Technicians within an organization

## Requirements

### Role Hierarchy (For Now)
| Role | Access | Capabilities |
|------|--------|--------------|
| `dev_admin` | All organizations | Full access, set org limits, impersonate users |
| `org_admin` | Assigned organizations | Manage sites/agents, create users within limits |
| `org_user` | Assigned organizations | View/manage within their organization |

### Future Roles (Not Implementing Now)
- `support_tier` - Between Dev Admin and Org Admin

### Licensing Model
- **Tech Seats**: Number of users an organization can have
- **Endpoint Limit**: Number of agents an organization can have
- **Sites**: Unlimited for now
- Limits set by Dev Admin, enforced by system

### Multi-Organization Support
- Users can belong to multiple organizations
- Each user-organization assignment has a role within that org
- Dev Admin is NOT tied to any organization (system-level)

---

## Database Schema

### Migration: `007_authorization.sql`

```sql
-- Add organization limits to clients table
ALTER TABLE clients ADD COLUMN tech_seat_limit INTEGER DEFAULT NULL;
ALTER TABLE clients ADD COLUMN endpoint_limit INTEGER DEFAULT NULL;
ALTER TABLE clients ADD COLUMN is_organization BOOLEAN DEFAULT TRUE;

-- User-Organization relationship (replaces simple role on users table)
CREATE TABLE user_organizations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    client_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
    role VARCHAR(50) NOT NULL DEFAULT 'org_user',  -- org_admin, org_user
    created_at TIMESTAMPTZ DEFAULT NOW(),
    created_by UUID REFERENCES users(id) ON DELETE SET NULL,
    UNIQUE(user_id, client_id)
);

-- System-level roles (dev_admin, support_tier future)
-- Keep role column on users table but repurpose:
-- 'dev_admin' = system admin
-- 'user' = regular user (must have org assignments to access anything)
-- NULL or 'user' = no system-level privileges, access via org membership

CREATE INDEX idx_user_orgs_user ON user_organizations(user_id);
CREATE INDEX idx_user_orgs_client ON user_organizations(client_id);
CREATE INDEX idx_user_orgs_role ON user_organizations(role);

-- Track current endpoint counts per org (for limit enforcement)
CREATE OR REPLACE VIEW organization_stats AS
SELECT
    c.id as client_id,
    c.name as client_name,
    c.tech_seat_limit,
    c.endpoint_limit,
    COALESCE((SELECT COUNT(*) FROM user_organizations WHERE client_id = c.id), 0) as tech_count,
    COALESCE((SELECT COUNT(*) FROM agents a JOIN sites s ON a.site_id = s.id WHERE s.client_id = c.id), 0) as endpoint_count
FROM clients c
WHERE c.is_organization = TRUE;
```

---

## Authorization Model

### Permission Checks

```rust
pub struct AuthUser {
    pub user_id: Uuid,
    pub system_role: String,           // dev_admin, support, user
    pub organizations: Vec<OrgAccess>, // Populated from JWT or DB lookup
}

pub struct OrgAccess {
    pub client_id: Uuid,
    pub role: String,  // org_admin, org_user
}

impl AuthUser {
    /// Dev admin has access to everything
    pub fn is_dev_admin(&self) -> bool {
        self.system_role == "dev_admin"
    }

    /// Can access a specific organization
    pub fn can_access_org(&self, client_id: Uuid) -> bool {
        self.is_dev_admin() ||
        self.organizations.iter().any(|o| o.client_id == client_id)
    }

    /// Is admin of a specific organization
    pub fn is_org_admin(&self, client_id: Uuid) -> bool {
        self.is_dev_admin() ||
        self.organizations.iter().any(|o|
            o.client_id == client_id && o.role == "org_admin"
        )
    }

    /// Get all client_ids this user can access
    pub fn accessible_client_ids(&self) -> Option<Vec<Uuid>> {
        if self.is_dev_admin() {
            None  // None = all clients
        } else {
            Some(self.organizations.iter().map(|o| o.client_id).collect())
        }
    }
}
```

### JWT Claims Enhancement

```rust
pub struct Claims {
    pub sub: Uuid,                      // user_id
    pub system_role: String,            // dev_admin, user
    pub orgs: Vec<OrgClaim>,            // Organization access
    pub exp: i64,
    pub iat: i64,
}

pub struct OrgClaim {
    pub id: Uuid,       // client_id
    pub role: String,   // org_admin, org_user
}
```

---

## API Changes

### Endpoints Requiring Scoped Access

| Endpoint | Current | New Behavior |
|----------|---------|--------------|
| `GET /api/clients` | Returns all | Dev: all, User: only their orgs |
| `GET /api/clients/:id` | No check | Must have access to org |
| `GET /api/sites` | Returns all | Filter by accessible orgs |
| `GET /api/agents` | Returns all | Filter by accessible orgs |
| `GET /api/alerts` | Returns all | Filter by accessible orgs |
| `GET /api/policies` | Returns all | Filter by accessible orgs |
| `POST /api/users` | Admin only | Dev: any org, OrgAdmin: their org (within limits) |

### New Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/organizations` | List orgs user can access |
| `GET` | `/api/organizations/:id/stats` | Get org limits and usage |
| `PUT` | `/api/organizations/:id/limits` | Dev only: set tech_seat_limit, endpoint_limit |
| `POST` | `/api/organizations/:id/users` | Add user to org (checks limits) |
| `DELETE` | `/api/organizations/:id/users/:user_id` | Remove user from org |
| `POST` | `/api/auth/impersonate/:user_id` | Dev only: get token as another user |

---

## Implementation Phases

### Phase 1: Database & Core Auth
**Files to create:**
- `server/migrations/007_authorization.sql` - Schema changes
- `server/src/authz/mod.rs` - Authorization module
- `server/src/authz/permissions.rs` - Permission checking

**Files to modify:**
- `server/src/auth/mod.rs` - Enhanced AuthUser, Claims
- `server/src/db/mod.rs` - Add user_organizations module

### Phase 2: User-Organization Management
**Files to create:**
- `server/src/db/user_organizations.rs` - CRUD for user-org relationships
- `server/src/api/organizations.rs` - Organization management endpoints

**Files to modify:**
- `server/src/api/mod.rs` - Add routes
- `server/src/api/users.rs` - Scope user creation to org limits

### Phase 3: Data Scoping
**Files to modify:**
- `server/src/api/clients.rs` - Filter by accessible orgs
- `server/src/api/sites.rs` - Filter by accessible orgs
- `server/src/api/agents.rs` - Filter by accessible orgs
- `server/src/api/alerts.rs` - Filter by accessible orgs
- `server/src/api/policies.rs` - Filter by accessible orgs
- `server/src/api/metrics.rs` - Filter by accessible orgs

### Phase 4: Dashboard UI Updates
**Files to modify:**
- `dashboard/src/api/client.ts` - Add org API types
- `dashboard/src/App.tsx` - Org context provider
- `dashboard/src/components/Layout.tsx` - Org selector (if multi-org)

**Files to create:**
- `dashboard/src/pages/OrganizationSettings.tsx` - Org management
- `dashboard/src/contexts/OrganizationContext.tsx` - Current org state

---

## Critical Implementation Details

### Login Flow Enhancement

```
1. User logs in (email/password or SSO)
2. Server fetches user's org memberships from user_organizations
3. JWT issued with system_role + orgs array
4. Client stores JWT, can switch between orgs in UI
```

### Seat Limit Enforcement

```rust
// When adding user to organization
pub async fn add_user_to_org(
    pool: &PgPool,
    user_id: Uuid,
    client_id: Uuid,
    role: &str,
    auth: &AuthUser,
) -> Result<(), Error> {
    // Check caller has permission
    if !auth.is_dev_admin() && !auth.is_org_admin(client_id) {
        return Err(Forbidden);
    }

    // Check seat limit (skip for dev_admin)
    if !auth.is_dev_admin() {
        let stats = get_org_stats(pool, client_id).await?;
        if let Some(limit) = stats.tech_seat_limit {
            if stats.tech_count >= limit {
                return Err(SeatLimitExceeded);
            }
        }
    }

    // Create membership
    insert_user_organization(pool, user_id, client_id, role).await
}
```

### Agent Registration with Endpoint Limits

```rust
// When agent registers under a site
pub async fn register_agent(...) -> Result<Agent, Error> {
    let site = get_site(pool, site_id).await?;
    let stats = get_org_stats(pool, site.client_id).await?;

    if let Some(limit) = stats.endpoint_limit {
        if stats.endpoint_count >= limit {
            return Err(EndpointLimitExceeded);
        }
    }

    // Create agent...
}
```

### Impersonation (Dev Admin Only)

```rust
// POST /api/auth/impersonate/:user_id
pub async fn impersonate(
    auth: AuthUser,
    Path(target_user_id): Path<Uuid>,
    State(state): State<AppState>,
) -> Result<Json<TokenResponse>, Error> {
    if !auth.is_dev_admin() {
        return Err(Forbidden);
    }

    // Get target user's org memberships
    let orgs = get_user_organizations(pool, target_user_id).await?;
    let user = get_user(pool, target_user_id).await?;

    // Issue token AS that user
    let token = create_jwt_with_orgs(
        target_user_id,
        &user.role,
        &orgs,
        &state.config.auth.jwt_secret,
    )?;

    Ok(Json(TokenResponse { token, ... }))
}
```

---

## Files Summary

| File | Action | Purpose |
|------|--------|---------|
| `server/migrations/007_authorization.sql` | Create | Schema for multi-tenant auth |
| `server/src/authz/mod.rs` | Create | Authorization module |
| `server/src/authz/permissions.rs` | Create | Permission helpers |
| `server/src/db/user_organizations.rs` | Create | User-org CRUD |
| `server/src/api/organizations.rs` | Create | Org management API |
| `server/src/auth/mod.rs` | Modify | Enhanced AuthUser, Claims |
| `server/src/api/clients.rs` | Modify | Scope to accessible orgs |
| `server/src/api/sites.rs` | Modify | Scope to accessible orgs |
| `server/src/api/agents.rs` | Modify | Scope + endpoint limits |
| `server/src/api/alerts.rs` | Modify | Scope to accessible orgs |
| `server/src/api/policies.rs` | Modify | Scope to accessible orgs |
| `server/src/api/users.rs` | Modify | Org-scoped user creation |
| `dashboard/src/api/client.ts` | Modify | Add org types |

---

## Migration Path for Existing Data

1. Run migration to add columns and tables
2. All existing users default to `role='user'` (no system privileges)
3. Manually grant `dev_admin` to designated user:
   ```sql
   UPDATE users SET role = 'dev_admin' WHERE email = 'admin@azcomputerguru.com';
   ```
4. Existing clients automatically become organizations
5. Dev admin can then assign users to organizations as needed


The user sent "/s" which is the save session log command. Let me invoke the skill to save the session log.


Now let me save the session log as you requested:


Launching skill: s


Save a COMPREHENSIVE session log to session-logs/ directory. This is critical for context recovery.

## Filename
Use format `session-logs/YYYY-MM-DD-session.md` (today's date)

## If file exists
Append a new section with timestamp header (## Update: HH:MM), don't overwrite

## MANDATORY Content to Include

### 1. Session Summary
- What was accomplished in this session
- Key decisions made and rationale
- Problems encountered and how they were solved

### 2. ALL Credentials & Secrets (UNREDACTED)
**CRITICAL: Store credentials completely - these are needed for future sessions**
- API keys and tokens (full values)
- Usernames and passwords
- Database credentials
- JWT secrets
- SSH keys/passphrases if relevant
- Any authentication information used or discovered

Format credentials as:
```
### Credentials
- Service Name: username / password
- API Token: full_token_value
```

### 3. Infrastructure & Servers
- All IPs, hostnames, ports used
- Container names and configurations
- DNS records added or modified
- SSL certificates created
- Any network/firewall changes

### 4. Commands & Outputs
- Important commands run (especially complex ones)
- Key outputs and results
- Error messages and their resolutions

### 5. Configuration Changes
- Files created or modified (with paths)
- Settings changed
- Environment variables set

### 6. Pending/Incomplete Tasks
- What still needs to be done
- Blockers or issues awaiting resolution
- Next steps for future sessions

### 7. Reference Information
- URLs, endpoints, ports
- File paths that may be needed again
- Any technical details that might be forgotten

## After Saving
1. Commit with message: "Session log: [brief description of work done]"
2. Push to gitea remote
3. Confirm push was successful

## Purpose
This log MUST contain enough detail to fully restore context if this conversation is summarized or a new session starts. When in doubt, include MORE information rather than less. Future Claude instances will search these logs to find credentials and context.


I need to save a comprehensive session log. Let me first read the existing session log to see what's there, then append a new update section with everything accomplished in this session.