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

  1. Upward Propagation: When a higher-level permission is granted, all lower-level permissions are automatically granted

    • Example: Enabling edit (level 3) automatically enables view (level 1) and create (level 2)
  2. Downward Propagation: When a lower-level permission is revoked, all higher-level permissions are automatically revoked

    • Example: Disabling create (level 2) automatically disables edit (level 3) and delete (level 4)
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 data
  • settings.userRoles: Available user roles
  • settings.rankedRoles: Roles sorted by level
  • settings.isFetching: Loading state for fetch operations
  • settings.isBusy: Loading state for save operations
  • userAccess.accessLevel: Current user’s access level

6. API Integration

6.1 Fetching Permissions

Permissions are fetched using a Redux thunk that:

  1. Retrieves user roles from the backend
  2. Creates a template resource structure
  3. Populates the template with retrieved permissions
  4. 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:

  1. Understand the permission hierarchy defined in permissionHierarchy.ts
  2. Be aware of the cascading effects when toggling permissions
  3. Use cloneDeep for safe manipulation of the deeply nested permission structure
  4. Follow the established pattern of path-based updates

8.2 Performance Considerations

  • Deep Cloning: The system uses lodash.cloneDeep which 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, and get
  • 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_ALL permission
  • Navigation: Located in the settings section of the admin interface

role #connects/platform #connects/feature