Admin Dashboard with OIDC Group-Based RBAC
Date: 2026-01-30
Status: Implemented
Type: Feature Addition
Summary​
Added a comprehensive admin dashboard with role-based access control (RBAC) using dual authorization methods:
- OIDC Group Membership: Users in
OIDC_REQUIRED_ADMIN_GROUPare automatically admins - MongoDB Profile: Users with
metadata.role = 'admin'in their MongoDB user document
Admin users can access platform metrics, user management, and usage statistics. Admins can also promote/demote other users via the dashboard.
Context​
The CAIPE platform needed administrative capabilities to:
- Monitor platform usage (DAU, MAU, conversation counts)
- Manage users and view activity
- Track system health and metrics
- Provide insights for capacity planning
Previously, there was no way to distinguish admin users from regular users, and no centralized dashboard for platform management.
Decision​
Implemented dual-method RBAC where admin access is granted via:
Method 1: OIDC Group (Primary)​
- Users belonging to
OIDC_REQUIRED_ADMIN_GROUPare automatically assignedrole: 'admin' - Checked during authentication and stored in session
- No database required
Method 2: MongoDB Profile (Fallback)​
- Users with
metadata.role = 'admin'in MongoDB are admins - Checked if user is NOT admin via OIDC
- Allows admins to promote other users without OIDC group changes
- Persists across sessions
Authorization Flow​
- Admin role determined during authentication (OIDC group first, then MongoDB)
- Role stored in session for client-side rendering
- Admin-only API routes check
session.rolebefore granting access - Admin tab conditionally rendered in UI based on role
Implementation​
1. Environment Variables​
Added new environment variable in ui/env.example:
# Admin group for elevated privileges
# Users in this group will have admin role and access to admin dashboard
# Leave empty to disable admin group checking (all users will be 'user' role)
OIDC_REQUIRED_ADMIN_GROUP=backstage-admins
2. Authentication Changes​
Updated ui/src/lib/auth-config.ts:
- Added
REQUIRED_ADMIN_GROUPconstant - Added
isAdminUser()helper function to check group membership - Set
token.roleduring JWT callback based on admin group - Added
roleto session and JWT types - Pass role to client via session
// Check if user is in admin group
export function isAdminUser(groups: string[]): boolean {
if (!REQUIRED_ADMIN_GROUP) return false;
return groups.some((group) => {
const groupLower = group.toLowerCase();
const adminGroupLower = REQUIRED_ADMIN_GROUP.toLowerCase();
return groupLower === adminGroupLower || groupLower.includes(`cn=${adminGroupLower}`);
});
}
3. API Middleware​
Updated ui/src/lib/api-middleware.ts:
- Modified
getAuthenticatedUser()to return both user and session - Added MongoDB fallback check: If user is not admin via OIDC, check
user.metadata.rolein MongoDB - Updated
withAuth()to pass session to handler - Set
user.rolefromsession.role(OIDC) or MongoDB profile
// Fallback: Check MongoDB user profile if not admin via OIDC
if (role !== 'admin') {
try {
const users = await getCollection<User>('users');
const dbUser = await users.findOne({ email: session.user.email });
if (dbUser?.metadata?.role === 'admin') {
role = 'admin';
console.log(`[Auth] User ${session.user.email} is admin via MongoDB profile`);
}
} catch (error) {
// MongoDB not available - continue with OIDC role
}
}
4. Admin API Routes​
Created two new API routes:
GET /api/admin/stats - Platform metrics
- Total users, conversations, messages
- DAU (Daily Active Users)
- MAU (Monthly Active Users)
- Daily activity for last 30 days
- Top users by conversations and messages
- Shared conversation stats
GET /api/admin/users - User management
- List all users with stats
- Conversation and message counts per user
- Last activity timestamps
- Role information
PATCH /api/admin/users/[email]/role - Update user role
- Promote user to admin:
{ "role": "admin" } - Demote user to regular:
{ "role": "user" } - Prevents self-demotion
- Only accessible by admin users
- Updates
metadata.rolein MongoDB user document
All admin routes:
- Check
session.role === 'admin'before granting access - Return 403 if user is not admin
- Require MongoDB (return 503 if not configured)
5. Admin Dashboard UI​
Created ui/src/app/(app)/admin/page.tsx:
Overview Cards:
- Total Users (with DAU/MAU breakdown)
- Total Conversations (today's count)
- Total Messages (today's count)
- Shared Conversations (percentage)
Daily Activity Chart:
- Last 7 days of activity
- Visual progress bars for users and conversations
Top Users:
- By conversations created
- By messages sent
User Management Table:
- Email, name, role
- Last activity date
- Usage stats (conversations & messages)
- Role management buttons:
- "Make Admin" for regular users
- "Remove Admin" for admin users
- Confirmation before role change
- Inline role updates via API
6. UI Integration​
Updated ui/src/components/layout/AppHeader.tsx:
- Added Shield icon for Admin tab
- Admin tab only visible if
session?.role === 'admin' - Red badge styling to distinguish admin area
Updated ui/src/components/layout/Sidebar.tsx:
- Added support for
activeTab="admin" - Shows admin quick links in sidebar when on admin page
Access Control Flow​
User logs in via OIDC
↓
Extract groups from OIDC profile
↓
Check if user in OIDC_REQUIRED_ADMIN_GROUP
↓ NO
Check MongoDB user.metadata.role
↓
Set session.role = 'admin' or 'user'
↓
Admin tab visible only if role === 'admin'
↓
Admin API routes check session.role (with MongoDB fallback)
↓
Return 403 if not admin
Admin Role Priority (Checked in Order)​
- OIDC Group (highest priority) -
OIDC_REQUIRED_ADMIN_GROUP - MongoDB Profile (fallback) -
user.metadata.role === 'admin' - Default -
'user'role
This dual-check ensures:
- OIDC-managed admins work immediately
- Manually promoted admins (via MongoDB) also have access
- Graceful fallback if MongoDB is unavailable
Example Configuration​
.env
# Required for all users to access platform
OIDC_REQUIRED_GROUP=backstage-access
# Required for admin dashboard access
OIDC_REQUIRED_ADMIN_GROUP=backstage-admins
# Group claim name (auto-detects if not set)
OIDC_GROUP_CLAIM=memberOf
Example LDAP-style Groups:
CN=backstage-access,OU=Groups,DC=example,DC=com → user role
CN=backstage-admins,OU=Groups,DC=example,DC=com → admin role
The code handles both simple group names (backstage-admins) and full Distinguished Names (DNs).
Security Considerations​
- Dual Authorization Methods:
- OIDC Group (recommended): Centrally managed, no database updates needed
- MongoDB Profile (fallback): Allows admin-promoted users, persists in database
- Session-Based Checks: Role is validated on every API request via session with MongoDB fallback
- No Client-Side Bypass: UI hiding is cosmetic; all enforcement is server-side
- MongoDB Required: Admin features require MongoDB for user/conversation data and role fallback
- Self-Demotion Protection: API prevents admins from demoting themselves
- Confirmation Required: UI prompts for confirmation before role changes
- Audit Logging: Role changes are logged with admin email and target user
Testing​
Method 1: OIDC Group Admin (Recommended)​
- Set
OIDC_REQUIRED_ADMIN_GROUP=backstage-adminsin.env - Ensure your OIDC user belongs to the admin group
- Log in via SSO
- Admin tab should appear in header
- Navigate to
/adminto view dashboard - API calls to
/api/admin/*should succeed
Method 2: MongoDB Profile Admin (Manual Promotion)​
- Start with a regular user (not in OIDC admin group)
- Manually update MongoDB:
db.users.updateOne(
{ email: "user@example.com" },
{ $set: { "metadata.role": "admin" } }
) - Log out and log back in (to refresh session)
- Admin tab should now appear
- Can now promote other users via the dashboard
Method 3: Admin-Promoted User​
- Log in as an existing admin
- Navigate to
/admin - Find the user to promote in the user list
- Click "Make Admin" button
- Confirm the action
- User will have admin access on next login
Testing Non-Admin Access​
- Log in with user NOT in admin group and NOT in MongoDB admin role
- Admin tab should be hidden
- Direct navigation to
/adminshould work (UI loads) - API calls to
/api/admin/*should return 403
Metrics Captured​
- DAU: Users with
last_login >= today - MAU: Users with
last_login >= this month - Daily Activity: 30-day rolling window of active users, conversations, messages
- User Stats: Per-user conversation/message counts
- Shared Conversations: Count and percentage of shared conversations
Future Enhancements​
- ✅ User Role Management: Allow admins to promote/demote users (IMPLEMENTED)
- Usage Quotas: Set per-user or org-wide limits
- Audit Logs: Track admin actions
- Advanced Analytics: Cost tracking, model usage, error rates
- Export Reports: CSV/PDF exports of metrics
- Real-time Metrics: WebSocket-based live dashboard updates
Migration Notes​
- Breaking Change: None - this is a new feature
- Backward Compatible: Yes - existing users remain as 'user' role
- Database Changes: None - roles are session-based, not stored
- Environment Variables: New optional variable
OIDC_REQUIRED_ADMIN_GROUP
References​
- Admin Dashboard:
ui/src/app/(app)/admin/page.tsx - Admin API Routes:
ui/src/app/api/admin/ - Auth Config:
ui/src/lib/auth-config.ts - API Middleware:
ui/src/lib/api-middleware.ts - Header Component:
ui/src/components/layout/AppHeader.tsx
Conventional Commit​
feat(admin): add admin dashboard with dual RBAC (OIDC + MongoDB)
- Add OIDC_REQUIRED_ADMIN_GROUP environment variable
- Implement isAdminUser() helper for OIDC group checking
- Add MongoDB profile fallback for admin role in api-middleware
- Add session.role to NextAuth session and JWT with MongoDB check
- Create /api/admin/stats, /api/admin/users, and role management endpoints
- Build admin dashboard UI with metrics and user management
- Add role management buttons (promote/demote users)
- Show Admin tab only for admin users (OIDC or MongoDB)
- Add 403 checks on all admin routes with MongoDB fallback
Admin access is granted via OIDC group membership OR MongoDB
user.metadata.role === 'admin'. Platform metrics include DAU,
MAU, daily activity, top users, and shared conversation stats.
Admins can promote/demote other users via the dashboard.
Signed-off-by: Sri Aradhyula <sraradhy@cisco.com>