diff --git a/api/utils.ts b/api/utils.ts index 8c17128e..9769ed6a 100644 --- a/api/utils.ts +++ b/api/utils.ts @@ -13,3 +13,5 @@ export {default as axiosInstance} from '../src/utils/axios'; export {objectKeys} from '../src/utils/utility-types'; export {whereBuilderInterTenantGetEntries} from '../src/db/models/navigation/utils'; + +export {getEnvCert, getEnvVariable, getEnvTokenVariable, isTrueArg} from '../src/utils/env-utils'; diff --git a/package-lock.json b/package-lock.json index 694319c6..609bfcdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "crc-32": "1.2.0", "db-errors": "^0.2.3", "dotenv": "^8.2.0", + "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", "lodash": "^4.17.21", "minimist": "^1.2.5", @@ -39,8 +40,9 @@ "@trendyol/jest-testcontainers": "^2.1.1", "@types/express": "^4.17.15", "@types/jest": "^29.5.12", + "@types/jsonwebtoken": "^9.0.7", "@types/lodash": "^4.17.4", - "@types/node": "^18.14.4", + "@types/node": "^20.17.12", "@types/pg": "^7.14.11", "@types/supertest": "^2.0.10", "@types/swagger-ui-express": "^4.1.7", @@ -2166,6 +2168,16 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz", @@ -2179,9 +2191,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.17.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.12.tgz", - "integrity": "sha512-d6xjC9fJ/nSnfDeU0AMDsaJyb1iHsqCSOdi84w4u+SlN/UgQdY5tRhpMzaFYsI4mnpvgTivEaQd0yOUhAtOnEQ==" + "version": "20.17.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.12.tgz", + "integrity": "sha512-vo/wmBgMIiEA23A/knMfn/cf37VnuF52nZh5ZoW0GWt4e4sxNquibrMRJ7UQsA06+MBx9r/H1jsI9grYjQCQlw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } }, "node_modules/@types/pg": { "version": "7.14.11", @@ -2260,6 +2276,23 @@ "@types/node": "*" } }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.71", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.71.tgz", + "integrity": "sha512-evXpcgtZm8FY4jqBSN8+DmOTcVkkvTmAayeo4Wf3m1xAruyVGzGuDh/Fb/WWX2yLItUiho42ozyJjB0dw//Tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -3433,6 +3466,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4351,6 +4390,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -8573,6 +8621,46 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -8588,6 +8676,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -8963,11 +9072,40 @@ "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -8975,6 +9113,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", @@ -11956,6 +12100,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index 0a0e20ba..ee75c6f7 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "crc-32": "1.2.0", "db-errors": "^0.2.3", "dotenv": "^8.2.0", + "jsonwebtoken": "^9.0.2", "knex": "^3.1.0", "lodash": "^4.17.21", "minimist": "^1.2.5", @@ -41,8 +42,9 @@ "@trendyol/jest-testcontainers": "^2.1.1", "@types/express": "^4.17.15", "@types/jest": "^29.5.12", + "@types/jsonwebtoken": "^9.0.7", "@types/lodash": "^4.17.4", - "@types/node": "^18.14.4", + "@types/node": "^20.17.12", "@types/pg": "^7.14.11", "@types/supertest": "^2.0.10", "@types/swagger-ui-express": "^4.1.7", diff --git a/scripts/setup-dev-env.js b/scripts/setup-dev-env.js index 65b82411..91ffece8 100755 --- a/scripts/setup-dev-env.js +++ b/scripts/setup-dev-env.js @@ -16,7 +16,6 @@ const appPath = path.join(__dirname, '..'); const templateFilePath = path.join(appPath, `dev/env/${templateName}`); let templateContent = readFileSync(templateFilePath).toString(); -const secretsSection = `${SECRETS_SECTION_START}\n${templateContent}\n${SECRETS_SECTION_END}`; if (envName === 'development') { try { @@ -29,6 +28,8 @@ if (envName === 'development') { } } +const templateSection = `${SECRETS_SECTION_START}\n${templateContent}\n${SECRETS_SECTION_END}`; + let currentEnv; try { @@ -39,4 +40,4 @@ try { currentEnv = `${SECRETS_SECTION_START}\n${SECRETS_SECTION_END}`; } -writeFileSync(path.join(appPath, '.env'), currentEnv.replace(REPLACE_REGEXP, secretsSection)); +writeFileSync(path.join(appPath, '.env'), currentEnv.replace(REPLACE_REGEXP, templateSection)); diff --git a/src/components/api-docs/utils.ts b/src/components/api-docs/utils.ts index c1e15f4b..338dc967 100644 --- a/src/components/api-docs/utils.ts +++ b/src/components/api-docs/utils.ts @@ -18,7 +18,7 @@ export const getAdditionalHeaders = ( config.appAuthPolicy === AuthPolicy.disabled || routeDescription.authPolicy === AuthPolicy.disabled; - if (!authDisabled && config.zitadelEnabled) { + if (!authDisabled && (config.zitadelEnabled || config.isAuthEnabled)) { headers.push( z.strictObject({ [AUTHORIZATION_HEADER]: z.string(), diff --git a/src/components/auth/constants/error-constants.ts b/src/components/auth/constants/error-constants.ts new file mode 100644 index 00000000..8015a29c --- /dev/null +++ b/src/components/auth/constants/error-constants.ts @@ -0,0 +1,5 @@ +// use `US.AUTH.<name>` prefix for most cases + +export const AUTH_ERRORS = { + UNAUTHORIZED_ACCESS: 'US.AUTH.UNAUTHORIZED_ACCESS', +}; diff --git a/src/components/auth/constants/role.ts b/src/components/auth/constants/role.ts new file mode 100644 index 00000000..94a271f5 --- /dev/null +++ b/src/components/auth/constants/role.ts @@ -0,0 +1,7 @@ +export enum UserRole { + Editor = 'datalens.editor', + Admin = 'datalens.admin', + Viewer = 'datalens.viewer', + Visitor = 'datalens.visitor', + Creator = 'datalens.creator', +} diff --git a/src/components/auth/middlewares/app-auth.ts b/src/components/auth/middlewares/app-auth.ts new file mode 100644 index 00000000..33f1e865 --- /dev/null +++ b/src/components/auth/middlewares/app-auth.ts @@ -0,0 +1,52 @@ +import {NextFunction, Request, Response} from '@gravity-ui/expresskit'; +import jwt, {type Algorithm} from 'jsonwebtoken'; + +import {AUTHORIZATION_HEADER, DL_AUTH_HEADER_KEY} from '../../../const/common'; +import {AUTH_ERRORS} from '../constants/error-constants'; +import type {AccessTokenPayload} from '../types/token'; + +const ALGORITHMS: Algorithm[] = ['PS256']; + +export const appAuth = async (req: Request, res: Response, next: NextFunction) => { + req.ctx.log('AUTH'); + + const authorization = req.headers[AUTHORIZATION_HEADER]; + + if (authorization) { + const accessToken = authorization.slice(DL_AUTH_HEADER_KEY.length + 1); + + if (accessToken) { + try { + req.ctx.log('CHECK_ACCESS_TOKEN'); + + const {userId, sessionId, roles} = jwt.verify( + accessToken, + req.ctx.config.authTokenPublicKey || '', + { + algorithms: ALGORITHMS, + }, + ) as AccessTokenPayload; + + req.originalContext.set('user', { + userId, + sessionId, + accessToken, + roles, + }); + + // for ctx info + res.locals.userId = userId; + res.locals.login = userId; + + req.ctx.log('CHECK_ACCESS_TOKEN_SUCCESS'); + + next(); + return; + } catch (err) { + req.ctx.logError('CHECK_ACCESS_TOKEN_ERROR', err); + } + } + } + + res.status(401).send({code: AUTH_ERRORS.UNAUTHORIZED_ACCESS, message: 'Unauthorized access'}); +}; diff --git a/src/components/auth/types/token.ts b/src/components/auth/types/token.ts new file mode 100644 index 00000000..965f2208 --- /dev/null +++ b/src/components/auth/types/token.ts @@ -0,0 +1,12 @@ +import type {UserRole} from '../constants/role'; + +export interface ExpirableTokenPayload { + iat: number; + exp: number; +} + +export interface AccessTokenPayload extends ExpirableTokenPayload { + userId: string; + sessionId: string; + roles: `${UserRole}`[]; +} diff --git a/src/components/auth/types/user.ts b/src/components/auth/types/user.ts new file mode 100644 index 00000000..e66f6d26 --- /dev/null +++ b/src/components/auth/types/user.ts @@ -0,0 +1,8 @@ +import type {UserRole} from '../constants/role'; + +export interface CtxUser { + userId: string; + sessionId: string; + accessToken: string; + roles: `${UserRole}`[]; +} diff --git a/src/components/zod/custom-types/string-boolean.ts b/src/components/zod/custom-types/string-boolean.ts index d3063d6f..498504fc 100644 --- a/src/components/zod/custom-types/string-boolean.ts +++ b/src/components/zod/custom-types/string-boolean.ts @@ -1,6 +1,5 @@ import {z} from 'zod'; -import Utils from '../../../utils'; +import {isTrueArg} from '../../../utils/env-utils'; -export const stringBoolean = () => - z.string().toLowerCase().transform(Utils.isTrueArg).pipe(z.boolean()); +export const stringBoolean = () => z.string().toLowerCase().transform(isTrueArg).pipe(z.boolean()); diff --git a/src/configs/common.ts b/src/configs/common.ts index 2c4b8f59..e911b379 100644 --- a/src/configs/common.ts +++ b/src/configs/common.ts @@ -2,7 +2,11 @@ import {AuthPolicy} from '@gravity-ui/expresskit'; import {AppConfig} from '@gravity-ui/nodekit'; import {APP_NAME, US_MASTER_TOKEN_HEADER} from '../const'; -import Utils from '../utils'; +import {getEnvCert, getEnvTokenVariable, getEnvVariable, isTrueArg} from '../utils/env-utils'; + +const isZitadelEnabled = isTrueArg(getEnvVariable('ZITADEL')); +const isAuthEnabled = isTrueArg(getEnvVariable('AUTH_ENABLED')); +const isAuthServiceEnabled = isZitadelEnabled || isAuthEnabled; export default { appName: APP_NAME, @@ -18,30 +22,32 @@ export default { extended: false, }, - appAuthPolicy: Utils.isTrueArg(Utils.getEnvVariable('ZITADEL')) - ? AuthPolicy.required - : AuthPolicy.disabled, + appAuthPolicy: isAuthServiceEnabled ? AuthPolicy.required : AuthPolicy.disabled, appSensitiveKeys: [US_MASTER_TOKEN_HEADER], - zitadelEnabled: Utils.isTrueArg(Utils.getEnvVariable('ZITADEL')), - zitadelUri: Utils.getEnvVariable('ZITADEL_URI') || 'http://localhost:8080', + // zitadel + zitadelEnabled: isZitadelEnabled, + zitadelUri: getEnvVariable('ZITADEL_URI') || 'http://localhost:8080', + clientId: getEnvVariable('CLIENT_ID') || '', + clientSecret: getEnvVariable('CLIENT_SECRET') || '', - clientId: Utils.getEnvVariable('CLIENT_ID') || '', - clientSecret: Utils.getEnvVariable('CLIENT_SECRET') || '', + // auth + isAuthEnabled: isAuthEnabled, + authTokenPublicKey: getEnvCert(process.env.AUTH_TOKEN_PUBLIC_KEY), multitenant: false, tenantIdOverride: 'common', dlsEnabled: false, - accessServiceEnabled: Utils.isTrueArg(Utils.getEnvVariable('ZITADEL')), - accessBindingsServiceEnabled: Utils.isTrueArg(Utils.getEnvVariable('ZITADEL')), + accessServiceEnabled: isAuthServiceEnabled, + accessBindingsServiceEnabled: isAuthServiceEnabled, - masterToken: Utils.getEnvTokenVariable('MASTER_TOKEN'), + masterToken: getEnvTokenVariable('MASTER_TOKEN'), features: {}, - debug: Utils.isTrueArg(Utils.getEnvVariable('DEBUG')), + debug: isTrueArg(getEnvVariable('DEBUG')), - swaggerEnabled: !Utils.isTrueArg(Utils.getEnvVariable('DISABLE_SWAGGER')), + swaggerEnabled: !isTrueArg(getEnvVariable('DISABLE_SWAGGER')), } as Partial<AppConfig>; diff --git a/src/controllers/entries/index.ts b/src/controllers/entries/index.ts index af1666ba..aa21d812 100644 --- a/src/controllers/entries/index.ts +++ b/src/controllers/entries/index.ts @@ -32,7 +32,7 @@ import { formatGetEntryResponse, } from '../../services/new/entry/formatters'; import * as ST from '../../types/services.types'; -import Utils from '../../utils'; +import {isTrueArg} from '../../utils/env-utils'; import {getEntriesData} from './get-entries-data'; @@ -46,8 +46,8 @@ export default { entryId: params.entryId, branch: query.branch as GetEntryArgs['branch'], revId: query.revId as GetEntryArgs['revId'], - includePermissionsInfo: Utils.isTrueArg(query.includePermissionsInfo), - includeLinks: Utils.isTrueArg(query.includeLinks), + includePermissionsInfo: isTrueArg(query.includePermissionsInfo), + includeLinks: isTrueArg(query.includeLinks), }, ); const formattedResponse = await formatGetEntryResponse(req.ctx, result); @@ -133,7 +133,7 @@ export default { unversionedData: body.unversionedData, links: body.links, permissionsMode: body.permissionsMode, - includePermissionsInfo: Utils.isTrueArg(body.includePermissionsInfo), + includePermissionsInfo: isTrueArg(body.includePermissionsInfo), initialPermissions: body.initialPermissions, initialParentId: body.initialParentId, ctx: req.ctx, @@ -246,7 +246,7 @@ export default { { entryId: params.entryId, direction: query.direction as Optional<RelationDirection>, - includePermissionsInfo: Utils.isTrueArg(query.includePermissionsInfo), + includePermissionsInfo: isTrueArg(query.includePermissionsInfo), page: (query.page && Number(query.page)) as number | undefined, pageSize: (query.pageSize && Number(query.pageSize)) as number | undefined, scope: query.scope as EntryScope | undefined, @@ -310,11 +310,11 @@ export default { filters: query.filters, page: query.page && Number(query.page), pageSize: query.pageSize && Number(query.pageSize), - includePermissionsInfo: Utils.isTrueArg(query.includePermissionsInfo), - ignoreWorkbookEntries: Utils.isTrueArg(query.ignoreWorkbookEntries), - includeData: Utils.isTrueArg(query.includeData), - includeLinks: Utils.isTrueArg(query.includeLinks), - excludeLocked: Utils.isTrueArg(query.excludeLocked), + includePermissionsInfo: isTrueArg(query.includePermissionsInfo), + ignoreWorkbookEntries: isTrueArg(query.ignoreWorkbookEntries), + includeData: isTrueArg(query.includeData), + includeLinks: isTrueArg(query.includeLinks), + excludeLocked: isTrueArg(query.excludeLocked), ctx: req.ctx, }); diff --git a/src/controllers/favorites.ts b/src/controllers/favorites.ts index 8f827dd7..da3292a9 100644 --- a/src/controllers/favorites.ts +++ b/src/controllers/favorites.ts @@ -4,7 +4,7 @@ import {prepareResponseAsync} from '../components/response-presenter'; import Entry from '../db/models/entry'; import FavoriteService from '../services/favorite.service'; import * as ST from '../types/services.types'; -import Utils from '../utils'; +import {isTrueArg} from '../utils/env-utils'; export default { getFavorites: async (req: Request, res: Response) => { @@ -16,8 +16,8 @@ export default { page: query.page && Number(query.page), pageSize: query.pageSize && Number(query.pageSize), scope: query.scope, - includePermissionsInfo: Utils.isTrueArg(query.includePermissionsInfo), - ignoreWorkbookEntries: Utils.isTrueArg(query.ignoreWorkbookEntries), + includePermissionsInfo: isTrueArg(query.includePermissionsInfo), + ignoreWorkbookEntries: isTrueArg(query.ignoreWorkbookEntries), ctx: req.ctx, }); diff --git a/src/controllers/structure-items.ts b/src/controllers/structure-items.ts index 174155d0..40ade930 100644 --- a/src/controllers/structure-items.ts +++ b/src/controllers/structure-items.ts @@ -3,7 +3,7 @@ import {Request, Response} from '@gravity-ui/expresskit'; import {prepareResponseAsync} from '../components/response-presenter'; import {Mode, OrderDirection, OrderField, getStructureItems} from '../services/new/structure-item'; import {formatStructureItems} from '../services/new/structure-item/formatters/format-structure-items'; -import Utils from '../utils'; +import {isTrueArg} from '../utils/env-utils'; export default { getStructureItems: async (req: Request, res: Response) => { @@ -13,13 +13,13 @@ export default { {ctx: req.ctx}, { collectionId: (query.collectionId as Optional<string>) ?? null, - includePermissionsInfo: Utils.isTrueArg(query.includePermissionsInfo), + includePermissionsInfo: isTrueArg(query.includePermissionsInfo), filterString: query.filterString as Optional<string>, page: query.page ? parseInt(query.page as string, 10) : undefined, pageSize: query.pageSize ? parseInt(query.pageSize as string, 10) : undefined, orderField: query.orderField as Optional<OrderField>, orderDirection: query.orderDirection as Optional<OrderDirection>, - onlyMy: Utils.isTrueArg(query.onlyMy), + onlyMy: isTrueArg(query.onlyMy), mode: query.mode as Optional<Mode>, }, ); diff --git a/src/controllers/workbooks/index.ts b/src/controllers/workbooks/index.ts index fee5598d..ca117a5f 100644 --- a/src/controllers/workbooks/index.ts +++ b/src/controllers/workbooks/index.ts @@ -23,7 +23,7 @@ import { formatWorkbooksList, } from '../../services/new/workbook/formatters'; import {getWorkbooksListByIds} from '../../services/new/workbook/get-workbooks-list-by-ids'; -import Utils from '../../utils'; +import {isTrueArg} from '../../utils/env-utils'; import {copyWorkbook} from './copy-workbook'; import {createWorkbook} from './create-workbook'; @@ -51,7 +51,7 @@ export default { }, { workbookId: params.workbookId, - includePermissionsInfo: Utils.isTrueArg(query.includePermissionsInfo), + includePermissionsInfo: isTrueArg(query.includePermissionsInfo), }, ); @@ -67,7 +67,7 @@ export default { {ctx: req.ctx}, { workbookId: params.workbookId, - includePermissionsInfo: Utils.isTrueArg(query.includePermissionsInfo), + includePermissionsInfo: isTrueArg(query.includePermissionsInfo), page: (query.page && Number(query.page)) as number | undefined, pageSize: (query.pageSize && Number(query.pageSize)) as number | undefined, createdBy: query.createdBy as any, @@ -89,13 +89,13 @@ export default { {ctx: req.ctx}, { collectionId: (query.collectionId as Optional<string>) ?? null, - includePermissionsInfo: Utils.isTrueArg(query.includePermissionsInfo), + includePermissionsInfo: isTrueArg(query.includePermissionsInfo), filterString: query.filterString as Optional<string>, page: (query.page && Number(query.page)) as number | undefined, pageSize: (query.pageSize && Number(query.pageSize)) as number | undefined, orderField: query.orderField as Optional<OrderField>, orderDirection: query.orderDirection as Optional<OrderDirection>, - onlyMy: Utils.isTrueArg(query.onlyMy), + onlyMy: isTrueArg(query.onlyMy), }, ); diff --git a/src/db/init-db.ts b/src/db/init-db.ts index b085ed23..ee2fc66b 100644 --- a/src/db/init-db.ts +++ b/src/db/init-db.ts @@ -6,6 +6,7 @@ import {initDB as initPosgresDB} from '@gravity-ui/postgreskit'; import {AppEnv, DEFAULT_QUERY_TIMEOUT} from '../const'; import {getTestDsnList} from '../tests/int/db'; import Utils from '../utils'; +import {isTrueArg} from '../utils/env-utils'; interface OrigImplFunction { (snakeCaseFormat: string): string; @@ -61,7 +62,7 @@ export const getKnexOptions = () => ({ return origImpl(snakeCaseFormat); }, - debug: Utils.isTrueArg(process.env.SQL_DEBUG), + debug: isTrueArg(process.env.SQL_DEBUG), }); export function initDB(nodekit: NodeKit) { @@ -72,7 +73,7 @@ export function initDB(nodekit: NodeKit) { dsnList = Utils.getDsnList(); } - const suppressStatusLogs = Utils.isTrueArg(process.env.US_SURPRESS_DB_STATUS_LOGS); + const suppressStatusLogs = isTrueArg(process.env.US_SURPRESS_DB_STATUS_LOGS); const dispatcherOptions = { healthcheckInterval: 5000, diff --git a/src/index.ts b/src/index.ts index e60a8f72..f948a767 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import { setCiEnv, waitDatabase, } from './components/middlewares'; +import {appAuth} from './components/auth/middlewares/app-auth'; import {AppEnv} from './const'; import {registry} from './registry'; import {getRoutes} from './routes'; @@ -56,6 +57,10 @@ if (nodekit.config.zitadelEnabled) { nodekit.config.appAuthHandler = authZitadel; } +if (nodekit.config.isAuthEnabled) { + nodekit.config.appAuthHandler = appAuth; +} + nodekit.config.appFinalErrorHandler = finalRequestHandler; const extendedRoutes = getRoutes(nodekit, {beforeAuth, afterAuth}); diff --git a/src/registry/common/components/iam/utils.ts b/src/registry/common/components/iam/utils.ts index 9909ccdd..c604eec7 100644 --- a/src/registry/common/components/iam/utils.ts +++ b/src/registry/common/components/iam/utils.ts @@ -1,5 +1,6 @@ import {AppContext, AppError} from '@gravity-ui/nodekit'; +import {UserRole} from '../../../../components/auth/constants/role'; import {OrganizationPermission} from '../../../../components/iam'; import {US_ERRORS} from '../../../../const'; import {ZitadelUserRole} from '../../../../types/zitadel'; @@ -12,31 +13,78 @@ const throwAccessServicePermissionDenied = () => { }); }; -export const checkOrganizationPermission: CheckOrganizationPermission = async (args: { +const checkAuthOrganizationPermission: CheckOrganizationPermission = async (args: { ctx: AppContext; permission: OrganizationPermission; }) => { const {ctx, permission} = args; - const {zitadelUserRole: role} = ctx.get('info'); + const roles = ctx.get('user')?.roles || []; switch (permission) { case OrganizationPermission.UseInstance: break; - case OrganizationPermission.ManageInstance: - if (role !== ZitadelUserRole.Admin) { + case OrganizationPermission.ManageInstance: { + if (roles.every((role) => role !== UserRole.Admin)) { throwAccessServicePermissionDenied(); } break; + } case OrganizationPermission.CreateCollectionInRoot: - case OrganizationPermission.CreateWorkbookInRoot: - if (role !== ZitadelUserRole.Editor && role !== ZitadelUserRole.Admin) { + case OrganizationPermission.CreateWorkbookInRoot: { + if (roles.every((role) => role !== UserRole.Editor && role !== UserRole.Admin)) { throwAccessServicePermissionDenied(); } break; + } default: throwAccessServicePermissionDenied(); } }; + +const checkZitadelOrganizationPermission: CheckOrganizationPermission = async (args: { + ctx: AppContext; + permission: OrganizationPermission; +}) => { + const {ctx, permission} = args; + const {zitadelUserRole} = ctx.get('info'); + + switch (permission) { + case OrganizationPermission.UseInstance: + break; + + case OrganizationPermission.ManageInstance: { + if (zitadelUserRole !== ZitadelUserRole.Admin) { + throwAccessServicePermissionDenied(); + } + break; + } + + case OrganizationPermission.CreateCollectionInRoot: + case OrganizationPermission.CreateWorkbookInRoot: { + if ( + zitadelUserRole !== ZitadelUserRole.Editor && + zitadelUserRole !== ZitadelUserRole.Admin + ) { + throwAccessServicePermissionDenied(); + } + break; + } + + default: + throwAccessServicePermissionDenied(); + } +}; + +export const checkOrganizationPermission: CheckOrganizationPermission = async (args: { + ctx: AppContext; + permission: OrganizationPermission; +}) => { + if (args.ctx.config.isAuthEnabled) { + await checkAuthOrganizationPermission(args); + } else { + await checkZitadelOrganizationPermission(args); + } +}; diff --git a/src/registry/common/entities/collection/collection.ts b/src/registry/common/entities/collection/collection.ts index 4b9dfbed..cb5b0225 100644 --- a/src/registry/common/entities/collection/collection.ts +++ b/src/registry/common/entities/collection/collection.ts @@ -1,6 +1,7 @@ import type {AppContext} from '@gravity-ui/nodekit'; import {AppError} from '@gravity-ui/nodekit'; +import {UserRole} from '../../../../components/auth/constants/role'; import {US_ERRORS} from '../../../../const'; import type {CollectionModel} from '../../../../db/models/new/collection'; import {CollectionPermission, Permissions} from '../../../../entities/collection/types'; @@ -76,8 +77,15 @@ export const Collection: CollectionConstructor = class Collection implements Col } private isEditorOrAdmin() { - const {zitadelUserRole: role} = this.ctx.get('info'); - return role === ZitadelUserRole.Editor || role === ZitadelUserRole.Admin; + const {isAuthEnabled} = this.ctx.config; + const user = this.ctx.get('user'); + const {zitadelUserRole} = this.ctx.get('info'); + return isAuthEnabled + ? (user?.roles || []).some( + (role) => role === UserRole.Editor || role === UserRole.Admin, + ) + : zitadelUserRole === ZitadelUserRole.Editor || + zitadelUserRole === ZitadelUserRole.Admin; } private getAllPermissions() { diff --git a/src/registry/common/entities/workbook/workbook.ts b/src/registry/common/entities/workbook/workbook.ts index b241c0c1..86ce3563 100644 --- a/src/registry/common/entities/workbook/workbook.ts +++ b/src/registry/common/entities/workbook/workbook.ts @@ -1,6 +1,7 @@ import type {AppContext} from '@gravity-ui/nodekit'; import {AppError} from '@gravity-ui/nodekit'; +import {UserRole} from '../../../../components/auth/constants/role'; import {US_ERRORS} from '../../../../const'; import type {WorkbookModel} from '../../../../db/models/new/workbook'; import {getMockedOperation} from '../../../../entities/utils'; @@ -78,8 +79,15 @@ export const Workbook: WorkbookConstructor<WorkbookInstance> = class Workbook } private isEditorOrAdmin() { - const {zitadelUserRole: role} = this.ctx.get('info'); - return role === ZitadelUserRole.Editor || role === ZitadelUserRole.Admin; + const {isAuthEnabled} = this.ctx.config; + const user = this.ctx.get('user'); + const {zitadelUserRole} = this.ctx.get('info'); + return isAuthEnabled + ? (user?.roles || []).some( + (role) => role === UserRole.Editor || role === UserRole.Admin, + ) + : zitadelUserRole === ZitadelUserRole.Editor || + zitadelUserRole === ZitadelUserRole.Admin; } private getAllPermissions() { diff --git a/src/utils/env-utils.ts b/src/utils/env-utils.ts new file mode 100644 index 00000000..4ba138a8 --- /dev/null +++ b/src/utils/env-utils.ts @@ -0,0 +1,40 @@ +import fs from 'node:fs'; + +import {TRUE_FLAGS} from '../const/common'; + +export const getEnvCert = (envCert?: string) => envCert?.replace(/\\n/g, '\n'); + +export function getEnvVariable(envVariableName: string) { + const valueFromEnv = process.env[envVariableName]; + if (valueFromEnv) { + return valueFromEnv; + } + const FILE_PATH_POSTFIX = '_FILE_PATH'; + const filePath = process.env[`${envVariableName}${FILE_PATH_POSTFIX}`]; + if (filePath) { + return fs.readFileSync(filePath, 'utf8').toString(); + } + return undefined; +} + +export function getEnvTokenVariable(envTokenVariableName: string) { + const TOKEN_SEPARATOR = ','; + const valueFromEnv = getEnvVariable(envTokenVariableName); + + if (!valueFromEnv) { + return undefined; + } + + if (valueFromEnv.includes(TOKEN_SEPARATOR)) { + return valueFromEnv + .split(TOKEN_SEPARATOR) + .map((token) => token && token.trim()) + .filter((token) => token); + } + + return [valueFromEnv.trim()]; +} + +export function isTrueArg(arg: any): boolean { + return TRUE_FLAGS.includes(arg); +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 367c7561..29a42c2e 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -309,6 +309,7 @@ export class Utils { ); } + /** @deprecated moved to env-utils */ static isTrueArg(arg: any): boolean { return TRUE_FLAGS.includes(arg); } @@ -370,6 +371,7 @@ export class Utils { return response && response.body; } + /** @deprecated moved to env-utils */ static getEnvVariable(envVariableName: string) { const valueFromEnv = process.env[envVariableName]; if (valueFromEnv) { @@ -383,6 +385,7 @@ export class Utils { return undefined; } + /** @deprecated moved to env-utils */ static getEnvTokenVariable(envTokenVariableName: string) { const TOKEN_SEPARATOR = ','; const valueFromEnv = Utils.getEnvVariable(envTokenVariableName); diff --git a/typings/nodekit.d.ts b/typings/nodekit.d.ts index 424cf554..b48018fc 100644 --- a/typings/nodekit.d.ts +++ b/typings/nodekit.d.ts @@ -1,5 +1,6 @@ import type {RouteConfig as ZodOpenApiRouteConfig} from '@asteasolutions/zod-to-openapi'; +import type {CtxUser} from '../src/components/auth/types/user'; import {FeaturesConfig} from '../src/components/features/types'; import type {Registry} from '../src/registry'; import {CtxInfo} from '../src/types/ctx'; @@ -17,14 +18,26 @@ export interface SharedAppConfig { masterToken: string[]; + // zitadel zitadelEnabled?: boolean; zitadelUri?: string; clientId?: string; clientSecret?: string; + // auth + isAuthEnabled?: boolean; + authTokenPublicKey?: string; + swaggerEnabled?: boolean; } +export interface SharedAppContextParams { + info: CtxInfo; + registry: Registry; + // auth + user?: CtxUser; +} + declare module '@gravity-ui/nodekit' { export interface AppConfig extends SharedAppConfig {} @@ -32,10 +45,7 @@ declare module '@gravity-ui/nodekit' { features?: FeaturesConfig; } - interface AppContextParams { - info: CtxInfo; - registry: Registry; - } + export interface AppContextParams extends SharedAppContextParams {} } declare module '@gravity-ui/expresskit' {