From e689338f0bf66d2689205fbc5e022e8ca62286ce Mon Sep 17 00:00:00 2001 From: Otavio Jacobi Date: Mon, 30 Sep 2024 20:41:28 -0300 Subject: [PATCH] WIP Change-type: patch --- package-lock.json | 33 ++++--- package.json | 4 +- src/lib/auth.ts | 243 +++++++++++++++++++++++++++++++++++++--------- 3 files changed, 222 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0bc80b660..758283d6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@balena/env-parsing": "^1.2.0", "@balena/es-version": "^1.0.3", "@balena/node-metrics-gatherer": "^6.0.3", - "@balena/pinejs": "^19.0.2", + "@balena/pinejs": "19.1.0-build-compile-auth-0b51d25e2044f45e8be9519886abba8ce4548ac9-2", "@balena/pinejs-webresource-cloudfront": "^0.2.1", "@sentry/node": "^8.30.0", "@types/basic-auth": "^1.1.8", @@ -78,7 +78,7 @@ "on-finished": "^2.4.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", - "pinejs-client-core": "^8.0.1", + "pinejs-client-core": "8.1.0-build-add-support-to-odata-fns-befd5b25dc3c8f2d663c138255c68e4108abf8c9-1", "proxy-addr": "^2.0.7", "randomstring": "^1.3.0", "rate-limiter-flexible": "^5.0.3", @@ -1441,10 +1441,9 @@ } }, "node_modules/@balena/pinejs": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/@balena/pinejs/-/pinejs-19.0.2.tgz", - "integrity": "sha512-Ismg+VJvzgZm8NEO1BIFazHxL0dG5qSxg9AsavTx7emRt6L/rX1ZgUynz7ExPtFJglnF7nvvkfT/e/oZj4nhaA==", - "license": "Apache-2.0", + "version": "19.1.0-build-compile-auth-0b51d25e2044f45e8be9519886abba8ce4548ac9-2", + "resolved": "https://registry.npmjs.org/@balena/pinejs/-/pinejs-19.1.0-build-compile-auth-0b51d25e2044f45e8be9519886abba8ce4548ac9-2.tgz", + "integrity": "sha512-VbPVnLNVcLNxL2g10KDW8D+JM2xrY8HOeATf/VNTN61glVa1Famf8FNuS6lKLI3bAPhRMrJOed1Pqex72Zk9Pg==", "dependencies": { "@balena/abstract-sql-compiler": "^9.2.0", "@balena/abstract-sql-to-typescript": "^4.0.6", @@ -1481,7 +1480,7 @@ "express-session": "^1.18.0", "lodash": "^4.17.21", "memoizee": "^0.4.17", - "pinejs-client-core": "^8.0.1", + "pinejs-client-core": "8.1.0-build-add-support-to-odata-fns-befd5b25dc3c8f2d663c138255c68e4108abf8c9-1", "randomstring": "^1.3.0", "typed-error": "^3.2.2" }, @@ -12370,10 +12369,9 @@ } }, "node_modules/pinejs-client-core": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-8.0.1.tgz", - "integrity": "sha512-kr5cAq+kGJ7xTYgADGQTDVEuEFoyGXkGscvqOyA4bZDnCyUxx0YjwPEosprM+CihkuxQy5FOAVM5iU4+C5u+4g==", - "license": "MIT", + "version": "8.1.0-build-add-support-to-odata-fns-befd5b25dc3c8f2d663c138255c68e4108abf8c9-1", + "resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-8.1.0-build-add-support-to-odata-fns-befd5b25dc3c8f2d663c138255c68e4108abf8c9-1.tgz", + "integrity": "sha512-mqY9v4DQoInGl+wBkF5ClVahJnfsnT5EfKHWUdBAy6i+eqFMoQu3+EadHYza7FdSSSOOkd9dt+qOF+CJ1Vk9lA==", "dependencies": { "@balena/abstract-sql-to-typescript": "^4.0.0" }, @@ -12479,6 +12477,19 @@ "node": "*" } }, + "node_modules/pinejs-client-supertest/node_modules/pinejs-client-core": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/pinejs-client-core/-/pinejs-client-core-8.0.1.tgz", + "integrity": "sha512-kr5cAq+kGJ7xTYgADGQTDVEuEFoyGXkGscvqOyA4bZDnCyUxx0YjwPEosprM+CihkuxQy5FOAVM5iU4+C5u+4g==", + "dev": true, + "dependencies": { + "@balena/abstract-sql-to-typescript": "^4.0.0" + }, + "engines": { + "node": ">=10.0.0", + "npm": ">=6.0.0" + } + }, "node_modules/pinejs-client-supertest/node_modules/type-detect": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", diff --git a/package.json b/package.json index 74c959b76..9065aca2b 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "@balena/env-parsing": "^1.2.0", "@balena/es-version": "^1.0.3", "@balena/node-metrics-gatherer": "^6.0.3", - "@balena/pinejs": "^19.0.2", + "@balena/pinejs": "19.1.0-build-compile-auth-0b51d25e2044f45e8be9519886abba8ce4548ac9-2", "@balena/pinejs-webresource-cloudfront": "^0.2.1", "@sentry/node": "^8.30.0", "@types/basic-auth": "^1.1.8", @@ -113,7 +113,7 @@ "on-finished": "^2.4.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", - "pinejs-client-core": "^8.0.1", + "pinejs-client-core": "8.1.0-build-add-support-to-odata-fns-befd5b25dc3c8f2d663c138255c68e4108abf8c9-1", "proxy-addr": "^2.0.7", "randomstring": "^1.3.0", "rate-limiter-flexible": "^5.0.3", diff --git a/src/lib/auth.ts b/src/lib/auth.ts index d50d39064..eecfe72e2 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -2,14 +2,16 @@ // Declares permissions assigned to default roles and API keys // -import type { sbvrUtils } from '@balena/pinejs'; - +import { sbvrUtils } from '@balena/pinejs'; +import type Model from '../balena-model.js'; import { API_VPN_SERVICE_API_KEY, IGNORE_FROZEN_DEVICE_PERMISSIONS, VPN_GUEST_API_KEY, VPN_SERVICE_API_KEY, } from './config.js'; +import { Filter } from 'pinejs-client-core'; +const { api } = sbvrUtils; const defaultWritePerms = ['create', 'update', 'delete'] as const; @@ -19,8 +21,36 @@ const writePerms = ( access: ReadonlyArray<(typeof defaultWritePerms)[number]> = defaultWritePerms, ): string[] => access.map((verb) => `${resource}.${verb}?${filter}`); +const compileAuth = ( + resource: TResource, + access: string, + $filter?: Filter +) => { + const options = $filter ? { $filter } : undefined; + return api.resin.compileAuth({ + modelName: 'resin', + resource, + access, + options + }); +}; + +const actorId = { '@': '__ACTOR_ID' } as const; +const matchesActorFilter = { actor: actorId } as const; + +const matchesUserFilter = { + user: { + $any: { + $alias: 'u', + $expr: { + u: matchesActorFilter, + }, + }, + }, +} as const; + const matchesActor = 'actor eq @__ACTOR_ID'; -const matchesUser = `user/any(u:u/${matchesActor})`; +// const matchesUser = `user/any(u:u/${matchesActor})`; const matchesNonFrozenDeviceActor = (alias = '') => { if (alias) { alias += '/'; @@ -30,6 +60,12 @@ const matchesNonFrozenDeviceActor = (alias = '') => { : ''; return `${alias}${matchesActor}${andIsNotFrozen}`; }; + +const matchesNonFrozenDeviceActorFilter = IGNORE_FROZEN_DEVICE_PERMISSIONS ? { + ...matchesActorFilter, + is_frozen: false, +} : matchesActorFilter; + const ownsDevice = `owns__device/any(d:d/${matchesActor})`; export const ROLES: { @@ -40,52 +76,169 @@ export const ROLES: { ], // also default-user (see below) 'named-user-api-key': [ - 'resin.actor.delete?id eq @__ACTOR_ID', - 'resin.api_key.read?is_of__actor eq @__ACTOR_ID', - 'resin.application.all', - 'resin.device_type.read', - 'resin.device_type_alias.read', - 'resin.cpu_architecture.read', - 'resin.application_config_variable.all', - 'resin.application_environment_variable.all', - 'resin.application_tag.all', - 'resin.application_type.all', - 'resin.device.all', - 'resin.device.tunnel-22222', - 'resin.device_config_variable.all', - 'resin.device_environment_variable.all', - 'resin.device_tag.all', - 'resin.device_service_environment_variable.all', - 'resin.image.all', - 'resin.image__is_part_of__release.all', - 'resin.image_environment_variable.all', - 'resin.image_install.all', - 'resin.image_label.all', - 'resin.organization.read', - 'resin.organization_membership.read', - 'resin.release.all', - 'resin.release_tag.all', - 'resin.service.all', - 'resin.service_environment_variable.all', - 'resin.service_install.all', - "resin.service_instance.read?service_type eq 'vpn'", - 'resin.service_label.all', - 'resin.user.read', - `resin.user__has__public_key.all?${matchesUser}`, - 'resin.release_asset.all', + // 'resin.actor.delete?id eq @__ACTOR_ID', + compileAuth('actor', 'delete', { + id: { + '@': '__ACTOR_ID', + }, + }), + + // 'resin.api_key.read?is_of__actor eq @__ACTOR_ID', + compileAuth('api_key', 'read', { + is_of__actor: { + '@': '__ACTOR_ID', + }, + }), + + // 'resin.application.all', + compileAuth('application', 'all'), + + // 'resin.device_type.read', + compileAuth('device_type', 'read'), + + // 'resin.device_type_alias.read', + compileAuth('device_type_alias', 'read'), + + // 'resin.cpu_architecture.read', + compileAuth('cpu_architecture', 'read'), + + // 'resin.application_config_variable.all', + compileAuth('application_config_variable', 'all'), + + // 'resin.application_environment_variable.all', + compileAuth('application_environment_variable', 'all'), + + // 'resin.application_tag.all', + compileAuth('application_tag', 'all'), + + // 'resin.application_type.all', + compileAuth('application_type', 'all'), + + // 'resin.device.all', + compileAuth('device', 'all'), + + // 'resin.device.tunnel-22222', + compileAuth('device', 'tunnel-22222'), + + // 'resin.device_config_variable.all', + compileAuth('device_config_variable', 'all'), + + // 'resin.device_environment_variable.all', + compileAuth('device_environment_variable', 'all'), + + // 'resin.device_tag.all', + compileAuth('device_tag', 'all'), + + // 'resin.device_service_environment_variable.all', + compileAuth('device_service_environment_variable', 'all'), + + // 'resin.image.all', + compileAuth('image', 'all'), + + // 'resin.image__is_part_of__release.all', + compileAuth('image__is_part_of__release', 'all'), + + // 'resin.image_environment_variable.all', + compileAuth('image_environment_variable', 'all'), + + // 'resin.image_install.all', + compileAuth('image_install', 'all'), + + // 'resin.image_label.all', + compileAuth('image_label', 'all'), + + // 'resin.organization.read', + compileAuth('organization', 'read'), + + // 'resin.organization_membership.read', + compileAuth('organization_membership', 'read'), + + // 'resin.release.all', + compileAuth('release', 'all'), + + // 'resin.release_tag.all', + compileAuth('release_tag', 'all'), + + // 'resin.service.all', + compileAuth('service', 'all'), + + // 'resin.service_environment_variable.all', + compileAuth('service_environment_variable', 'all'), + + // 'resin.service_install.all', + compileAuth('service_install', 'all'), + + // "resin.service_instance.read?service_type eq 'vpn'", + compileAuth('service_instance', 'read', { service_type: 'vpn' }), + + // 'resin.service_label.all', + compileAuth('service_label', 'all'), + + // 'resin.user.read', + compileAuth('user', 'read'), + + // `resin.user__has__public_key.all?${matchesUser}`, + compileAuth('user__has__public_key', 'all', matchesUserFilter), + + // 'resin.release_asset.all', + compileAuth('release_asset', 'all'), ], }; +export const canAccess = { + $fn: { + $scope: 'Auth', + $method: 'canAccess' + } +} as const; + export const DEVICE_API_KEY_PERMISSIONS = [ - 'resin.device_type.read?describes__device/canAccess()', - `resin.device.read?${matchesNonFrozenDeviceActor()}`, - `resin.device.update?${matchesNonFrozenDeviceActor()}`, - 'resin.application.read?owns__device/canAccess() or (is_public eq true and is_for__device_type/any(dt:dt/describes__device/canAccess()))', - 'resin.application_tag.read?application/canAccess()', - 'resin.device_config_variable.read?device/canAccess()', - `resin.device_config_variable.create?device/any(d:${matchesNonFrozenDeviceActor( - 'd', - )})`, + // 'resin.device_type.read?describes__device/canAccess()', + compileAuth('device_type', 'read', { describes__device: canAccess }), + + // `resin.device.read?${matchesNonFrozenDeviceActorFilter()}`, + // `resin.device.update?${matchesNonFrozenDeviceActorFilter()}`, + + // 'resin.application.read?owns__device/canAccess() or (is_public eq true and is_for__device_type/any(dt:dt/describes__device/canAccess()))', + compileAuth('application', 'read', { + $or: { + owns__device: canAccess, + $and: { + is_public: true, + is_for__device_type: { + $any: { + $alias: 'dt', + $expr: { + dt: { + describes__device: canAccess, + }, + }, + }, + }, + }, + } + }), + + // 'resin.application_tag.read?application/canAccess()', + compileAuth('application_tag', 'read', { application: canAccess }), + + // 'resin.device_config_variable.read?device/canAccess()', + compileAuth('device_config_variable', 'read', { device: canAccess }), + + // `resin.device_config_variable.create?device/any(d:${matchesNonFrozenDeviceActor( + // 'd', + // )})`, + compileAuth('device_config_variable', 'create', { + device: { + $any: { + $alias: 'd', + $expr: { + d: matchesNonFrozenDeviceActorFilter, + } + } + } + }), + `resin.device_config_variable.update?device/any(d:${matchesNonFrozenDeviceActor( 'd', )})`,