RBAC Integration Guide
This guide provides comprehensive documentation for integrating Role-Based Access Control (RBAC) into canisters and frontend suites within the Hello World DAO platform.
Table of Contents
- Role Model Overview
- Auth-Service Candid API
- Frontend Integration
- Canister Integration
- Migration Guide
- Security Considerations
- Related Documentation
Role Model Overview
The platform implements a five-tier role hierarchy managed by the auth-service canister:
| Role | Description | Default Assignment | Typical Permissions |
|---|---|---|---|
| Admin | Full system access | Manually assigned by existing Admin or controller | KYC review, member management, system monitoring, governance oversight, treasury management, content moderation |
| Moderator | Content and community management | Manually assigned by Admin | Content moderation, community discussions, reporting |
| Developer | Platform developers and technical contributors | Manually assigned by Admin | Architecture docs, API reference, technical documentation |
| Member | Standard authenticated user | Assigned on registration or membership verification | Create proposals, vote on proposals, view treasury, participate in discussions |
| User | Non-member authenticated user | Default session role for users with no explicit roles | Basic platform access, view public content, register for membership |
Role Assignment Rules:
- Sessions for users with no explicit roles default to
User(non-member public user) Memberrole is assigned upon successful registration or membership verificationModeratorandAdminroles must be explicitly assigned by an existingAdminor the canister controller- Roles are additive: a user can have multiple roles (e.g., Admin + Moderator + Member)
- Role changes take effect on the next login (roles are cached in the session token)
Auth-Service Candid API
The auth-service canister provides the following role management methods:
assign_role
Assigns a role to a user. Only callable by Admin users or the canister controller.
Signature:
assign_role : (user_id: text, role: Role) -> (Result)Parameters:
user_id- Principal ID of the user as a text stringrole- Role variant:variant { Admin },variant { Moderator },variant { Developer },variant { Member }, orvariant { User }
Returns:
variant { Ok }- Role successfully assignedvariant { Err: text }- Error message (e.g., "Unauthorized", "User not found")
Example (dfx CLI):
# Assign Admin role to a user
dfx canister call auth-service assign_role '("2vxsx-fae", variant { Admin })' --network ic
# Assign Moderator role
dfx canister call auth-service assign_role '("2vxsx-fae", variant { Moderator })' --network ic
# Assign Developer role
dfx canister call auth-service assign_role '("2vxsx-fae", variant { Developer })' --network icError Codes:
"Unauthorized"- Caller is not an Admin or controller"User not found"- User ID does not exist in the system"Role already assigned"- User already has the specified role
revoke_role
Revokes a role from a user. Only callable by Admin users or the canister controller.
Signature:
revoke_role : (user_id: text, role: Role) -> (Result)Parameters:
user_id- Principal ID of the user as a text stringrole- Role variant to revoke:variant { Admin },variant { Moderator },variant { Developer },variant { Member }, orvariant { User }
Returns:
variant { Ok }- Role successfully revokedvariant { Err: text }- Error message
Example (dfx CLI):
# Revoke Admin role from a user
dfx canister call auth-service revoke_role '("2vxsx-fae", variant { Admin })' --network icError Codes:
"Unauthorized"- Caller is not an Admin or controller"User not found"- User ID does not exist"Role not assigned"- User does not have the specified role
Important: Revoking a role does not immediately affect active sessions. The user's session will continue with the old roles until they log in again.
get_user_roles
Retrieves all roles assigned to a user. Query method (read-only).
Signature:
get_user_roles : (user_id: text) -> (vec Role) queryParameters:
user_id- Principal ID of the user as a text string
Returns:
- Vector of Role variants (e.g.,
[variant { Admin }, variant { Member }])
Example (dfx CLI):
# Get all roles for a user
dfx canister call auth-service get_user_roles '("2vxsx-fae")' --network ic --queryNote: Returns an empty vector [] if the user has no roles or does not exist.
has_role
Checks if a user has a specific role. Query method (read-only).
Signature:
has_role : (user_id: text, role: Role) -> (bool) queryParameters:
user_id- Principal ID of the user as a text stringrole- Role variant to check:variant { Admin },variant { Moderator },variant { Developer },variant { Member }, orvariant { User }
Returns:
true- User has the specified rolefalse- User does not have the role (or user does not exist)
Example (dfx CLI):
# Check if a user is an Admin
dfx canister call auth-service has_role '("2vxsx-fae", variant { Admin })' --network ic --queryvalidate_session_with_role
Validates a session token and checks if the user has a required role. Query method (read-only).
Signature:
validate_session_with_role : (access_token: text, required_role: text) -> (Result_SessionInfo) queryParameters:
access_token- Session token to validaterequired_role- Required role as lowercase string:"admin","moderator","developer","member", or"user"
Returns:
variant { Ok: SessionInfo }- Session valid and user has the required roleSessionInfocontains:user_id,email,roles(vec text),created_at,expires_at
variant { Err: text }- Error message
Example (Rust inter-canister call):
use ic_cdk::api::call::call;
#[update]
async fn admin_only_method(caller_token: String) -> Result<String, String> {
// Validate that the caller has Admin role
let result: Result<(Result<SessionInfo, String>,), _> = call(
auth_service_principal,
"validate_session_with_role",
(caller_token, "admin".to_string()),
).await;
match result {
Ok((Ok(session_info),)) => {
// User is authenticated and has Admin role
Ok(format!("Admin action performed by {}", session_info.user_id))
}
Ok((Err(e),)) => Err(format!("Unauthorized: {}", e)),
Err((code, msg)) => Err(format!("Call failed: {:?} - {}", code, msg)),
}
}Error Codes:
"Invalid token"- Token is malformed or expired"Session not found"- Token does not correspond to an active session"Role not found"- User does not have the required role"Session expired"- Token has passed its expiration time
Use Cases:
- Backend canisters can validate that a caller has a specific role before executing sensitive operations
- Eliminates the need to store role information in every canister
- Centralizes role management in the auth-service
Frontend Integration
The @hello-world-co-op/auth package (v0.2.0+) provides React components and hooks for role-based UI rendering and route protection.
Role Propagation Flow
sequenceDiagram
participant Suite as Frontend Suite
participant OB as oracle-bridge
participant AS as auth-service
participant Store as nanostores
participant Component as React Component
Suite->>OB: GET /api/auth/session (cookie)
OB->>AS: validate_session
AS-->>OB: SessionInfo + roles
OB-->>Suite: { authenticated: true, user_id, roles: ["admin", "member"] }
Suite->>Store: Update $userRoles atom
Store->>Component: useRoles() reads $userRoles
Component->>Component: Render based on rolesuseRoles Hook
The useRoles() hook provides access to the current user's roles.
Import:
import { useRoles } from '@hello-world-co-op/auth';Usage:
function AdminPanel() {
const { roles, isLoading, hasRole, isAdmin } = useRoles();
if (isLoading) {
return <div>Loading permissions...</div>;
}
if (!isAdmin) {
return <div>Access Denied</div>;
}
return (
<div>
<h1>Admin Panel</h1>
{hasRole('moderator') && <ModeratorTools />}
</div>
);
}Return Value:
{
roles: string[]; // Array of role names: ["admin", "developer", "member"]
hasRole: (role: string) => boolean; // Check if user has a specific role
isAdmin: boolean; // Convenience: true if user has "admin" role
}
// Note: For loading state, use useAuth() which provides isLoadingRole Name Format:
- Role names are always lowercase strings:
"admin","moderator","developer","member","user" - The
hasRole()comparison is case-sensitive — always use lowercase
RoleGuard Component
The <RoleGuard> component conditionally renders children based on role requirements.
Import:
import { RoleGuard } from '@hello-world-co-op/auth';Basic Usage:
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* Only visible to Admin users */}
<RoleGuard requiredRole="admin">
<AdminSection />
</RoleGuard>
{/* Visible to Moderator users */}
<RoleGuard requiredRole="moderator">
<ModerationPanel />
</RoleGuard>
{/* For multiple roles, use hasRole() from useRoles() hook */}
</div>
);
}Props:
interface RoleGuardProps {
requiredRole: string; // Required role name (e.g., "admin")
fallback?: React.ReactNode; // Content to show if role check fails (default: null)
children: React.ReactNode; // Protected content
}With Fallback Content:
<RoleGuard
requiredRole="admin"
fallback={<div>Admin access required</div>}
>
<AdminTools />
</RoleGuard>Route Protection:
// In App.tsx or routing configuration
<Route
path="/admin/*"
element={
<RoleGuard requiredRole="admin" fallback={<Navigate to="/unauthorized" />}>
<AdminDashboard />
</RoleGuard>
}
/>Important: RoleGuard is a UI convenience only. Always verify roles server-side in canister methods. Frontend guards can be bypassed by users modifying client-side code.
hasRole and isAdmin
The hasRole() function and isAdmin boolean are available from the useRoles() or useAuth() hooks. They are not standalone exports — they require React context.
Usage:
import { useRoles } from '@hello-world-co-op/auth';
function AdminPanel() {
const { hasRole, isAdmin } = useRoles();
// Single role check
if (!isAdmin) return <AccessDenied />;
// Multiple role check
const canModerate = hasRole('admin') || hasRole('moderator');
return <div>{canModerate && <ModerationTools />}</div>;
}Signature:
hasRole(role: string): boolean // Case-sensitive; roles are always lowercase
isAdmin: boolean // Equivalent to hasRole('admin')Note: This function reads from the global $userRoles nanostore atom. It should only be used after authentication has completed and roles have been loaded.
isAdmin Convenience
The isAdmin boolean is available from useRoles() as a shorthand for hasRole('admin').
Usage:
const { isAdmin } = useRoles();
if (isAdmin) {
// Show admin-only UI
}This is equivalent to:
const { hasRole } = useRoles();
if (hasRole('admin')) {
// Show admin-only UI
}Role State Management
Roles are stored in the $userRoles nanostore atom and synchronized with the authentication session:
// src/stores/auth.ts
import { atom } from 'nanostores';
export const $userRoles = atom<string[]>([]);
// Updated when session is fetched from oracle-bridge
export async function fetchSession() {
const response = await fetch(`${ORACLE_BRIDGE_URL}/api/auth/session`, {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
$userRoles.set(data.roles || []); // ["admin", "member"]
}
}Canister Integration
Backend canisters can validate that a caller has a specific role by calling auth-service::validate_session_with_role.
Inter-Canister Role Check Pattern
Example: Admin-Only Canister Method
use ic_cdk::api::call::call;
use candid::{CandidType, Deserialize, Principal};
#[derive(CandidType, Deserialize)]
struct SessionInfo {
user_id: String,
email: String,
roles: Vec<String>,
created_at: u64,
expires_at: u64,
}
#[update]
async fn approve_kyc_submission(
access_token: String,
submission_id: String,
) -> Result<String, String> {
// Validate that the caller has Admin role
let auth_service_id = Principal::from_text(AUTH_SERVICE_CANISTER_ID)
.map_err(|e| format!("Invalid auth service ID: {}", e))?;
let result: Result<(Result<SessionInfo, String>,), _> = call(
auth_service_id,
"validate_session_with_role",
(access_token.clone(), "admin".to_string()),
).await;
match result {
Ok((Ok(session_info),)) => {
// User is authenticated and has Admin role
ic_cdk::println!("Admin {} approved KYC {}", session_info.user_id, submission_id);
// Perform admin operation
approve_submission_internal(submission_id)
}
Ok((Err(e),)) => {
// Auth check failed
Err(format!("Access denied: {}", e))
}
Err((code, msg)) => {
// Inter-canister call failed
Err(format!("Auth service error: {:?} - {}", code, msg))
}
}
}
fn approve_submission_internal(submission_id: String) -> Result<String, String> {
// Actual business logic here
Ok(format!("Submission {} approved", submission_id))
}Role-Gated Method Examples
Multiple Roles Accepted:
#[update]
async fn moderate_content(
access_token: String,
content_id: String,
action: String,
) -> Result<String, String> {
// Accept either Admin or Moderator role
let auth_service_id = Principal::from_text(AUTH_SERVICE_CANISTER_ID)?;
// Try Moderator role first
let moderator_check: Result<(Result<SessionInfo, String>,), _> = call(
auth_service_id,
"validate_session_with_role",
(access_token.clone(), "moderator".to_string()),
).await;
if let Ok((Ok(session_info),)) = moderator_check {
return moderate_content_internal(content_id, action, session_info.user_id);
}
// Fall back to Admin role
let admin_check: Result<(Result<SessionInfo, String>,), _> = call(
auth_service_id,
"validate_session_with_role",
(access_token, "admin".to_string()),
).await;
match admin_check {
Ok((Ok(session_info),)) => {
moderate_content_internal(content_id, action, session_info.user_id)
}
_ => Err("Access denied: Moderator or Admin role required".to_string()),
}
}Member-Level Access (Default):
#[update]
async fn create_proposal(
access_token: String,
title: String,
description: String,
) -> Result<String, String> {
// Any authenticated member can create proposals
let auth_service_id = Principal::from_text(AUTH_SERVICE_CANISTER_ID)?;
let result: Result<(Result<SessionInfo, String>,), _> = call(
auth_service_id,
"validate_session_with_role",
(access_token, "member".to_string()),
).await;
match result {
Ok((Ok(session_info),)) => {
create_proposal_internal(title, description, session_info.user_id)
}
Ok((Err(e),)) => Err(format!("Authentication required: {}", e)),
Err((code, msg)) => Err(format!("Auth service error: {:?} - {}", code, msg)),
}
}Error Handling Best Practices
Fail-Closed Approach:
// WRONG - fails open (allows access on error)
let is_admin = match check_admin_role(token).await {
Ok(true) => true,
_ => false, // Treats errors as "not admin" - could allow unintended access
};
// CORRECT - fails closed (denies access on error)
let session_info = match validate_admin_role(token).await {
Ok(info) => info,
Err(e) => return Err(format!("Access denied: {}", e)),
};
// Only proceed if validation succeededLogging Unauthorized Attempts:
#[update]
async fn admin_method(access_token: String) -> Result<String, String> {
let result = validate_admin_role(access_token.clone()).await;
match result {
Ok(session_info) => {
// Success - proceed
perform_admin_action(session_info.user_id)
}
Err(e) => {
// Log unauthorized attempt
ic_cdk::println!(
"SECURITY: Unauthorized admin access attempt - token: {} error: {}",
&access_token[..8], // Log only token prefix for security
e
);
Err(format!("Access denied: {}", e))
}
}
}Migration Guide
Follow these steps to upgrade an existing suite from authentication-only to RBAC support.
Step 1: Update @hello-world-co-op/auth to ^0.2.0
In package.json:
{
"dependencies": {
"@hello-world-co-op/auth": "^0.2.0"
}
}Install the update:
npm installVerify the version:
npm list @hello-world-co-op/authExpected output:
@hello-world-co-op/auth@0.2.0Step 2: Wrap App in AuthProvider (if not already)
If your suite uses the Auth Bridge pattern (think-tank, dao, dao-admin), ensure AuthProviderBridge is wrapping your app root.
In src/App.tsx or src/main.tsx:
import { AuthProviderBridge } from './components/auth/AuthProviderBridge';
function App() {
return (
<AuthProviderBridge>
{/* Your routes and components */}
</AuthProviderBridge>
);
}For suites using auth directly (governance-suite): Roles are automatically loaded from the session endpoint. No additional setup required.
Step 3: Use RoleGuard for Protected Routes
Replace custom auth guards or admin checks with RoleGuard:
Before (custom guard):
// src/components/AdminGuard.tsx (custom implementation)
function AdminGuard({ children }: { children: React.ReactNode }) {
const { user } = useAuth();
if (!user || user.role !== 'admin') {
return <Navigate to="/unauthorized" />;
}
return <>{children}</>;
}
// In routes
<Route path="/admin/*" element={<AdminGuard><AdminDashboard /></AdminGuard>} />After (using RoleGuard):
import { RoleGuard } from '@hello-world-co-op/auth';
// In routes
<Route
path="/admin/*"
element={
<RoleGuard requiredRole="admin" fallback={<Navigate to="/unauthorized" />}>
<AdminDashboard />
</RoleGuard>
}
/>Remove custom guard file:
rm src/components/AdminGuard.tsxStep 4: Use useRoles for Conditional UI
Replace custom role checks with the useRoles() hook:
Before:
import { useAuth } from '@hello-world-co-op/auth';
function Dashboard() {
const { user } = useAuth();
const isAdmin = user?.role === 'admin';
return (
<div>
<h1>Dashboard</h1>
{isAdmin && <AdminPanel />}
</div>
);
}After:
import { useRoles } from '@hello-world-co-op/auth';
function Dashboard() {
const { isAdmin } = useRoles();
return (
<div>
<h1>Dashboard</h1>
{isAdmin && <AdminPanel />}
</div>
);
}Step 5: Update Canister Methods (if applicable)
If your suite calls backend canister methods that require role verification, update the method signatures to accept an access_token parameter and call validate_session_with_role.
Before (no role check):
#[update]
fn approve_action(action_id: String) -> Result<String, String> {
// Anyone can call this - no auth check!
approve_action_internal(action_id)
}After (with role check):
#[update]
async fn approve_action(
access_token: String,
action_id: String,
) -> Result<String, String> {
// Verify caller has Admin role
validate_admin_role(access_token).await?;
approve_action_internal(action_id)
}
async fn validate_admin_role(token: String) -> Result<SessionInfo, String> {
let auth_service_id = Principal::from_text(AUTH_SERVICE_CANISTER_ID)
.map_err(|e| format!("Auth service ID error: {}", e))?;
let result: Result<(Result<SessionInfo, String>,), _> = call(
auth_service_id,
"validate_session_with_role",
(token, "admin".to_string()),
).await;
match result {
Ok((Ok(info),)) => Ok(info),
Ok((Err(e),)) => Err(format!("Access denied: {}", e)),
Err((code, msg)) => Err(format!("Auth error: {:?} - {}", code, msg)),
}
}Step 6: Update Frontend API Calls
Update frontend service functions to pass the access token to backend methods:
Before:
// src/services/adminService.ts
export async function approveAction(actionId: string): Promise<ApiResponse<string>> {
const actor = await createActor('admin-canister-id');
const result = await actor.approve_action(actionId);
return handleApiResponse(result);
}After:
// src/services/adminService.ts
export async function approveAction(actionId: string): Promise<ApiResponse<string>> {
const token = getAccessToken(); // Get token from auth store
if (!token) {
return { success: false, error: 'Not authenticated' };
}
const actor = await createActor('admin-canister-id');
const result = await actor.approve_action(token, actionId);
return handleApiResponse(result);
}
function getAccessToken(): string | null {
// Get token from auth store/cookie
return localStorage.getItem('access_token');
}Migration Checklist
- [ ] Updated
@hello-world-co-op/authto v0.2.0+ - [ ] Verified
AuthProviderBridgewraps app root (for Auth Bridge suites) - [ ] Replaced custom admin guards with
<RoleGuard requiredRole="admin"> - [ ] Updated conditional UI to use
useRoles()hook - [ ] Updated canister methods to call
validate_session_with_role - [ ] Updated frontend service calls to pass access tokens
- [ ] Removed custom role management code
- [ ] Tested admin/moderator/member access flows
- [ ] Updated suite README with RBAC notes
Security Considerations
Fail-Closed Design
Always deny access if role verification fails or errors occur.
// CORRECT - fail closed
match validate_role(token).await {
Ok(session_info) => perform_action(session_info),
Err(_) => return Err("Access denied".to_string()), // Deny on error
}
// WRONG - fail open
let allowed = match validate_role(token).await {
Ok(_) => true,
Err(_) => false, // Could allow unintended access if validation service is down
};
if allowed {
perform_action();
}Role Caching in Sessions
Roles are cached in session tokens at login time. This has important implications:
- Performance: Role checks are fast (no database lookup per request)
- Staleness: Role changes do not take effect until the user logs in again
- Session Duration: Current sessions have a 24-hour lifetime
Example Scenario:
- User logs in at 9:00 AM → Session created with
roles: ["member"] - Admin promotes user to Moderator at 10:00 AM → Role stored in auth-service
- User still sees Member-only UI until they log out and back in
- User logs in again at 2:00 PM → New session with
roles: ["moderator", "member"]
Mitigation: For time-sensitive role changes (e.g., emergency role revocation), invalidate the user's session to force re-authentication:
# Force session refresh by invalidating current session (requires backend support)
dfx canister call auth-service invalidate_user_sessions '("user-id")' --network icSession Expiry and Role Staleness
Session tokens expire after 24 hours. After expiration:
- The session is no longer valid
- Role checks fail with
"Session expired" - User must log in again to get a fresh session with current roles
Best Practice: Monitor session expiry in the frontend and prompt users to re-authenticate before token expiration:
// src/stores/auth.ts
export function checkSessionExpiry() {
const session = $session.get();
if (!session) return;
const now = Date.now();
const expiresAt = session.expires_at * 1000; // Convert to milliseconds
const timeUntilExpiry = expiresAt - now;
// Warn user 5 minutes before expiry
if (timeUntilExpiry < 5 * 60 * 1000 && timeUntilExpiry > 0) {
showNotification('Your session is about to expire. Please save your work.');
}
// Force refresh if expired
if (timeUntilExpiry <= 0) {
logout();
navigateToLogin();
}
}Server-Side Audit Logging
Always log unauthorized access attempts for security monitoring:
#[update]
async fn admin_method(access_token: String) -> Result<String, String> {
match validate_admin_role(access_token.clone()).await {
Ok(session_info) => {
ic_cdk::println!("AUDIT: Admin action by {}", session_info.user_id);
perform_admin_action()
}
Err(e) => {
// Log unauthorized attempt
ic_cdk::println!(
"SECURITY: Unauthorized admin access attempt - token prefix: {} error: {}",
&access_token[..8],
e
);
Err(format!("Access denied: {}", e))
}
}
}Audit Log Fields:
- Timestamp (automatic via IC system time)
- User ID or token prefix
- Method called
- Role required
- Success/failure
- Error message if failed
Frontend Guards are UX Only
Critical: Frontend role guards (RoleGuard, useRoles) are UI conveniences only. They improve user experience by hiding irrelevant UI, but they do NOT provide security.
Why Frontend Guards are Not Secure:
- Users can modify client-side JavaScript
- Browser DevTools can bypass React components
- Network requests can be crafted manually (e.g., via curl)
Always Verify Server-Side:
// Backend canister method - ALWAYS CHECK ROLE HERE
#[update]
async fn admin_action(access_token: String, data: String) -> Result<String, String> {
// This is the REAL security check
validate_admin_role(access_token).await?;
// Safe to proceed
perform_admin_operation(data)
}Frontend:
// Frontend component - UX convenience only
function AdminPanel() {
const { isAdmin } = useRoles();
// This hides the UI, but doesn't secure the backend
if (!isAdmin) {
return <div>Access Denied</div>;
}
return (
<button onClick={callAdminAction}>
Admin Action
</button>
);
}
async function callAdminAction() {
// Backend will verify role again - this is the REAL check
const result = await adminService.performAction(token, data);
}Token Storage Security
Access tokens should be stored securely:
Recommended (HTTP-only cookie):
// Token is in HTTP-only cookie (set by oracle-bridge)
// Not accessible to JavaScript - reduces XSS risk
await fetch(`${API_URL}/api/admin/action`, {
credentials: 'include', // Sends cookie automatically
});Acceptable (localStorage with precautions):
// If using localStorage (less secure but sometimes necessary)
const token = localStorage.getItem('access_token');
// ALWAYS validate token format before using
if (!token || token.length < 20) {
throw new Error('Invalid token');
}
// NEVER log the full token
console.log('Token prefix:', token.substring(0, 8));Never:
- Store tokens in URL parameters
- Store tokens in sessionStorage (vulnerable to XSS like localStorage, but also lost on tab close)
- Log full tokens to console
- Send tokens via GET query parameters
Rate Limiting and Abuse Prevention
Implement rate limiting on sensitive role-protected endpoints:
use std::collections::HashMap;
use ic_cdk::api::time;
thread_local! {
static RATE_LIMIT: RefCell<HashMap<String, Vec<u64>>> = RefCell::new(HashMap::new());
}
fn check_rate_limit(user_id: &str, max_calls: usize, window_seconds: u64) -> Result<(), String> {
let now = time() / 1_000_000_000; // Convert to seconds
RATE_LIMIT.with(|rl| {
let mut map = rl.borrow_mut();
let calls = map.entry(user_id.to_string()).or_insert_with(Vec::new);
// Remove old calls outside the time window
calls.retain(|×tamp| now - timestamp < window_seconds);
if calls.len() >= max_calls {
return Err(format!("Rate limit exceeded: {} calls per {} seconds", max_calls, window_seconds));
}
calls.push(now);
Ok(())
})
}
#[update]
async fn admin_action(access_token: String, data: String) -> Result<String, String> {
let session_info = validate_admin_role(access_token).await?;
// Rate limit: 10 calls per minute
check_rate_limit(&session_info.user_id, 10, 60)?;
perform_admin_operation(data)
}Related Documentation
- FAS Architecture - RBAC architecture section
- FAS Troubleshooting - RBAC debugging section
- Auth Package Documentation - Full API reference
- Auth-Service Canister - Backend role management
- DAO Admin Suite README - Real-world RBAC implementation example