Skip to content

Commit 8cb4030

Browse files
authored
Add granular role-based permissions and super admin support (#41)
* Migrate from legacy role-based to granular permission-based access control - Replace legacy Role type (admin/manager/agent) with permission-based system - Add Permission, CustomRole, and RolePermission models for granular RBAC - Update User model to use RoleID referencing CustomRole instead of Role string - Add TeamRole type for team membership roles (manager/agent within teams) - Implement HasPermission cache with Redis support for efficient permission checks - Update all handlers to use permission-based checks (HasPermission) - Add roles.go handler for CustomRole CRUD operations - Seed system roles (admin, manager, agent) per organization with default permissions - Update JWT claims to include RoleID instead of legacy Role - Remove route-level role checking in main.go (now handled by handlers) - Update SSOProvider to use DefaultRoleName string for auto-created users - Update all tests to work with new permission system * Add super admin support for cross-organization access - Add IsSuperAdmin field to User model for platform-wide admin access - Include IsSuperAdmin in JWT claims and middleware context - Update HasPermission to automatically grant all permissions to super admins - Add ScopeToOrg helper to conditionally bypass org filtering for super admins - Update key handlers (contacts, users, roles, teams) to use org scoping helper - Make default admin user a super admin during initial setup * Add roles and permissions management UI - Create RolesView with full CRUD for custom roles - Add reusable PermissionMatrix component for permission selection - Add roles and permissions API services - Create roles Pinia store with permission grouping - Update UsersView to use role selection from roles store - Ensure permissions are seeded in migrations for existing installations * Fix lint issues and add proper ESLint configuration Backend (Go): - Fix file.Close() error not checked in campaigns.go and templates.go - Fix capitalized error strings in flows.go - Convert if/else to switch statement in flows.go - Simplify embedded field access in cache.go - Export GetRolePermissionsCached for reuse Frontend: - Add .eslintrc.cjs with proper Vue 3 + TypeScript config - Add .gitignore for frontend directory - Fix duplicate keyframes in tailwind.config.cjs (radix -> reka) - Fix side effects in computed function in ApiMockDialog.vue * Add tests for roles and permissions Backend (Go): - Add comprehensive unit tests for roles CRUD operations - Add tests for permission listing and role constraints - Update test database utilities to include roles tables Frontend (E2E): - Add Playwright E2E tests for roles management - Test role creation, editing, deletion - Test permission selection in role dialog - Test system role restrictions - Simplify test scripts (remove Vitest, use E2E only) * Fix roles tests and delete role handler bug - Fix testApp to include Redis connection for permission caching - Fix auth_test to create actual role before assigning to user - Fix roles_test to use existing permissions instead of creating duplicates - Fix DeleteRole handler to check correct column (role_id instead of custom_role_id) * Fix role object handling and E2E test issues Backend: - Preload Role in Login handler so role object is returned - Populate user.Role in Register response - Fix backend tests to create proper roles for test users Frontend: - Update User interface to use role object instead of string - Fix all role checks to use role?.name instead of role - Update auth store userRole computed to return role.name - Fix websocket.ts role comparisons E2E Tests: - Update playwright config to use Vite dev server port 3000 - Fix roles.spec.ts locators to be more specific - Fix LoginPage to wait for network idle after login - Update global-setup to include organization_name * Add test:e2e script for CI workflow * Fix CI test race condition with sequential package execution Add -p 1 flag to go test to run packages sequentially. This prevents race conditions where multiple test packages share the same test database and one package's cleanup truncates data while another package is inserting. * Enable and fix E2E global-setup to properly create test users - Enable globalSetup in playwright.config.ts (was commented out) - Update global-setup to properly create test users with correct roles: 1. Register admin@test.com (creates org with admin role) 2. Use admin token to fetch role IDs (manager, agent) 3. Create manager@test.com and agent@test.com via users API with their respective roles in the same organization * Use default superadmin to create test users in global-setup Login as the default superadmin (admin@admin.com) created by migrations to create test users. This ensures proper permissions for user creation. * Add super admin support with organization management Backend: - Add is_super_admin field to UserRequest/UserResponse - Allow super admins to set/modify super admin status on users - Add ListOrganizations endpoint (super admin only) - Add GetCurrentOrganization endpoint - ScopeToOrg already bypasses org filtering for super admins Frontend: - Add is_super_admin to User interfaces in auth and users stores - Add super admin toggle in user form (visible only to super admins) - Add organizations API service - Show super admin badge in users table * Add organization switcher for super admins Backend: - Update getOrgIDFromContext to allow super admins to override org via X-Organization-ID header - Add X-Organization-ID to CORS allowed headers Frontend: - Add organizations store to manage org list and selection - Add OrganizationSwitcher component in sidebar (visible only to super admins) - Update API interceptor to include X-Organization-ID header when org is selected - Integrate organization switcher into AppLayout Super admins can now: - View all organizations in dropdown - Switch between organizations to manage their data - Select "All Organizations" to see data across all orgs * Fix organization switcher visibility and add loading states * Add error display and console logging for organizations API debugging * Fix organization filtering for super admins - getOrgIDFromContext now returns uuid.Nil when super admin views "all orgs" - ScopeToOrg/ScopedQuery only bypass filtering when orgID is uuid.Nil - When super admin selects a specific org, data is filtered to that org - Add getOrgIDForCreate helper for create operations (always returns valid org) * Remove 'All Organizations' option - always require specific org selection * Remove unused getOrgIDForCreate function * Fix: Remove unused function and add Redis nil checks for tests * Fix users search test to use specific email to avoid multiple matches * Implement granular permissions system with real-time updates Backend: - Add permission checks to chatbot flow handlers - Cache role permissions in Redis with automatic invalidation - Broadcast permission updates via WebSocket when roles change - Add user-targeted WebSocket broadcasts for permission notifications - Load permissions from cache in login and /me endpoints Frontend: - Replace legacy role-based routing with permission-based access - Add navigation filtering based on user permissions - Auto-redirect to first accessible page on login - Show parent menus if user has any child permission - Refresh user permissions via WebSocket without page reload - Improve PermissionMatrix UX: collapse by default, auto-expand selected, sort selected groups to top - Remove debug console.log statements Router changes: - All routes now use permission meta instead of roles - getFirstAccessibleRoute() finds best landing page for user * Fix role_permissions table empty for existing setups Add SeedSystemRolesForAllOrgs() call in migration flow to fix existing organizations where system roles exist but have no permissions linked. This is idempotent - skips roles that already have permissions. * Add migration for existing users' roles and super admin - MigrateExistingUserRoles: Maps old role column (admin/manager/agent) to new role_id for existing users. Safe for fresh installs - checks if old column exists before proceeding. - Set admin@admin.com as super admin if exists - All functions are idempotent - safe to run multiple times * Remove "all organizations" view from backend - getOrgIDFromContext: Falls back to user's org instead of returning uuid.Nil when super admin has no X-Organization-ID header - ScopedQuery/ScopeToOrg: Always filter by organization, never return unfiltered queries This aligns backend with frontend which removed the "All Organizations" option from the organization switcher. * Add E2E tests for custom role permissions - Add ApiHelper class for reusable API operations in tests - Add permission-based UI tests that: - Create custom roles with specific permissions - Create users with those roles - Verify sidebar menu visibility based on permissions - Verify page access/redirect based on permissions - Compare admin vs limited role access - Add role fixtures to helpers 10 new tests covering permission-based access control. * Add test-results to gitignore * Refetch data on organization change in settings views Add watcher for organizationsStore.selectedOrgId in: - UsersView - RolesView - TeamsView - TemplatesView - AccountsView - WebhooksView This ensures data is refreshed when super admin switches organizations. * Fix getOrganizationID to check X-Organization-ID header The getOrganizationID function now checks for the X-Organization-ID header override, allowing super admins to switch organizations and have all API endpoints respect the selected org. This fixes the issue where user list wasn't updating when switching organizations because the function only read from JWT context. * Restrict X-Organization-ID header to super admins only Security fix: Only super admins can use the X-Organization-ID header to switch organizations. Regular users and API key users are locked to their own organization. This prevents API key users from accessing other organizations by sending the header. * Add E2E tests for organization switching - Test super admin can see organization switcher - Test switching organization updates users list - Test regular user cannot see organization switcher - Test API respects X-Organization-ID header for super admin only - Test organization data isolation between orgs * Improve organization switch E2E tests with data isolation - Add register() helper to create new organizations - Add getOrganizations() and getUsersWithOrgHeader() helpers - Create test organizations dynamically instead of requiring pre-existing ones - Test users from one org are not visible in another org - Test regular user cannot use X-Organization-ID header to access other orgs * Trigger CI for organization-switch tests * Add documentation for granular roles and permissions - Update README with new permissions feature description - Fix configuration docs link to use hosted URL - Add roles-permissions.mdx feature guide covering: - System roles (admin, manager, agent) - Custom roles creation - Permission matrix - Super admin organization switching - UI behavior based on permissions - Add roles.mdx API reference with all endpoints - Update users.mdx to reflect role_id based system - Add new docs to sidebar navigation
1 parent 2f8c651 commit 8cb4030

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+7801
-975
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ jobs:
5656
TEST_REDIS_URL: redis://localhost:6379
5757
run: |
5858
# Run tests with coverage, excluding test utility packages from coverage calculation
59-
go test -v -race -coverprofile=coverage.out $(go list ./... | grep -v /test/)
59+
# -p 1 runs packages sequentially to avoid database conflicts (all packages share the test DB)
60+
go test -v -race -p 1 -coverprofile=coverage.out $(go list ./... | grep -v /test/)
6061
6162
- name: Display coverage
6263
run: go tool cover -func=coverage.out | grep "total:"

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,6 @@ logs/
5353
tmp/
5454
temp/
5555
uploads/
56+
57+
# Test results
58+
test-results/

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ Modern, open-source WhatsApp Business Platform. Single binary app.
1111
- **Multi-tenant Architecture**
1212
Support multiple organizations with isolated data and configurations.
1313

14-
- **Role-Based Access Control**
15-
Three roles (Admin, Manager, Agent) with granular permissions.
14+
- **Granular Roles & Permissions**
15+
Customizable roles with fine-grained permissions. Create custom roles, assign specific permissions per resource (users, contacts, templates, etc.), and control access at the action level (read, create, update, delete). Super admins can manage multiple organizations.
1616

1717
- **WhatsApp Cloud API Integration**
1818
Connect with Meta's WhatsApp Business API for messaging.
@@ -94,7 +94,7 @@ make build-prod
9494
./whatomate server -migrate
9595
```
9696

97-
See [configuration docs](docs/src/content/docs/getting-started/configuration.mdx) for detailed setup options.
97+
See [configuration docs](https://shridarpatil.github.io/whatomate/getting-started/configuration/) for detailed setup options.
9898

9999
## CLI Usage
100100

cmd/whatomate/main.go

Lines changed: 15 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"github.com/shridarpatil/whatomate/internal/frontend"
1515
"github.com/shridarpatil/whatomate/internal/handlers"
1616
"github.com/shridarpatil/whatomate/internal/middleware"
17-
"github.com/shridarpatil/whatomate/internal/models"
1817
"github.com/shridarpatil/whatomate/internal/queue"
1918
"github.com/shridarpatil/whatomate/internal/websocket"
2019
"github.com/shridarpatil/whatomate/internal/worker"
@@ -432,68 +431,8 @@ func setupRoutes(g *fastglue.Fastglue, app *handlers.App, lo logf.Logger, basePa
432431
return r
433432
}
434433

435-
path := string(r.RequestCtx.Path())
436-
437-
// Only apply to authenticated API routes
438-
if len(path) < 4 || path[:4] != "/api" {
439-
return r
440-
}
441-
442-
// Get role from context (set by auth middleware)
443-
role, ok := r.RequestCtx.UserValue("role").(models.Role)
444-
if !ok {
445-
return r // Auth middleware will handle unauthenticated requests
446-
}
447-
448-
// Admin-only routes: user management, API keys, and SSO settings
449-
if (len(path) >= 10 && path[:10] == "/api/users") ||
450-
(len(path) >= 13 && path[:13] == "/api/api-keys") ||
451-
(len(path) >= 17 && path[:17] == "/api/settings/sso") {
452-
if role != models.RoleAdmin {
453-
r.RequestCtx.SetStatusCode(403)
454-
r.RequestCtx.SetBodyString(`{"status":"error","message":"Admin access required"}`)
455-
return nil
456-
}
457-
}
458-
459-
// Manager+ routes: agents cannot access these
460-
if role == models.RoleAgent {
461-
// Agent-accessible exceptions under restricted prefixes
462-
agentAllowedPaths := []string{
463-
"/api/chatbot/transfers",
464-
"/api/analytics/agents",
465-
}
466-
467-
isAllowed := false
468-
for _, allowed := range agentAllowedPaths {
469-
if len(path) >= len(allowed) && path[:len(allowed)] == allowed {
470-
isAllowed = true
471-
break
472-
}
473-
}
474-
475-
if !isAllowed {
476-
managerRoutes := []string{
477-
"/api/accounts",
478-
"/api/templates",
479-
"/api/flows",
480-
"/api/campaigns",
481-
"/api/chatbot",
482-
"/api/analytics",
483-
}
484-
for _, prefix := range managerRoutes {
485-
if len(path) >= len(prefix) && path[:len(prefix)] == prefix {
486-
r.RequestCtx.SetStatusCode(403)
487-
r.RequestCtx.SetBodyString(`{"status":"error","message":"Access denied"}`)
488-
return nil
489-
}
490-
}
491-
}
492-
493-
// Agents can only create contacts, not modify/delete
494-
// PUT and DELETE for contacts are allowed if it's their assigned contact (checked in handler)
495-
}
496-
434+
// Route-level permission checks are now handled at the handler level
435+
// using the granular permission system (HasPermission checks)
497436
return r
498437
})
499438

@@ -510,6 +449,14 @@ func setupRoutes(g *fastglue.Fastglue, app *handlers.App, lo logf.Logger, basePa
510449
g.PUT("/api/users/{id}", app.UpdateUser)
511450
g.DELETE("/api/users/{id}", app.DeleteUser)
512451

452+
// Roles & Permissions (admin only - enforced by middleware)
453+
g.GET("/api/roles", app.ListRoles)
454+
g.POST("/api/roles", app.CreateRole)
455+
g.GET("/api/roles/{id}", app.GetRole)
456+
g.PUT("/api/roles/{id}", app.UpdateRole)
457+
g.DELETE("/api/roles/{id}", app.DeleteRole)
458+
g.GET("/api/permissions", app.ListPermissions)
459+
513460
// API Keys (admin only - enforced by middleware)
514461
g.GET("/api/api-keys", app.ListAPIKeys)
515462
g.POST("/api/api-keys", app.CreateAPIKey)
@@ -649,6 +596,10 @@ func setupRoutes(g *fastglue.Fastglue, app *handlers.App, lo logf.Logger, basePa
649596
g.GET("/api/org/settings", app.GetOrganizationSettings)
650597
g.PUT("/api/org/settings", app.UpdateOrganizationSettings)
651598

599+
// Organizations (super admin only)
600+
g.GET("/api/organizations", app.ListOrganizations)
601+
g.GET("/api/organizations/current", app.GetCurrentOrganization)
602+
652603
// SSO Settings (admin only - enforced by middleware)
653604
g.GET("/api/settings/sso", app.GetSSOSettings)
654605
g.PUT("/api/settings/sso/{provider}", app.UpdateSSOProvider)
@@ -714,7 +665,7 @@ func corsWrapper(next fasthttp.RequestHandler) fasthttp.RequestHandler {
714665

715666
ctx.Response.Header.Set("Access-Control-Allow-Origin", origin)
716667
ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
717-
ctx.Response.Header.Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-Requested-With")
668+
ctx.Response.Header.Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key, X-Requested-With, X-Organization-ID")
718669
ctx.Response.Header.Set("Access-Control-Allow-Credentials", "true")
719670
ctx.Response.Header.Set("Access-Control-Max-Age", "86400")
720671

docs/astro.config.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export default defineConfig({
2424
label: 'Features',
2525
items: [
2626
{ label: 'Dashboard', slug: 'features/dashboard' },
27+
{ label: 'Roles & Permissions', slug: 'features/roles-permissions' },
2728
{ label: 'Chatbot Automation', slug: 'features/chatbot' },
2829
{ label: 'Canned Responses', slug: 'features/canned-responses' },
2930
{ label: 'Templates', slug: 'features/templates' },
@@ -38,6 +39,7 @@ export default defineConfig({
3839
{ label: 'Authentication', slug: 'api-reference/authentication' },
3940
{ label: 'API Keys', slug: 'api-reference/api-keys' },
4041
{ label: 'Users', slug: 'api-reference/users' },
42+
{ label: 'Roles', slug: 'api-reference/roles' },
4143
{ label: 'Accounts', slug: 'api-reference/accounts' },
4244
{ label: 'Contacts', slug: 'api-reference/contacts' },
4345
{ label: 'Messages', slug: 'api-reference/messages' },

0 commit comments

Comments
 (0)