@tmlmobilidade/types
Version:
342 lines (341 loc) • 15.5 kB
JavaScript
/* * */
import { AgenciesPermissionSchema } from './agencies.js';
import { AlertsPermissionSchema } from './alerts.js';
import { RidesPermissionSchema, SamsPermissionSchema } from './controller.js';
import { AnnotationsPermissionSchema, EventsPermissionSchema, HolidaysPermissionSchema, YearPeriodsPermissionSchema } from './dates.js';
import { FaresPermissionSchema } from './fares.js';
import { GtfsValidationsPermissionSchema } from './gtfs-validations.js';
import { HomePermissionSchema } from './home.js';
import { LinesPermissionSchema } from './lines.js';
import { OrganizationsPermissionSchema } from './organizations.js';
import { PerformancePermissionSchema } from './performance.js';
import { PlansPermissionSchema } from './plans.js';
import { RolesPermissionSchema } from './roles.js';
import { StopsPermissionSchema } from './stops.js';
import { TypologiesPermissionSchema } from './typologies.js';
import { UsersPermissionSchema } from './users.js';
import { VehiclesPermissionSchema } from './vehicles.js';
import { ZonesPermissionSchema } from './zones.js';
import { z } from 'zod';
/* * */
export const PermissionSchema = z.discriminatedUnion('scope', [
AgenciesPermissionSchema,
AlertsPermissionSchema,
RidesPermissionSchema,
SamsPermissionSchema,
GtfsValidationsPermissionSchema,
HomePermissionSchema,
OrganizationsPermissionSchema,
PerformancePermissionSchema,
PlansPermissionSchema,
RolesPermissionSchema,
StopsPermissionSchema,
UsersPermissionSchema,
VehiclesPermissionSchema,
FaresPermissionSchema,
AnnotationsPermissionSchema,
YearPeriodsPermissionSchema,
HolidaysPermissionSchema,
EventsPermissionSchema,
ZonesPermissionSchema,
TypologiesPermissionSchema,
LinesPermissionSchema,
]);
/**
* PermissionCatalog provides a structured catalog of all available permissions
* in the system, categorized by scope and their respective actions.
* Use it to reference required permissions in components and services.
*/
export class PermissionCatalog {
//
static ALLOW_ALL_FLAG = 'allow_all';
/**
* Generates the complete permission catalog by extracting
* scopes and actions from the defined PermissionSchema.
* @return A catalog object mapping scopes to their actions.
*/
static get all() {
// Initialize catalog object
const catalog = {};
// Iterate over each schema option
for (const schemaOption of PermissionSchema.options) {
// Extract scope name and actions
const scopeName = schemaOption.shape.scope.value;
const actions = schemaOption.shape.action.options;
// Build catalog entry
catalog[scopeName] = {
actions: Object.fromEntries(actions.map((a) => [a, a])),
scope: scopeName,
};
}
// Return the completed catalog
return catalog;
}
/**
* Get a specific permission from a full list by scope and action.
* @param permissionEntries The full list of permissions of the user.
* @param scope The resource scope of the permission to filter by.
* @param action The action of the permission to filter by.
* @returns The filtered Permission object or undefined if not found.
*/
static get(permissionEntries, scope, action) {
return permissionEntries.find((p) => p.scope === scope && p.action === action);
}
/**
* Get all permissions for a given scope and set of actions in one call.
* More efficient than calling hasPermission or hasPermissionResource multiple times.
* @param args Arguments object containing permissions, scope, actions, and optional resource.
* @param args.resource.requireAll If true, uses hasPermissionResourceAll (user must have permission for ALL values in array). If false/undefined, uses hasPermissionResource (user needs permission for ANY value).
* @returns Object with boolean values for each action in the scope.
*/
static getScopePermissions({ actions, permissions, resource, scope }) {
if (!permissions) {
return Object.keys(actions).reduce((acc, key) => ({ ...acc, [key]: false }), {});
}
const result = {};
for (const [key, action] of Object.entries(actions)) {
if (!action || typeof action !== 'string') {
result[key] = false;
continue;
}
if (resource) {
// Use hasPermissionResourceAll if requireAll is true, otherwise use hasPermissionResource
if (resource.requireAll) {
result[key] = this.hasPermissionResourceAll({
action,
permissions,
resource_key: resource.key,
scope,
value: resource.value,
});
}
else {
result[key] = this.hasPermissionResource({
action,
permissions,
resource_key: resource.key,
scope,
value: resource.value,
});
}
}
else {
result[key] = this.hasPermission(permissions, scope, action);
}
}
return result;
}
/**
* Check if a list of permission entries has the requested scope/action pair.
* @param permissionEntries The list of permission entries to check against.
* @param scope The required scope to check.
* @param action The required action to check.
* @returns The permission object or undefined if not found.
*/
static hasPermission(permissionEntries, scope, action) {
return permissionEntries.find(p => p.scope === scope && p.action === action) !== undefined;
}
/**
* Collect allowed values for a resource key across multiple scope/action checks.
*
* Use this when a caller needs to build a database filter before loading
* documents. For example, a list endpoint can combine `lines.read`,
* `lines.update`, and `zones.nav` permissions into a single set of
* allowed `agency_ids`, then query MongoDB with `{ agency_ids: { $in: values } }`.
*
* This complements `hasPermissionResource`: that method answers whether
* a user can access a known resource value, while this method answers which
* resource values the user can access for a set of allowed permissions.
*
* If any matching permission contains PermissionCatalog.ALLOW_ALL_FLAG for
* the resource key, this returns `{ allowAll: true, values: [] }`.
*
* @param permissions The list of permissions from a user or request.
* @param checks The scope/action pairs whose resource values should be merged.
* @param resource_key The permission resource key to collect, e.g. `agency_ids`.
* @returns The aggregated resource access for the requested checks.
*/
static getPermissionResourceAccess(args) {
//
if (!args.permissions?.length || !args.checks.length) {
return { allowAll: false, values: [] };
}
const values = new Set();
for (const check of args.checks) {
const matchingPermissions = args.permissions.filter(permission => permission.scope === check.scope && permission.action === check.action);
for (const permission of matchingPermissions) {
const resourceValues = permission['resources']?.[args.resource_key];
if (!resourceValues)
continue;
if (Array.isArray(resourceValues)) {
if (resourceValues.includes(this.ALLOW_ALL_FLAG)) {
return { allowAll: true, values: [] };
}
for (const resourceValue of resourceValues) {
values.add(resourceValue);
}
continue;
}
if (resourceValues === this.ALLOW_ALL_FLAG) {
return { allowAll: true, values: [] };
}
values.add(resourceValues);
}
}
return {
allowAll: false,
values: [...values],
};
//
}
/**
* Check if a permission exists in a list of permissions, with additional check for a given resource value.
* If a `value` exists in a `resource` of a User `permissions` object that
* matches the given `action` and `scope`. For example, if you want to check if
* a user has access to a specific `agency_id`, you set `value=43` and `resource_key='agency_ids'`.
* If the provided `permissions` object contains the value `43` inside the `scope='plans'`,
* `action='create'` and `resource_key='agency_ids'` the function will return true.
* @param permissions The list of permissions (from a user or request).
* @param value The permission value to check against.
* @param resource_key The key of the resource.
* @param scope The scope of the permission.
* @param action The action of the permission.
* @returns The permission.
*/
static hasPermissionResource({ action, permissions, resource_key, scope, value }) {
//
//
// Return false if no permissions
if (!permissions)
return false;
//
// Find the permission with the given action and scope
const foundPermission = permissions.find(p => p.action === action && p.scope === scope);
if (!foundPermission)
return false;
//
// Check if value exists in the permission.resources[resource_key]
const resourceValues = foundPermission['resources']?.[resource_key];
if (!resourceValues)
return false;
//
// If resourceValues is an Array, check if value is in the array
// or if it contains the ALLOW_ALL_FLAG.
if (Array.isArray(resourceValues) && resourceValues.includes(this.ALLOW_ALL_FLAG))
return true;
//
// If value is an array, check if there's any overlap between value and resourceValues
if (Array.isArray(value)) {
if (Array.isArray(resourceValues)) {
return value.some(v => resourceValues.includes(v));
}
return value.includes(resourceValues);
}
//
// If value is not an array, check if it's in resourceValues
if (Array.isArray(resourceValues) && resourceValues.includes(value))
return true;
//
// If resourceValues is not an Array, check if it is equal to the requested value
if (resourceValues === value)
return true;
//
// Otherwise, return false
return false;
//
}
/**
* Checks whether the user has permission to perform a specific action
* within a specific scope for ALL values in an array.
* This is stricter than `hasPermissionResource` which only requires permission for ANY value.
* Use this for operations like update/delete where the user must have permission for all agencies involved.
* @param permissions The list of permissions (from a user or request).
* @param value The permission value(s) to check against - if array, ALL values must be permitted.
* @param resource_key The key of the resource.
* @param scope The scope of the permission.
* @param action The action of the permission.
* @returns True if user has permission for ALL values, false otherwise.
*/
static hasPermissionResourceAll({ action, permissions, resource_key, scope, value }) {
//
//
// Return false if no permissions
if (!permissions)
return false;
//
// Find the permission with the given action and scope
const foundPermission = permissions.find(p => p.action === action && p.scope === scope);
if (!foundPermission)
return false;
//
// Check if value exists in the permission.resources[resource_key]
const resourceValues = foundPermission['resources']?.[resource_key];
if (!resourceValues)
return false;
//
// If resourceValues contains the ALLOW_ALL_FLAG, user has permission for everything
if (Array.isArray(resourceValues) && resourceValues.includes(this.ALLOW_ALL_FLAG))
return true;
//
// If value is an array, check if ALL elements are in resourceValues
if (Array.isArray(value)) {
if (Array.isArray(resourceValues)) {
return value.every(v => resourceValues.includes(v));
}
return value.every(v => v === resourceValues);
}
//
// If value is not an array, check if it's in resourceValues
if (Array.isArray(resourceValues) && resourceValues.includes(value))
return true;
//
// If resourceValues is not an Array, check if it is equal to the requested value
if (resourceValues === value)
return true;
//
// Otherwise, return false
return false;
//
}
/**
* Sanitizes a list of permissions by removing any entries
* that do not correspond to valid scopes and actions
* defined in the PermissionCatalog.
* @param existingEntries Array of Permission objects to sanitize.
* @return A cleaned array containing only valid permissions.
*/
static sanitize(existingEntries) {
// Create a new array to hold valid permissions
const cleanedPermissions = {};
// Iterate through each permission entry of the user
for (const permissionEntry of existingEntries) {
// Validate the permission entry
const validationResult = PermissionSchema.safeParse(permissionEntry);
if (!validationResult.success)
continue;
// Permission is valid; keep it
cleanedPermissions[`${permissionEntry.scope}:${permissionEntry.action}`] = permissionEntry;
}
// Return the cleaned permissions array
return Object.values(cleanedPermissions);
}
/**
* Sanitizes a list of permissions by removing any entries
* that do not correspond to valid scopes and actions
* defined in the PermissionCatalog.
* @param existingEntries Array of Permission objects to sanitize.
* @return A cleaned array containing only valid permissions.
*/
static updatePermissionResource(permissionEntries, scope, action, resources) {
// Create a copy of the existing permissions
const updatedPermissions = JSON.parse(JSON.stringify(permissionEntries));
// Find the index of the permission to update
const permissionIndex = updatedPermissions.findIndex(p => p.scope === scope && p.action === action);
if (permissionIndex === -1)
return updatedPermissions;
// Update the permission at the found index
updatedPermissions[permissionIndex]['resources'] = { ...updatedPermissions[permissionIndex]['resources'], ...resources };
// Return the updated permissions array
return updatedPermissions;
}
}