Skip to main content

Overview

AmbioSys implements a comprehensive Role-Based Access Control (RBAC) system that manages user access to system modules and actions. The permission model supports:
  • Role creation and management
  • User-to-role assignment
  • Module-level permissions with view and block controls
  • Bulk permission updates
  • Dynamic permission versioning for cache invalidation

RBAC Architecture

Components

1

Roles

Roles are named groups that define a set of permissions. Each role has a unique key, display name, and description.
2

Modules

Modules represent distinct areas or features of the application. Each module has a path and can have multiple actions.
3

Permissions

Permissions link roles to modules, defining what each role can access. Permissions include can_view and is_blocked flags.
4

User Assignments

Users can be assigned to multiple roles through the user_roles junction table.

Database Schema

roles
├── role_id (PK)
├── role_key (unique)
├── role_name
├── description
└── created_by

modules
├── module_id (PK)
├── module_name
├── module_path (unique)
├── module_description
└── is_blocked

role_module_permissions
├── id (PK)
├── role_id (FK)
├── module_id (FK)
├── can_view
└── is_blocked

user_roles
├── user_id (FK)
└── role_id (FK)

Role Management

List All Roles

Retrieve all roles in the system ordered by name.
GET /api/roles
From /Backend/web-ambiotec/src/routes/roles.js:6-17:
router.get('/', async (req, res) => {
  const client = await pgPool.connect();
  try {
    const { rows } = await client.query(
      'SELECT * FROM db_ambiotec.roles ORDER BY role_name'
    );
    return res.json({ success: true, roles: rows });
  } catch (error) {
    console.error('Error listing roles', error);
    return res.status(500).json({ 
      success: false, 
      error: 'Error listing roles' 
    });
  } finally {
    client.release();
  }
});
Response:
{
  "success": true,
  "roles": [
    {
      "role_id": 1,
      "role_key": "admin",
      "role_name": "Administrator",
      "description": "Full system access",
      "created_by": 1,
      "created_at": "2024-01-15T10:30:00Z"
    },
    {
      "role_id": 2,
      "role_key": "operator",
      "role_name": "Operator",
      "description": "Field operations access",
      "created_by": 1,
      "created_at": "2024-01-15T10:35:00Z"
    }
  ]
}

Create a Role

Create a new role with a unique key and name.
POST /api/roles
Request Body:
{
  "role_key": "supervisor",
  "role_name": "Supervisor",
  "description": "Supervises field operations and reviews reports"
}
From /Backend/web-ambiotec/src/routes/roles.js:19-35:
router.post('/', async (req, res) => {
  const client = await pgPool.connect();
  try {
    const { role_key, role_name, description } = req.body || {};
    
    if (!role_key || !role_name) {
      return res.status(400).json({ 
        success: false, 
        error: 'role_key and role_name required' 
      });
    }
    
    const createdBy = req.user?.usuario_id ?? req.user?.user_id ?? null;
    
    const { rows } = await client.query(
      `INSERT INTO db_ambiotec.roles 
       (role_key, role_name, description, created_by) 
       VALUES ($1,$2,$3,$4) RETURNING *`,
      [role_key, role_name, description, createdBy]
    );
    
    return res.status(201).json({ success: true, role: rows[0] });
  } catch (error) {
    console.error('Error creating role', error);
    return res.status(500).json({ 
      success: false, 
      error: 'Error creating role' 
    });
  } finally { 
    client.release(); 
  }
});
The role_key must be unique across the system. Use lowercase, hyphenated format for consistency (e.g., field-operator, data-analyst).
Response:
{
  "success": true,
  "role": {
    "role_id": 3,
    "role_key": "supervisor",
    "role_name": "Supervisor",
    "description": "Supervises field operations and reviews reports",
    "created_by": 1,
    "created_at": "2024-03-03T14:20:00Z"
  }
}

User-Role Assignment

Assign User to Role

