@allma/core-sdk
Version:
Core SDK with shared utilities (logging, auth, S3 utils) for building on the Allma serverless AI orchestration platform.
122 lines • 7.16 kB
JavaScript
import { buildErrorResponse, createApiGatewayResponse } from './apiResponseBuilder.js';
import { AdminRole, AdminUserPermissionsSchema, getDefaultPermissionsForRole, isInCognitoGroup } from '@allma/core-types';
import { log_debug, log_error, log_info, log_warn } from './logger.js';
// Constants for Cognito group and custom attribute names
export const ADMIN_COGNITO_GROUP_NAME = 'Admins';
export const CUSTOM_ADMIN_ROLES_ATTRIBUTE = 'custom:admin_roles';
/**
* Extracts and validates authentication and authorization information from an API Gateway event.
* This function is now a pure context extractor and does not enforce group membership.
*
* @param event The APIGatewayProxyEventV2.
* @param correlationId Optional correlation ID for logging.
* @param options Optional configuration for the custom roles attribute.
* @returns An AuthContext object.
* @throws Error if unauthorized (e.g., missing claims).
*/
export function getAuthContext(event, correlationId, options) {
const claims = event.requestContext.authorizer?.jwt?.claims;
if (!claims) {
log_warn('Unauthorized: No JWT claims found in event.requestContext.authorizer.', {}, correlationId);
throw new Error('Unauthorized');
}
const userId = claims.sub;
const username = claims.email;
const cognitoGroupsClaimValue = claims['cognito:groups'];
let cognitoGroups = [];
if (Array.isArray(cognitoGroupsClaimValue)) {
cognitoGroups = cognitoGroupsClaimValue.filter(group => typeof group === 'string');
log_debug('Parsed cognito:groups from JWT array.', { rawGroups: cognitoGroupsClaimValue, parsed: cognitoGroups }, correlationId);
}
else if (typeof cognitoGroupsClaimValue === 'string') {
let processableString = cognitoGroupsClaimValue.trim();
if (processableString.startsWith('[') && processableString.endsWith(']')) {
processableString = processableString.substring(1, processableString.length - 1);
}
cognitoGroups = processableString.split(',').map(group => group.trim()).filter(group => group.length > 0);
log_debug('Parsed cognito:groups from JWT string.', { rawString: cognitoGroupsClaimValue, processedString: processableString, parsed: cognitoGroups }, correlationId);
}
else if (cognitoGroupsClaimValue !== undefined && cognitoGroupsClaimValue !== null) {
log_warn('cognito:groups claim is present but is not an array or string.', { type: typeof cognitoGroupsClaimValue, value: cognitoGroupsClaimValue }, correlationId);
}
const customRolesAttr = options?.customRolesAttribute || CUSTOM_ADMIN_ROLES_ATTRIBUTE;
const customAdminRolesString = claims[customRolesAttr];
let adminUserPermissions = { roles: [], permissions: [] };
if (typeof customAdminRolesString === 'string') {
try {
const parsedPermissions = JSON.parse(customAdminRolesString);
const validationResult = AdminUserPermissionsSchema.safeParse(parsedPermissions);
if (validationResult.success) {
adminUserPermissions = validationResult.data;
}
else {
log_warn(`Invalid ${customRolesAttr} attribute format for user.`, { userId, errors: validationResult.error.flatten().fieldErrors }, correlationId);
}
}
catch (e) {
log_warn(`Failed to parse ${customRolesAttr} attribute for user.`, { userId, customAdminRolesString, error: e.message }, correlationId);
}
}
const isSuperAdmin = adminUserPermissions.roles.includes(AdminRole.SUPER_ADMIN);
const effectivePermissions = new Set();
adminUserPermissions.roles.forEach(role => {
getDefaultPermissionsForRole(role).forEach(p => effectivePermissions.add(p));
});
adminUserPermissions.permissions.forEach(p => effectivePermissions.add(p));
const authContext = {
userId,
username,
cognitoGroups,
adminPermissions: {
roles: adminUserPermissions.roles,
permissions: Array.from(effectivePermissions),
},
isAuthenticated: true,
isSuperAdmin,
hasPermission: (permission) => (isSuperAdmin || effectivePermissions.has(permission)),
};
log_info(`Admin API request auth context established for user: ${username} (ID: ${userId})`, { groups: cognitoGroups, roles: authContext.adminPermissions.roles, permissions: authContext.adminPermissions.permissions.join(',') }, correlationId);
return authContext;
}
/**
* Higher-order function to wrap Lambda handlers with authentication and authorization.
* This middleware enforces membership in a specified Cognito group and provides an
* AuthContext for granular permission checks within the handler.
*
* @param handler The original Lambda handler function, now receiving an `AuthContext` object.
* @param options Optional configuration to specify the required group and custom roles attribute.
* @returns A new Lambda handler function compatible with API Gateway.
*/
export function withAdminAuth(handler, options) {
return async (event) => {
const correlationId = event.requestContext.requestId || event.headers['X-Correlation-Id'] || 'UNKNOWN';
try {
// Step 1: Extract context. This will throw 'Unauthorized' if claims are missing.
const authContext = getAuthContext(event, correlationId, options?.customRolesAttribute
? { customRolesAttribute: options.customRolesAttribute }
: undefined);
// Step 2: Enforce group membership.
const requiredGroup = options?.requiredGroup || ADMIN_COGNITO_GROUP_NAME;
if (!isInCognitoGroup(authContext.cognitoGroups, requiredGroup)) {
log_warn(`Forbidden: User '${authContext.username}' (ID: ${authContext.userId}) is not a member of the required '${requiredGroup}' group. User groups: ${authContext.cognitoGroups.join(', ')}`, {}, correlationId);
throw new Error('Forbidden');
}
// Step 3: Call the original handler if authorization succeeds.
return await handler(event, authContext);
}
catch (error) {
log_error(`Authorization error in Lambda middleware: ${error.message}`, { errorName: error.name, stack: error.stack }, correlationId);
log_error(`Authorization error | event is : ${JSON.stringify(event)}`, {}, correlationId);
if (error.message === 'Unauthorized') {
return createApiGatewayResponse(401, buildErrorResponse('Unauthorized: Authentication token is missing or invalid.', 'UNAUTHORIZED'), correlationId);
}
else if (error.message === 'Forbidden') {
return createApiGatewayResponse(403, buildErrorResponse('Forbidden: You do not have access to this resource.', 'FORBIDDEN'), correlationId);
}
else {
return createApiGatewayResponse(500, buildErrorResponse('Internal Server Error: Failed to process authentication.', 'SERVER_ERROR'), correlationId);
}
}
};
}
//# sourceMappingURL=authUtils.js.map