import { AuthPermissionError, ForbiddenError, InvalidOperationError, UnimplementedError } from '@tamanu/errors';
import { PERMISSION_SCHEMA } from '@tamanu/constants';
import { getAbilityForUser, getPermissionsForRoles } from './rolesToPermissions';
// copied from casl source as it's not exported directly
// (we could use casl's ForbiddenError.throwUnlessCan function except there's some
// strange error going on where the export appears to strip properties assigned via
// Object.defineProperty - probably straightforward enough to find a workaround, but
// we only need this one function out of it so it's not worth it.
export function getSubjectName(subject) {
    if (!subject || typeof subject === 'string') {
        return subject;
    }
    if (typeof subject === 'object' && Object.hasOwn(subject, '__caslSubjectType__')) {
        return subject.__caslSubjectType__;
    }
    const Type = typeof subject === 'object' ? subject.constructor : subject;
    return Type.modelName || Type.name;
}
function assertValidPermissionSchema(subject, action) {
    const noun = getSubjectName(subject);
    if (noun) {
        const allowedVerbs = PERMISSION_SCHEMA[noun];
        if (!allowedVerbs) {
            throw new InvalidOperationError(`Permissions for noun "${noun}" are not defined in the schema.`);
        }
        if (!allowedVerbs.includes(action)) {
            throw new InvalidOperationError(`Verb "${action}" is not valid for noun "${noun}"`);
        }
    }
}
export async function constructPermission(req, res, next) {
    try {
        const impersonateRoleId = req.impersonateRoleId;
        // eslint-disable-next-line require-atomic-updates
        req.ability = await getAbilityForUser(req.models, req.user, {
            impersonateRoleId
        });
        next();
    } catch (e) {
        next(e);
    }
}
const checkIfHasPermission = (req, action, subject)=>{
    if (req.flagPermissionChecked) {
        req.flagPermissionChecked();
    }
    // allow a null permission to let things through - this means all endpoints
    // still need an explicit permission check, even if it's a null one!
    if (!action) {
        return;
    }
    // Validate noun/verb against PERMISSION_SCHEMA in dev/test so devs are forced
    // to register new combinations. Skipped in production to avoid blocking real
    // actions as there is a risk that the schema is incomplete.
    if (![
        'production',
        'staging'
    ].includes(process.env.NODE_ENV)) {
        assertValidPermissionSchema(subject, action);
    }
    const { ability } = req;
    if (!ability) {
        throw new AuthPermissionError(`${action} ${subject}`);
    }
    return ability.can(action, subject);
};
// this middleware goes at the top of the middleware stack
export function ensurePermissionCheck(req, res, next) {
    const originalResSend = res.send;
    req.checkPermission = (action, subject)=>{
        const hasPermission = checkIfHasPermission(req, action, subject);
        if (!hasPermission) {
            const rule = req.ability.relevantRuleFor(action, subject);
            const reason = rule && rule.reason || `No permission to perform action "${action}" on "${getSubjectName(subject)}"`;
            throw new ForbiddenError(reason);
        }
    };
    req.checkForOneOfPermissions = (actions, subject)=>{
        const permissionChecks = actions.map((action)=>{
            try {
                return checkIfHasPermission(req, action, subject);
            } catch (error) {
                // If the verb is invalid for this noun, treat it as "no permission"
                // rather than throwing, so we can check the remaining verbs
                if (error instanceof InvalidOperationError) {
                    return false;
                }
                throw error;
            }
        });
        const hasPermission = permissionChecks.some(Boolean);
        if (!hasPermission) {
            const reason = `No permission to perform any of actions "${actions.join(', ')}" on "${getSubjectName(subject)}"`;
            throw new ForbiddenError(reason);
        }
    };
    req.checkListOrReadPermission = (subject)=>{
        req.checkForOneOfPermissions([
            'list',
            'read'
        ], subject);
    };
    req.flagPermissionChecked = ()=>{
        res.send = originalResSend;
    };
    res.send = ()=>{
        res.send = originalResSend;
        throw new UnimplementedError('No permission check was implemented for this endpoint.');
    };
    next();
}
// eslint-disable-next-line no-unused-vars
export async function getPermissions(req, res, _next) {
    const { user, models } = req;
    // `flagPermissionChecked` is only set when the route is mounted under
    // `ensurePermissionCheck` (facility-server). On central-server the
    // `/permissions` endpoint lives in `authModule`, which is mounted before
    // that middleware, so guard the call.
    if (req.flagPermissionChecked) {
        req.flagPermissionChecked();
    }
    const roleString = req.impersonateRoleId && user.role === 'admin' ? req.impersonateRoleId : user.role;
    const permissions = await getPermissionsForRoles(models, roleString);
    res.send({
        permissions
    });
}

//# sourceMappingURL=middleware.js.map