Assign a specific user to a role.
POST /api/roles/:roleId/users/:userId
From /Backend/web-ambiotec/src/routes/roles.js:37-53:
router.post('/:roleId/users/:userId', async (req, res) => {
  const client = await pgPool.connect();
  const roleId = Number(req.params.roleId);
  const userId = Number(req.params.userId);
  
  if (!Number.isInteger(roleId) || !Number.isInteger(userId)) {
    return res.status(400).json({ 
      success: false, 
      error: 'Invalid ids' 
    });
  }
  
  try {
    const { rows } = await client.query(
      `INSERT INTO db_ambiotec.user_roles (user_id, role_id) 
       VALUES ($1,$2) ON CONFLICT DO NOTHING RETURNING *`,
      [userId, roleId]
    );
    return res.json({ success: true, assigned: rows[0] ?? null });
  } catch (error) {
    console.error('Error assigning role', error);
    return res.status(500).json({ 
      success: false, 
      error: 'Error assigning role' 
    });
  } finally { 
    client.release(); 
  }
});
Example:
curl -X POST https://api.ambiosys.com/api/roles/2/users/42
Response:
{
  "success": true,
  "assigned": {
    "user_id": 42,
    "role_id": 2
  }
}
The ON CONFLICT DO NOTHING clause ensures idempotent behavior. Assigning the same user to a role multiple times won’t create duplicates or throw errors.

Remove User from Role

Remove a user’s role assignment.
DELETE /api/roles/:roleId/users/:userId
From /Backend/web-ambiotec/src/routes/roles.js:55-68:
router.delete('/:roleId/users/:userId', async (req, res) => {
  const client = await pgPool.connect();
  const roleId = Number(req.params.roleId);
  const userId = Number(req.params.userId);
  
  if (!Number.isInteger(roleId) || !Number.isInteger(userId)) {
    return res.status(400).json({ 
      success: false, 
      error: 'Invalid ids' 
    });
  }
  
  try {
    const { rowCount } = await client.query(
      'DELETE FROM db_ambiotec.user_roles WHERE role_id=$1 AND user_id=$2', 
      [roleId, userId]
    );
    return res.json({ success: true, removed: rowCount > 0 });
  } catch (error) {
    console.error('Error removing role assignment', error);
    return res.status(500).json({ 
      success: false, 
      error: 'Error removing role assignment' 
    });
  } finally { 
    client.release(); 
  }
});

List Users for a Role

Get all users assigned to a specific role.
GET /api/roles/:roleId/users
From /Backend/web-ambiotec/src/routes/roles.js:70-88:
router.get('/:roleId/users', async (req, res) => {
  const client = await pgPool.connect();
  const roleId = Number(req.params.roleId);
  
  if (!Number.isInteger(roleId)) {
    return res.status(400).json({ 
      success: false, 
      error: 'Invalid role id' 
    });
  }
  
  try {
    const { rows } = await client.query(
      `SELECT u.user_id, u.email, u.first_name, u.last_name
       FROM db_ambiotec.user_roles ur
       JOIN db_ambiotec.users u ON u.user_id = ur.user_id
       WHERE ur.role_id = $1`,
      [roleId]
    );
    return res.json({ success: true, users: rows });
  } catch (error) {
    console.error('Error listing users for role', error);
    return res.status(500).json({ 
      success: false, 
      error: 'Error listing users for role' 
    });
  } finally { 
    client.release(); 
  }
});
Response:
{
  "success": true,
  "users": [
    {
      "user_id": 42,
      "email": "[email protected]",
      "first_name": "John",
      "last_name": "Doe"
    },
    {
      "user_id": 58,
      "email": "[email protected]",
      "first_name": "Alice",
      "first_name": "Smith"
    }
  ]
}

Module Permissions

Get Role Permissions

Retrieve all module permissions for a specific role.
GET /api/roles/:roleId/permissions
From /Backend/web-ambiotec/src/routes/roles.js:90-124:
router.get('/:roleId/permissions', async (req, res) => {
  const client = await pgPool.connect();
  const roleId = Number(req.params.roleId);
  
  if (!Number.isInteger(roleId)) {
    return res.status(400).json({ 
      success: false, 
      error: 'Invalid role id' 
    });
  }
  
  try {
    const { rows } = await client.query(
      `
      SELECT
        m.module_id,
        m.module_name,
        m.module_path,
        m.module_description,
        rmp.can_view,
        rmp.is_blocked,
        rmp.id AS role_module_permission_id
      FROM db_ambiotec.modules m
      LEFT JOIN db_ambiotec.role_module_permissions rmp
        ON rmp.module_id = m.module_id AND rmp.role_id = $1
      ORDER BY m.module_name
      `,
      [roleId]
    );
    
    const permissions = rows.map((row) => ({
      ...row,
      has_permission: row.role_module_permission_id != null,
    }));
    
    return res.json({ success: true, permissions });
  } catch (error) {
    console.error('Error listing role permissions', error);
    return res.status(500).json({ 
      success: false, 
      error: 'Error listing role permissions' 
    });
  } finally {
    client.release();
  }
});
Response:
{
  "success": true,
  "permissions": [
    {
      "module_id": 1,
      "module_name": "Dashboard",
      "module_path": "/dashboard",
      "module_description": "Main dashboard view",
      "can_view": true,
      "is_blocked": false,
      "role_module_permission_id": 15,
      "has_permission": true
    },
    {
      "module_id": 2,
      "module_name": "Reports",
      "module_path": "/reports",
      "module_description": "Report generation and viewing",
      "can_view": null,
      "is_blocked": null,
      "role_module_permission_id": null,
      "has_permission": false
    }
  ]
}
The LEFT JOIN ensures all modules are returned, even those without permissions for the role. The has_permission flag indicates whether the role has any permission record for that module.

