All files / realm/blong-access/adapter/db accessAuthorizationList.ts

95.13% Statements 137/144
84.84% Branches 28/33
100% Functions 3/3
95.13% Lines 137/144

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 1451x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 18x 18x 15x 3x 3x 18x 18x 18x 315x 315x 315x 128x 17x 17x 128x 16x 18x 18x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 18x 18x 18x 18x 18x 18x 18x 18x     18x 18x 18x 18x           18x 18x 18x 18x 16x 16x 16x 16x 16x 16x 17x 17x 7x 17x 10x 10x 17x 16x 16x 18x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 149x 149x 9x 9x 9x 149x 149x 10x 10x 10x 10x 10x 10x 10x 10x 10x 16x 16x 16x 16x 1x  
import {type IMeta, handler} from '@feasibleone/blong';
 
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
 
interface CacheEntry {
    actions: Set<string>;
    timestamp: number;
}
 
// ---------------------------------------------------------------------------
// Module-level cache: roleBit → {action names, timestamp}
// ---------------------------------------------------------------------------
 
const _actionCache = new Map<number, CacheEntry>();
let _cacheTtl = 30_000; // 30 000 ms = 30 seconds default
let _lastCleanup = Date.now();
 
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
 
/**
 * Decode a Buffer (or JSON-deserialized Buffer object) into the list of set bit
 * positions.  Each set bit corresponds to a `roleBit` value in `access_role`.
 *
 * Handles both native Buffer and the JSON-deserialized form
 * `{type: 'Buffer', data: number[]}` that arrives over HTTP/MLE transport.
 */
function getSetBits(input: Buffer | {type: 'Buffer'; data: number[]}): number[] {
    const data: number[] = Buffer.isBuffer(input)
        ? [...input]
        : Array.isArray((input as {data?: number[]})?.data)
          ? (input as {data: number[]}).data
          : [];
    const bits: number[] = [];
    for (let byteIndex = 0; byteIndex < data.length; byteIndex++) {
        const byte = data[byteIndex];
        if (byte === 0) continue;
        for (let bitIndex = 0; bitIndex < 8; bitIndex++) {
            if (byte & (1 << bitIndex)) {
                bits.push(byteIndex * 8 + bitIndex);
            }
        }
    }
    return bits;
}
 
/**
 * Normalise an action resource name to a methodId:
 * lowercase, dots removed.
 */
const _methodId = (name: string): string => name.toLowerCase().replaceAll('.', '');
 
// ---------------------------------------------------------------------------
// Handler
// ---------------------------------------------------------------------------
 
export default handler(
    () =>
        async function accessAuthorizationList(
            params: {permissionMap: Buffer; ttl?: number},
            _$meta: IMeta,
        ): Promise<string[]> {
            const qb: any = this.config?.context?.queryBuilder;
            if (!qb) throw new Error('Database not available');
 
            // Allow caller to override TTL
            if (typeof params.ttl === 'number' && params.ttl > 0) {
                _cacheTtl = params.ttl;
            }
 
            // Periodic cache cleanup
            const now = Date.now();
            if (now - _lastCleanup > _cacheTtl * 2) {
                for (const [key, entry] of _actionCache) {
                    if (now - entry.timestamp > _cacheTtl) _actionCache.delete(key);
                }
                _lastCleanup = now;
            }
 
            // Decode which role bits are set in the permissionMap bitmask
            const setBits = getSetBits(params.permissionMap);
            if (setBits.length === 0) return [];
 
            // Separate cached vs uncached role bits
            const uncachedBits: number[] = [];
            const result: string[] = [];
 
            for (const bit of setBits) {
                const cached = _actionCache.get(bit);
                if (cached && now - cached.timestamp < _cacheTtl) {
                    for (const action of cached.actions) result.push(action);
                } else {
                    uncachedBits.push(bit);
                }
            }
 
            // Fetch uncached role→action mappings from DB
            if (uncachedBits.length > 0) {
                const rows: Array<{roleBit: number; resourceName: string}> = await qb
                    .select('r.roleBit', 'cr.resourceName')
                    .from('access_role as r')
                    .join('core_triple as t1', function (this: any) {
                        this.on('t1.subjectId', 'r.roleId').andOnVal(
                            't1.predicateName',
                            'hasCapability',
                        );
                    })
                    .join('core_triple as t2', function (this: any) {
                        this.on('t2.subjectId', 't1.objectId').andOnVal(
                            't2.predicateName',
                            'hasAction',
                        );
                    })
                    .join('core_resource as cr', 'cr.resourceId', 't2.objectId')
                    .whereIn('r.roleBit', uncachedBits)
                    .distinct('cr.resourceName');
 
                // Group actions by roleBit
                const bitGroups = new Map<number, Set<string>>();
                for (const row of rows) {
                    let group = bitGroups.get(row.roleBit);
                    if (!group) {
                        group = new Set<string>();
                        bitGroups.set(row.roleBit, group);
                    }
                    group.add(row.resourceName);
                }
 
                // Update cache and collect results
                for (const bit of uncachedBits) {
                    const group = bitGroups.get(bit);
                    const actionSet = group ?? new Set<string>();
                    _actionCache.set(bit, {actions: actionSet, timestamp: now});
                    for (const a of actionSet) result.push(a);
                }
            }
 
            // Deduplicate and normalise to methodId format
            return [...new Set(result)].map(_methodId);
        },
);