1. Overview
The Access Control system in Nimbly implements a sophisticated role-based permission management framework that allows administrators to control what actions users with specific roles can perform throughout the application. The system features:
- Hierarchical permissions with automatic propagation of changes
- Fine-grained access control at feature and resource levels
- Role-based restrictions where higher-level roles control lower-level permissions
- UI-based configuration for administrators to manage permissions visually
- Reset capability to restore default permission configurations
This permission management system governs access across all application modules including admin functions, analytics, issue tracking, and more.
2. Architecture & System Design
2.1 File Structure
src/
├── pages/
│ └── permissions.tsx # Page component with layout wrapper
├── components/
│ └── permissions/
│ ├── Permissions.tsx # Main container with permission logic
│ ├── PermissionsContainer.tsx # UI rendering component
│ ├── typings.ts # Type definitions
│ └── utils/
│ ├── permissionHierarchy.ts # Defines hierarchy levels
│ ├── resetResourcesPermission.ts # Reset utility
│ ├── createTemplateResource.ts # Creates permission templates
│ └── updateResourcePermissions.ts # Updates permissions
├── reducers/
│ ├── settings.ts # Redux slice for permissions
│ └── useraccess.ts # User access control
└── routes/
└── admin-routes.js # Route configuration with access control
2.2 Core Components
- PermissionPage: Simple wrapper with Layout and Suspense for code-splitting
- Permissions: Container component managing state and Redux connections
- Implements permission hierarchy logic
- Handles permission updates with propagation
- Manages save and reset functionality
- PermissionContainer: UI component for permissions display
- Renders tabbed interface for different permission categories
- Displays permission tables with role columns and feature rows
- Provides checkboxes for toggling permissions
- Shows save and reset buttons with loading states
3. Permission Hierarchy System
3.1 Hierarchical Structure
// src/components/permissions/utils/permissionHierarchy.ts
export default {
view: 1,
create: 2,
edit: 3,
delete: 4,
} as PermissionHierarchy;This hierarchy defines the relationship between permission types and governs how permissions cascade:
3.2 Propagation Rules
-
Upward Propagation: When a higher-level permission is granted, all lower-level permissions are automatically granted
- Example: Enabling
edit(level 3) automatically enablesview(level 1) andcreate(level 2)
- Example: Enabling
-
Downward Propagation: When a lower-level permission is revoked, all higher-level permissions are automatically revoked
- Example: Disabling
create(level 2) automatically disablesedit(level 3) anddelete(level 4)
- Example: Disabling
graph TD A[Permission Levels] --> B[view: 1] A --> C[create: 2] A --> D[edit: 3] A --> E[delete: 4] F[Upward Propagation] --> G["Enable edit triggers view, create"] H[Downward Propagation] --> I["Disable create triggers disable edit, delete"]
3.3 Implementation
const handleChangeValue = (path: string[], value: boolean) => {
const clonedPermissions: RestructuredResource = cloneDeep(permissions);
const role = path[path.length - 1];
const currentPermission = path[path.length - 2];
const currentPermissionLevel = permissionHierarchy[currentPermission];
const targetResource = path.slice(0, 4);
const permissionAccesses = getNestedObject(clonedPermissions, targetResource);
const accesses: string[] = Object.keys(permissionAccesses) || [];
if (value === true) {
// Enable lower-level permissions when higher-level is enabled
accesses.forEach((access) => {
if (
permissionHierarchy[access] &&
permissionHierarchy[access] < currentPermissionLevel
) {
set(
clonedPermissions,
[...targetResource, access, role].join("."),
value
);
}
});
} else if (value === false) {
// Disable higher-level permissions when lower-level is disabled
accesses.forEach((access) => {
if (
permissionHierarchy[access] &&
permissionHierarchy[access] > currentPermissionLevel
) {
set(
clonedPermissions,
[...targetResource, access, role].join("."),
value
);
}
});
}
set(clonedPermissions, path.join("."), value);
props.updatePermissionResource(clonedPermissions);
};4. Role-Based Access Control
4.1 Role Structure
type RoleLabel = {
value: string; // Role identifier
label: string; // Display name
level: number; // Hierarchical level
origin: string | null; // Source of the role definition
};4.2 Role Hierarchy
Roles have levels that determine their position in the organizational hierarchy:
- Higher-level roles (lower level numbers) have more privileges
- Users can only modify permissions for roles with higher level numbers than their own
graph TD A[Super Admin] --> B[Admin] B --> C[Manager] C --> D[Staff] D --> E[Viewer] F[Access Rule] --> G["Users can only modify roles with levels higher than their own"]
4.3 Access Restriction Logic
// Only allow modification of roles with lower level than current user
if (clickEnabled && typeof accessLevel === "number") {
clickEnabled = role.level < accessLevel;
}4.4 Organization-Specific Logic
const isNimbly =
props.profile.organization === "sustainnovation" ||
props.profile.organization === "nimbly" ||
false;Special behavior is implemented for specific organizations (Nimbly/Sustainnovation).
5. Data Structures
5.1 Permission Data Structure
{
"[category]": {
"[feature]": {
"[access]": {
"permissions": {
"[permissionType]": {
"[role]": boolean
}
}
}
}
}
}This deeply nested structure allows for comprehensive permission control across the application.
Example:
{
"admin": {
"department": {
"all": {
"permissions": {
"view": {
"admin": true,
"manager": true,
"staff": false
},
"create": {
"admin": true,
"manager": false,
"staff": false
}
}
}
}
}
}5.2 Redux State Shape
The Redux store maintains:
settings.permissions: The structured permission datasettings.userRoles: Available user rolessettings.rankedRoles: Roles sorted by levelsettings.isFetching: Loading state for fetch operationssettings.isBusy: Loading state for save operationsuserAccess.accessLevel: Current user’s access level
6. API Integration
6.1 Fetching Permissions
Permissions are fetched using a Redux thunk that:
- Retrieves user roles from the backend
- Creates a template resource structure
- Populates the template with retrieved permissions
- Updates Redux state
const fetchResource = (): ThunkResult => async (dispatch) => {
dispatch(setIsFetching(true));
try {
// API call to fetch roles and permissions
const response = await fetch(`${apiURL}/user-roles/all`, {
headers: { authorization: token },
});
const result = await response.json();
// Process data and update state
// ...
dispatch(setPermissionResource(updatedResource));
} catch (err) {
toast.error(JSON.stringify(err));
} finally {
dispatch(setIsFetching(false));
}
};6.2 Saving Permissions
const saveResource =
(resource: RestructuredResource): ThunkResult =>
async (dispatch) => {
dispatch(setIsBusy(true));
try {
// Transform UI format to backend format
const parsedResource: Resource = revertData(resource, userRoles);
// API call to save permissions
const response = await fetch(`${apiURL}/user-roles/all`, {
method: "PUT",
headers: {
authorization: token,
"Content-Type": "application/json",
},
body: JSON.stringify({ roles: parsedResource }),
});
// Handle response
if (response.status === 200) {
toast.success(i18n.t("addOn.settings.permissions.success"));
}
} catch (err) {
toast.error(err);
dispatch(fetchResource()); // Refetch on failure
} finally {
dispatch(setIsBusy(false));
}
};6.3 Permission Reset
const handleResetPermissions = async () => {
setResetLoading(true);
try {
// Fetch default permissions
const defaultResources = await resetResourcesPermission(props.userRoles);
if (defaultResources) {
props.updatePermissionResource(defaultResources);
} else {
toast.error(t("addOn.settings.permissions.resetFail"));
}
} finally {
setResetLoading(false);
setShowConfirmationResetModal(false);
}
};7. UI Components & Interaction
7.1 Tab Navigation
The permission UI is organized into tabs representing different application modules:
- Admin
- Analytics
- Issue Tracker
- etc.
graph LR A[Tab Navigation] --> B[Admin] A --> C[Analytics] A --> DIssue Tracker A --> E[Other Modules]
7.2 Permission Table
For each tab, permissions are displayed in a table format:
- Rows represent features or resources
- Columns represent user roles
- Cells contain checkboxes for toggling permissions
┌───────────────┬─────────┬─────────┬────────┬────────┐
│ Feature │ Admin │ Manager │ Staff │ Viewer │
├───────────────┼─────────┼─────────┼────────┼────────┤
│ Department │ │ │ │ │
├───────────────┼─────────┼─────────┼────────┼────────┤
│ ├─ View │ ✓ │ ✓ │ ✓ │ ✓ │
│ ├─ Create │ ✓ │ ✓ │ ✗ │ ✗ │
│ ├─ Edit │ ✓ │ ✓ │ ✗ │ ✗ │
│ └─ Delete │ ✓ │ ✗ │ ✗ │ ✗ │
├───────────────┼─────────┼─────────┼────────┼────────┤
│ Sites │ │ │ │ │
├───────────────┼─────────┼─────────┼────────┼────────┤
│ ├─ View │ ✓ │ ✓ │ ✓ │ ✓ │
│ ├─ Create │ ✓ │ ✓ │ ✗ │ ✗ │
│ ├─ Edit │ ✓ │ ✓ │ ✗ │ ✗ │
│ └─ Delete │ ✓ │ ✗ │ ✗ │ ✗ │
└───────────────┴─────────┴─────────┴────────┴────────┘
7.3 Action Buttons
- Save: Persists current permission configuration to the backend
- Reset: Reverts permissions to default configuration after confirmation
7.4 Loading States
- Skeleton loaders during initial fetch
- Spinner overlays during save and reset operations
- Disabled controls when operations are in progress
8. Development Guidelines
8.1 Permission Modifications
When modifying the permission system:
- Understand the permission hierarchy defined in
permissionHierarchy.ts - Be aware of the cascading effects when toggling permissions
- Use
cloneDeepfor safe manipulation of the deeply nested permission structure - Follow the established pattern of path-based updates
8.2 Performance Considerations
- Deep Cloning: The system uses
lodash.cloneDeepwhich can be performance-intensive for large permission structures - Nested Updates: The deeply nested permission structure requires careful updates to maintain performance
- State Management: Consider memoization for derived permission values to reduce re-renders
8.3 Edge Cases
- Permission Denial: Users cannot modify permissions for roles with lower or equal levels to their own
- Reset Confirmation: Prevents accidental reset with a confirmation modal
- Error Handling: Refetches permissions on save errors to maintain consistency
- Organization-Specific Behavior: Special logic for certain organizations
9. Libraries and Dependencies
- React & Redux: Core UI and state management
- Lodash: Deep object manipulation with
cloneDeep,set, andget - React-toastify: Toast notifications for operation feedback
- React-i18next: Internationalization
- Styled-components: Component styling
- Clsx: Conditional class name construction
- Custom Components: UI elements like
Checkbox,LoadingSpinner,Tooltip
10. Routing & Access
- Route:
/settings/permissions - Component:
PermissionsPage - Access Control: Requires
RoleResources.SETTING_PERMISSION_ACCESS_ALLpermission - Navigation: Located in the settings section of the admin interface
role #connects/platform #connects/feature