diff --git a/package-lock.json b/package-lock.json index 139c614..57cbf9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1990,9 +1990,9 @@ } }, "node_modules/@lightbase/pull-through-cache": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@lightbase/pull-through-cache/-/pull-through-cache-0.1.2.tgz", - "integrity": "sha512-nTosjLM02B/I30CO60u7EvIc+k0DGqxW5uCMzI58zZ4UjgjG2/RDjdoscOFkTizZ8pK+qKgE1zOWaMdjNVZwsQ==" + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@lightbase/pull-through-cache/-/pull-through-cache-0.2.1.tgz", + "integrity": "sha512-2FtndHp4ywU7VHTFsIhYzsmbdLBmX5Yo2uokiPMNdWswvc0Xp0kTB50lJEeAuXgYoNtUuV66nSeObqkH6C79rg==" }, "node_modules/@lightbasenl/backend": { "resolved": "vendor/backend", @@ -9019,7 +9019,7 @@ "name": "@lightbasenl/backend", "version": "0.50.2", "dependencies": { - "@lightbase/pull-through-cache": "0.1.2", + "@lightbase/pull-through-cache": "0.2.1", "@xmldom/xmldom": "0.8.10", "bcrypt": "5.1.1", "rate-limiter-flexible": "5.0.3", diff --git a/vendor/backend/package.json b/vendor/backend/package.json index cfb7cde..c2be0a1 100644 --- a/vendor/backend/package.json +++ b/vendor/backend/package.json @@ -13,7 +13,7 @@ ], "scripts": {}, "dependencies": { - "@lightbase/pull-through-cache": "0.1.2", + "@lightbase/pull-through-cache": "0.2.1", "@xmldom/xmldom": "0.8.10", "bcrypt": "5.1.1", "rate-limiter-flexible": "5.0.3", @@ -30,5 +30,5 @@ "url": "https://github.com/lightbasenl/platform-components.git", "directory": "packages/backend" }, - "gitHead": "b335a6c8aa6f5e14489b582b02b6fa45beda00b6" + "gitHead": "ec78a715b0421e9b0ba377bc4fc9880fb6323de4" } diff --git a/vendor/backend/src/auth/keycloak-based/events.js b/vendor/backend/src/auth/keycloak-based/events.js index 08cfa18..db76572 100644 --- a/vendor/backend/src/auth/keycloak-based/events.js +++ b/vendor/backend/src/auth/keycloak-based/events.js @@ -408,6 +408,8 @@ export async function authKeycloakBasedVerifyAndReadToken( } catch (e) { throw AppError.validationError( "authKeycloakBased.verifyAndReadToken.invalidToken", + {}, + e, ); } } diff --git a/vendor/backend/src/auth/permissions/controller.js b/vendor/backend/src/auth/permissions/controller.js index f493a48..dfe0f9d 100644 --- a/vendor/backend/src/auth/permissions/controller.js +++ b/vendor/backend/src/auth/permissions/controller.js @@ -1,3 +1,4 @@ +/* eslint-disable jsdoc/check-types */ import { newEventFromEvent } from "@compas/stdlib"; import { backendGetTenantAndUser } from "../../events.js"; import { sql } from "../../services.js"; @@ -19,8 +20,8 @@ import { /** * @typedef {(tenants: - * QueryResultBackendTenant[]) => - * PermissionMandatoryRole[]} PermissionBuildMandatoryRoles + * Array) => + * Array} PermissionBuildMandatoryRoles */ /** diff --git a/vendor/backend/src/auth/user.events.js b/vendor/backend/src/auth/user.events.js index 7b78005..b27ef7f 100644 --- a/vendor/backend/src/auth/user.events.js +++ b/vendor/backend/src/auth/user.events.js @@ -1,3 +1,5 @@ +/* eslint-disable jsdoc/check-types */ + import { AppError, eventStart, @@ -10,6 +12,7 @@ import { import { query, queueWorkerAddJob } from "@compas/store"; import speakeasy from "speakeasy"; import { + onAuthRequireUserCallback, queries, queryPermission, queryRole, @@ -89,7 +92,10 @@ const authQueries = { * @property {boolean|undefined} [requireDigidBased] * @property {boolean|undefined} [requireKeycloakBased] * @property {boolean|undefined} [requirePasswordBased] - * @property {AuthPermissionIdentifier[]|undefined} [requiredPermissions] + * @property {AuthPermissionIdentifier[]|undefined} [requiredPermissions] Require all + * provided permissions + * @property {AuthPermissionIdentifier[]|undefined} [oneOfRequiredPermissions] Require + * one of the provided permissions */ /** @@ -271,12 +277,12 @@ export async function authCreateUser(event, sql, data, options) { * isVerified?: boolean, * }, * withPermissions?: { - * permissions?: AuthPermissionIdentifier[], - * roles?: string[], + * permissions?: Array, + * roles?: Array, * }, * withMultitenant?: { * syncUsersAcrossAllTenants?: boolean, - * tenants?: string[], + * tenants?: Array, * } * }} options * @returns {Promise} @@ -633,8 +639,8 @@ export async function authRequireUser( } if ( - Array.isArray(options.requiredPermissions) && - options.requiredPermissions.length > 0 + Array.isArray(options.requiredPermissions) || + Array.isArray(options.oneOfRequiredPermissions) ) { const permissionSet = new Set(); // @ts-expect-error @@ -645,20 +651,38 @@ export async function authRequireUser( } } - const missingPermissions = []; - for (const requiredPermission of options.requiredPermissions) { - if (!permissionSet.has(requiredPermission)) { - missingPermissions.push(requiredPermission); + if (options.requiredPermissions) { + const missingPermissions = []; + for (const requiredPermission of options.requiredPermissions) { + if (!permissionSet.has(requiredPermission)) { + missingPermissions.push(requiredPermission); + } } - } - if (missingPermissions.length > 0) { - throw AppError.validationError(`${eventKey}.missingPermissions`, { - missingPermissions, - }); + if (missingPermissions.length > 0) { + throw AppError.validationError(`${eventKey}.missingPermissions`, { + missingPermissions, + }); + } + } else if (options.oneOfRequiredPermissions) { + let hasOnePermission = false; + for (const requiredPermission of options.oneOfRequiredPermissions) { + if (permissionSet.has(requiredPermission)) { + hasOnePermission = true; + break; + } + } + + if (!hasOnePermission) { + throw AppError.validationError(`${eventKey}.missingPermissions`, { + missingPermissions: options.oneOfRequiredPermissions, + }); + } } } + onAuthRequireUserCallback(user); + eventStop(event); return user; diff --git a/vendor/backend/src/feature-flag/cache.js b/vendor/backend/src/feature-flag/cache.js index 4089824..67d1579 100644 --- a/vendor/backend/src/feature-flag/cache.js +++ b/vendor/backend/src/feature-flag/cache.js @@ -1,6 +1,7 @@ import { AppError } from "@compas/stdlib"; import { PullThroughCache } from "@lightbase/pull-through-cache"; import { queryFeatureFlag, sql } from "../services.js"; +import { cacheEventToSentryMetric } from "../util.js"; /** * Short TTL feature flag cache. Keeps all flags for 5 seconds in memory, @@ -16,6 +17,9 @@ export const featureFlagCache = new PullThroughCache() }) .withFetcher({ fetcher: featureFlagFetcher, + }) + .withEventCallback({ + callback: cacheEventToSentryMetric("featureFlag"), }); /** diff --git a/vendor/backend/src/feature-flag/events.js b/vendor/backend/src/feature-flag/events.js index 45992f5..99f7219 100644 --- a/vendor/backend/src/feature-flag/events.js +++ b/vendor/backend/src/feature-flag/events.js @@ -158,8 +158,7 @@ export async function featureFlagSetDynamic( if (featureFlagCache.isEnabled()) { // Clear the cache if enabled. This method function is often only used in test code, which most likely disables the cache anyways. - featureFlagCache.disable(); - featureFlagCache.enable(); + featureFlagCache.clearAll(); } eventStop(event); diff --git a/vendor/backend/src/multitenant/cache.js b/vendor/backend/src/multitenant/cache.js index 3b4bde4..0688b7d 100644 --- a/vendor/backend/src/multitenant/cache.js +++ b/vendor/backend/src/multitenant/cache.js @@ -1,6 +1,7 @@ import { isNil, uuid } from "@compas/stdlib"; import { PullThroughCache } from "@lightbase/pull-through-cache"; import { queryTenant, sql, tenantBuilder } from "../services.js"; +import { cacheEventToSentryMetric } from "../util.js"; /** * Frequently sampled tenant cache. @@ -19,6 +20,9 @@ export const tenantCache = new PullThroughCache() }) .withFetcher({ fetcher: tenantFetcher, + }) + .withEventCallback({ + callback: cacheEventToSentryMetric("tenant"), }); /** diff --git a/vendor/backend/src/services.js b/vendor/backend/src/services.js index 649a49f..b77171e 100644 --- a/vendor/backend/src/services.js +++ b/vendor/backend/src/services.js @@ -120,6 +120,12 @@ export let userBuilder = { */ export let tenantBuilder = {}; +/** + * @type {(user: QueryResultAuthUser) => void}} + */ +// eslint-disable-next-line no-unused-vars +export let onAuthRequireUserCallback = (_user) => {}; + /** @type {import("@compas/store").SessionTransportSettings} */ // @ts-expect-error // @@ -162,6 +168,7 @@ export let sessionDurationCallback = () => ({ * @param {{ * userBuilder?: AuthUserQueryBuilder, * tenantBuilder?: BackendTenantQueryBuilder, + * onAuthRequireUserCallback?: (user: QueryResultAuthUser) => void, * shouldPasswordBasedForcePasswordResetAfterSixMonths?: boolean, * shouldPasswordBasedRollingLoginAttemptBlock?: boolean, * shouldPasswordBasedUpdatePasswordRemoveCurrentSession?: boolean, @@ -277,6 +284,9 @@ export async function backendInitServices(other) { tenantBuilder = { ...other.tenantBuilder, ...tenantBuilder }; } + onAuthRequireUserCallback = + other.onAuthRequireUserCallback ?? onAuthRequireUserCallback; + passwordBasedForcePasswordResetAfterSixMonths = other.shouldPasswordBasedForcePasswordResetAfterSixMonths ?? false; passwordBasedRollingLoginAttemptBlock = diff --git a/vendor/backend/src/util.js b/vendor/backend/src/util.js index 964f996..b4e7937 100644 --- a/vendor/backend/src/util.js +++ b/vendor/backend/src/util.js @@ -1,5 +1,24 @@ import { existsSync } from "node:fs"; -import { AppError, isNil, pathJoin } from "@compas/stdlib"; +import { _compasSentryExport, AppError, isNil, pathJoin } from "@compas/stdlib"; + +/** + * Count the different PullThroughCache events as their own metric + * + * @param {string} cacheName + * @returns {(function(string): void)} + */ +export function cacheEventToSentryMetric(cacheName) { + return (event) => { + if (_compasSentryExport?.metrics?.increment) { + _compasSentryExport.metrics.increment(`cache.${event}`, 1, { + tags: { + cacheName, + }, + unit: "none", + }); + } + }; +} /** * Takes an AppError and normalizes it to a 401, to simplify frontend error handling on