Update Role Permissions (Bulk)

Update multiple module permissions for a role in a single transaction.
PUT /api/roles/:roleId/permissions
Request Body:
{
  "permissions": [
    {
      "module_id": 1,
      "enabled": true,
      "can_view": true,
      "is_blocked": false
    },
    {
      "module_id": 2,
      "enabled": true,
      "can_view": true,
      "is_blocked": false
    },
    {
      "module_id": 3,
      "enabled": false
    }
  ]
}
From /Backend/web-ambiotec/src/routes/roles.js:126-167:
router.put('/:roleId/permissions', async (req, res) => {
  const client = await pgPool.connect();
  const roleId = Number(req.params.roleId);
  const { permissions } = req.body || {};
  
  if (!Number.isInteger(roleId)) {
    return res.status(400).json({ 
      success: false, 
      error: 'Invalid role id' 
    });
  }
  if (!Array.isArray(permissions)) {
    return res.status(400).json({ 
      success: false, 
      error: 'permissions array required' 
    });
  }

  try {
    await client.query('BEGIN');
    
    for (const perm of permissions) {
      const moduleId = Number(perm?.module_id);
      if (!Number.isInteger(moduleId)) continue;
      
      const enabled = perm?.enabled !== false;
      
      if (!enabled) {
        // Remove permission if disabled
        await client.query(
          `DELETE FROM db_ambiotec.role_module_permissions 
           WHERE role_id = $1 AND module_id = $2`,
          [roleId, moduleId]
        );
        continue;
      }
      
      // Update or insert permission
      const canView = Boolean(perm?.can_view);
      const isBlocked = Boolean(perm?.is_blocked);
      
      // Delete existing then insert (upsert pattern)
      await client.query(
        `DELETE FROM db_ambiotec.role_module_permissions 
         WHERE role_id = $1 AND module_id = $2`,
        [roleId, moduleId]
      );
      await client.query(
        `INSERT INTO db_ambiotec.role_module_permissions 
         (role_id, module_id, can_view, is_blocked) 
         VALUES ($1, $2, $3, $4)`,
        [roleId, moduleId, canView, isBlocked]
      );
    }
    
    await client.query('COMMIT');
    return res.json({ success: true });
  } catch (error) {
    await client.query('ROLLBACK');
    console.error('Error updating role permissions', error);
    return res.status(500).json({ 
      success: false, 
      error: 'Error updating role permissions' 
    });
  } finally {
    client.release();
  }
});
This endpoint uses a transactional approach. If any permission update fails, all changes are rolled back. Ensure all module_id values are valid before submitting.

Delete a Module Permission

Remove a specific module permission from a role.
DELETE /api/roles/:roleId/permissions/:moduleId
From /Backend/web-ambiotec/src/routes/roles.js:169-189:
router.delete('/:roleId/permissions/:moduleId', async (req, res) => {
  const client = await pgPool.connect();
  const roleId = Number(req.params.roleId);
  const moduleId = Number(req.params.moduleId);
  
  if (!Number.isInteger(roleId) || !Number.isInteger(moduleId)) {
    return res.status(400).json({ 
      success: false, 
      error: 'Invalid ids' 
    });
  }
  
  try {
    const { rowCount } = await client.query(
      `DELETE FROM db_ambiotec.role_module_permissions 
       WHERE role_id = $1 AND module_id = $2`,
      [roleId, moduleId]
    );
    return res.json({ success: true, removed: rowCount > 0 });
  } catch (error) {
    console.error('Error deleting role permission', error);
    return res.status(500).json({ 
      success: false, 
      error: 'Error deleting role permission' 
    });
  } finally {
    client.release();
  }
});

User Permissions API

Get User Permissions Version

Retrieve the current permission version for cache invalidation.
GET /api/permissions/getPermissionsVersion/:id
From /Backend/web-ambiotec/src/routes/permissions.js:6-25:
router.get('/getPermissionsVersion/:id', async (req, res) => {
  const client = await pgPool.connect();
  const { id } = req.params;
  try {
    const { rows } = await client.query(
      'SELECT * FROM db_ambiotec.fn_get_user_permissions_version($1)', 
      [id]
    );
    return res.status(200).json({
      success: true,
      version: rows[0] 
        ? rows[0]?.fn_get_user_permissions_version 
        : null
    });
  } catch (error) {
    console.error('Error al obtener la versión de permisos', error);
    res.status(500).json({ 
      error: 'Error al obtener la versión de permisos' 
    });
  } finally {
    client.release();
  }
});
Response:
{
  "success": true,
  "version": "v1.2024-03-03T14:30:00Z"
}
The permissions version allows client applications to cache permissions locally and only refresh when the version changes, improving performance.

Get User Permissions

Retrieve complete permission set for a user including modules and actions.
GET /api/permissions/getPermission/:id
From /Backend/web-ambiotec/src/routes/permissions.js:27-44:
router.get('/getPermission/:id', async (req, res) => {
  const client = await pgPool.connect();
  const { id } = req.params;
  try {
    const { rows } = await client.query(
      'SELECT * FROM db_ambiotec.fn_get_user_permissions_modules_actions($1)', 
      [id]
    );
    return res.status(200).json({
      success: true,
      permissions: rows[0] 
        ? rows[0]?.fn_get_user_permissions_modules_actions 
        : null
    });
  } catch (error) {
    console.error('Error al obtener permisos', error);
    res.status(500).json({ 
      error: 'Error al obtener los permisos' 
    });
  } finally {
    client.release();
  }
});
Response:
{
  "success": true,
  "permissions": {
    "modules": [
      {
        "module_id": 1,
        "module_name": "Dashboard",
        "can_view": true,
        "is_blocked": false,
        "actions": [
          {
            "action_id": 1,
            "action_name": "view_reports",
            "can_execute": true
          }
        ]
      }
    ]
  }
}

Permission Flags

can_view

  • Type: Boolean
  • Purpose: Determines if the user can view the module
  • Default: false
  • Usage: Controls visibility in navigation and direct access to module routes

is_blocked

  • Type: Boolean
  • Purpose: Explicitly blocks access to a module regardless of other permissions
  • Default: false
  • Usage: Override permission for security or compliance reasons
When is_blocked is true, the module is completely inaccessible to the role, even if can_view is true. Block flags take precedence over view permissions.

Best Practices

1

Principle of Least Privilege

Grant users only the minimum permissions required to perform their job functions. Start with no permissions and add as needed.
2

Role Hierarchy

Design roles in a hierarchical structure (e.g., Viewer → Operator → Supervisor → Admin) and assign permissions accordingly.
3

Use Role Keys Consistently

Establish a naming convention for role keys (e.g., field-operator, data-analyst) and document role purposes.
4

Audit Permission Changes

Track who creates roles and when. The created_by field stores the user who created each role for accountability.
5

Bulk Updates for Consistency

Use the bulk permission update endpoint when configuring roles to ensure atomic, transactional updates.
6

Cache Permission Versions

Implement permission caching on the client side and use the version endpoint to determine when to refresh.

Common Permission Patterns

Read-Only Role

{
  "role_key": "viewer",
  "role_name": "Viewer",
  "permissions": [
    {
      "module_id": 1,
      "enabled": true,
      "can_view": true,
      "is_blocked": false
    }
  ]
}

Operator Role with Restrictions

{
  "role_key": "operator",
  "role_name": "Operator",
  "permissions": [
    {
      "module_id": 1,
      "enabled": true,
      "can_view": true,
      "is_blocked": false
    },
    {
      "module_id": 5,
      "enabled": true,
      "can_view": false,
      "is_blocked": true
    }
  ]
}

Administrator Role

{
  "role_key": "admin",
  "role_name": "Administrator",
  "permissions": [
    {
      "module_id": 1,
      "enabled": true,
      "can_view": true,
      "is_blocked": false
    },
    // ... all modules with can_view: true
  ]
}

Build docs developers (and LLMs) love