mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Add org-scoped auth to org-level endpoints, add migration file for org enableAuth field
This commit is contained in:
26839
backend/package-lock.json
generated
26839
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,75 +1,127 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"name": "backend",
|
||||||
"@godaddy/terminus": "^4.11.2",
|
|
||||||
"@sentry/node": "^7.14.0",
|
|
||||||
"@sentry/tracing": "^7.19.0",
|
|
||||||
"@types/crypto-js": "^4.1.1",
|
|
||||||
"axios": "^1.1.3",
|
|
||||||
"bigint-conversion": "^2.2.2",
|
|
||||||
"cookie-parser": "^1.4.6",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"crypto-js": "^4.1.1",
|
|
||||||
"dotenv": "^16.0.1",
|
|
||||||
"express": "^4.18.1",
|
|
||||||
"express-rate-limit": "^6.7.0",
|
|
||||||
"express-validator": "^6.14.2",
|
|
||||||
"handlebars": "^4.7.7",
|
|
||||||
"helmet": "^5.1.1",
|
|
||||||
"jsonwebtoken": "^8.5.1",
|
|
||||||
"jsrp": "^0.2.4",
|
|
||||||
"mongoose": "^6.7.2",
|
|
||||||
"nodemailer": "^6.8.0",
|
|
||||||
"posthog-node": "^2.1.0",
|
|
||||||
"query-string": "^7.1.3",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"stripe": "^10.7.0",
|
|
||||||
"tweetnacl": "^1.0.3",
|
|
||||||
"tweetnacl-util": "^0.15.1",
|
|
||||||
"typescript": "^4.9.3"
|
|
||||||
},
|
|
||||||
"name": "infisical-api",
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "src/index.js",
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run build && node build/index.js",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"dev": "nodemon",
|
"dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine",
|
||||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./src/json ./build",
|
"dev:docker": "nodemon",
|
||||||
"lint": "eslint . --ext .ts",
|
"build": "rimraf dist && tsup && cp -R ./src/lib/validator/disposable_emails.txt ./dist && cp -R ./src/services/smtp/templates ./dist",
|
||||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
"start": "node dist/main.mjs",
|
||||||
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
|
"type:check": "tsc --noEmit",
|
||||||
"lint-staged": "lint-staged"
|
"lint:fix": "eslint --fix --ext js,ts ./src",
|
||||||
},
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
"repository": {
|
"test:e2e": "vitest run -c vitest.e2e.config.ts",
|
||||||
"type": "git",
|
"test:e2e-watch": "vitest -c vitest.e2e.config.ts",
|
||||||
"url": "git+https://github.com/Infisical/infisical-api.git"
|
"test:e2e-coverage": "vitest run --coverage -c vitest.e2e.config.ts",
|
||||||
|
"generate:component": "tsx ./scripts/create-backend-file.ts",
|
||||||
|
"generate:schema": "tsx ./scripts/generate-schema-types.ts",
|
||||||
|
"migration:new": "tsx ./scripts/create-migration.ts",
|
||||||
|
"migration:up": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:up",
|
||||||
|
"migration:down": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:down",
|
||||||
|
"migration:list": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:list",
|
||||||
|
"migration:latest": "knex --knexfile ./src/db/knexfile.ts --client pg migrate:latest",
|
||||||
|
"migration:rollback": "knex --knexfile ./src/db/knexfile.ts migrate:rollback",
|
||||||
|
"seed:new": "tsx ./scripts/create-seed-file.ts",
|
||||||
|
"seed:run": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
|
||||||
|
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest && npm run seed:run"
|
||||||
},
|
},
|
||||||
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/Infisical/infisical-api/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Infisical/infisical-api#readme",
|
|
||||||
"description": "",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@posthog/plugin-scaffold": "^1.3.4",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/jmespath": "^0.15.2",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"@types/express": "^4.17.14",
|
"@types/jsrp": "^0.2.6",
|
||||||
"@types/jsonwebtoken": "^8.5.9",
|
"@types/libsodium-wrappers": "^0.7.13",
|
||||||
"@types/node": "^18.11.3",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/nodemailer": "^6.4.6",
|
"@types/node": "^20.9.5",
|
||||||
"@types/swagger-jsdoc": "^6.0.1",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/passport-github": "^1.1.12",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
"@types/passport-google-oauth20": "^2.0.14",
|
||||||
"@typescript-eslint/parser": "^5.40.1",
|
"@types/pg": "^8.10.9",
|
||||||
"eslint": "^8.26.0",
|
"@types/picomatch": "^2.3.3",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"@types/prompt-sync": "^4.2.3",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"@types/uuid": "^9.0.7",
|
||||||
"install": "^0.13.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"jest": "^29.3.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"nodemon": "^2.0.19",
|
"eslint-plugin-import": "^2.29.1",
|
||||||
"npm": "^8.19.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"prettier": "^2.7.1",
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||||
"ts-node": "^10.9.1"
|
"nodemon": "^3.0.2",
|
||||||
|
"pino-pretty": "^10.2.3",
|
||||||
|
"prompt-sync": "^4.2.0",
|
||||||
|
"rimraf": "^5.0.5",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"tsup": "^8.0.1",
|
||||||
|
"tsx": "^4.4.0",
|
||||||
|
"typescript": "^5.3.2",
|
||||||
|
"vite-tsconfig-paths": "^4.2.2",
|
||||||
|
"vitest": "^1.0.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-secrets-manager": "^3.485.0",
|
||||||
|
"@casl/ability": "^6.5.0",
|
||||||
|
"@fastify/cookie": "^9.2.0",
|
||||||
|
"@fastify/cors": "^8.4.1",
|
||||||
|
"@fastify/etag": "^5.1.0",
|
||||||
|
"@fastify/formbody": "^7.4.0",
|
||||||
|
"@fastify/helmet": "^11.1.1",
|
||||||
|
"@fastify/passport": "^2.4.0",
|
||||||
|
"@fastify/rate-limit": "^9.0.0",
|
||||||
|
"@fastify/session": "^10.7.0",
|
||||||
|
"@fastify/swagger": "^8.12.0",
|
||||||
|
"@fastify/swagger-ui": "^1.10.1",
|
||||||
|
"@node-saml/passport-saml": "^4.0.4",
|
||||||
|
"@octokit/rest": "^20.0.2",
|
||||||
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
|
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
||||||
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.20.0",
|
||||||
|
"@typescript-eslint/parser": "^6.20.0",
|
||||||
|
"@ucast/mongo2js": "^1.3.4",
|
||||||
|
"ajv": "^8.12.0",
|
||||||
|
"argon2": "^0.31.2",
|
||||||
|
"aws-sdk": "^2.1532.0",
|
||||||
|
"axios": "^1.6.2",
|
||||||
|
"axios-retry": "^4.0.0",
|
||||||
|
"bcrypt": "^5.1.1",
|
||||||
|
"bullmq": "^5.1.1",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"eslint": "^8.56.0",
|
||||||
|
"eslint-config-airbnb-base": "^15.0.0",
|
||||||
|
"eslint-config-airbnb-typescript": "^17.1.0",
|
||||||
|
"fastify": "^4.24.3",
|
||||||
|
"fastify-plugin": "^4.5.1",
|
||||||
|
"handlebars": "^4.7.8",
|
||||||
|
"ioredis": "^5.3.2",
|
||||||
|
"jmespath": "^0.16.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"jsrp": "^0.2.4",
|
||||||
|
"knex": "^3.0.1",
|
||||||
|
"libsodium-wrappers": "^0.7.13",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"mysql2": "^3.6.5",
|
||||||
|
"nanoid": "^5.0.4",
|
||||||
|
"node-cache": "^5.1.2",
|
||||||
|
"nodemailer": "^6.9.7",
|
||||||
|
"ora": "^7.0.1",
|
||||||
|
"passport-github": "^1.1.0",
|
||||||
|
"passport-gitlab2": "^5.0.0",
|
||||||
|
"passport-google-oauth20": "^2.0.0",
|
||||||
|
"pg": "^8.11.3",
|
||||||
|
"picomatch": "^3.0.1",
|
||||||
|
"pino": "^8.16.2",
|
||||||
|
"posthog-node": "^3.6.0",
|
||||||
|
"probot": "^12.3.3",
|
||||||
|
"smee-client": "^2.0.0",
|
||||||
|
"tweetnacl": "^1.0.3",
|
||||||
|
"tweetnacl-util": "^0.15.1",
|
||||||
|
"uuid": "^9.0.1",
|
||||||
|
"zod": "^3.22.4",
|
||||||
|
"zod-to-json-schema": "^3.22.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
121
backend/src/@types/fastify.d.ts
vendored
Normal file
121
backend/src/@types/fastify.d.ts
vendored
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import "fastify";
|
||||||
|
|
||||||
|
import { TUsers } from "@app/db/schemas";
|
||||||
|
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
|
import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||||
|
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||||
|
import { TSecretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
|
||||||
|
import { TSecretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||||
|
import { TSecretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
||||||
|
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||||
|
import { TTrustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
|
import { TAuthMode } from "@app/server/plugins/auth/inject-identity";
|
||||||
|
import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
|
import { TAuthLoginFactory } from "@app/services/auth/auth-login-service";
|
||||||
|
import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
|
||||||
|
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
|
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
|
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||||
|
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
||||||
|
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||||
|
import { TIntegrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||||
|
import { TOrgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||||
|
import { TOrgServiceFactory } from "@app/services/org/org-service";
|
||||||
|
import { TProjectServiceFactory } from "@app/services/project/project-service";
|
||||||
|
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
|
import { TProjectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||||
|
import { TProjectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
||||||
|
import { TProjectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||||
|
import { TProjectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
|
import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
|
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
|
||||||
|
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
|
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
||||||
|
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
||||||
|
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
|
||||||
|
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||||
|
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
import { TUserServiceFactory } from "@app/services/user/user-service";
|
||||||
|
import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
|
||||||
|
|
||||||
|
declare module "fastify" {
|
||||||
|
interface FastifyRequest {
|
||||||
|
realIp: string;
|
||||||
|
// used for mfa session authentication
|
||||||
|
mfa: {
|
||||||
|
userId: string;
|
||||||
|
orgId?: string;
|
||||||
|
user: TUsers;
|
||||||
|
};
|
||||||
|
// identity injection. depending on which kinda of token the information is filled in auth
|
||||||
|
auth: TAuthMode;
|
||||||
|
permission: {
|
||||||
|
type: ActorType;
|
||||||
|
id: string;
|
||||||
|
orgId?: string;
|
||||||
|
};
|
||||||
|
// passport data
|
||||||
|
passportUser: {
|
||||||
|
isUserCompleted: string;
|
||||||
|
providerAuthToken: string;
|
||||||
|
};
|
||||||
|
auditLogInfo: Pick<TCreateAuditLogDTO, "userAgent" | "userAgentType" | "ipAddress" | "actor">;
|
||||||
|
ssoConfig: Awaited<ReturnType<TSamlConfigServiceFactory["getSaml"]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FastifyInstance {
|
||||||
|
services: {
|
||||||
|
login: TAuthLoginFactory;
|
||||||
|
password: TAuthPasswordFactory;
|
||||||
|
signup: TAuthSignupFactory;
|
||||||
|
authToken: TAuthTokenServiceFactory;
|
||||||
|
permission: TPermissionServiceFactory;
|
||||||
|
org: TOrgServiceFactory;
|
||||||
|
orgRole: TOrgRoleServiceFactory;
|
||||||
|
superAdmin: TSuperAdminServiceFactory;
|
||||||
|
user: TUserServiceFactory;
|
||||||
|
apiKey: TApiKeyServiceFactory;
|
||||||
|
project: TProjectServiceFactory;
|
||||||
|
projectMembership: TProjectMembershipServiceFactory;
|
||||||
|
projectEnv: TProjectEnvServiceFactory;
|
||||||
|
projectKey: TProjectKeyServiceFactory;
|
||||||
|
projectRole: TProjectRoleServiceFactory;
|
||||||
|
secret: TSecretServiceFactory;
|
||||||
|
secretTag: TSecretTagServiceFactory;
|
||||||
|
secretImport: TSecretImportServiceFactory;
|
||||||
|
projectBot: TProjectBotServiceFactory;
|
||||||
|
folder: TSecretFolderServiceFactory;
|
||||||
|
integration: TIntegrationServiceFactory;
|
||||||
|
integrationAuth: TIntegrationAuthServiceFactory;
|
||||||
|
webhook: TWebhookServiceFactory;
|
||||||
|
serviceToken: TServiceTokenServiceFactory;
|
||||||
|
identity: TIdentityServiceFactory;
|
||||||
|
identityAccessToken: TIdentityAccessTokenServiceFactory;
|
||||||
|
identityProject: TIdentityProjectServiceFactory;
|
||||||
|
identityUa: TIdentityUaServiceFactory;
|
||||||
|
secretApprovalPolicy: TSecretApprovalPolicyServiceFactory;
|
||||||
|
secretApprovalRequest: TSecretApprovalRequestServiceFactory;
|
||||||
|
secretRotation: TSecretRotationServiceFactory;
|
||||||
|
snapshot: TSecretSnapshotServiceFactory;
|
||||||
|
saml: TSamlConfigServiceFactory;
|
||||||
|
auditLog: TAuditLogServiceFactory;
|
||||||
|
secretScanning: TSecretScanningServiceFactory;
|
||||||
|
license: TLicenseServiceFactory;
|
||||||
|
trustedIp: TTrustedIpServiceFactory;
|
||||||
|
secretBlindIndex: TSecretBlindIndexServiceFactory;
|
||||||
|
telemetry: TTelemetryServiceFactory;
|
||||||
|
};
|
||||||
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
|
// everywhere else access using service layer
|
||||||
|
store: {
|
||||||
|
user: Pick<TUserDALFactory, "findById">;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
24
backend/src/db/migrations/20240204171758_org-based-auth.ts
Normal file
24
backend/src/db/migrations/20240204171758_org-based-auth.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.boolean("authEnabled").defaultTo(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await knex(TableName.Organization)
|
||||||
|
.whereIn(
|
||||||
|
"id",
|
||||||
|
knex(TableName.SamlConfig)
|
||||||
|
.select("orgId")
|
||||||
|
.where("isActive", true)
|
||||||
|
)
|
||||||
|
.update({ authEnabled: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||||
|
t.dropColumn("authEnabled");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
22
backend/src/db/schemas/organizations.ts
Normal file
22
backend/src/db/schemas/organizations.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const OrganizationsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
customerId: z.string().nullable().optional(),
|
||||||
|
slug: z.string(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
authEnabled: z.boolean().default(false).nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
export type TOrganizationsInsert = Omit<TOrganizations, TImmutableDBKeys>;
|
||||||
|
export type TOrganizationsUpdate = Partial<Omit<TOrganizations, TImmutableDBKeys>>;
|
||||||
404
backend/src/ee/routes/v1/license-router.ts
Normal file
404
backend/src/ee/routes/v1/license-router.ts
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
// TODO(akhilmhdh): Fix this when licence service gets it type
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerLicenseRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/plans/table",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({ billingCycle: z.enum(["monthly", "yearly"]) }),
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.getOrgPlansTableByBillCycle({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
billingCycle: req.query.billingCycle
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/plan",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.object({ plan: z.any() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const plan = await server.services.license.getOrgPlan({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return { plan };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/plans",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
querystring: z.object({ workspaceId: z.string().trim().optional() }),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.getOrgPlan({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/session/trial",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
body: z.object({ success_url: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.startOrgTrial({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
success_url: req.body.success_url
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/customer-portal-session",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.createOrganizationPortalSession({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/plan/billing",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.getOrgBillingInfo({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/plan/table",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.getOrgPlanTable({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/billing-details",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.getOrgBillingDetails({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/billing-details",
|
||||||
|
method: "PATCH",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
body: z.object({
|
||||||
|
email: z.string().trim().email().optional(),
|
||||||
|
name: z.string().trim().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.updateOrgBillingDetails({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
name: req.body.name,
|
||||||
|
email: req.body.email
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/billing-details/payment-methods",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.getOrgPmtMethods({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/billing-details/payment-methods",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
body: z.object({
|
||||||
|
success_url: z.string().trim(),
|
||||||
|
cancel_url: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.addOrgPmtMethods({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
success_url: req.body.success_url,
|
||||||
|
cancel_url: req.body.cancel_url
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/billing-details/payment-methods/:pmtMethodId",
|
||||||
|
method: "DELETE",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim(),
|
||||||
|
pmtMethodId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.delOrgPmtMethods({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
pmtMethodId: req.params.pmtMethodId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/billing-details/tax-ids",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.getOrgTaxIds({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/billing-details/tax-ids",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
type: z.string().trim(),
|
||||||
|
value: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.addOrgTaxId({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
type: req.body.type,
|
||||||
|
value: req.body.value
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/billing-details/tax-ids/:taxId",
|
||||||
|
method: "DELETE",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim(),
|
||||||
|
taxId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.delOrgTaxId({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
taxId: req.params.taxId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/invoices",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.getOrgTaxInvoices({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:organizationId/licenses",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.any()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const data = await server.services.license.getOrgLicenses({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
157
backend/src/ee/routes/v1/org-role-router.ts
Normal file
157
backend/src/ee/routes/v1/org-role-router.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:organizationId/roles",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
slug: z.string().trim(),
|
||||||
|
name: z.string().trim(),
|
||||||
|
description: z.string().trim().optional(),
|
||||||
|
permissions: z.any().array()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
role: OrgRolesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const role = await server.services.orgRole.createRole(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.body,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { role };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:organizationId/roles/:roleId",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim(),
|
||||||
|
roleId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
slug: z.string().trim().optional(),
|
||||||
|
name: z.string().trim().optional(),
|
||||||
|
description: z.string().trim().optional(),
|
||||||
|
permissions: z.any().array()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
role: OrgRolesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const role = await server.services.orgRole.updateRole(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.params.roleId,
|
||||||
|
req.body,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { role };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:organizationId/roles/:roleId",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim(),
|
||||||
|
roleId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
role: OrgRolesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const role = await server.services.orgRole.deleteRole(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.params.roleId,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { role };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:organizationId/roles",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
data: z.object({
|
||||||
|
roles: OrgRolesSchema.omit({ permissions: true })
|
||||||
|
.merge(z.object({ permissions: z.unknown() }))
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const roles = await server.services.orgRole.listRoles(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { data: { roles } };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
*/
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:organizationId/permissions",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
membership: OrgMembershipsSchema,
|
||||||
|
permissions: z.any().array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { permissions, membership } = await server.services.orgRole.getUserPermission(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { permissions, membership };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
294
backend/src/ee/routes/v1/saml-router.ts
Normal file
294
backend/src/ee/routes/v1/saml-router.ts
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
// All the any rules are disabled because passport typesense with fastify is really poor
|
||||||
|
|
||||||
|
import { Authenticator } from "@fastify/passport";
|
||||||
|
import fastifySession from "@fastify/session";
|
||||||
|
import { MultiSamlStrategy } from "@node-saml/passport-saml";
|
||||||
|
import { FastifyRequest } from "fastify";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SamlConfigsSchema } from "@app/db/schemas";
|
||||||
|
import { SamlProviders, TGetSamlCfgDTO } from "@app/ee/services/saml-config/saml-config-types";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
|
type TSAMLConfig = {
|
||||||
|
callbackUrl: string;
|
||||||
|
entryPoint: string;
|
||||||
|
issuer: string;
|
||||||
|
cert: string;
|
||||||
|
audience: string;
|
||||||
|
wantAuthnResponseSigned?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const passport = new Authenticator({ key: "saml", userProperty: "passportUser" });
|
||||||
|
await server.register(fastifySession, { secret: appCfg.COOKIE_SECRET_SIGN_KEY });
|
||||||
|
await server.register(passport.initialize());
|
||||||
|
await server.register(passport.secureSession());
|
||||||
|
server.decorateRequest("ssoConfig", null);
|
||||||
|
passport.use(
|
||||||
|
new MultiSamlStrategy(
|
||||||
|
{
|
||||||
|
passReqToCallback: true,
|
||||||
|
// eslint-disable-next-line
|
||||||
|
getSamlOptions: async (req, done) => {
|
||||||
|
try {
|
||||||
|
const { samlConfigId, orgSlug } = req.params;
|
||||||
|
|
||||||
|
let ssoLookupDetails: TGetSamlCfgDTO;
|
||||||
|
|
||||||
|
if (orgSlug) {
|
||||||
|
ssoLookupDetails = {
|
||||||
|
type: "orgSlug",
|
||||||
|
orgSlug
|
||||||
|
}
|
||||||
|
} else if (samlConfigId) {
|
||||||
|
ssoLookupDetails = {
|
||||||
|
type: "ssoId",
|
||||||
|
id: samlConfigId
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({ message: "Missing sso identitier or org slug" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ssoConfig = await server.services.saml.getSaml(ssoLookupDetails);
|
||||||
|
if (!ssoConfig) throw new BadRequestError({ message: "SSO config not found" });
|
||||||
|
|
||||||
|
const samlConfig: TSAMLConfig = {
|
||||||
|
callbackUrl: `${appCfg.SITE_URL}/api/v1/sso/saml2/${samlConfigId}`,
|
||||||
|
entryPoint: ssoConfig.entryPoint,
|
||||||
|
issuer: ssoConfig.issuer,
|
||||||
|
cert: ssoConfig.cert,
|
||||||
|
audience: appCfg.SITE_URL || ""
|
||||||
|
};
|
||||||
|
if (ssoConfig.authProvider === SamlProviders.JUMPCLOUD_SAML) {
|
||||||
|
samlConfig.wantAuthnResponseSigned = false;
|
||||||
|
}
|
||||||
|
if (ssoConfig.authProvider === SamlProviders.AZURE_SAML) {
|
||||||
|
if (req.body.RelayState && JSON.parse(req.body.RelayState).spIntiaited) {
|
||||||
|
samlConfig.audience = `spn:${ssoConfig.issuer}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(req as unknown as FastifyRequest).ssoConfig = ssoConfig;
|
||||||
|
done(null, samlConfig);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
done(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line
|
||||||
|
async (req, profile, cb) => {
|
||||||
|
try {
|
||||||
|
const serverCfg = getServerCfg();
|
||||||
|
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||||
|
const { firstName } = profile;
|
||||||
|
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
|
||||||
|
|
||||||
|
if (!email || !firstName) {
|
||||||
|
throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
||||||
|
email,
|
||||||
|
firstName: profile.firstName as string,
|
||||||
|
lastName: profile.lastName as string,
|
||||||
|
isSignupAllowed: Boolean(serverCfg.allowSignUp),
|
||||||
|
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||||
|
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
||||||
|
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
||||||
|
});
|
||||||
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
cb(null, {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/redirect/saml2/organizations/:orgSlug",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
orgSlug: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
callback_port: z.string().optional()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
preValidation: (req, res) =>
|
||||||
|
(
|
||||||
|
passport.authenticate("saml", {
|
||||||
|
failureRedirect: "/",
|
||||||
|
additionalParams: {
|
||||||
|
RelayState: JSON.stringify({
|
||||||
|
spInitiated: true,
|
||||||
|
callbackPort: req.query.callback_port ?? ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} as any) as any
|
||||||
|
)(req, res),
|
||||||
|
handler: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/redirect/saml2/:samlConfigId",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
samlConfigId: z.string().trim()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
callback_port: z.string().optional()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
preValidation: (req, res) =>
|
||||||
|
(
|
||||||
|
passport.authenticate("saml", {
|
||||||
|
failureRedirect: "/",
|
||||||
|
additionalParams: {
|
||||||
|
RelayState: JSON.stringify({
|
||||||
|
spInitiated: true,
|
||||||
|
callbackPort: req.query.callback_port ?? ""
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} as any) as any
|
||||||
|
)(req, res),
|
||||||
|
handler: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/saml2/:samlConfigId",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
samlConfigId: z.string().trim()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
preValidation: passport.authenticate("saml", {
|
||||||
|
session: false,
|
||||||
|
failureFlash: true,
|
||||||
|
failureRedirect: "/login/provider/error"
|
||||||
|
// this is due to zod type difference
|
||||||
|
}) as any,
|
||||||
|
handler: (req, res) => {
|
||||||
|
if (req.passportUser.isUserCompleted) {
|
||||||
|
return res.redirect(
|
||||||
|
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return res.redirect(
|
||||||
|
`${appCfg.SITE_URL}/signup/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/config",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
organization: z.string(),
|
||||||
|
orgId: z.string(),
|
||||||
|
authProvider: z.string(),
|
||||||
|
isActive: z.boolean(),
|
||||||
|
entryPoint: z.string(),
|
||||||
|
issuer: z.string(),
|
||||||
|
cert: z.string()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const saml = await server.services.saml.getSaml({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.query.organizationId,
|
||||||
|
type: "org"
|
||||||
|
});
|
||||||
|
return saml;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/config",
|
||||||
|
method: "POST",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
organizationId: z.string(),
|
||||||
|
authProvider: z.nativeEnum(SamlProviders),
|
||||||
|
isActive: z.boolean(),
|
||||||
|
entryPoint: z.string(),
|
||||||
|
issuer: z.string(),
|
||||||
|
cert: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: SamlConfigsSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const saml = await server.services.saml.createSamlCfg({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return saml;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/config",
|
||||||
|
method: "PATCH",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
body: z
|
||||||
|
.object({
|
||||||
|
authProvider: z.nativeEnum(SamlProviders),
|
||||||
|
isActive: z.boolean(),
|
||||||
|
entryPoint: z.string(),
|
||||||
|
issuer: z.string(),
|
||||||
|
cert: z.string()
|
||||||
|
})
|
||||||
|
.partial()
|
||||||
|
.merge(z.object({ organizationId: z.string() })),
|
||||||
|
response: {
|
||||||
|
200: SamlConfigsSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const saml = await server.services.saml.updateSamlCfg({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return saml;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
121
backend/src/ee/routes/v1/secret-scanning-router.ts
Normal file
121
backend/src/ee/routes/v1/secret-scanning-router.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { GitAppOrgSchema, SecretScanningGitRisksSchema } from "@app/db/schemas";
|
||||||
|
import { SecretScanningRiskStatus } from "@app/ee/services/secret-scanning/secret-scanning-types";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerSecretScanningRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/create-installation-session/organization",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({ organizationId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
sessionId: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const session = await server.services.secretScanning.createInstallationSession({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.body.organizationId
|
||||||
|
});
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/link-installation",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
installationId: z.string(),
|
||||||
|
sessionId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: GitAppOrgSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { installatedApp } = await server.services.secretScanning.linkInstallationToOrg({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return installatedApp;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/installation-status/organization/:organizationId",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.object({ appInstallationCompleted: z.boolean() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const appInstallationCompleted = await server.services.secretScanning.getOrgInstallationStatus({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return { appInstallationCompleted };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/organization/:organizationId/risks",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.object({ risks: SecretScanningGitRisksSchema.array() })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { risks } = await server.services.secretScanning.getRisksByOrg({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
return { risks };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/organization/:organizationId/risks/:riskId/status",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim(), riskId: z.string().trim() }),
|
||||||
|
body: z.object({ status: z.nativeEnum(SecretScanningRiskStatus) }),
|
||||||
|
response: {
|
||||||
|
200: SecretScanningGitRisksSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { risk } = await server.services.secretScanning.updateRiskStatus({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
riskId: req.params.riskId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
return risk;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
93
backend/src/ee/services/license/licence-fns.ts
Normal file
93
backend/src/ee/services/license/licence-fns.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { request } from "@app/lib/config/request";
|
||||||
|
|
||||||
|
import { TFeatureSet } from "./license-types";
|
||||||
|
|
||||||
|
export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||||
|
_id: null,
|
||||||
|
slug: null,
|
||||||
|
tier: -1,
|
||||||
|
workspaceLimit: null,
|
||||||
|
workspacesUsed: 0,
|
||||||
|
memberLimit: null,
|
||||||
|
membersUsed: 0,
|
||||||
|
environmentLimit: null,
|
||||||
|
environmentsUsed: 0,
|
||||||
|
secretVersioning: true,
|
||||||
|
pitRecovery: false,
|
||||||
|
ipAllowlisting: false,
|
||||||
|
rbac: false,
|
||||||
|
customRateLimits: false,
|
||||||
|
customAlerts: false,
|
||||||
|
auditLogs: false,
|
||||||
|
auditLogsRetentionDays: 0,
|
||||||
|
samlSSO: false,
|
||||||
|
status: null,
|
||||||
|
trial_end: null,
|
||||||
|
has_used_trial: true,
|
||||||
|
secretApproval: false,
|
||||||
|
secretRotation: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setupLicenceRequestWithStore = (baseURL: string, refreshUrl: string, licenseKey: string) => {
|
||||||
|
let token: string;
|
||||||
|
const licenceReq = axios.create({
|
||||||
|
baseURL,
|
||||||
|
timeout: 35 * 1000
|
||||||
|
// signal: AbortSignal.timeout(60 * 1000)
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshLicence = async () => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const {
|
||||||
|
data: { token: authToken }
|
||||||
|
} = await request.post<{ token: string }>(
|
||||||
|
refreshUrl,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
baseURL: appCfg.LICENSE_SERVER_URL,
|
||||||
|
headers: {
|
||||||
|
"X-API-KEY": licenseKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
token = authToken;
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
licenceReq.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
if (token && config.headers) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(err) => Promise.reject(err)
|
||||||
|
);
|
||||||
|
|
||||||
|
licenceReq.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
async (err) => {
|
||||||
|
const originalRequest = (err as AxiosError).config;
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
if ((err as AxiosError)?.response?.status === 401 && !(originalRequest as any)._retry) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
(originalRequest as any)._retry = true; // injected
|
||||||
|
|
||||||
|
// refresh
|
||||||
|
await refreshLicence();
|
||||||
|
|
||||||
|
licenceReq.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||||
|
return licenceReq(originalRequest!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { request: licenceReq, refreshLicence };
|
||||||
|
};
|
||||||
508
backend/src/ee/services/license/license-service.ts
Normal file
508
backend/src/ee/services/license/license-service.ts
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
// eslint-disable @typescript-eslint/no-unsafe-assignment
|
||||||
|
|
||||||
|
// TODO(akhilmhdh): With tony find out the api structure and fill it here
|
||||||
|
|
||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import NodeCache from "node-cache";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { getDefaultOnPremFeatures, setupLicenceRequestWithStore } from "./licence-fns";
|
||||||
|
import { TLicenseDALFactory } from "./license-dal";
|
||||||
|
import {
|
||||||
|
InstanceType,
|
||||||
|
TAddOrgPmtMethodDTO,
|
||||||
|
TAddOrgTaxIdDTO,
|
||||||
|
TCreateOrgPortalSession,
|
||||||
|
TDelOrgPmtMethodDTO,
|
||||||
|
TDelOrgTaxIdDTO,
|
||||||
|
TFeatureSet,
|
||||||
|
TGetOrgBillInfoDTO,
|
||||||
|
TGetOrgTaxIdDTO,
|
||||||
|
TOrgInvoiceDTO,
|
||||||
|
TOrgLicensesDTO,
|
||||||
|
TOrgPlanDTO,
|
||||||
|
TOrgPlansTableDTO,
|
||||||
|
TOrgPmtMethodsDTO,
|
||||||
|
TStartOrgTrialDTO,
|
||||||
|
TUpdateOrgBillingDetailsDTO
|
||||||
|
} from "./license-types";
|
||||||
|
|
||||||
|
type TLicenseServiceFactoryDep = {
|
||||||
|
orgDAL: Pick<TOrgDALFactory, "findOrgById">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
licenseDAL: TLicenseDALFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
||||||
|
|
||||||
|
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
|
||||||
|
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/licence-login";
|
||||||
|
|
||||||
|
const FEATURE_CACHE_KEY = (orgId: string, projectId?: string) => `${orgId}-${projectId || ""}`;
|
||||||
|
export const licenseServiceFactory = ({ orgDAL, permissionService, licenseDAL }: TLicenseServiceFactoryDep) => {
|
||||||
|
let isValidLicense = false;
|
||||||
|
let instanceType = InstanceType.OnPrem;
|
||||||
|
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
|
||||||
|
const featureStore = new NodeCache({ stdTTL: 60 });
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const licenseServerCloudApi = setupLicenceRequestWithStore(
|
||||||
|
appCfg.LICENSE_SERVER_URL || "",
|
||||||
|
LICENSE_SERVER_CLOUD_LOGIN,
|
||||||
|
appCfg.LICENSE_SERVER_KEY || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const licenseServerOnPremApi = setupLicenceRequestWithStore(
|
||||||
|
appCfg.LICENSE_SERVER_URL || "",
|
||||||
|
LICENSE_SERVER_ON_PREM_LOGIN,
|
||||||
|
appCfg.LICENSE_KEY || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
try {
|
||||||
|
if (appCfg.LICENSE_SERVER_KEY) {
|
||||||
|
const token = await licenseServerCloudApi.refreshLicence();
|
||||||
|
if (token) instanceType = InstanceType.Cloud;
|
||||||
|
logger.info(`Instance type: ${InstanceType.Cloud}`);
|
||||||
|
isValidLicense = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (appCfg.LICENSE_KEY) {
|
||||||
|
const token = await licenseServerOnPremApi.refreshLicence();
|
||||||
|
if (token) {
|
||||||
|
const {
|
||||||
|
data: { currentPlan }
|
||||||
|
} = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>("/api/license/v1/plan");
|
||||||
|
onPremFeatures = currentPlan;
|
||||||
|
instanceType = InstanceType.EnterpriseOnPrem;
|
||||||
|
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
|
||||||
|
isValidLicense = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// this means this is self hosted oss version
|
||||||
|
// else it would reach catch statement
|
||||||
|
isValidLicense = true;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`init-license: encountered an error when init license [error]`, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlan = async (orgId: string, projectId?: string) => {
|
||||||
|
logger.info(`getPlan: attempting to fetch plan for [orgId=${orgId}] [projectId=${projectId}]`);
|
||||||
|
try {
|
||||||
|
if (instanceType === InstanceType.Cloud) {
|
||||||
|
const cachedPlan = featureStore.get<TFeatureSet>(FEATURE_CACHE_KEY(orgId, projectId));
|
||||||
|
if (cachedPlan) return cachedPlan;
|
||||||
|
|
||||||
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!org) throw new BadRequestError({ message: "Org not found" });
|
||||||
|
const {
|
||||||
|
data: { currentPlan }
|
||||||
|
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
|
||||||
|
`/api/license-server/v1/customers/${org.customerId}/cloud-plan`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
workspaceId: projectId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
featureStore.set(FEATURE_CACHE_KEY(org.id, projectId), currentPlan);
|
||||||
|
return currentPlan;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`getPlan: encountered an error when fetching pan [orgId=${orgId}] [projectId=${projectId}] [error]`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
return onPremFeatures;
|
||||||
|
}
|
||||||
|
return onPremFeatures;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshPlan = async (orgId: string, projectId?: string) => {
|
||||||
|
if (instanceType === InstanceType.Cloud) {
|
||||||
|
featureStore.del(FEATURE_CACHE_KEY(orgId, projectId));
|
||||||
|
await getPlan(orgId, projectId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateOrgCustomerId = async (orgName: string, email: string) => {
|
||||||
|
if (instanceType === InstanceType.Cloud) {
|
||||||
|
const {
|
||||||
|
data: { customerId }
|
||||||
|
} = await licenseServerCloudApi.request.post<{ customerId: string }>(
|
||||||
|
"/api/license-server/v1/customers",
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
name: orgName
|
||||||
|
},
|
||||||
|
{ timeout: 5000, signal: AbortSignal.timeout(5000) }
|
||||||
|
);
|
||||||
|
return customerId;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeOrgCustomer = async (customerId: string) => {
|
||||||
|
await licenseServerCloudApi.request.delete(`/api/license-server/v1/customers/${customerId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSubscriptionOrgMemberCount = async (orgId: string) => {
|
||||||
|
if (instanceType === InstanceType.Cloud) {
|
||||||
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!org) throw new BadRequestError({ message: "Org not found" });
|
||||||
|
|
||||||
|
const count = await licenseDAL.countOfOrgMembers(orgId);
|
||||||
|
if (org?.customerId) {
|
||||||
|
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
|
||||||
|
quantity: count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
featureStore.del(orgId);
|
||||||
|
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
|
||||||
|
const usedSeats = await licenseDAL.countOfOrgMembers(null);
|
||||||
|
await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { usedSeats });
|
||||||
|
}
|
||||||
|
await refreshPlan(orgId);
|
||||||
|
};
|
||||||
|
|
||||||
|
// below all are api calls
|
||||||
|
const getOrgPlansTableByBillCycle = async ({ orgId, actor, actorId, actorOrgScope, billingCycle }: TOrgPlansTableDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
const { data } = await licenseServerCloudApi.request.get(
|
||||||
|
`/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrgPlan = async ({ orgId, actor, actorId, actorOrgScope, projectId }: TOrgPlanDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
const plan = await getPlan(orgId, projectId);
|
||||||
|
return plan;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startOrgTrial = async ({ orgId, actorId, actor, actorOrgScope, success_url }: TStartOrgTrialDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { url }
|
||||||
|
} = await licenseServerCloudApi.request.post(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/session/trial`,
|
||||||
|
{ success_url }
|
||||||
|
);
|
||||||
|
featureStore.del(FEATURE_CACHE_KEY(orgId));
|
||||||
|
return { url };
|
||||||
|
};
|
||||||
|
|
||||||
|
const createOrganizationPortalSession = async ({ orgId, actorId, actor, actorOrgScope }: TCreateOrgPortalSession) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { pmtMethods }
|
||||||
|
} = await licenseServerCloudApi.request.get<{ pmtMethods: string[] }>(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pmtMethods.length < 1) {
|
||||||
|
// case: organization has no payment method on file
|
||||||
|
// -> redirect to add payment method portal
|
||||||
|
const {
|
||||||
|
data: { url }
|
||||||
|
} = await licenseServerCloudApi.request.post(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`,
|
||||||
|
{
|
||||||
|
success_url: `${appCfg.SITE_URL}/dashboard`,
|
||||||
|
cancel_url: `${appCfg.SITE_URL}/dashboard`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { url };
|
||||||
|
}
|
||||||
|
// case: organization has payment method on file
|
||||||
|
// -> redirect to billing portal
|
||||||
|
const {
|
||||||
|
data: { url }
|
||||||
|
} = await licenseServerCloudApi.request.post(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details/billing-portal`,
|
||||||
|
{
|
||||||
|
return_url: `${appCfg.SITE_URL}/dashboard`
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { url };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrgBillingInfo = async ({ orgId, actor, actorId, actorOrgScope }: TGetOrgBillInfoDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { data } = await licenseServerCloudApi.request.get(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// returns org current plan feature table
|
||||||
|
const getOrgPlanTable = async ({ orgId, actor, actorId, actorOrgScope }: TGetOrgBillInfoDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { data } = await licenseServerCloudApi.request.get(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrgBillingDetails = async ({ orgId, actor, actorId, actorOrgScope }: TGetOrgBillInfoDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await licenseServerCloudApi.request.get(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateOrgBillingDetails = async ({ actorId, actor, actorOrgScope, orgId, name, email }: TUpdateOrgBillingDetailsDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { data } = await licenseServerCloudApi.request.patch(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details`,
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrgPmtMethods = async ({ orgId, actor, actorId, actorOrgScope }: TOrgPmtMethodsDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { pmtMethods }
|
||||||
|
} = await licenseServerCloudApi.request.get(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`
|
||||||
|
);
|
||||||
|
return pmtMethods;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addOrgPmtMethods = async ({ orgId, actor, actorId, actorOrgScope, success_url, cancel_url }: TAddOrgPmtMethodDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
data: { url }
|
||||||
|
} = await licenseServerCloudApi.request.post(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`,
|
||||||
|
{
|
||||||
|
success_url,
|
||||||
|
cancel_url
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { url };
|
||||||
|
};
|
||||||
|
|
||||||
|
const delOrgPmtMethods = async ({ actorId, actor, actorOrgScope, orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await licenseServerCloudApi.request.delete(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods/${pmtMethodId}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrgTaxIds = async ({ orgId, actor, actorId, actorOrgScope }: TGetOrgTaxIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
data: { tax_ids: taxIds }
|
||||||
|
} = await licenseServerCloudApi.request.get(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details/tax-ids`
|
||||||
|
);
|
||||||
|
return taxIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
const addOrgTaxId = async ({ actorId, actor, actorOrgScope, orgId, type, value }: TAddOrgTaxIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await licenseServerCloudApi.request.post(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details/tax-ids`,
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
value
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const delOrgTaxId = async ({ orgId, actor, actorId, actorOrgScope, taxId }: TDelOrgTaxIdDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await licenseServerCloudApi.request.delete(
|
||||||
|
`/api/license-server/v1/customers/${organization.customerId}/billing-details/tax-ids/${taxId}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrgTaxInvoices = async ({ actorId, actor, actorOrgScope, orgId }: TOrgInvoiceDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { invoices }
|
||||||
|
} = await licenseServerCloudApi.request.get(`/api/license-server/v1/customers/${organization.customerId}/invoices`);
|
||||||
|
return invoices;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrgLicenses = async ({ orgId, actor, actorId, actorOrgScope }: TOrgLicensesDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { licenses }
|
||||||
|
} = await licenseServerCloudApi.request.get(`/api/license-server/v1/customers/${organization.customerId}/licenses`);
|
||||||
|
return licenses;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
generateOrgCustomerId,
|
||||||
|
removeOrgCustomer,
|
||||||
|
init,
|
||||||
|
get isValidLicense() {
|
||||||
|
return isValidLicense;
|
||||||
|
},
|
||||||
|
getPlan,
|
||||||
|
updateSubscriptionOrgMemberCount,
|
||||||
|
refreshPlan,
|
||||||
|
getOrgPlan,
|
||||||
|
getOrgPlansTableByBillCycle,
|
||||||
|
startOrgTrial,
|
||||||
|
createOrganizationPortalSession,
|
||||||
|
getOrgBillingInfo,
|
||||||
|
getOrgPlanTable,
|
||||||
|
getOrgBillingDetails,
|
||||||
|
updateOrgBillingDetails,
|
||||||
|
addOrgPmtMethods,
|
||||||
|
delOrgPmtMethods,
|
||||||
|
getOrgPmtMethods,
|
||||||
|
getOrgLicenses,
|
||||||
|
getOrgTaxInvoices,
|
||||||
|
getOrgTaxIds,
|
||||||
|
addOrgTaxId,
|
||||||
|
delOrgTaxId
|
||||||
|
};
|
||||||
|
};
|
||||||
74
backend/src/ee/services/license/license-types.ts
Normal file
74
backend/src/ee/services/license/license-types.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export enum InstanceType {
|
||||||
|
OnPrem = "self-hosted",
|
||||||
|
EnterpriseOnPrem = "enterprise-self-hosted",
|
||||||
|
Cloud = "cloud"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TFeatureSet = {
|
||||||
|
_id: null;
|
||||||
|
slug: null;
|
||||||
|
tier: -1;
|
||||||
|
workspaceLimit: null;
|
||||||
|
workspacesUsed: 0;
|
||||||
|
memberLimit: null;
|
||||||
|
membersUsed: 0;
|
||||||
|
environmentLimit: null;
|
||||||
|
environmentsUsed: 0;
|
||||||
|
secretVersioning: true;
|
||||||
|
pitRecovery: false;
|
||||||
|
ipAllowlisting: false;
|
||||||
|
rbac: false;
|
||||||
|
customRateLimits: false;
|
||||||
|
customAlerts: false;
|
||||||
|
auditLogs: false;
|
||||||
|
auditLogsRetentionDays: 0;
|
||||||
|
samlSSO: false;
|
||||||
|
status: null;
|
||||||
|
trial_end: null;
|
||||||
|
has_used_trial: true;
|
||||||
|
secretApproval: false;
|
||||||
|
secretRotation: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOrgPlansTableDTO = {
|
||||||
|
billingCycle: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TOrgPlanDTO = {
|
||||||
|
projectId?: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TStartOrgTrialDTO = {
|
||||||
|
success_url: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TCreateOrgPortalSession = TOrgPermission;
|
||||||
|
|
||||||
|
export type TGetOrgBillInfoDTO = TOrgPermission;
|
||||||
|
|
||||||
|
export type TOrgPlanTableDTO = TOrgPermission;
|
||||||
|
|
||||||
|
export type TOrgBillingDetailsDTO = TOrgPermission;
|
||||||
|
|
||||||
|
export type TUpdateOrgBillingDetailsDTO = TOrgPermission & {
|
||||||
|
name?: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOrgPmtMethodsDTO = TOrgPermission;
|
||||||
|
|
||||||
|
export type TAddOrgPmtMethodDTO = TOrgPermission & { success_url: string; cancel_url: string };
|
||||||
|
|
||||||
|
export type TDelOrgPmtMethodDTO = TOrgPermission & { pmtMethodId: string };
|
||||||
|
|
||||||
|
export type TGetOrgTaxIdDTO = TOrgPermission;
|
||||||
|
|
||||||
|
export type TAddOrgTaxIdDTO = TOrgPermission & { type: string; value: string };
|
||||||
|
|
||||||
|
export type TDelOrgTaxIdDTO = TOrgPermission & { taxId: string };
|
||||||
|
|
||||||
|
export type TOrgInvoiceDTO = TOrgPermission;
|
||||||
|
|
||||||
|
export type TOrgLicensesDTO = TOrgPermission;
|
||||||
85
backend/src/ee/services/permission/permission-dal.ts
Normal file
85
backend/src/ee/services/permission/permission-dal.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { selectAllTableCols } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TPermissionDALFactory = ReturnType<typeof permissionDALFactory>;
|
||||||
|
|
||||||
|
export const permissionDALFactory = (db: TDbClient) => {
|
||||||
|
const getOrgPermission = async (userId: string, orgId: string) => {
|
||||||
|
try {
|
||||||
|
const membership = await db(TableName.OrgMembership)
|
||||||
|
.leftJoin(TableName.OrgRoles, `${TableName.OrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
|
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.where("userId", userId)
|
||||||
|
.where(`${TableName.OrgMembership}.orgId`, orgId)
|
||||||
|
.select(`${TableName.Organization}.authEnabled as orgAuthEnabled`)
|
||||||
|
.select("permissions")
|
||||||
|
.select(selectAllTableCols(TableName.OrgMembership))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetOrgPermission" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrgIdentityPermission = async (identityId: string, orgId: string) => {
|
||||||
|
try {
|
||||||
|
const membership = await db(TableName.IdentityOrgMembership)
|
||||||
|
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
|
.join(TableName.Organization, `${TableName.IdentityOrgMembership}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.where("identityId", identityId)
|
||||||
|
.where(`${TableName.IdentityOrgMembership}.orgId`, orgId)
|
||||||
|
.select(selectAllTableCols(TableName.IdentityOrgMembership))
|
||||||
|
.select(`${TableName.Organization}.authEnabled as orgAuthEnabled`)
|
||||||
|
.select("permissions")
|
||||||
|
.first();
|
||||||
|
return membership;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetOrgIdentityPermission" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectPermission = async (userId: string, projectId: string) => {
|
||||||
|
try {
|
||||||
|
const membership = await db(TableName.ProjectMembership)
|
||||||
|
.leftJoin(TableName.ProjectRoles, `${TableName.ProjectMembership}.roleId`, `${TableName.ProjectRoles}.id`)
|
||||||
|
.where("userId", userId)
|
||||||
|
.where(`${TableName.ProjectMembership}.projectId`, projectId)
|
||||||
|
.select(selectAllTableCols(TableName.ProjectMembership))
|
||||||
|
.select("permissions")
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetProjectPermission" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectIdentityPermission = async (identityId: string, projectId: string) => {
|
||||||
|
try {
|
||||||
|
const membership = await db(TableName.IdentityProjectMembership)
|
||||||
|
.leftJoin(
|
||||||
|
TableName.ProjectRoles,
|
||||||
|
`${TableName.IdentityProjectMembership}.roleId`,
|
||||||
|
`${TableName.ProjectRoles}.id`
|
||||||
|
)
|
||||||
|
.where("identityId", identityId)
|
||||||
|
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||||
|
.select(selectAllTableCols(TableName.IdentityProjectMembership))
|
||||||
|
.select("permissions")
|
||||||
|
.first();
|
||||||
|
return membership;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "GetProjectIdentityPermission" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getOrgPermission,
|
||||||
|
getOrgIdentityPermission,
|
||||||
|
getProjectPermission,
|
||||||
|
getProjectIdentityPermission
|
||||||
|
};
|
||||||
|
};
|
||||||
240
backend/src/ee/services/permission/permission-service.ts
Normal file
240
backend/src/ee/services/permission/permission-service.ts
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
import { createMongoAbility, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
|
import { PackRule, unpackRules } from "@casl/ability/extra";
|
||||||
|
import { MongoQuery } from "@ucast/mongo2js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OrgMembershipRole,
|
||||||
|
ProjectMembershipRole,
|
||||||
|
ServiceTokenScopes,
|
||||||
|
TIdentityProjectMemberships,
|
||||||
|
TProjectMemberships
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { conditionsMatcher } from "@app/lib/casl";
|
||||||
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
import { TOrgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||||
|
import { TProjectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
||||||
|
import { TServiceTokenDALFactory } from "@app/services/service-token/service-token-dal";
|
||||||
|
|
||||||
|
import { orgAdminPermissions, orgMemberPermissions, orgNoAccessPermissions, OrgPermissionSet } from "./org-permission";
|
||||||
|
import { TPermissionDALFactory } from "./permission-dal";
|
||||||
|
import {
|
||||||
|
buildServiceTokenProjectPermission,
|
||||||
|
projectAdminPermissions,
|
||||||
|
projectMemberPermissions,
|
||||||
|
projectNoAccessPermissions,
|
||||||
|
ProjectPermissionSet,
|
||||||
|
projectViewerPermission
|
||||||
|
} from "./project-permission";
|
||||||
|
|
||||||
|
type TPermissionServiceFactoryDep = {
|
||||||
|
orgRoleDAL: Pick<TOrgRoleDALFactory, "findOne">;
|
||||||
|
projectRoleDAL: Pick<TProjectRoleDALFactory, "findOne">;
|
||||||
|
serviceTokenDAL: Pick<TServiceTokenDALFactory, "findById">;
|
||||||
|
permissionDAL: TPermissionDALFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TPermissionServiceFactory = ReturnType<typeof permissionServiceFactory>;
|
||||||
|
|
||||||
|
export const permissionServiceFactory = ({
|
||||||
|
permissionDAL,
|
||||||
|
orgRoleDAL,
|
||||||
|
projectRoleDAL,
|
||||||
|
serviceTokenDAL
|
||||||
|
}: TPermissionServiceFactoryDep) => {
|
||||||
|
const buildOrgPermission = (role: string, permission?: unknown) => {
|
||||||
|
switch (role) {
|
||||||
|
case OrgMembershipRole.Admin:
|
||||||
|
return orgAdminPermissions;
|
||||||
|
case OrgMembershipRole.Member:
|
||||||
|
return orgMemberPermissions;
|
||||||
|
case OrgMembershipRole.NoAccess:
|
||||||
|
return orgNoAccessPermissions;
|
||||||
|
case OrgMembershipRole.Custom:
|
||||||
|
return createMongoAbility<OrgPermissionSet>(
|
||||||
|
unpackRules<RawRuleOf<MongoAbility<OrgPermissionSet>>>(
|
||||||
|
permission as PackRule<RawRuleOf<MongoAbility<OrgPermissionSet>>>[]
|
||||||
|
),
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({ name: "OrgRoleInvalid", message: "Org role not found" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildProjectPermission = (role: string, permission?: unknown) => {
|
||||||
|
switch (role) {
|
||||||
|
case ProjectMembershipRole.Admin:
|
||||||
|
return projectAdminPermissions;
|
||||||
|
case ProjectMembershipRole.Member:
|
||||||
|
return projectMemberPermissions;
|
||||||
|
case ProjectMembershipRole.Viewer:
|
||||||
|
return projectViewerPermission;
|
||||||
|
case ProjectMembershipRole.NoAccess:
|
||||||
|
return projectNoAccessPermissions;
|
||||||
|
case ProjectMembershipRole.Custom:
|
||||||
|
return createMongoAbility<ProjectPermissionSet>(
|
||||||
|
unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
|
||||||
|
permission as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]
|
||||||
|
),
|
||||||
|
{
|
||||||
|
conditionsMatcher
|
||||||
|
}
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw new BadRequestError({
|
||||||
|
name: "ProjectRoleInvalid",
|
||||||
|
message: "Project role not found"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get user permission in an organization
|
||||||
|
* */
|
||||||
|
const getUserOrgPermission = async (userId: string, orgId: string, orgScope?: string) => {
|
||||||
|
const membership = await permissionDAL.getOrgPermission(userId, orgId);
|
||||||
|
if (!membership) throw new UnauthorizedError({ name: "User not in org" });
|
||||||
|
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||||
|
throw new BadRequestError({ name: "Custom permission not found" });
|
||||||
|
}
|
||||||
|
if (membership.orgAuthEnabled && membership.orgId !== orgScope) {
|
||||||
|
throw new BadRequestError({ name: "Cannot access org-scoped resource" });
|
||||||
|
}
|
||||||
|
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIdentityOrgPermission = async (identityId: string, orgId: string, orgScope?: string) => {
|
||||||
|
const membership = await permissionDAL.getOrgIdentityPermission(identityId, orgId);
|
||||||
|
if (!membership) throw new UnauthorizedError({ name: "Identity not in org" });
|
||||||
|
if (membership.role === OrgMembershipRole.Custom && !membership.permissions) {
|
||||||
|
throw new BadRequestError({ name: "Custom permission not found" });
|
||||||
|
}
|
||||||
|
if (membership.orgAuthEnabled && membership.orgId !== orgScope) {
|
||||||
|
throw new BadRequestError({ name: "Cannot access org-scoped resource" });
|
||||||
|
}
|
||||||
|
return { permission: buildOrgPermission(membership.role, membership.permissions), membership };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrgPermission = async (type: ActorType, id: string, orgId: string, orgScope?: string) => {
|
||||||
|
switch (type) {
|
||||||
|
case ActorType.USER:
|
||||||
|
return getUserOrgPermission(id, orgId, orgScope);
|
||||||
|
case ActorType.IDENTITY:
|
||||||
|
return getIdentityOrgPermission(id, orgId, orgScope);
|
||||||
|
default:
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Permission not defined",
|
||||||
|
name: "Get org permission"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// instead of actor type this will fetch by role slug. meaning it can be the pre defined slugs like
|
||||||
|
// admin member or user defined ones like biller etc
|
||||||
|
const getOrgPermissionByRole = async (role: string, orgId: string) => {
|
||||||
|
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
|
||||||
|
if (isCustomRole) {
|
||||||
|
const orgRole = await orgRoleDAL.findOne({ slug: role, orgId });
|
||||||
|
if (!orgRole) throw new BadRequestError({ message: "Role not found" });
|
||||||
|
return {
|
||||||
|
permission: buildOrgPermission(OrgMembershipRole.Custom, orgRole.permissions),
|
||||||
|
role: orgRole
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { permission: buildOrgPermission(role, []) };
|
||||||
|
};
|
||||||
|
|
||||||
|
// user permission for a project in an organization
|
||||||
|
const getUserProjectPermission = async (userId: string, projectId: string) => {
|
||||||
|
const membership = await permissionDAL.getProjectPermission(userId, projectId);
|
||||||
|
if (!membership) throw new UnauthorizedError({ name: "User not in project" });
|
||||||
|
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
|
||||||
|
throw new BadRequestError({ name: "Custom permission not found" });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
permission: buildProjectPermission(membership.role, membership.permissions),
|
||||||
|
membership
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIdentityProjectPermission = async (identityId: string, projectId: string) => {
|
||||||
|
const membership = await permissionDAL.getProjectIdentityPermission(identityId, projectId);
|
||||||
|
if (!membership) throw new UnauthorizedError({ name: "Identity not in project" });
|
||||||
|
if (membership.role === ProjectMembershipRole.Custom && !membership.permissions) {
|
||||||
|
throw new BadRequestError({ name: "Custom permission not found" });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
permission: buildProjectPermission(membership.role, membership.permissions),
|
||||||
|
membership
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServiceTokenProjectPermission = async (serviceTokenId: string, projectId: string) => {
|
||||||
|
const serviceToken = await serviceTokenDAL.findById(serviceTokenId);
|
||||||
|
if (serviceToken.projectId !== projectId)
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Failed to find service authorization for given project"
|
||||||
|
});
|
||||||
|
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
|
||||||
|
return {
|
||||||
|
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
|
||||||
|
membership: undefined
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type TProjectPermissionRT<T extends ActorType> = T extends ActorType.SERVICE
|
||||||
|
? { permission: MongoAbility<ProjectPermissionSet, MongoQuery>; membership: undefined }
|
||||||
|
: {
|
||||||
|
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||||
|
membership: (T extends ActorType.USER ? TProjectMemberships : TIdentityProjectMemberships) & {
|
||||||
|
permissions?: unknown;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectPermission = async <T extends ActorType>(
|
||||||
|
type: T,
|
||||||
|
id: string,
|
||||||
|
projectId: string
|
||||||
|
): Promise<TProjectPermissionRT<T>> => {
|
||||||
|
switch (type) {
|
||||||
|
case ActorType.USER:
|
||||||
|
return getUserProjectPermission(id, projectId) as Promise<TProjectPermissionRT<T>>;
|
||||||
|
case ActorType.SERVICE:
|
||||||
|
return getServiceTokenProjectPermission(id, projectId) as Promise<TProjectPermissionRT<T>>;
|
||||||
|
case ActorType.IDENTITY:
|
||||||
|
return getIdentityProjectPermission(id, projectId) as Promise<TProjectPermissionRT<T>>;
|
||||||
|
default:
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Permission not defined",
|
||||||
|
name: "Get project permission"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjectPermissionByRole = async (role: string, projectId: string) => {
|
||||||
|
const isCustomRole = !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole);
|
||||||
|
if (isCustomRole) {
|
||||||
|
const projectRole = await projectRoleDAL.findOne({ slug: role, projectId });
|
||||||
|
if (!projectRole) throw new BadRequestError({ message: "Role not found" });
|
||||||
|
return {
|
||||||
|
permission: buildProjectPermission(ProjectMembershipRole.Custom, projectRole.permissions),
|
||||||
|
role: projectRole
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { permission: buildProjectPermission(role, []) };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
getUserOrgPermission,
|
||||||
|
getOrgPermission,
|
||||||
|
getUserProjectPermission,
|
||||||
|
getProjectPermission,
|
||||||
|
getOrgPermissionByRole,
|
||||||
|
getProjectPermissionByRole,
|
||||||
|
buildOrgPermission,
|
||||||
|
buildProjectPermission
|
||||||
|
};
|
||||||
|
};
|
||||||
400
backend/src/ee/services/saml-config/saml-config-service.ts
Normal file
400
backend/src/ee/services/saml-config/saml-config-service.ts
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import {
|
||||||
|
OrgMembershipRole,
|
||||||
|
OrgMembershipStatus,
|
||||||
|
SecretKeyEncoding,
|
||||||
|
TSamlConfigs,
|
||||||
|
TSamlConfigsUpdate
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import {
|
||||||
|
decryptSymmetric,
|
||||||
|
encryptSymmetric,
|
||||||
|
generateAsymmetricKeyPair,
|
||||||
|
generateSymmetricKey,
|
||||||
|
infisicalSymmetricDecrypt,
|
||||||
|
infisicalSymmetricEncypt
|
||||||
|
} from "@app/lib/crypto/encryption";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
import { TOrgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
|
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||||
|
|
||||||
|
import { TLicenseServiceFactory } from "../license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||||
|
import { TSamlConfigDALFactory } from "./saml-config-dal";
|
||||||
|
import {
|
||||||
|
SamlProviders,
|
||||||
|
TCreateSamlCfgDTO,
|
||||||
|
TGetSamlCfgDTO,
|
||||||
|
TSamlLoginDTO,
|
||||||
|
TUpdateSamlCfgDTO
|
||||||
|
} from "./saml-config-types";
|
||||||
|
|
||||||
|
type TSamlConfigServiceFactoryDep = {
|
||||||
|
samlConfigDAL: TSamlConfigDALFactory;
|
||||||
|
userDAL: Pick<TUserDALFactory, "create" | "findUserByEmail" | "transaction" | "updateById">;
|
||||||
|
orgDAL: Pick<TOrgDALFactory, "createMembership" | "updateMembershipById" | "findMembership" | "findOrgById" | "findOne" | "updateById">;
|
||||||
|
orgBotDAL: Pick<TOrgBotDALFactory, "findOne" | "create" | "transaction">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>;
|
||||||
|
|
||||||
|
export const samlConfigServiceFactory = ({
|
||||||
|
samlConfigDAL,
|
||||||
|
orgBotDAL,
|
||||||
|
orgDAL,
|
||||||
|
userDAL,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
}: TSamlConfigServiceFactoryDep) => {
|
||||||
|
const createSamlCfg = async ({
|
||||||
|
cert,
|
||||||
|
actor,
|
||||||
|
actorOrgScope,
|
||||||
|
orgId,
|
||||||
|
issuer,
|
||||||
|
actorId,
|
||||||
|
isActive,
|
||||||
|
entryPoint,
|
||||||
|
authProvider
|
||||||
|
}: TCreateSamlCfgDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Sso);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (!plan.samlSSO)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
|
||||||
|
});
|
||||||
|
|
||||||
|
const orgBot = await orgBotDAL.transaction(async (tx) => {
|
||||||
|
const doc = await orgBotDAL.findOne({ orgId }, tx);
|
||||||
|
if (doc) return doc;
|
||||||
|
|
||||||
|
const { privateKey, publicKey } = generateAsymmetricKeyPair();
|
||||||
|
const key = generateSymmetricKey();
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: privateKeyIV,
|
||||||
|
tag: privateKeyTag,
|
||||||
|
encoding: privateKeyKeyEncoding,
|
||||||
|
algorithm: privateKeyAlgorithm
|
||||||
|
} = infisicalSymmetricEncypt(privateKey);
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSymmetricKey,
|
||||||
|
iv: symmetricKeyIV,
|
||||||
|
tag: symmetricKeyTag,
|
||||||
|
encoding: symmetricKeyKeyEncoding,
|
||||||
|
algorithm: symmetricKeyAlgorithm
|
||||||
|
} = infisicalSymmetricEncypt(key);
|
||||||
|
|
||||||
|
return orgBotDAL.create(
|
||||||
|
{
|
||||||
|
name: "Infisical org bot",
|
||||||
|
publicKey,
|
||||||
|
privateKeyIV,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
symmetricKeyIV,
|
||||||
|
symmetricKeyTag,
|
||||||
|
encryptedSymmetricKey,
|
||||||
|
symmetricKeyAlgorithm,
|
||||||
|
orgId,
|
||||||
|
privateKeyTag,
|
||||||
|
privateKeyAlgorithm,
|
||||||
|
privateKeyKeyEncoding,
|
||||||
|
symmetricKeyKeyEncoding
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const key = infisicalSymmetricDecrypt({
|
||||||
|
ciphertext: orgBot.encryptedSymmetricKey,
|
||||||
|
iv: orgBot.symmetricKeyIV,
|
||||||
|
tag: orgBot.symmetricKeyTag,
|
||||||
|
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ciphertext: encryptedEntryPoint, iv: entryPointIV, tag: entryPointTag } = encryptSymmetric(entryPoint, key);
|
||||||
|
const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key);
|
||||||
|
|
||||||
|
const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key);
|
||||||
|
const samlConfig = await samlConfigDAL.create({
|
||||||
|
orgId,
|
||||||
|
authProvider,
|
||||||
|
isActive,
|
||||||
|
encryptedEntryPoint,
|
||||||
|
entryPointIV,
|
||||||
|
entryPointTag,
|
||||||
|
encryptedIssuer,
|
||||||
|
issuerIV,
|
||||||
|
issuerTag,
|
||||||
|
encryptedCert,
|
||||||
|
certIV,
|
||||||
|
certTag
|
||||||
|
});
|
||||||
|
await orgDAL.updateById(orgId, { authEnabled: isActive });
|
||||||
|
|
||||||
|
return samlConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateSamlCfg = async ({
|
||||||
|
orgId,
|
||||||
|
actor,
|
||||||
|
actorOrgScope,
|
||||||
|
cert,
|
||||||
|
actorId,
|
||||||
|
issuer,
|
||||||
|
isActive,
|
||||||
|
entryPoint,
|
||||||
|
authProvider
|
||||||
|
}: TUpdateSamlCfgDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (!plan.samlSSO)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateQuery: TSamlConfigsUpdate = { authProvider, isActive };
|
||||||
|
const orgBot = await orgBotDAL.findOne({ orgId });
|
||||||
|
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||||
|
const key = infisicalSymmetricDecrypt({
|
||||||
|
ciphertext: orgBot.encryptedSymmetricKey,
|
||||||
|
iv: orgBot.symmetricKeyIV,
|
||||||
|
tag: orgBot.symmetricKeyTag,
|
||||||
|
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entryPoint) {
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedEntryPoint,
|
||||||
|
iv: entryPointIV,
|
||||||
|
tag: entryPointTag
|
||||||
|
} = encryptSymmetric(entryPoint, key);
|
||||||
|
updateQuery.encryptedEntryPoint = encryptedEntryPoint;
|
||||||
|
updateQuery.entryPointIV = entryPointIV;
|
||||||
|
updateQuery.entryPointTag = entryPointTag;
|
||||||
|
}
|
||||||
|
if (issuer) {
|
||||||
|
const { ciphertext: encryptedIssuer, iv: issuerIV, tag: issuerTag } = encryptSymmetric(issuer, key);
|
||||||
|
updateQuery.encryptedIssuer = encryptedIssuer;
|
||||||
|
updateQuery.issuerIV = issuerIV;
|
||||||
|
updateQuery.issuerTag = issuerTag;
|
||||||
|
}
|
||||||
|
if (cert) {
|
||||||
|
const { ciphertext: encryptedCert, iv: certIV, tag: certTag } = encryptSymmetric(cert, key);
|
||||||
|
updateQuery.encryptedCert = encryptedCert;
|
||||||
|
updateQuery.certIV = certIV;
|
||||||
|
updateQuery.certTag = certTag;
|
||||||
|
}
|
||||||
|
const [ssoConfig] = await samlConfigDAL.update({ orgId }, updateQuery);
|
||||||
|
await orgDAL.updateById(orgId, { authEnabled: isActive });
|
||||||
|
|
||||||
|
return ssoConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSaml = async (dto: TGetSamlCfgDTO) => {
|
||||||
|
let ssoConfig: TSamlConfigs | undefined;
|
||||||
|
if (dto.type === "org") {
|
||||||
|
ssoConfig = await samlConfigDAL.findOne({ orgId: dto.orgId });
|
||||||
|
if (!ssoConfig) return;
|
||||||
|
} else if (dto.type === "orgSlug") {
|
||||||
|
const org = await orgDAL.findOne({ slug: dto.orgSlug });
|
||||||
|
if (!org) return;
|
||||||
|
ssoConfig = await samlConfigDAL.findOne({ orgId: org.id });
|
||||||
|
} else if (dto.type === "ssoId") {
|
||||||
|
// TODO:
|
||||||
|
// We made this change because saml config ids were not moved over during the migration
|
||||||
|
// This will patch this issue.
|
||||||
|
// Remove in the future
|
||||||
|
const UUIDToMongoId: Record<string, string> = {
|
||||||
|
"64c81ff7905fadcfead01e9a": "0978bcbe-8f94-4d95-8600-009787262613",
|
||||||
|
"652d4777c74d008c85c8bed5": "42044bf5-119e-443e-a51b-0308ac7e45ea",
|
||||||
|
"6527df39771217236f8721f6": "6311ec4b-d692-4422-b52a-337f719ae6b0",
|
||||||
|
"650374a561d12cd3d835aeb8": "6453516c-930d-4ff0-ad3b-496ba6eb80ca",
|
||||||
|
"655d67d10a0f4d307c8b1536": "73b9f1b1-f946-4f18-9a2d-310f157f7df5",
|
||||||
|
"64f23239a5d4ed17f1e544c4": "9256337f-e3da-43d7-8266-39c9276e8426",
|
||||||
|
"65348e49db355e6e4782571f": "b8a227c7-843e-410e-8982-b4976a599b69",
|
||||||
|
"657a219fc8a80c2eff97eb38": "fcab1573-ae7f-4fcf-9645-646207acf035"
|
||||||
|
};
|
||||||
|
|
||||||
|
const id = UUIDToMongoId[dto.id] ?? dto.id;
|
||||||
|
|
||||||
|
ssoConfig = await samlConfigDAL.findById(id);
|
||||||
|
}
|
||||||
|
if (!ssoConfig) throw new BadRequestError({ message: "Failed to find organization SSO data" });
|
||||||
|
|
||||||
|
// when dto is type id means it's internally used
|
||||||
|
if (dto.type === "org") {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(dto.actor, dto.actorId, ssoConfig.orgId, dto.actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Sso);
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
entryPointTag,
|
||||||
|
entryPointIV,
|
||||||
|
encryptedEntryPoint,
|
||||||
|
certTag,
|
||||||
|
certIV,
|
||||||
|
encryptedCert,
|
||||||
|
issuerTag,
|
||||||
|
issuerIV,
|
||||||
|
encryptedIssuer
|
||||||
|
} = ssoConfig;
|
||||||
|
|
||||||
|
const orgBot = await orgBotDAL.findOne({ orgId: ssoConfig.orgId });
|
||||||
|
if (!orgBot) throw new BadRequestError({ message: "Org bot not found", name: "OrgBotNotFound" });
|
||||||
|
const key = infisicalSymmetricDecrypt({
|
||||||
|
ciphertext: orgBot.encryptedSymmetricKey,
|
||||||
|
iv: orgBot.symmetricKeyIV,
|
||||||
|
tag: orgBot.symmetricKeyTag,
|
||||||
|
keyEncoding: orgBot.symmetricKeyKeyEncoding as SecretKeyEncoding
|
||||||
|
});
|
||||||
|
|
||||||
|
let entryPoint = "";
|
||||||
|
if (encryptedEntryPoint && entryPointIV && entryPointTag) {
|
||||||
|
entryPoint = decryptSymmetric({
|
||||||
|
ciphertext: encryptedEntryPoint,
|
||||||
|
key,
|
||||||
|
tag: entryPointTag,
|
||||||
|
iv: entryPointIV
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let issuer = "";
|
||||||
|
if (encryptedIssuer && issuerTag && issuerIV) {
|
||||||
|
issuer = decryptSymmetric({
|
||||||
|
key,
|
||||||
|
tag: issuerTag,
|
||||||
|
iv: issuerIV,
|
||||||
|
ciphertext: encryptedIssuer
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let cert = "";
|
||||||
|
if (encryptedCert && certTag && certIV) {
|
||||||
|
cert = decryptSymmetric({ key, tag: certTag, iv: certIV, ciphertext: encryptedCert });
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: ssoConfig.id,
|
||||||
|
organization: ssoConfig.orgId,
|
||||||
|
orgId: ssoConfig.orgId,
|
||||||
|
authProvider: ssoConfig.authProvider,
|
||||||
|
isActive: ssoConfig.isActive,
|
||||||
|
entryPoint,
|
||||||
|
issuer,
|
||||||
|
cert
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const samlLogin = async ({
|
||||||
|
firstName,
|
||||||
|
email,
|
||||||
|
lastName,
|
||||||
|
authProvider,
|
||||||
|
orgId,
|
||||||
|
relayState,
|
||||||
|
isSignupAllowed
|
||||||
|
}: TSamlLoginDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
let user = await userDAL.findUserByEmail(email);
|
||||||
|
const isSamlSignUpDisabled = !isSignupAllowed && !user;
|
||||||
|
if (isSamlSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Saml SSO login" });
|
||||||
|
|
||||||
|
const organization = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!organization) throw new BadRequestError({ message: "Org not found" });
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
const hasSamlEnabled = (user.authMethods || []).some((method) =>
|
||||||
|
Object.values(SamlProviders).includes(method as SamlProviders)
|
||||||
|
);
|
||||||
|
await userDAL.transaction(async (tx) => {
|
||||||
|
if (!hasSamlEnabled) {
|
||||||
|
await userDAL.updateById(user.id, { authMethods: [authProvider] }, tx);
|
||||||
|
}
|
||||||
|
const [orgMembership] = await orgDAL.findMembership({ userId: user.id, orgId }, { tx });
|
||||||
|
if (!orgMembership) {
|
||||||
|
await orgDAL.createMembership(
|
||||||
|
{
|
||||||
|
userId: user.id,
|
||||||
|
orgId,
|
||||||
|
inviteEmail: email,
|
||||||
|
role: OrgMembershipRole.Member,
|
||||||
|
status: OrgMembershipStatus.Accepted
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
} else if (orgMembership.status === OrgMembershipStatus.Invited) {
|
||||||
|
await orgDAL.updateMembershipById(
|
||||||
|
orgMembership.id,
|
||||||
|
{
|
||||||
|
status: OrgMembershipStatus.Accepted
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
user = await userDAL.transaction(async (tx) => {
|
||||||
|
const newUser = await userDAL.create(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
authMethods: [authProvider]
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await orgDAL.createMembership({
|
||||||
|
inviteEmail: email,
|
||||||
|
orgId,
|
||||||
|
role: OrgMembershipRole.Member,
|
||||||
|
status: OrgMembershipStatus.Invited
|
||||||
|
});
|
||||||
|
return newUser;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const isUserCompleted = Boolean(user.isAccepted);
|
||||||
|
const providerAuthToken = jwt.sign(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
organizationName: organization.name,
|
||||||
|
organizationId: organization.id,
|
||||||
|
authMethod: authProvider,
|
||||||
|
isUserCompleted,
|
||||||
|
...(relayState
|
||||||
|
? {
|
||||||
|
callbackPort: (JSON.parse(relayState) as { callbackPort: string }).callbackPort
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
},
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { isUserCompleted, providerAuthToken };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSamlCfg,
|
||||||
|
updateSamlCfg,
|
||||||
|
getSaml,
|
||||||
|
samlLogin
|
||||||
|
};
|
||||||
|
};
|
||||||
47
backend/src/ee/services/saml-config/saml-config-types.ts
Normal file
47
backend/src/ee/services/saml-config/saml-config-types.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export enum SamlProviders {
|
||||||
|
OKTA_SAML = "okta-saml",
|
||||||
|
AZURE_SAML = "azure-saml",
|
||||||
|
JUMPCLOUD_SAML = "jumpcloud-saml"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TCreateSamlCfgDTO = {
|
||||||
|
authProvider: SamlProviders;
|
||||||
|
isActive: boolean;
|
||||||
|
entryPoint: string;
|
||||||
|
issuer: string;
|
||||||
|
cert: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TUpdateSamlCfgDTO = Partial<{
|
||||||
|
authProvider: SamlProviders;
|
||||||
|
isActive: boolean;
|
||||||
|
entryPoint: string;
|
||||||
|
issuer: string;
|
||||||
|
cert: string;
|
||||||
|
}> &
|
||||||
|
TOrgPermission;
|
||||||
|
|
||||||
|
export type TGetSamlCfgDTO =
|
||||||
|
| { type: "org"; orgId: string; actor: ActorType; actorId: string, actorOrgScope?: string }
|
||||||
|
| {
|
||||||
|
type: "orgSlug",
|
||||||
|
orgSlug: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "ssoId";
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSamlLoginDTO = {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName?: string;
|
||||||
|
authProvider: string;
|
||||||
|
orgId: string;
|
||||||
|
isSignupAllowed: boolean;
|
||||||
|
// saml thingy
|
||||||
|
relayState?: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { WebhookEventMap } from "@octokit/webhooks-types";
|
||||||
|
import { ProbotOctokit } from "probot";
|
||||||
|
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TGitAppDALFactory } from "./git-app-dal";
|
||||||
|
import { TGitAppInstallSessionDALFactory } from "./git-app-install-session-dal";
|
||||||
|
import { TSecretScanningDALFactory } from "./secret-scanning-dal";
|
||||||
|
import { TSecretScanningQueueFactory } from "./secret-scanning-queue";
|
||||||
|
import {
|
||||||
|
SecretScanningRiskStatus,
|
||||||
|
TGetOrgInstallStatusDTO,
|
||||||
|
TGetOrgRisksDTO,
|
||||||
|
TInstallAppSessionDTO,
|
||||||
|
TLinkInstallSessionDTO,
|
||||||
|
TUpdateRiskStatusDTO
|
||||||
|
} from "./secret-scanning-types";
|
||||||
|
|
||||||
|
type TSecretScanningServiceFactoryDep = {
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
secretScanningDAL: TSecretScanningDALFactory;
|
||||||
|
gitAppInstallSessionDAL: TGitAppInstallSessionDALFactory;
|
||||||
|
gitAppOrgDAL: TGitAppDALFactory;
|
||||||
|
secretScanningQueue: TSecretScanningQueueFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSecretScanningServiceFactory = ReturnType<typeof secretScanningServiceFactory>;
|
||||||
|
|
||||||
|
export const secretScanningServiceFactory = ({
|
||||||
|
secretScanningDAL,
|
||||||
|
gitAppOrgDAL,
|
||||||
|
gitAppInstallSessionDAL,
|
||||||
|
permissionService,
|
||||||
|
secretScanningQueue
|
||||||
|
}: TSecretScanningServiceFactoryDep) => {
|
||||||
|
const createInstallationSession = async ({ actor, orgId, actorId, actorOrgScope }: TInstallAppSessionDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||||
|
|
||||||
|
const sessionId = crypto.randomBytes(16).toString("hex");
|
||||||
|
await gitAppInstallSessionDAL.upsert({ orgId, sessionId, userId: actorId });
|
||||||
|
return { sessionId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkInstallationToOrg = async ({ sessionId, actorId, installationId, actor, actorOrgScope }: TLinkInstallSessionDTO) => {
|
||||||
|
const session = await gitAppInstallSessionDAL.findOne({ sessionId });
|
||||||
|
if (!session) throw new UnauthorizedError({ message: "Session not found" });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, session.orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.SecretScanning);
|
||||||
|
const installatedApp = await gitAppOrgDAL.transaction(async (tx) => {
|
||||||
|
await gitAppInstallSessionDAL.deleteById(session.id, tx);
|
||||||
|
return gitAppOrgDAL.upsert({ orgId: session.orgId, installationId, userId: actorId }, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const octokit = new ProbotOctokit({
|
||||||
|
auth: {
|
||||||
|
appId: appCfg.SECRET_SCANNING_GIT_APP_ID,
|
||||||
|
privateKey: appCfg.SECRET_SCANNING_PRIVATE_KEY,
|
||||||
|
installationId: installationId.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { repositories }
|
||||||
|
} = await octokit.apps.listReposAccessibleToInstallation();
|
||||||
|
await Promise.all(
|
||||||
|
repositories.map(({ id, full_name }) =>
|
||||||
|
secretScanningQueue.startFullRepoScan({
|
||||||
|
organizationId: session.orgId,
|
||||||
|
installationId,
|
||||||
|
repository: { id, fullName: full_name }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return { installatedApp };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOrgInstallationStatus = async ({ actorId, orgId, actor, actorOrgScope }: TGetOrgInstallStatusDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||||
|
|
||||||
|
const appInstallation = await gitAppOrgDAL.findOne({ orgId });
|
||||||
|
return Boolean(appInstallation);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRisksByOrg = async ({ actor, orgId, actorId, actorOrgScope }: TGetOrgRisksDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.SecretScanning);
|
||||||
|
const risks = await secretScanningDAL.find({ orgId }, { sort: [["createdAt", "desc"]] });
|
||||||
|
return { risks };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRiskStatus = async ({ actorId, orgId, actor, actorOrgScope, riskId, status }: TUpdateRiskStatusDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
||||||
|
|
||||||
|
const isRiskResolved = Boolean(
|
||||||
|
[
|
||||||
|
SecretScanningRiskStatus.FalsePositive,
|
||||||
|
SecretScanningRiskStatus.Revoked,
|
||||||
|
SecretScanningRiskStatus.NotRevoked
|
||||||
|
].includes(status)
|
||||||
|
);
|
||||||
|
|
||||||
|
const risk = await secretScanningDAL.updateById(riskId, {
|
||||||
|
status,
|
||||||
|
isResolved: isRiskResolved
|
||||||
|
});
|
||||||
|
return { risk };
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRepoPushEvent = async (payload: WebhookEventMap["push"]) => {
|
||||||
|
const { commits, repository, installation, pusher } = payload;
|
||||||
|
if (!commits || !repository || !installation || !pusher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installationLink = await gitAppOrgDAL.findOne({
|
||||||
|
installationId: String(installation.id)
|
||||||
|
});
|
||||||
|
if (!installationLink) return;
|
||||||
|
|
||||||
|
await secretScanningQueue.startPushEventScan({
|
||||||
|
commits,
|
||||||
|
pusher: { name: pusher.name, email: pusher.email },
|
||||||
|
repository: { fullName: repository.full_name, id: repository.id },
|
||||||
|
organizationId: installationLink.orgId,
|
||||||
|
installationId: String(installation?.id)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRepoDeleteEvent = async (installationId: string, repositoryIds: string[]) => {
|
||||||
|
await secretScanningDAL.transaction(async (tx) => {
|
||||||
|
if (repositoryIds.length) {
|
||||||
|
await Promise.all(repositoryIds.map((repoId) => secretScanningDAL.delete({ repositoryId: repoId }, tx)));
|
||||||
|
}
|
||||||
|
await gitAppOrgDAL.delete({ installationId }, tx);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createInstallationSession,
|
||||||
|
linkInstallationToOrg,
|
||||||
|
getOrgInstallationStatus,
|
||||||
|
getRisksByOrg,
|
||||||
|
updateRiskStatus,
|
||||||
|
handleRepoPushEvent,
|
||||||
|
handleRepoDeleteEvent
|
||||||
|
};
|
||||||
|
};
|
||||||
21
backend/src/lib/types/index.ts
Normal file
21
backend/src/lib/types/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export type TOrgPermission = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
orgId: string;
|
||||||
|
actorOrgScope?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TProjectPermission = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
projectId: string;
|
||||||
|
actorOrgScope?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RequiredKeys<T> = {
|
||||||
|
[K in keyof T]-?: undefined extends T[K] ? never : K;
|
||||||
|
}[keyof T];
|
||||||
|
|
||||||
|
export type PickRequired<T> = Pick<T, RequiredKeys<T>>;
|
||||||
119
backend/src/server/plugins/auth/inject-identity.ts
Normal file
119
backend/src/server/plugins/auth/inject-identity.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { FastifyRequest } from "fastify";
|
||||||
|
import fp from "fastify-plugin";
|
||||||
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { TServiceTokens, TUsers } from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { ActorType, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types";
|
||||||
|
|
||||||
|
export type TAuthMode =
|
||||||
|
| {
|
||||||
|
authMode: AuthMode.JWT;
|
||||||
|
actor: ActorType.USER;
|
||||||
|
userId: string;
|
||||||
|
tokenVersionId: string; // the session id of token used
|
||||||
|
user: TUsers;
|
||||||
|
orgId?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
authMode: AuthMode.API_KEY;
|
||||||
|
actor: ActorType.USER;
|
||||||
|
userId: string;
|
||||||
|
user: TUsers;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
authMode: AuthMode.SERVICE_TOKEN;
|
||||||
|
serviceToken: TServiceTokens;
|
||||||
|
actor: ActorType.SERVICE;
|
||||||
|
serviceTokenId: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
authMode: AuthMode.IDENTITY_ACCESS_TOKEN;
|
||||||
|
actor: ActorType.IDENTITY;
|
||||||
|
identityId: string;
|
||||||
|
identityName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractAuth = async (req: FastifyRequest, jwtSecret: string) => {
|
||||||
|
const apiKey = req.headers?.["x-api-key"];
|
||||||
|
if (apiKey) {
|
||||||
|
return { authMode: AuthMode.API_KEY, token: apiKey, actor: ActorType.USER } as const;
|
||||||
|
}
|
||||||
|
const authHeader = req.headers?.authorization;
|
||||||
|
if (!authHeader) return { authMode: null, token: null };
|
||||||
|
|
||||||
|
const authTokenValue = authHeader.slice(7); // slice of after Bearer
|
||||||
|
if (authTokenValue.startsWith("st.")) {
|
||||||
|
return {
|
||||||
|
authMode: AuthMode.SERVICE_TOKEN,
|
||||||
|
token: authTokenValue,
|
||||||
|
actor: ActorType.SERVICE
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedToken = jwt.verify(authTokenValue, jwtSecret) as JwtPayload;
|
||||||
|
switch (decodedToken.authTokenType) {
|
||||||
|
case AuthTokenType.ACCESS_TOKEN:
|
||||||
|
return {
|
||||||
|
authMode: AuthMode.JWT,
|
||||||
|
token: decodedToken as AuthModeJwtTokenPayload,
|
||||||
|
actor: ActorType.USER
|
||||||
|
} as const;
|
||||||
|
case AuthTokenType.API_KEY:
|
||||||
|
return { authMode: AuthMode.API_KEY, token: decodedToken, actor: ActorType.USER } as const;
|
||||||
|
case AuthTokenType.IDENTITY_ACCESS_TOKEN:
|
||||||
|
return {
|
||||||
|
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
|
||||||
|
token: decodedToken as TIdentityAccessTokenJwtPayload,
|
||||||
|
actor: ActorType.IDENTITY
|
||||||
|
} as const;
|
||||||
|
default:
|
||||||
|
return { authMode: null, token: null } as const;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const injectIdentity = fp(async (server: FastifyZodProvider) => {
|
||||||
|
server.decorateRequest("auth", null);
|
||||||
|
server.addHook("onRequest", async (req) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const { authMode, token, actor } = await extractAuth(req, appCfg.AUTH_SECRET);
|
||||||
|
if (!authMode) return;
|
||||||
|
|
||||||
|
switch (authMode) {
|
||||||
|
case AuthMode.JWT: {
|
||||||
|
const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token);
|
||||||
|
req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId, actor, orgId };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AuthMode.IDENTITY_ACCESS_TOKEN: {
|
||||||
|
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp);
|
||||||
|
req.auth = {
|
||||||
|
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
|
||||||
|
actor,
|
||||||
|
identityId: identity.identityId,
|
||||||
|
identityName: identity.name
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AuthMode.SERVICE_TOKEN: {
|
||||||
|
const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token);
|
||||||
|
req.auth = {
|
||||||
|
authMode: AuthMode.SERVICE_TOKEN as const,
|
||||||
|
serviceToken,
|
||||||
|
serviceTokenId: serviceToken.id,
|
||||||
|
actor
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AuthMode.API_KEY: {
|
||||||
|
const user = await server.services.apiKey.fnValidateApiKey(token as string);
|
||||||
|
req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new UnauthorizedError({ name: "Unknown token strategy" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
19
backend/src/server/plugins/auth/inject-permission.ts
Normal file
19
backend/src/server/plugins/auth/inject-permission.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import fp from "fastify-plugin";
|
||||||
|
|
||||||
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
// inject permission type needed based on auth extracted
|
||||||
|
export const injectPermission = fp(async (server) => {
|
||||||
|
server.decorateRequest("permission", null);
|
||||||
|
server.addHook("onRequest", async (req) => {
|
||||||
|
if (!req.auth) return;
|
||||||
|
|
||||||
|
if (req.auth.actor === ActorType.USER) {
|
||||||
|
req.permission = { type: ActorType.USER, id: req.auth.userId, orgId: req.auth?.orgId };
|
||||||
|
} else if (req.auth.actor === ActorType.IDENTITY) {
|
||||||
|
req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId, orgId: undefined };
|
||||||
|
} else if (req.auth.actor === ActorType.SERVICE) {
|
||||||
|
req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId, orgId: undefined };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
540
backend/src/server/routes/index.ts
Normal file
540
backend/src/server/routes/index.ts
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { registerV1EERoutes } from "@app/ee/routes/v1";
|
||||||
|
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
|
||||||
|
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
|
||||||
|
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
|
||||||
|
import { licenseDALFactory } from "@app/ee/services/license/license-dal";
|
||||||
|
import { licenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { permissionDALFactory } from "@app/ee/services/permission/permission-dal";
|
||||||
|
import { permissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
||||||
|
import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service";
|
||||||
|
import { secretApprovalPolicyApproverDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-approver-dal";
|
||||||
|
import { secretApprovalPolicyDALFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-dal";
|
||||||
|
import { secretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
|
||||||
|
import { secretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
|
||||||
|
import { secretApprovalRequestReviewerDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-reviewer-dal";
|
||||||
|
import { secretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
|
||||||
|
import { secretApprovalRequestServiceFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-service";
|
||||||
|
import { secretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
|
||||||
|
import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue";
|
||||||
|
import { secretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
|
||||||
|
import { gitAppDALFactory } from "@app/ee/services/secret-scanning/git-app-dal";
|
||||||
|
import { gitAppInstallSessionDALFactory } from "@app/ee/services/secret-scanning/git-app-install-session-dal";
|
||||||
|
import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal";
|
||||||
|
import { secretScanningQueueFactory } from "@app/ee/services/secret-scanning/secret-scanning-queue";
|
||||||
|
import { secretScanningServiceFactory } from "@app/ee/services/secret-scanning/secret-scanning-service";
|
||||||
|
import { secretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
||||||
|
import { snapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
|
||||||
|
import { snapshotFolderDALFactory } from "@app/ee/services/secret-snapshot/snapshot-folder-dal";
|
||||||
|
import { snapshotSecretDALFactory } from "@app/ee/services/secret-snapshot/snapshot-secret-dal";
|
||||||
|
import { trustedIpDALFactory } from "@app/ee/services/trusted-ip/trusted-ip-dal";
|
||||||
|
import { trustedIpServiceFactory } from "@app/ee/services/trusted-ip/trusted-ip-service";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { TQueueServiceFactory } from "@app/queue";
|
||||||
|
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
|
||||||
|
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
|
||||||
|
import { authDALFactory } from "@app/services/auth/auth-dal";
|
||||||
|
import { authLoginServiceFactory } from "@app/services/auth/auth-login-service";
|
||||||
|
import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service";
|
||||||
|
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
|
||||||
|
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
|
||||||
|
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
||||||
|
import { identityDALFactory } from "@app/services/identity/identity-dal";
|
||||||
|
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
|
||||||
|
import { identityServiceFactory } from "@app/services/identity/identity-service";
|
||||||
|
import { identityAccessTokenDALFactory } from "@app/services/identity-access-token/identity-access-token-dal";
|
||||||
|
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
|
||||||
|
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
|
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||||
|
import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal";
|
||||||
|
import { identityUaDALFactory } from "@app/services/identity-ua/identity-ua-dal";
|
||||||
|
import { identityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
||||||
|
import { integrationDALFactory } from "@app/services/integration/integration-dal";
|
||||||
|
import { integrationServiceFactory } from "@app/services/integration/integration-service";
|
||||||
|
import { integrationAuthDALFactory } from "@app/services/integration-auth/integration-auth-dal";
|
||||||
|
import { integrationAuthServiceFactory } from "@app/services/integration-auth/integration-auth-service";
|
||||||
|
import { incidentContactDALFactory } from "@app/services/org/incident-contacts-dal";
|
||||||
|
import { orgBotDALFactory } from "@app/services/org/org-bot-dal";
|
||||||
|
import { orgDALFactory } from "@app/services/org/org-dal";
|
||||||
|
import { orgRoleDALFactory } from "@app/services/org/org-role-dal";
|
||||||
|
import { orgRoleServiceFactory } from "@app/services/org/org-role-service";
|
||||||
|
import { orgServiceFactory } from "@app/services/org/org-service";
|
||||||
|
import { projectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
import { projectServiceFactory } from "@app/services/project/project-service";
|
||||||
|
import { projectBotDALFactory } from "@app/services/project-bot/project-bot-dal";
|
||||||
|
import { projectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||||
|
import { projectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||||
|
import { projectEnvServiceFactory } from "@app/services/project-env/project-env-service";
|
||||||
|
import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal";
|
||||||
|
import { projectKeyServiceFactory } from "@app/services/project-key/project-key-service";
|
||||||
|
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||||
|
import { projectMembershipServiceFactory } from "@app/services/project-membership/project-membership-service";
|
||||||
|
import { projectRoleDALFactory } from "@app/services/project-role/project-role-dal";
|
||||||
|
import { projectRoleServiceFactory } from "@app/services/project-role/project-role-service";
|
||||||
|
import { secretDALFactory } from "@app/services/secret/secret-dal";
|
||||||
|
import { secretQueueFactory } from "@app/services/secret/secret-queue";
|
||||||
|
import { secretServiceFactory } from "@app/services/secret/secret-service";
|
||||||
|
import { secretVersionDALFactory } from "@app/services/secret/secret-version-dal";
|
||||||
|
import { secretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
|
||||||
|
import { secretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
|
||||||
|
import { secretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
|
||||||
|
import { secretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||||
|
import { secretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
|
import { secretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
|
||||||
|
import { secretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
|
||||||
|
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
||||||
|
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||||
|
import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
||||||
|
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
|
||||||
|
import { serviceTokenServiceFactory } from "@app/services/service-token/service-token-service";
|
||||||
|
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||||
|
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
|
import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||||
|
import { telemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||||
|
import { userDALFactory } from "@app/services/user/user-dal";
|
||||||
|
import { userServiceFactory } from "@app/services/user/user-service";
|
||||||
|
import { webhookDALFactory } from "@app/services/webhook/webhook-dal";
|
||||||
|
import { webhookServiceFactory } from "@app/services/webhook/webhook-service";
|
||||||
|
|
||||||
|
import { injectAuditLogInfo } from "../plugins/audit-log";
|
||||||
|
import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||||
|
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||||
|
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
|
||||||
|
import { registerV1Routes } from "./v1";
|
||||||
|
import { registerV2Routes } from "./v2";
|
||||||
|
import { registerV3Routes } from "./v3";
|
||||||
|
|
||||||
|
export const registerRoutes = async (
|
||||||
|
server: FastifyZodProvider,
|
||||||
|
{ db, smtp: smtpService, queue: queueService }: { db: Knex; smtp: TSmtpService; queue: TQueueServiceFactory }
|
||||||
|
) => {
|
||||||
|
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
|
||||||
|
|
||||||
|
// db layers
|
||||||
|
const userDAL = userDALFactory(db);
|
||||||
|
const authDAL = authDALFactory(db);
|
||||||
|
const authTokenDAL = tokenDALFactory(db);
|
||||||
|
const orgDAL = orgDALFactory(db);
|
||||||
|
const orgBotDAL = orgBotDALFactory(db);
|
||||||
|
const incidentContactDAL = incidentContactDALFactory(db);
|
||||||
|
const orgRoleDAL = orgRoleDALFactory(db);
|
||||||
|
const superAdminDAL = superAdminDALFactory(db);
|
||||||
|
const apiKeyDAL = apiKeyDALFactory(db);
|
||||||
|
|
||||||
|
const projectDAL = projectDALFactory(db);
|
||||||
|
const projectMembershipDAL = projectMembershipDALFactory(db);
|
||||||
|
const projectRoleDAL = projectRoleDALFactory(db);
|
||||||
|
const projectEnvDAL = projectEnvDALFactory(db);
|
||||||
|
const projectKeyDAL = projectKeyDALFactory(db);
|
||||||
|
const projectBotDAL = projectBotDALFactory(db);
|
||||||
|
|
||||||
|
const secretDAL = secretDALFactory(db);
|
||||||
|
const secretTagDAL = secretTagDALFactory(db);
|
||||||
|
const folderDAL = secretFolderDALFactory(db);
|
||||||
|
const folderVersionDAL = secretFolderVersionDALFactory(db);
|
||||||
|
const secretImportDAL = secretImportDALFactory(db);
|
||||||
|
const secretVersionDAL = secretVersionDALFactory(db);
|
||||||
|
const secretVersionTagDAL = secretVersionTagDALFactory(db);
|
||||||
|
const secretBlindIndexDAL = secretBlindIndexDALFactory(db);
|
||||||
|
|
||||||
|
const integrationDAL = integrationDALFactory(db);
|
||||||
|
const integrationAuthDAL = integrationAuthDALFactory(db);
|
||||||
|
const webhookDAL = webhookDALFactory(db);
|
||||||
|
const serviceTokenDAL = serviceTokenDALFactory(db);
|
||||||
|
|
||||||
|
const identityDAL = identityDALFactory(db);
|
||||||
|
const identityAccessTokenDAL = identityAccessTokenDALFactory(db);
|
||||||
|
const identityOrgMembershipDAL = identityOrgDALFactory(db);
|
||||||
|
const identityProjectDAL = identityProjectDALFactory(db);
|
||||||
|
|
||||||
|
const identityUaDAL = identityUaDALFactory(db);
|
||||||
|
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||||
|
|
||||||
|
const auditLogDAL = auditLogDALFactory(db);
|
||||||
|
const trustedIpDAL = trustedIpDALFactory(db);
|
||||||
|
|
||||||
|
// ee db layer ops
|
||||||
|
const permissionDAL = permissionDALFactory(db);
|
||||||
|
const samlConfigDAL = samlConfigDALFactory(db);
|
||||||
|
const sapApproverDAL = secretApprovalPolicyApproverDALFactory(db);
|
||||||
|
const secretApprovalPolicyDAL = secretApprovalPolicyDALFactory(db);
|
||||||
|
const secretApprovalRequestDAL = secretApprovalRequestDALFactory(db);
|
||||||
|
const sarReviewerDAL = secretApprovalRequestReviewerDALFactory(db);
|
||||||
|
const sarSecretDAL = secretApprovalRequestSecretDALFactory(db);
|
||||||
|
|
||||||
|
const secretRotationDAL = secretRotationDALFactory(db);
|
||||||
|
const snapshotDAL = snapshotDALFactory(db);
|
||||||
|
const snapshotSecretDAL = snapshotSecretDALFactory(db);
|
||||||
|
const snapshotFolderDAL = snapshotFolderDALFactory(db);
|
||||||
|
|
||||||
|
const gitAppInstallSessionDAL = gitAppInstallSessionDALFactory(db);
|
||||||
|
const gitAppOrgDAL = gitAppDALFactory(db);
|
||||||
|
const secretScanningDAL = secretScanningDALFactory(db);
|
||||||
|
const licenseDAL = licenseDALFactory(db);
|
||||||
|
|
||||||
|
const permissionService = permissionServiceFactory({
|
||||||
|
permissionDAL,
|
||||||
|
orgRoleDAL,
|
||||||
|
projectRoleDAL,
|
||||||
|
serviceTokenDAL
|
||||||
|
});
|
||||||
|
const licenseService = licenseServiceFactory({ permissionService, orgDAL, licenseDAL });
|
||||||
|
const trustedIpService = trustedIpServiceFactory({
|
||||||
|
licenseService,
|
||||||
|
projectDAL,
|
||||||
|
trustedIpDAL,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
const auditLogQueue = auditLogQueueServiceFactory({
|
||||||
|
auditLogDAL,
|
||||||
|
queueService,
|
||||||
|
projectDAL,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
const auditLogService = auditLogServiceFactory({ auditLogDAL, permissionService, auditLogQueue });
|
||||||
|
const sapService = secretApprovalPolicyServiceFactory({
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
secretApprovalPolicyApproverDAL: sapApproverDAL,
|
||||||
|
permissionService,
|
||||||
|
secretApprovalPolicyDAL
|
||||||
|
});
|
||||||
|
const samlService = samlConfigServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
orgBotDAL,
|
||||||
|
orgDAL,
|
||||||
|
userDAL,
|
||||||
|
samlConfigDAL,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
|
const telemetryService = telemetryServiceFactory();
|
||||||
|
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL });
|
||||||
|
const userService = userServiceFactory({ userDAL });
|
||||||
|
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService });
|
||||||
|
const passwordService = authPaswordServiceFactory({
|
||||||
|
tokenService,
|
||||||
|
smtpService,
|
||||||
|
authDAL,
|
||||||
|
userDAL
|
||||||
|
});
|
||||||
|
const orgService = orgServiceFactory({
|
||||||
|
licenseService,
|
||||||
|
samlConfigDAL,
|
||||||
|
orgRoleDAL,
|
||||||
|
permissionService,
|
||||||
|
orgDAL,
|
||||||
|
incidentContactDAL,
|
||||||
|
tokenService,
|
||||||
|
projectDAL,
|
||||||
|
smtpService,
|
||||||
|
userDAL,
|
||||||
|
orgBotDAL
|
||||||
|
});
|
||||||
|
const signupService = authSignupServiceFactory({
|
||||||
|
tokenService,
|
||||||
|
smtpService,
|
||||||
|
authDAL,
|
||||||
|
userDAL,
|
||||||
|
orgDAL,
|
||||||
|
orgService,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
const orgRoleService = orgRoleServiceFactory({ permissionService, orgRoleDAL });
|
||||||
|
const superAdminService = superAdminServiceFactory({
|
||||||
|
userDAL,
|
||||||
|
authService: loginService,
|
||||||
|
serverCfgDAL: superAdminDAL,
|
||||||
|
orgService
|
||||||
|
});
|
||||||
|
const apiKeyService = apiKeyServiceFactory({ apiKeyDAL, userDAL });
|
||||||
|
|
||||||
|
const secretScanningQueue = secretScanningQueueFactory({
|
||||||
|
telemetryService,
|
||||||
|
smtpService,
|
||||||
|
secretScanningDAL,
|
||||||
|
queueService,
|
||||||
|
orgMembershipDAL: orgDAL
|
||||||
|
});
|
||||||
|
const secretScanningService = secretScanningServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
gitAppOrgDAL,
|
||||||
|
gitAppInstallSessionDAL,
|
||||||
|
secretScanningDAL,
|
||||||
|
secretScanningQueue
|
||||||
|
});
|
||||||
|
const projectService = projectServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
projectDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
folderDAL,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
const projectMembershipService = projectMembershipServiceFactory({
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectDAL,
|
||||||
|
permissionService,
|
||||||
|
orgDAL,
|
||||||
|
userDAL,
|
||||||
|
smtpService,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectRoleDAL,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
projectEnvDAL,
|
||||||
|
licenseService,
|
||||||
|
projectDAL,
|
||||||
|
folderDAL
|
||||||
|
});
|
||||||
|
const projectKeyService = projectKeyServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
projectKeyDAL,
|
||||||
|
projectMembershipDAL
|
||||||
|
});
|
||||||
|
const projectRoleService = projectRoleServiceFactory({ permissionService, projectRoleDAL });
|
||||||
|
|
||||||
|
const snapshotService = secretSnapshotServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
licenseService,
|
||||||
|
folderDAL,
|
||||||
|
secretDAL,
|
||||||
|
snapshotDAL,
|
||||||
|
snapshotFolderDAL,
|
||||||
|
snapshotSecretDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
folderVersionDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretVersionTagDAL
|
||||||
|
});
|
||||||
|
const webhookService = webhookServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
webhookDAL,
|
||||||
|
projectEnvDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretTagService = secretTagServiceFactory({ secretTagDAL, permissionService });
|
||||||
|
const folderService = secretFolderServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
folderDAL,
|
||||||
|
folderVersionDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
snapshotService
|
||||||
|
});
|
||||||
|
const secretImportService = secretImportServiceFactory({
|
||||||
|
projectEnvDAL,
|
||||||
|
folderDAL,
|
||||||
|
permissionService,
|
||||||
|
secretImportDAL,
|
||||||
|
secretDAL
|
||||||
|
});
|
||||||
|
const projectBotService = projectBotServiceFactory({ permissionService, projectBotDAL });
|
||||||
|
const integrationAuthService = integrationAuthServiceFactory({
|
||||||
|
integrationAuthDAL,
|
||||||
|
integrationDAL,
|
||||||
|
permissionService,
|
||||||
|
projectBotDAL,
|
||||||
|
projectBotService
|
||||||
|
});
|
||||||
|
const secretQueueService = secretQueueFactory({
|
||||||
|
queueService,
|
||||||
|
secretDAL,
|
||||||
|
folderDAL,
|
||||||
|
integrationAuthService,
|
||||||
|
projectBotService,
|
||||||
|
integrationDAL,
|
||||||
|
secretImportDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
webhookDAL,
|
||||||
|
orgDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
smtpService,
|
||||||
|
projectDAL
|
||||||
|
});
|
||||||
|
const secretBlindIndexService = secretBlindIndexServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
secretDAL,
|
||||||
|
secretBlindIndexDAL
|
||||||
|
});
|
||||||
|
const secretService = secretServiceFactory({
|
||||||
|
folderDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretVersionTagDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
permissionService,
|
||||||
|
secretDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
snapshotService,
|
||||||
|
secretQueueService,
|
||||||
|
secretImportDAL,
|
||||||
|
projectBotService
|
||||||
|
});
|
||||||
|
const sarService = secretApprovalRequestServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
folderDAL,
|
||||||
|
secretTagDAL,
|
||||||
|
secretApprovalRequestSecretDAL: sarSecretDAL,
|
||||||
|
secretApprovalRequestReviewerDAL: sarReviewerDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
secretApprovalRequestDAL,
|
||||||
|
secretService,
|
||||||
|
snapshotService,
|
||||||
|
secretQueueService
|
||||||
|
});
|
||||||
|
const secretRotationQueue = secretRotationQueueFactory({
|
||||||
|
telemetryService,
|
||||||
|
secretRotationDAL,
|
||||||
|
queue: queueService,
|
||||||
|
secretDAL,
|
||||||
|
secretVersionDAL,
|
||||||
|
projectBotService
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretRotationService = secretRotationServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
secretRotationDAL,
|
||||||
|
secretRotationQueue,
|
||||||
|
projectDAL,
|
||||||
|
licenseService,
|
||||||
|
secretDAL,
|
||||||
|
folderDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrationService = integrationServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
folderDAL,
|
||||||
|
integrationDAL,
|
||||||
|
integrationAuthDAL,
|
||||||
|
secretQueueService
|
||||||
|
});
|
||||||
|
const serviceTokenService = serviceTokenServiceFactory({
|
||||||
|
projectEnvDAL,
|
||||||
|
serviceTokenDAL,
|
||||||
|
userDAL,
|
||||||
|
permissionService
|
||||||
|
});
|
||||||
|
|
||||||
|
const identityService = identityServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
identityDAL,
|
||||||
|
identityOrgMembershipDAL
|
||||||
|
});
|
||||||
|
const identityAccessTokenService = identityAccessTokenServiceFactory({ identityAccessTokenDAL });
|
||||||
|
const identityProjectService = identityProjectServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
projectDAL,
|
||||||
|
identityProjectDAL,
|
||||||
|
identityOrgMembershipDAL
|
||||||
|
});
|
||||||
|
const identityUaService = identityUaServiceFactory({
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
permissionService,
|
||||||
|
identityDAL,
|
||||||
|
identityAccessTokenDAL,
|
||||||
|
identityUaClientSecretDAL,
|
||||||
|
identityUaDAL,
|
||||||
|
licenseService
|
||||||
|
});
|
||||||
|
|
||||||
|
await superAdminService.initServerCfg();
|
||||||
|
await auditLogQueue.startAuditLogPruneJob();
|
||||||
|
// setup the communication with license key server
|
||||||
|
await licenseService.init();
|
||||||
|
// inject all services
|
||||||
|
server.decorate<FastifyZodProvider["services"]>("services", {
|
||||||
|
login: loginService,
|
||||||
|
password: passwordService,
|
||||||
|
signup: signupService,
|
||||||
|
user: userService,
|
||||||
|
permission: permissionService,
|
||||||
|
org: orgService,
|
||||||
|
orgRole: orgRoleService,
|
||||||
|
apiKey: apiKeyService,
|
||||||
|
authToken: tokenService,
|
||||||
|
superAdmin: superAdminService,
|
||||||
|
project: projectService,
|
||||||
|
projectMembership: projectMembershipService,
|
||||||
|
projectKey: projectKeyService,
|
||||||
|
projectEnv: projectEnvService,
|
||||||
|
projectRole: projectRoleService,
|
||||||
|
secret: secretService,
|
||||||
|
secretTag: secretTagService,
|
||||||
|
folder: folderService,
|
||||||
|
secretImport: secretImportService,
|
||||||
|
projectBot: projectBotService,
|
||||||
|
integration: integrationService,
|
||||||
|
integrationAuth: integrationAuthService,
|
||||||
|
webhook: webhookService,
|
||||||
|
serviceToken: serviceTokenService,
|
||||||
|
identity: identityService,
|
||||||
|
identityAccessToken: identityAccessTokenService,
|
||||||
|
identityProject: identityProjectService,
|
||||||
|
identityUa: identityUaService,
|
||||||
|
secretApprovalPolicy: sapService,
|
||||||
|
secretApprovalRequest: sarService,
|
||||||
|
secretRotation: secretRotationService,
|
||||||
|
snapshot: snapshotService,
|
||||||
|
saml: samlService,
|
||||||
|
auditLog: auditLogService,
|
||||||
|
secretScanning: secretScanningService,
|
||||||
|
license: licenseService,
|
||||||
|
trustedIp: trustedIpService,
|
||||||
|
secretBlindIndex: secretBlindIndexService,
|
||||||
|
telemetry: telemetryService
|
||||||
|
});
|
||||||
|
|
||||||
|
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||||
|
user: userDAL
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.register(injectIdentity, { userDAL, serviceTokenDAL });
|
||||||
|
await server.register(injectPermission);
|
||||||
|
await server.register(injectAuditLogInfo);
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/api/status",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
date: z.date(),
|
||||||
|
message: z.literal("Ok"),
|
||||||
|
emailConfigured: z.boolean().optional(),
|
||||||
|
inviteOnlySignup: z.boolean().optional(),
|
||||||
|
redisConfigured: z.boolean().optional(),
|
||||||
|
secretScanningConfigured: z.boolean().optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: () => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
const serverCfg = getServerCfg();
|
||||||
|
return {
|
||||||
|
date: new Date(),
|
||||||
|
message: "Ok" as const,
|
||||||
|
emailConfigured: cfg.isSmtpConfigured,
|
||||||
|
inviteOnlySignup: Boolean(serverCfg.allowSignUp),
|
||||||
|
redisConfigured: cfg.isRedisConfigured,
|
||||||
|
secretScanningConfigured: cfg.isSecretScanningConfigured
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// register routes for v1
|
||||||
|
await server.register(
|
||||||
|
async (v1Server) => {
|
||||||
|
await v1Server.register(registerV1EERoutes);
|
||||||
|
await v1Server.register(registerV1Routes);
|
||||||
|
},
|
||||||
|
{ prefix: "/api/v1" }
|
||||||
|
);
|
||||||
|
await server.register(registerV2Routes, { prefix: "/api/v2" });
|
||||||
|
await server.register(registerV3Routes, { prefix: "/api/v3" });
|
||||||
|
};
|
||||||
101
backend/src/server/routes/v1/auth-router.ts
Normal file
101
backend/src/server/routes/v1/auth-router.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode, AuthModeRefreshJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/logout",
|
||||||
|
method: "POST",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (req.auth.authMode === AuthMode.JWT) {
|
||||||
|
await server.services.login.logout(req.permission.id, req.auth.tokenVersionId);
|
||||||
|
}
|
||||||
|
void res.cookie("jid", "", {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
return { message: "Successfully logged out" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/checkAuth",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.literal("Authenticated")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: () => ({ message: "Authenticated" as const })
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/token",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
token: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const refreshToken = req.cookies.jid;
|
||||||
|
const appCfg = getConfig();
|
||||||
|
if (!refreshToken)
|
||||||
|
throw new BadRequestError({
|
||||||
|
name: "Auth token route",
|
||||||
|
message: "Failed to find refresh token"
|
||||||
|
});
|
||||||
|
|
||||||
|
const decodedToken = jwt.verify(refreshToken, appCfg.AUTH_SECRET) as AuthModeRefreshJwtTokenPayload;
|
||||||
|
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN)
|
||||||
|
throw new UnauthorizedError({ message: "Invalid token", name: "Auth token route" });
|
||||||
|
|
||||||
|
const tokenVersion = await server.services.authToken.getUserTokenSessionById(
|
||||||
|
decodedToken.tokenVersionId,
|
||||||
|
decodedToken.userId
|
||||||
|
);
|
||||||
|
if (!tokenVersion) throw new UnauthorizedError({ message: "Invalid token", name: "Auth token route" });
|
||||||
|
|
||||||
|
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion)
|
||||||
|
throw new UnauthorizedError({ message: "Invalid token", name: "Auth token route" });
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||||
|
userId: decodedToken.userId,
|
||||||
|
tokenVersionId: tokenVersion.id,
|
||||||
|
accessVersion: tokenVersion.accessVersion,
|
||||||
|
organizationId: decodedToken.organizationId
|
||||||
|
},
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { token };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
128
backend/src/server/routes/v1/identity-router.ts
Normal file
128
backend/src/server/routes/v1/identity-router.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { IdentitiesSchema, OrgMembershipRole } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().trim(),
|
||||||
|
organizationId: z.string().trim(),
|
||||||
|
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identity: IdentitiesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identity = await server.services.identity.createIdentity({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
...req.body,
|
||||||
|
orgId: req.body.organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_IDENTITY,
|
||||||
|
metadata: {
|
||||||
|
name: identity.name,
|
||||||
|
identityId: identity.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identity };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:identityId",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().trim().optional(),
|
||||||
|
role: z.string().trim().min(1).optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identity: IdentitiesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identity = await server.services.identity.updateIdentity({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
id: req.params.identityId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identity.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_IDENTITY,
|
||||||
|
metadata: {
|
||||||
|
name: identity.name,
|
||||||
|
identityId: identity.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identity };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:identityId",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identity: IdentitiesSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identity = await server.services.identity.deleteIdentity({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
id: req.params.identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identity.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_IDENTITY,
|
||||||
|
metadata: {
|
||||||
|
identityId: identity.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { identity };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
364
backend/src/server/routes/v1/identity-ua.ts
Normal file
364
backend/src/server/routes/v1/identity-ua.ts
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { IdentityUaClientSecretsSchema, IdentityUniversalAuthsSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||||
|
|
||||||
|
export const sanitizedClientSecretSchema = IdentityUaClientSecretsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
description: true,
|
||||||
|
clientSecretPrefix: true,
|
||||||
|
clientSecretNumUses: true,
|
||||||
|
clientSecretNumUsesLimit: true,
|
||||||
|
clientSecretTTL: true,
|
||||||
|
identityUAId: true,
|
||||||
|
isClientSecretRevoked: true
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/universal-auth/login",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
clientId: z.string().trim(),
|
||||||
|
clientSecret: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
expiresIn: z.coerce.number(),
|
||||||
|
accessTokenMaxTTL: z.coerce.number(),
|
||||||
|
tokenType: z.literal("Bearer")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { identityUa, accessToken, identityAccessToken, validClientSecretInfo } =
|
||||||
|
await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp);
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH,
|
||||||
|
metadata: {
|
||||||
|
clientSecretId: validClientSecretInfo.id,
|
||||||
|
identityId: identityUa.identityId,
|
||||||
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
|
identityUniversalAuthId: identityUa.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
tokenType: "Bearer" as const,
|
||||||
|
expiresIn: identityUa.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityUa.accessTokenMaxTTL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/universal-auth/identities/:identityId",
|
||||||
|
method: "POST",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
clientSecretTrustedIps: z
|
||||||
|
.object({
|
||||||
|
ipAddress: z.string().trim()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
|
||||||
|
accessTokenTrustedIps: z
|
||||||
|
.object({
|
||||||
|
ipAddress: z.string().trim()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]),
|
||||||
|
accessTokenTTL: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1)
|
||||||
|
.refine((value) => value !== 0, {
|
||||||
|
message: "accessTokenTTL must have a non zero number"
|
||||||
|
})
|
||||||
|
.default(2592000),
|
||||||
|
accessTokenMaxTTL: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.refine((value) => value !== 0, {
|
||||||
|
message: "accessTokenMaxTTL must have a non zero number"
|
||||||
|
})
|
||||||
|
.default(2592000), // 30 days
|
||||||
|
accessTokenNumUsesLimit: z.number().int().min(0).default(0)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identityUniversalAuth: IdentityUniversalAuthsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identityUniversalAuth = await server.services.identityUa.attachUa({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
...req.body,
|
||||||
|
identityId: req.params.identityId
|
||||||
|
});
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identityUniversalAuth.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityUniversalAuth.identityId,
|
||||||
|
accessTokenTTL: identityUniversalAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL,
|
||||||
|
accessTokenTrustedIps: identityUniversalAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||||
|
clientSecretTrustedIps: identityUniversalAuth.clientSecretTrustedIps as TIdentityTrustedIp[],
|
||||||
|
accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityUniversalAuth };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/universal-auth/identities/:identityId",
|
||||||
|
method: "PATCH",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
clientSecretTrustedIps: z
|
||||||
|
.object({
|
||||||
|
ipAddress: z.string().trim()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.optional(),
|
||||||
|
accessTokenTrustedIps: z
|
||||||
|
.object({
|
||||||
|
ipAddress: z.string().trim()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.min(1)
|
||||||
|
.optional(),
|
||||||
|
accessTokenTTL: z.number().int().min(0).optional(),
|
||||||
|
accessTokenNumUsesLimit: z.number().int().min(0).optional(),
|
||||||
|
accessTokenMaxTTL: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.refine((value) => value !== 0, {
|
||||||
|
message: "accessTokenMaxTTL must have a non zero number"
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identityUniversalAuth: IdentityUniversalAuthsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identityUniversalAuth = await server.services.identityUa.updateUa({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
...req.body,
|
||||||
|
identityId: req.params.identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identityUniversalAuth.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityUniversalAuth.identityId,
|
||||||
|
accessTokenTTL: identityUniversalAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL,
|
||||||
|
accessTokenTrustedIps: identityUniversalAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||||
|
clientSecretTrustedIps: identityUniversalAuth.clientSecretTrustedIps as TIdentityTrustedIp[],
|
||||||
|
accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityUniversalAuth };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/universal-auth/identities/:identityId",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identityUniversalAuth: IdentityUniversalAuthsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identityUniversalAuth = await server.services.identityUa.getIdentityUa({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
identityId: req.params.identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identityUniversalAuth.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityUniversalAuth.identityId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityUniversalAuth };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/universal-auth/identities/:identityId/client-secrets",
|
||||||
|
method: "POST",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
description: z.string().trim().default(""),
|
||||||
|
numUsesLimit: z.number().min(0).default(0),
|
||||||
|
ttl: z.number().min(0).default(0)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
clientSecret: z.string(),
|
||||||
|
clientSecretData: sanitizedClientSecretSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { clientSecret, clientSecretData, orgId } = await server.services.identityUa.createUaClientSecret({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
identityId: req.params.identityId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET,
|
||||||
|
metadata: {
|
||||||
|
identityId: req.params.identityId,
|
||||||
|
clientSecretId: clientSecretData.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { clientSecret, clientSecretData };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/universal-auth/identities/:identityId/client-secrets",
|
||||||
|
method: "GET",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
clientSecretData: sanitizedClientSecretSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { clientSecrets: clientSecretData, orgId } = await server.services.identityUa.getUaClientSecrets({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
identityId: req.params.identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS,
|
||||||
|
metadata: {
|
||||||
|
identityId: req.params.identityId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { clientSecretData };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke",
|
||||||
|
method: "POST",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string(),
|
||||||
|
clientSecretId: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
clientSecretData: sanitizedClientSecretSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const clientSecretData = await server.services.identityUa.revokeUaClientSecret({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
identityId: req.params.identityId,
|
||||||
|
clientSecretId: req.params.clientSecretId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: clientSecretData.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET,
|
||||||
|
metadata: {
|
||||||
|
identityId: clientSecretData.identityId,
|
||||||
|
clientSecretId: clientSecretData.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { clientSecretData };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
71
backend/src/server/routes/v1/invite-org-router.ts
Normal file
71
backend/src/server/routes/v1/invite-org-router.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { UsersSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/signup",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
inviteeEmail: z.string().trim().email(),
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
completeInviteLink: z.string().optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
if (req.auth.actor !== ActorType.USER) return;
|
||||||
|
const completeInviteLink = await server.services.org.inviteUserToOrganization({
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
userId: req.permission.id,
|
||||||
|
inviteeEmail: req.body.inviteeEmail,
|
||||||
|
actorOrgScope: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
completeInviteLink,
|
||||||
|
message: `Send an invite link to ${req.body.inviteeEmail}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/verify",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
email: z.string().trim().email(),
|
||||||
|
organizationId: z.string().trim(),
|
||||||
|
code: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
token: z.string().optional(),
|
||||||
|
user: UsersSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { user, token } = await server.services.org.verifyUserToOrg({
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
code: req.body.code,
|
||||||
|
email: req.body.email
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: "Successfully verified email",
|
||||||
|
user,
|
||||||
|
token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
180
backend/src/server/routes/v1/organization-router.ts
Normal file
180
backend/src/server/routes/v1/organization-router.ts
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { IncidentContactsSchema, OrganizationsSchema, OrgMembershipsSchema, UsersSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
organizations: OrganizationsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const organizations = await server.services.org.findAllOrganizationOfUser(req.permission.id);
|
||||||
|
return { organizations };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:organizationId",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
organization: OrganizationsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const organization = await server.services.org.findOrganizationById(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId.
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { organization };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:organizationId/users",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
users: OrgMembershipsSchema.merge(
|
||||||
|
z.object({
|
||||||
|
user: UsersSchema.pick({
|
||||||
|
email: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
id: true
|
||||||
|
}).merge(z.object({ publicKey: z.string().nullable() }))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.omit({ createdAt: true, updatedAt: true })
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const users = await server.services.org.findAllOrgMembers(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { users };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:organizationId/name",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
body: z.object({ name: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
organization: OrganizationsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const organization = await server.services.org.updateOrgName(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.body.name,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
message: "Successfully changed organization name",
|
||||||
|
organization
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:organizationId/incidentContactOrg",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
incidentContactsOrg: IncidentContactsSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const incidentContactsOrg = await req.server.services.org.findIncidentContacts(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { incidentContactsOrg };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:organizationId/incidentContactOrg",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
|
body: z.object({ email: z.string().email().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
incidentContactsOrg: IncidentContactsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const incidentContactsOrg = await req.server.services.org.createIncidentContact(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.body.email,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { incidentContactsOrg };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:organizationId/incidentContactOrg/:incidentContactId",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim(), incidentContactId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
incidentContactsOrg: IncidentContactsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const incidentContactsOrg = await req.server.services.org.deleteIncidentContact(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.params.incidentContactId,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { incidentContactsOrg };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
366
backend/src/server/routes/v1/project-router.ts
Normal file
366
backend/src/server/routes/v1/project-router.ts
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
IntegrationsSchema,
|
||||||
|
ProjectKeysSchema,
|
||||||
|
ProjectMembershipsSchema,
|
||||||
|
ProjectsSchema,
|
||||||
|
UserEncryptionKeysSchema,
|
||||||
|
UsersSchema
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { integrationAuthPubSchema } from "../sanitizedSchemas";
|
||||||
|
import { sanitizedServiceTokenSchema } from "../v2/service-token-router";
|
||||||
|
|
||||||
|
const projectWithEnv = ProjectsSchema.merge(
|
||||||
|
z.object({
|
||||||
|
_id: z.string(),
|
||||||
|
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
url: "/:workspaceId/keys",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
publicKeys: z
|
||||||
|
.object({
|
||||||
|
publicKey: z.string().optional(),
|
||||||
|
userId: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const publicKeys = await server.services.projectKey.getProjectPublicKeys({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
return { publicKeys };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:workspaceId/users",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
users: ProjectMembershipsSchema.merge(
|
||||||
|
z.object({
|
||||||
|
user: UsersSchema.pick({
|
||||||
|
email: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
id: true
|
||||||
|
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.omit({ createdAt: true, updatedAt: true })
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const users = await server.services.projectMembership.getProjectMemberships({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
return { users };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspaces: projectWithEnv.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspaces = await server.services.project.getProjects(req.permission.id);
|
||||||
|
return { workspaces };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:workspaceId",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspace: projectWithEnv.optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspace = await server.services.project.getAProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
return { workspace };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
workspaceName: z.string().trim(),
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspace: projectWithEnv
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspace = await server.services.project.createProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
orgId: req.body.organizationId,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
workspaceName: req.body.workspaceName
|
||||||
|
});
|
||||||
|
return { workspace };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:workspaceId",
|
||||||
|
method: "DELETE",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspace: ProjectsSchema.optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspace = await server.services.project.deleteProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
return { workspace };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:workspaceId/name",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
workspace: ProjectsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspace = await server.services.project.updateName({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
name: req.body.name
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
message: "Successfully changed workspace name",
|
||||||
|
workspace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:workspaceId/auto-capitalization",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
autoCapitalization: z.boolean()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string(),
|
||||||
|
workspace: ProjectsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspace = await server.services.project.toggleAutoCapitalization({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
autoCapitalization: req.body.autoCapitalization
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
message: "Successfully changed workspace settings",
|
||||||
|
workspace
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:workspaceId/invite-signup",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
email: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
invitee: UsersSchema,
|
||||||
|
latestKey: ProjectKeysSchema.optional()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { invitee, latestKey } = await server.services.projectMembership.inviteUserToProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
email: req.body.email
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
projectId: req.params.workspaceId,
|
||||||
|
event: {
|
||||||
|
type: EventType.ADD_WORKSPACE_MEMBER,
|
||||||
|
metadata: {
|
||||||
|
userId: invitee.id,
|
||||||
|
email: invitee.email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { invitee, latestKey };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:workspaceId/integrations",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
integrations: IntegrationsSchema.merge(
|
||||||
|
z.object({
|
||||||
|
environment: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const integrations = await server.services.integration.listIntegrationByProject({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
return { integrations };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:workspaceId/authorizations",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
authorizations: integrationAuthPubSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const authorizations = await server.services.integrationAuth.listIntegrationAuthByProjectId({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
return { authorizations };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/:workspaceId/service-token-data",
|
||||||
|
method: "GET",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
workspaceId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
serviceTokenData: sanitizedServiceTokenSchema.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const serviceTokenData = await server.services.serviceToken.getProjectServiceTokens({
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectId: req.params.workspaceId
|
||||||
|
});
|
||||||
|
return { serviceTokenData };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
43
backend/src/server/routes/v2/identity-org-router.ts
Normal file
43
backend/src/server/routes/v2/identity-org-router.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:orgId/identity-memberships",
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
orgId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
identityMemberships: IdentityOrgMembershipsSchema.merge(
|
||||||
|
z.object({
|
||||||
|
customRole: OrgRolesSchema.pick({
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
slug: true,
|
||||||
|
permissions: true,
|
||||||
|
description: true
|
||||||
|
}).optional(),
|
||||||
|
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||||
|
})
|
||||||
|
).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const identityMemberships = await server.services.identity.listOrgIdentities({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.orgId
|
||||||
|
});
|
||||||
|
return { identityMemberships };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
98
backend/src/server/routes/v2/mfa-router.ts
Normal file
98
backend/src/server/routes/v2/mfa-router.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
server.decorateRequest("mfa", null);
|
||||||
|
server.addHook("preParsing", async (req, res) => {
|
||||||
|
const authorizationHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authorizationHeader || !authorizationHeader.startsWith("Bearer ")) {
|
||||||
|
void res.status(401).send({ error: "Missing bearer token" });
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
const token = authorizationHeader.split(" ")[1];
|
||||||
|
if (!token) {
|
||||||
|
void res.status(401).send({ error: "Missing bearer token" });
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedToken = jwt.verify(token, cfg.AUTH_SECRET) as AuthModeMfaJwtTokenPayload;
|
||||||
|
if (decodedToken.authTokenType !== AuthTokenType.MFA_TOKEN) throw new Error("Unauthorized access");
|
||||||
|
|
||||||
|
const user = await server.store.user.findById(decodedToken.userId);
|
||||||
|
if (!user) throw new Error("User not found");
|
||||||
|
req.mfa = { userId: user.id, user, orgId: decodedToken.organizationId };
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/mfa/send",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.login.resendMfaToken(req.mfa.userId);
|
||||||
|
return { message: "Successfully send new mfa code" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
url: "/mfa/verify",
|
||||||
|
method: "POST",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
mfaToken: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
encryptionVersion: z.number().default(1).nullable().optional(),
|
||||||
|
protectedKey: z.string().nullable(),
|
||||||
|
protectedKeyIV: z.string().nullable(),
|
||||||
|
protectedKeyTag: z.string().nullable(),
|
||||||
|
publicKey: z.string(),
|
||||||
|
encryptedPrivateKey: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
token: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const userAgent = req.headers["user-agent"];
|
||||||
|
if (!userAgent) throw new Error("user agent header is required");
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const { user, token } = await server.services.login.verifyMfaToken({
|
||||||
|
userAgent,
|
||||||
|
ip: req.realIp,
|
||||||
|
userId: req.mfa.userId,
|
||||||
|
orgId: req.mfa.orgId,
|
||||||
|
mfaToken: req.body.mfaToken
|
||||||
|
});
|
||||||
|
|
||||||
|
void res.setCookie("jid", token.refresh, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: "/",
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: appCfg.HTTPS_ENABLED
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...user,
|
||||||
|
token: token.access,
|
||||||
|
protectedKey: user.protectedKey || null,
|
||||||
|
protectedKeyIV: user.protectedKeyIV || null,
|
||||||
|
protectedKeyTag: user.protectedKeyTag || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
188
backend/src/server/routes/v2/organization-router.ts
Normal file
188
backend/src/server/routes/v2/organization-router.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { OrganizationsSchema, OrgMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:organizationId/memberships",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
users: OrgMembershipsSchema.merge(
|
||||||
|
z.object({
|
||||||
|
user: UsersSchema.pick({
|
||||||
|
email: true,
|
||||||
|
firstName: true,
|
||||||
|
lastName: true,
|
||||||
|
id: true
|
||||||
|
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.omit({ createdAt: true, updatedAt: true })
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
if (req.auth.actor !== ActorType.USER) return;
|
||||||
|
|
||||||
|
const users = await server.services.org.findAllOrgMembers(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { users };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:organizationId/workspaces",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
workspaces: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
organization: z.string(),
|
||||||
|
environments: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
slug: z.string()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const workspaces = await server.services.org.findAllWorkspaces({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgScope: req.permission.orgId,
|
||||||
|
orgId: req.params.organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { workspaces };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:organizationId/memberships/:membershipId",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
|
||||||
|
body: z.object({
|
||||||
|
role: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
membership: OrgMembershipsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
if (req.auth.actor !== ActorType.USER) return;
|
||||||
|
|
||||||
|
const membership = await server.services.org.updateOrgMembership({
|
||||||
|
userId: req.permission.id,
|
||||||
|
role: req.body.role,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
membershipId: req.params.membershipId,
|
||||||
|
actorOrgScope: req.permission.orgId
|
||||||
|
});
|
||||||
|
return { membership };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:organizationId/memberships/:membershipId",
|
||||||
|
schema: {
|
||||||
|
params: z.object({ organizationId: z.string().trim(), membershipId: z.string().trim() }),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
membership: OrgMembershipsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
if (req.auth.actor !== ActorType.USER) return;
|
||||||
|
|
||||||
|
const membership = await server.services.org.deleteOrgMembership({
|
||||||
|
userId: req.permission.id,
|
||||||
|
orgId: req.params.organizationId,
|
||||||
|
membershipId: req.params.membershipId,
|
||||||
|
actorOrgScope: req.permission.orgId
|
||||||
|
});
|
||||||
|
return { membership };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
organization: OrganizationsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
if (req.auth.actor !== ActorType.USER) return;
|
||||||
|
|
||||||
|
const organization = await server.services.org.createOrganization(
|
||||||
|
req.permission.id,
|
||||||
|
req.auth.user.email,
|
||||||
|
req.body.name
|
||||||
|
);
|
||||||
|
return { organization };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:organizationId",
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
organizationId: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
organization: OrganizationsSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
|
||||||
|
handler: async (req) => {
|
||||||
|
if (req.auth.actor !== ActorType.USER) return;
|
||||||
|
|
||||||
|
const organization = await server.services.org.deleteOrganizationById(
|
||||||
|
req.permission.id,
|
||||||
|
req.params.organizationId,
|
||||||
|
req.permission.orgId
|
||||||
|
);
|
||||||
|
return { organization };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
157
backend/src/services/auth-token/auth-token-service.ts
Normal file
157
backend/src/services/auth-token/auth-token-service.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
|
||||||
|
import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { AuthModeJwtTokenPayload } from "../auth/auth-type";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { TTokenDALFactory } from "./auth-token-dal";
|
||||||
|
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
|
||||||
|
|
||||||
|
type TAuthTokenServiceFactoryDep = {
|
||||||
|
tokenDAL: TTokenDALFactory;
|
||||||
|
userDAL: Pick<TUserDALFactory, "findById">;
|
||||||
|
};
|
||||||
|
export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
|
||||||
|
|
||||||
|
export const getTokenConfig = (tokenType: TokenType) => {
|
||||||
|
// generate random token based on specified token use-case
|
||||||
|
// type [type]
|
||||||
|
switch (tokenType) {
|
||||||
|
case TokenType.TOKEN_EMAIL_CONFIRMATION: {
|
||||||
|
// generate random 6-digit code
|
||||||
|
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
|
case TokenType.TOKEN_EMAIL_MFA: {
|
||||||
|
// generate random 6-digit code
|
||||||
|
const token = String(crypto.randomInt(10 ** 5, 10 ** 6 - 1));
|
||||||
|
const triesLeft = 5;
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 300000);
|
||||||
|
return { token, triesLeft, expiresAt };
|
||||||
|
}
|
||||||
|
case TokenType.TOKEN_EMAIL_ORG_INVITATION: {
|
||||||
|
// generate random hex
|
||||||
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 259200000);
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
|
case TokenType.TOKEN_EMAIL_PASSWORD_RESET: {
|
||||||
|
// generate random hex
|
||||||
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
|
const expiresAt = new Date();
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFactoryDep) => {
|
||||||
|
const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => {
|
||||||
|
const { token, ...tkCfg } = getTokenConfig(type);
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const tokenHash = await bcrypt.hash(token, appCfg.SALT_ROUNDS);
|
||||||
|
await tokenDAL.transaction(async (tx) => {
|
||||||
|
await tokenDAL.delete({ userId, type, orgId: orgId || null }, tx);
|
||||||
|
const newToken = await tokenDAL.create(
|
||||||
|
{
|
||||||
|
tokenHash,
|
||||||
|
expiresAt: tkCfg.expiresAt,
|
||||||
|
type,
|
||||||
|
userId,
|
||||||
|
orgId,
|
||||||
|
triesLeft: tkCfg?.triesLeft
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return newToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateTokenForUser = async ({
|
||||||
|
type,
|
||||||
|
userId,
|
||||||
|
code,
|
||||||
|
orgId
|
||||||
|
}: TValidateTokenForUserDTO): Promise<TAuthTokens | undefined> => {
|
||||||
|
const token = await tokenDAL.findOne({ type, userId, orgId: orgId || null });
|
||||||
|
// validate token
|
||||||
|
if (!token) throw new Error("Failed to find token");
|
||||||
|
if (token?.expiresAt && new Date(token.expiresAt) < new Date()) {
|
||||||
|
await tokenDAL.delete({ type, userId, orgId });
|
||||||
|
throw new Error("Token expired. Please try again");
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidToken = await bcrypt.compare(code, token.tokenHash);
|
||||||
|
if (!isValidToken) {
|
||||||
|
if (token?.triesLeft) {
|
||||||
|
if (token.triesLeft === 1) {
|
||||||
|
await tokenDAL.deleteTokenForUser({ type, userId, orgId: orgId || null });
|
||||||
|
} else {
|
||||||
|
await tokenDAL.decrementTriesField({ type, userId, orgId: orgId || null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error("Invalid token");
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedToken = await tokenDAL.delete({ type, userId, orgId: orgId || null });
|
||||||
|
return deletedToken?.[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserTokenSession = async ({
|
||||||
|
userId,
|
||||||
|
ip,
|
||||||
|
userAgent
|
||||||
|
}: TIssueAuthTokenDTO): Promise<TAuthTokenSessions | undefined> => {
|
||||||
|
let session = await tokenDAL.findOneTokenSession({ userId, ip, userAgent });
|
||||||
|
if (!session) {
|
||||||
|
session = await tokenDAL.insertTokenSession(userId, ip, userAgent);
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearTokenSessionById = async (userId: string, sessionId: string): Promise<TAuthTokenSessions | undefined> =>
|
||||||
|
tokenDAL.incrementTokenSessionVersion(userId, sessionId);
|
||||||
|
|
||||||
|
const getUserTokenSessionById = async (id: string, userId: string) => tokenDAL.findOneTokenSession({ id, userId });
|
||||||
|
|
||||||
|
const getTokenSessionByUser = async (userId: string) => tokenDAL.findTokenSessions({ userId });
|
||||||
|
|
||||||
|
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
|
||||||
|
|
||||||
|
// to parse jwt identity in inject identity plugin
|
||||||
|
const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => {
|
||||||
|
const session = await tokenDAL.findOneTokenSession({
|
||||||
|
id: token.tokenVersionId,
|
||||||
|
userId: token.userId
|
||||||
|
});
|
||||||
|
if (!session) throw new UnauthorizedError({ name: "Session not found" });
|
||||||
|
if (token.accessVersion !== session.accessVersion) throw new UnauthorizedError({ name: "Stale session" });
|
||||||
|
|
||||||
|
const user = await userDAL.findById(session.userId);
|
||||||
|
if (!user || !user.isAccepted) throw new UnauthorizedError({ name: "Token user not found" });
|
||||||
|
|
||||||
|
return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createTokenForUser,
|
||||||
|
validateTokenForUser,
|
||||||
|
getUserTokenSession,
|
||||||
|
clearTokenSessionById,
|
||||||
|
getTokenSessionByUser,
|
||||||
|
revokeAllMySessions,
|
||||||
|
fnValidateJwtIdentity,
|
||||||
|
getUserTokenSessionById
|
||||||
|
};
|
||||||
|
};
|
||||||
45
backend/src/services/auth/auth-fns.ts
Normal file
45
backend/src/services/auth/auth-fns.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type";
|
||||||
|
|
||||||
|
export const validateProviderAuthToken = (providerToken: string, email: string) => {
|
||||||
|
if (!providerToken) throw new UnauthorizedError();
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const decodedToken = jwt.verify(providerToken, appCfg.AUTH_SECRET) as AuthModeProviderJwtTokenPayload;
|
||||||
|
|
||||||
|
if (decodedToken.authTokenType !== AuthTokenType.PROVIDER_TOKEN) throw new UnauthorizedError();
|
||||||
|
if (decodedToken.email !== email) throw new Error("Invalid auth credentials");
|
||||||
|
|
||||||
|
if (decodedToken.organizationId) {
|
||||||
|
return { orgId: decodedToken.organizationId };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [null, null];
|
||||||
|
if (AUTH_TOKEN_TYPE === null) {
|
||||||
|
throw new BadRequestError({ message: "Missing Authorization Header in the request header." });
|
||||||
|
}
|
||||||
|
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (AUTH_TOKEN_VALUE === null) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Missing Authorization Body in the request header"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload;
|
||||||
|
if (!validate) return decodedToken;
|
||||||
|
|
||||||
|
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError();
|
||||||
|
if (decodedToken.userId !== userId) throw new UnauthorizedError();
|
||||||
|
};
|
||||||
322
backend/src/services/auth/auth-login-service.ts
Normal file
322
backend/src/services/auth/auth-login-service.ts
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { validateProviderAuthToken } from "./auth-fns";
|
||||||
|
import {
|
||||||
|
TLoginClientProofDTO,
|
||||||
|
TLoginGenServerPublicKeyDTO,
|
||||||
|
TOauthLoginDTO,
|
||||||
|
TVerifyMfaTokenDTO
|
||||||
|
} from "./auth-login-type";
|
||||||
|
import { AuthMethod, AuthTokenType } from "./auth-type";
|
||||||
|
|
||||||
|
type TAuthLoginServiceFactoryDep = {
|
||||||
|
userDAL: TUserDALFactory;
|
||||||
|
tokenService: TAuthTokenServiceFactory;
|
||||||
|
smtpService: TSmtpService;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||||
|
export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }: TAuthLoginServiceFactoryDep) => {
|
||||||
|
/*
|
||||||
|
* Private
|
||||||
|
* Not exported. This is to update user device list
|
||||||
|
* If new device is found. Will be saved and a mail will be send
|
||||||
|
*/
|
||||||
|
const updateUserDeviceSession = async (user: TUsers, ip: string, userAgent: string) => {
|
||||||
|
const devices = await UserDeviceSchema.parseAsync(user.devices || []);
|
||||||
|
const isDeviceSeen = devices.some((device) => device.ip === ip && device.userAgent === userAgent);
|
||||||
|
|
||||||
|
if (!isDeviceSeen) {
|
||||||
|
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
||||||
|
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) });
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.NewDeviceJoin,
|
||||||
|
subjectLine: "Successful login from new device",
|
||||||
|
recipients: [user.email],
|
||||||
|
substitutions: {
|
||||||
|
email: user.email,
|
||||||
|
timestamp: new Date().toString(),
|
||||||
|
ip,
|
||||||
|
userAgent
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Private
|
||||||
|
* Send mfa code via email
|
||||||
|
* */
|
||||||
|
const sendUserMfaCode = async ({
|
||||||
|
userId,
|
||||||
|
email
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
}) => {
|
||||||
|
const code = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_MFA,
|
||||||
|
userId
|
||||||
|
});
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.EmailMfa,
|
||||||
|
subjectLine: "Infisical MFA code",
|
||||||
|
recipients: [email],
|
||||||
|
substitutions: {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check user device and send mail if new device
|
||||||
|
* generate the auth and refresh token. fn shared by mfa verification and login verification with mfa disabled
|
||||||
|
*/
|
||||||
|
const generateUserTokens = async ({
|
||||||
|
user,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
organizationId
|
||||||
|
}: {
|
||||||
|
user: TUsers;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
organizationId?: string;
|
||||||
|
}) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
await updateUserDeviceSession(user, ip, userAgent);
|
||||||
|
const tokenSession = await tokenService.getUserTokenSession({
|
||||||
|
userAgent,
|
||||||
|
ip,
|
||||||
|
userId: user.id
|
||||||
|
});
|
||||||
|
if (!tokenSession) throw new Error("Failed to create token");
|
||||||
|
const accessToken = jwt.sign(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||||
|
userId: user.id,
|
||||||
|
tokenVersionId: tokenSession.id,
|
||||||
|
accessVersion: tokenSession.accessVersion,
|
||||||
|
organizationId
|
||||||
|
},
|
||||||
|
cfg.AUTH_SECRET,
|
||||||
|
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshToken = jwt.sign(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.REFRESH_TOKEN,
|
||||||
|
userId: user.id,
|
||||||
|
tokenVersionId: tokenSession.id,
|
||||||
|
refreshVersion: tokenSession.refreshVersion,
|
||||||
|
organizationId
|
||||||
|
},
|
||||||
|
cfg.AUTH_SECRET,
|
||||||
|
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
|
||||||
|
);
|
||||||
|
|
||||||
|
return { access: accessToken, refresh: refreshToken };
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Step 1 of login. To get server public key in exchange of client public key
|
||||||
|
*/
|
||||||
|
const loginGenServerPublicKey = async ({
|
||||||
|
email,
|
||||||
|
providerAuthToken,
|
||||||
|
clientPublicKey
|
||||||
|
}: TLoginGenServerPublicKeyDTO) => {
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByEmail(email);
|
||||||
|
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
|
||||||
|
throw new Error("Failed to find user");
|
||||||
|
}
|
||||||
|
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
|
||||||
|
validateProviderAuthToken(providerAuthToken as string, email);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
|
||||||
|
const userEncKeys = await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
||||||
|
clientPublicKey,
|
||||||
|
serverPrivateKey: serverSrpKey.privateKey
|
||||||
|
});
|
||||||
|
if (!userEncKeys) throw new Error("Failed to update encryption key");
|
||||||
|
return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Step 2 of login. Pass the client proof and with multi factor setup handle the required steps
|
||||||
|
*/
|
||||||
|
const loginExchangeClientProof = async ({
|
||||||
|
email,
|
||||||
|
clientProof,
|
||||||
|
providerAuthToken,
|
||||||
|
ip,
|
||||||
|
userAgent
|
||||||
|
}: TLoginClientProofDTO) => {
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByEmail(email);
|
||||||
|
if (!userEnc) throw new Error("Failed to find user");
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
let organizationId;
|
||||||
|
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
|
||||||
|
const { orgId } = validateProviderAuthToken(providerAuthToken as string, email);
|
||||||
|
organizationId = orgId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
|
||||||
|
const isValidClientProof = await srpCheckClientProof(
|
||||||
|
userEnc.salt,
|
||||||
|
userEnc.verifier,
|
||||||
|
userEnc.serverPrivateKey,
|
||||||
|
userEnc.clientPublicKey,
|
||||||
|
clientProof
|
||||||
|
);
|
||||||
|
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
|
||||||
|
|
||||||
|
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
|
||||||
|
serverPrivateKey: null,
|
||||||
|
clientPublicKey: null
|
||||||
|
});
|
||||||
|
// send multi factor auth token if they it enabled
|
||||||
|
if (userEnc.isMfaEnabled) {
|
||||||
|
const mfaToken = jwt.sign({
|
||||||
|
authTokenType: AuthTokenType.MFA_TOKEN,
|
||||||
|
userId: userEnc.userId,
|
||||||
|
organizationId
|
||||||
|
}, cfg.AUTH_SECRET, {
|
||||||
|
expiresIn: cfg.JWT_MFA_LIFETIME
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendUserMfaCode({
|
||||||
|
userId: userEnc.userId,
|
||||||
|
email: userEnc.email
|
||||||
|
});
|
||||||
|
|
||||||
|
return { isMfaEnabled: true, token: mfaToken } as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await generateUserTokens({
|
||||||
|
user: {
|
||||||
|
...userEnc,
|
||||||
|
id: userEnc.userId
|
||||||
|
},
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { token, isMfaEnabled: false, user: userEnc } as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Multi factor authentication re-send code, Get user id from token
|
||||||
|
* saved in frontend
|
||||||
|
*/
|
||||||
|
const resendMfaToken = async (userId: string) => {
|
||||||
|
const user = await userDAL.findById(userId);
|
||||||
|
if (!user) return;
|
||||||
|
await sendUserMfaCode({
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Multi factor authentication verification of code
|
||||||
|
* Third step of login in which user completes with mfa
|
||||||
|
* */
|
||||||
|
const verifyMfaToken = async ({ userId, mfaToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
|
||||||
|
await tokenService.validateTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_MFA,
|
||||||
|
userId,
|
||||||
|
code: mfaToken
|
||||||
|
});
|
||||||
|
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
|
if (!userEnc) throw new Error("Failed to authenticate user");
|
||||||
|
|
||||||
|
const token = await generateUserTokens({
|
||||||
|
user: {
|
||||||
|
...userEnc,
|
||||||
|
id: userEnc.userId
|
||||||
|
},
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
organizationId: orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { token, user: userEnc };
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* OAuth2 login for google,github, and other oauth2 provider
|
||||||
|
* */
|
||||||
|
const oauth2Login = async ({
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
authMethod,
|
||||||
|
callbackPort,
|
||||||
|
isSignupAllowed
|
||||||
|
}: TOauthLoginDTO) => {
|
||||||
|
let user = await userDAL.findUserByEmail(email);
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const isOauthSignUpDisabled = !isSignupAllowed && !user;
|
||||||
|
if (isOauthSignUpDisabled) throw new BadRequestError({ message: "User signup disabled", name: "Oauth 2 login" });
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
user = await userDAL.create({ email, firstName, lastName, authMethods: [authMethod] });
|
||||||
|
}
|
||||||
|
const isLinkingRequired = !user?.authMethods?.includes(authMethod);
|
||||||
|
const isUserCompleted = user.isAccepted;
|
||||||
|
const providerAuthToken = jwt.sign(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
authMethod,
|
||||||
|
isUserCompleted,
|
||||||
|
isLinkingRequired,
|
||||||
|
...(callbackPort
|
||||||
|
? {
|
||||||
|
callbackPort
|
||||||
|
}
|
||||||
|
: {})
|
||||||
|
},
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isUserCompleted, providerAuthToken };
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* logout user by incrementing the version by 1 meaning any old session will become invalid
|
||||||
|
* as there number is behind
|
||||||
|
* */
|
||||||
|
const logout = async (userId: string, sessionId: string) => {
|
||||||
|
await tokenService.clearTokenSessionById(userId, sessionId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
loginGenServerPublicKey,
|
||||||
|
loginExchangeClientProof,
|
||||||
|
logout,
|
||||||
|
oauth2Login,
|
||||||
|
resendMfaToken,
|
||||||
|
verifyMfaToken,
|
||||||
|
generateUserTokens
|
||||||
|
};
|
||||||
|
};
|
||||||
32
backend/src/services/auth/auth-login-type.ts
Normal file
32
backend/src/services/auth/auth-login-type.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { AuthMethod } from "./auth-type";
|
||||||
|
|
||||||
|
export type TLoginGenServerPublicKeyDTO = {
|
||||||
|
email: string;
|
||||||
|
clientPublicKey: string;
|
||||||
|
providerAuthToken?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TLoginClientProofDTO = {
|
||||||
|
email: string;
|
||||||
|
clientProof: string;
|
||||||
|
providerAuthToken?: string;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyMfaTokenDTO = {
|
||||||
|
userId: string;
|
||||||
|
mfaToken: string;
|
||||||
|
ip: string;
|
||||||
|
userAgent: string;
|
||||||
|
orgId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOauthLoginDTO = {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName?: string;
|
||||||
|
authMethod: AuthMethod;
|
||||||
|
callbackPort?: string;
|
||||||
|
isSignupAllowed?: boolean;
|
||||||
|
};
|
||||||
68
backend/src/services/auth/auth-type.ts
Normal file
68
backend/src/services/auth/auth-type.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
export enum AuthMethod {
|
||||||
|
EMAIL = "email",
|
||||||
|
GOOGLE = "google",
|
||||||
|
GITHUB = "github",
|
||||||
|
GITLAB = "gitlab",
|
||||||
|
OKTA_SAML = "okta-saml",
|
||||||
|
AZURE_SAML = "azure-saml",
|
||||||
|
JUMPCLOUD_SAML = "jumpcloud-saml"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AuthTokenType {
|
||||||
|
ACCESS_TOKEN = "accessToken",
|
||||||
|
REFRESH_TOKEN = "refreshToken",
|
||||||
|
SIGNUP_TOKEN = "signupToken", // TODO: remove in favor of claim
|
||||||
|
MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim
|
||||||
|
PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim
|
||||||
|
API_KEY = "apiKey",
|
||||||
|
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
||||||
|
SERVICE_REFRESH_TOKEN = "serviceRefreshToken",
|
||||||
|
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AuthMode {
|
||||||
|
JWT = "jwt",
|
||||||
|
SERVICE_TOKEN = "serviceToken",
|
||||||
|
API_KEY = "apiKey",
|
||||||
|
IDENTITY_ACCESS_TOKEN = "identityAccessToken"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ActorType { // would extend to AWS, Azure, ...
|
||||||
|
USER = "user", // userIdentity
|
||||||
|
SERVICE = "service",
|
||||||
|
IDENTITY = "identity",
|
||||||
|
Machine = "machine"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthModeJwtTokenPayload = {
|
||||||
|
authTokenType: AuthTokenType.ACCESS_TOKEN;
|
||||||
|
userId: string;
|
||||||
|
tokenVersionId: string;
|
||||||
|
accessVersion: number;
|
||||||
|
organizationId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthModeMfaJwtTokenPayload = {
|
||||||
|
authTokenType: AuthTokenType.MFA_TOKEN;
|
||||||
|
userId: string;
|
||||||
|
organizationId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthModeRefreshJwtTokenPayload = {
|
||||||
|
authTokenType: AuthTokenType.REFRESH_TOKEN;
|
||||||
|
userId: string;
|
||||||
|
tokenVersionId: string;
|
||||||
|
refreshVersion: number;
|
||||||
|
organizationId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthModeProviderJwtTokenPayload = {
|
||||||
|
authTokenType: AuthTokenType.PROVIDER_TOKEN;
|
||||||
|
email: string;
|
||||||
|
organizationId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthModeProviderSignUpTokenPayload = {
|
||||||
|
authTokenType: AuthTokenType.SIGNUP_TOKEN;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
435
backend/src/services/identity-ua/identity-ua-service.ts
Normal file
435
backend/src/services/identity-ua/identity-ua-service.ts
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import bcrypt from "bcrypt";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
|
||||||
|
|
||||||
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
|
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||||
|
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
|
||||||
|
import { TIdentityUaClientSecretDALFactory } from "./identity-ua-client-secret-dal";
|
||||||
|
import { TIdentityUaDALFactory } from "./identity-ua-dal";
|
||||||
|
import {
|
||||||
|
TAttachUaDTO,
|
||||||
|
TCreateUaClientSecretDTO,
|
||||||
|
TGetUaClientSecretsDTO,
|
||||||
|
TGetUaDTO,
|
||||||
|
TRevokeUaClientSecretDTO,
|
||||||
|
TUpdateUaDTO
|
||||||
|
} from "./identity-ua-types";
|
||||||
|
|
||||||
|
type TIdentityUaServiceFactoryDep = {
|
||||||
|
identityUaDAL: TIdentityUaDALFactory;
|
||||||
|
identityUaClientSecretDAL: TIdentityUaClientSecretDALFactory;
|
||||||
|
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
|
||||||
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
|
identityDAL: Pick<TIdentityDALFactory, "updateById">;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIdentityUaServiceFactory = ReturnType<typeof identityUaServiceFactory>;
|
||||||
|
|
||||||
|
export const identityUaServiceFactory = ({
|
||||||
|
identityUaDAL,
|
||||||
|
identityUaClientSecretDAL,
|
||||||
|
identityAccessTokenDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
identityDAL,
|
||||||
|
permissionService,
|
||||||
|
licenseService
|
||||||
|
}: TIdentityUaServiceFactoryDep) => {
|
||||||
|
const login = async (clientId: string, clientSecret: string, ip: string) => {
|
||||||
|
const identityUa = await identityUaDAL.findOne({ clientId });
|
||||||
|
if (!identityUa) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
checkIPAgainstBlocklist({
|
||||||
|
ipAddress: ip,
|
||||||
|
trustedIps: identityUa.clientSecretTrustedIps as TIp[]
|
||||||
|
});
|
||||||
|
const clientSecrtInfo = await identityUaClientSecretDAL.find({
|
||||||
|
identityUAId: identityUa.id,
|
||||||
|
isClientSecretRevoked: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const validClientSecretInfo = clientSecrtInfo.find(({ clientSecretHash }) =>
|
||||||
|
bcrypt.compareSync(clientSecret, clientSecretHash)
|
||||||
|
);
|
||||||
|
if (!validClientSecretInfo) throw new UnauthorizedError();
|
||||||
|
|
||||||
|
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
|
||||||
|
if (clientSecretTTL > 0) {
|
||||||
|
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
|
||||||
|
const ttlInMilliseconds = clientSecretTTL * 1000;
|
||||||
|
const currentDate = new Date();
|
||||||
|
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
|
||||||
|
|
||||||
|
if (currentDate > expirationTime) {
|
||||||
|
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
|
||||||
|
isClientSecretRevoked: true
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Failed to authenticate identity credentials due to expired client secret"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses === clientSecretNumUsesLimit) {
|
||||||
|
// number of times client secret can be used for
|
||||||
|
// a login operation reached
|
||||||
|
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
|
||||||
|
isClientSecretRevoked: true
|
||||||
|
});
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Failed to authenticate identity credentials due to client secret number of uses limit reached"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
|
||||||
|
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo.id, tx);
|
||||||
|
const newToken = await identityAccessTokenDAL.create(
|
||||||
|
{
|
||||||
|
identityId: identityUa.identityId,
|
||||||
|
isAccessTokenRevoked: false,
|
||||||
|
identityUAClientSecretId: uaClientSecretDoc.id,
|
||||||
|
accessTokenTTL: identityUa.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityUa.accessTokenMaxTTL,
|
||||||
|
accessTokenNumUses: 0,
|
||||||
|
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return newToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const accessToken = jwt.sign(
|
||||||
|
{
|
||||||
|
identityId: identityUa.identityId,
|
||||||
|
clientSecretId: validClientSecretInfo.id,
|
||||||
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: identityAccessToken.accessTokenMaxTTL === 0 ? undefined : identityAccessToken.accessTokenMaxTTL
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { accessToken, identityUa, validClientSecretInfo, identityAccessToken };
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachUa = async ({
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
identityId,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenTrustedIps,
|
||||||
|
clientSecretTrustedIps,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgScope
|
||||||
|
}: TAttachUaDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity.authMethod)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to add universal auth to already configured identity"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||||
|
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||||
|
const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => {
|
||||||
|
if (
|
||||||
|
!plan.ipAllowlisting &&
|
||||||
|
clientSecretTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||||
|
clientSecretTrustedIp.ipAddress !== "::/0"
|
||||||
|
)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
||||||
|
});
|
||||||
|
if (!isValidIpOrCidr(clientSecretTrustedIp.ipAddress))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
|
});
|
||||||
|
return extractIPDetails(clientSecretTrustedIp.ipAddress);
|
||||||
|
});
|
||||||
|
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||||
|
if (
|
||||||
|
!plan.ipAllowlisting &&
|
||||||
|
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||||
|
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||||
|
)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
||||||
|
});
|
||||||
|
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
|
});
|
||||||
|
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
const identityUa = await identityUaDAL.transaction(async (tx) => {
|
||||||
|
const doc = await identityUaDAL.create(
|
||||||
|
{
|
||||||
|
identityId: identityMembershipOrg.identityId,
|
||||||
|
clientId: crypto.randomUUID(),
|
||||||
|
clientSecretTrustedIps: JSON.stringify(reformattedClientSecretTrustedIps),
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await identityDAL.updateById(
|
||||||
|
identityMembershipOrg.identityId,
|
||||||
|
{
|
||||||
|
authMethod: IdentityAuthMethod.Univeral
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
return { ...identityUa, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateUa = async ({
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
identityId,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenTrustedIps,
|
||||||
|
clientSecretTrustedIps,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgScope
|
||||||
|
}: TUpdateUaDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to updated universal auth"
|
||||||
|
});
|
||||||
|
|
||||||
|
const uaIdentityAuth = await identityUaDAL.findOne({ identityId });
|
||||||
|
|
||||||
|
if (
|
||||||
|
(accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL) > 0 &&
|
||||||
|
(accessTokenTTL || uaIdentityAuth.accessTokenMaxTTL) > (accessTokenMaxTTL || uaIdentityAuth.accessTokenMaxTTL)
|
||||||
|
) {
|
||||||
|
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||||
|
const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map((clientSecretTrustedIp) => {
|
||||||
|
if (
|
||||||
|
!plan.ipAllowlisting &&
|
||||||
|
clientSecretTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||||
|
clientSecretTrustedIp.ipAddress !== "::/0"
|
||||||
|
)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
||||||
|
});
|
||||||
|
if (!isValidIpOrCidr(clientSecretTrustedIp.ipAddress))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
|
});
|
||||||
|
return extractIPDetails(clientSecretTrustedIp.ipAddress);
|
||||||
|
});
|
||||||
|
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||||
|
if (
|
||||||
|
!plan.ipAllowlisting &&
|
||||||
|
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||||
|
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||||
|
)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
|
||||||
|
});
|
||||||
|
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||||
|
});
|
||||||
|
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedUaAuth = await identityUaDAL.updateById(uaIdentityAuth.id, {
|
||||||
|
clientSecretTrustedIps: reformattedClientSecretTrustedIps
|
||||||
|
? JSON.stringify(reformattedClientSecretTrustedIps)
|
||||||
|
: undefined,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||||
|
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||||
|
: undefined
|
||||||
|
});
|
||||||
|
return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIdentityUa = async ({ identityId, actorId, actor, actorOrgScope }: TGetUaDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have universal auth"
|
||||||
|
});
|
||||||
|
|
||||||
|
const uaIdentityAuth = await identityUaDAL.findOne({ identityId });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||||
|
return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUaClientSecret = async ({
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgScope,
|
||||||
|
identityId,
|
||||||
|
ttl,
|
||||||
|
description,
|
||||||
|
numUsesLimit
|
||||||
|
}: TCreateUaClientSecretDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have universal auth"
|
||||||
|
});
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
identityMembershipOrg.identityId,
|
||||||
|
identityMembershipOrg.orgId,
|
||||||
|
actorOrgScope
|
||||||
|
);
|
||||||
|
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasPriviledge)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Failed to add identity to project with more privileged role"
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const clientSecret = crypto.randomBytes(32).toString("hex");
|
||||||
|
const clientSecretHash = await bcrypt.hash(clientSecret, appCfg.SALT_ROUNDS);
|
||||||
|
const identityUniversalAuth = await identityUaDAL.findOne({
|
||||||
|
identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
const identityUaClientSecret = await identityUaClientSecretDAL.create({
|
||||||
|
identityUAId: identityUniversalAuth.id,
|
||||||
|
description,
|
||||||
|
clientSecretPrefix: clientSecret.slice(0, 4),
|
||||||
|
clientSecretHash,
|
||||||
|
clientSecretNumUses: 0,
|
||||||
|
clientSecretNumUsesLimit: numUsesLimit,
|
||||||
|
clientSecretTTL: ttl,
|
||||||
|
isClientSecretRevoked: false
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
clientSecret,
|
||||||
|
clientSecretData: identityUaClientSecret,
|
||||||
|
uaAuth: identityUniversalAuth,
|
||||||
|
orgId: identityMembershipOrg.orgId
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUaClientSecrets = async ({ actor, actorId, actorOrgScope, identityId }: TGetUaClientSecretsDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have universal auth"
|
||||||
|
});
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
identityMembershipOrg.identityId,
|
||||||
|
identityMembershipOrg.orgId,
|
||||||
|
actorOrgScope
|
||||||
|
);
|
||||||
|
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasPriviledge)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Failed to add identity to project with more privileged role"
|
||||||
|
});
|
||||||
|
|
||||||
|
const identityUniversalAuth = await identityUaDAL.findOne({
|
||||||
|
identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientSecrets = await identityUaClientSecretDAL.find({
|
||||||
|
identityUAId: identityUniversalAuth.id,
|
||||||
|
isClientSecretRevoked: false
|
||||||
|
});
|
||||||
|
return { clientSecrets, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const revokeUaClientSecret = async ({ identityId, actorId, actor, actorOrgScope, clientSecretId }: TRevokeUaClientSecretDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new BadRequestError({ message: "Failed to find identity" });
|
||||||
|
if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.Univeral)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have universal auth"
|
||||||
|
});
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityMembershipOrg.orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
identityMembershipOrg.identityId,
|
||||||
|
identityMembershipOrg.orgId,
|
||||||
|
actorOrgScope
|
||||||
|
);
|
||||||
|
const hasPriviledge = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasPriviledge)
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Failed to add identity to project with more privileged role"
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
|
||||||
|
isClientSecretRevoked: true
|
||||||
|
});
|
||||||
|
return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
login,
|
||||||
|
attachUa,
|
||||||
|
updateUa,
|
||||||
|
getIdentityUa,
|
||||||
|
createUaClientSecret,
|
||||||
|
getUaClientSecrets,
|
||||||
|
revokeUaClientSecret
|
||||||
|
};
|
||||||
|
};
|
||||||
139
backend/src/services/identity/identity-service.ts
Normal file
139
backend/src/services/identity/identity-service.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
|
||||||
|
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
|
import { TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
import { TIdentityDALFactory } from "./identity-dal";
|
||||||
|
import { TIdentityOrgDALFactory } from "./identity-org-dal";
|
||||||
|
import { TCreateIdentityDTO, TDeleteIdentityDTO, TUpdateIdentityDTO } from "./identity-types";
|
||||||
|
|
||||||
|
type TIdentityServiceFactoryDep = {
|
||||||
|
identityDAL: TIdentityDALFactory;
|
||||||
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
|
||||||
|
|
||||||
|
export const identityServiceFactory = ({
|
||||||
|
identityDAL,
|
||||||
|
identityOrgMembershipDAL,
|
||||||
|
permissionService
|
||||||
|
}: TIdentityServiceFactoryDep) => {
|
||||||
|
const createIdentity = async ({ name, role, actor, orgId, actorId, actorOrgScope }: TCreateIdentityDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||||
|
role,
|
||||||
|
orgId
|
||||||
|
);
|
||||||
|
const isCustomRole = Boolean(customRole);
|
||||||
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged identity" });
|
||||||
|
|
||||||
|
const identity = await identityDAL.transaction(async (tx) => {
|
||||||
|
const newIdentity = await identityDAL.create({ name }, tx);
|
||||||
|
await identityOrgMembershipDAL.create(
|
||||||
|
{
|
||||||
|
identityId: newIdentity.id,
|
||||||
|
orgId,
|
||||||
|
role: isCustomRole ? OrgMembershipRole.Custom : role,
|
||||||
|
roleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return newIdentity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return identity;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateIdentity = async ({ id, role, name, actor, actorId, actorOrgScope }: TUpdateIdentityDTO) => {
|
||||||
|
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
|
||||||
|
if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityOrgMembership.orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
const { permission: identityRolePermission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
id,
|
||||||
|
identityOrgMembership.orgId,
|
||||||
|
actorOrgScope
|
||||||
|
);
|
||||||
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
||||||
|
if (!hasRequiredPriviledges)
|
||||||
|
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
||||||
|
|
||||||
|
let customRole: TOrgRoles | undefined;
|
||||||
|
if (role) {
|
||||||
|
const { permission: rolePermission, role: customOrgRole } = await permissionService.getOrgPermissionByRole(
|
||||||
|
role,
|
||||||
|
identityOrgMembership.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
const isCustomRole = Boolean(customOrgRole);
|
||||||
|
const hasRequiredNewRolePermission = isAtLeastAsPrivileged(permission, rolePermission);
|
||||||
|
if (!hasRequiredNewRolePermission)
|
||||||
|
throw new BadRequestError({ message: "Failed to create a more privileged identity" });
|
||||||
|
if (isCustomRole) customRole = customOrgRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = await identityDAL.transaction(async (tx) => {
|
||||||
|
const newIdentity = name ? await identityDAL.updateById(id, { name }, tx) : await identityDAL.findById(id, tx);
|
||||||
|
if (role) {
|
||||||
|
await identityOrgMembershipDAL.update(
|
||||||
|
{ identityId: id },
|
||||||
|
{
|
||||||
|
role: customRole ? OrgMembershipRole.Custom : role,
|
||||||
|
roleId: customRole?.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return newIdentity;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...identity, orgId: identityOrgMembership.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteIdentity = async ({ actorId, actor, actorOrgScope, id }: TDeleteIdentityDTO) => {
|
||||||
|
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
|
||||||
|
if (!identityOrgMembership) throw new BadRequestError({ message: `Failed to find identity with id ${id}` });
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, identityOrgMembership.orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||||
|
const { permission: identityRolePermission } = await permissionService.getOrgPermission(
|
||||||
|
ActorType.IDENTITY,
|
||||||
|
id,
|
||||||
|
identityOrgMembership.orgId
|
||||||
|
);
|
||||||
|
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission);
|
||||||
|
if (!hasRequiredPriviledges)
|
||||||
|
throw new ForbiddenRequestError({ message: "Failed to delete more privileged identity" });
|
||||||
|
|
||||||
|
const deletedIdentity = await identityDAL.deleteById(id);
|
||||||
|
return { ...deletedIdentity, orgId: identityOrgMembership.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const listOrgIdentities = async ({ orgId, actor, actorId, actorOrgScope }: TOrgPermission) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
const identityMemberhips = await identityOrgMembershipDAL.findByOrgId(orgId);
|
||||||
|
return identityMemberhips;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createIdentity,
|
||||||
|
updateIdentity,
|
||||||
|
deleteIdentity,
|
||||||
|
listOrgIdentities
|
||||||
|
};
|
||||||
|
};
|
||||||
196
backend/src/services/org/org-dal.ts
Normal file
196
backend/src/services/org/org-dal.ts
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import {
|
||||||
|
TableName,
|
||||||
|
TOrganizations,
|
||||||
|
TOrganizationsInsert,
|
||||||
|
TOrgMemberships,
|
||||||
|
TOrgMembershipsInsert,
|
||||||
|
TOrgMembershipsUpdate,
|
||||||
|
TUserEncryptionKeys
|
||||||
|
} from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { buildFindFilter, selectAllTableCols, TFindFilter, TFindOpt, withTransaction, ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TOrgDALFactory = ReturnType<typeof orgDALFactory>;
|
||||||
|
|
||||||
|
export const orgDALFactory = (db: TDbClient) => {
|
||||||
|
const orgOrm = ormify(db, TableName.Organization);
|
||||||
|
|
||||||
|
const findOrgById = async (orgId: string) => {
|
||||||
|
try {
|
||||||
|
const org = await db(TableName.Organization).where({ id: orgId }).first();
|
||||||
|
return org;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find org by id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// special query
|
||||||
|
const findAllOrgsByUserId = async (userId: string): Promise<TOrganizations[]> => {
|
||||||
|
try {
|
||||||
|
const org = await db(TableName.OrgMembership)
|
||||||
|
.where({ userId })
|
||||||
|
.join(TableName.Organization, `${TableName.OrgMembership}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Organization));
|
||||||
|
return org;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find all org by user id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findOrgByProjectId = async (projectId: string): Promise<TOrganizations> => {
|
||||||
|
try {
|
||||||
|
const [org] = await db(TableName.Project)
|
||||||
|
.where({ [`${TableName.Project}.id` as "id"]: projectId })
|
||||||
|
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||||
|
.select(selectAllTableCols(TableName.Organization));
|
||||||
|
|
||||||
|
return org;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find org by project id" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// special query
|
||||||
|
const findAllOrgMembers = async (orgId: string) => {
|
||||||
|
try {
|
||||||
|
const members = await db(TableName.OrgMembership)
|
||||||
|
.where({ orgId })
|
||||||
|
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
|
||||||
|
.leftJoin<TUserEncryptionKeys>(
|
||||||
|
TableName.UserEncryptionKey,
|
||||||
|
`${TableName.UserEncryptionKey}.userId`,
|
||||||
|
`${TableName.Users}.id`
|
||||||
|
)
|
||||||
|
.select(
|
||||||
|
db.ref("id").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("inviteEmail").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("orgId").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("role").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("roleId").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("status").withSchema(TableName.OrgMembership),
|
||||||
|
db.ref("email").withSchema(TableName.Users),
|
||||||
|
db.ref("firstName").withSchema(TableName.Users),
|
||||||
|
db.ref("lastName").withSchema(TableName.Users),
|
||||||
|
db.ref("id").withSchema(TableName.Users).as("userId"),
|
||||||
|
db.ref("publicKey").withSchema(TableName.UserEncryptionKey)
|
||||||
|
);
|
||||||
|
return members.map(({ email, firstName, lastName, userId, publicKey, ...data }) => ({
|
||||||
|
...data,
|
||||||
|
user: { email, firstName, lastName, id: userId, publicKey }
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find all org members" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const create = async (dto: TOrganizationsInsert, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const [organization] = await (tx || db)(TableName.Organization).insert(dto).returning("*");
|
||||||
|
return organization;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Create organization" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteById = async (orgId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const [org] = await (tx || db)(TableName.Organization).where({ id: orgId }).delete().returning("*");
|
||||||
|
return org;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Update organization" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateById = async (orgId: string, data: Partial<TOrganizations>) => {
|
||||||
|
try {
|
||||||
|
const [org] = await db(TableName.Organization)
|
||||||
|
.where({ id: orgId })
|
||||||
|
.update({ ...data })
|
||||||
|
.returning("*");
|
||||||
|
return org;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Update organization" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// MEMBERSHIP OPERATIONS
|
||||||
|
// --------------------
|
||||||
|
// const orgMembershipOrm = ormify(db, TableName.OrgMembership);
|
||||||
|
|
||||||
|
const createMembership = async (data: TOrgMembershipsInsert, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const [membership] = await (tx || db)(TableName.OrgMembership).insert(data).returning("*");
|
||||||
|
return membership;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Create org membership" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMembershipById = async (id: string, data: TOrgMembershipsUpdate, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const [membership] = await (tx || db)(TableName.OrgMembership).where({ id }).update(data).returning("*");
|
||||||
|
return membership;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Update org membership" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateMembership = async (filter: Partial<TOrgMemberships>, data: TOrgMembershipsUpdate, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const membership = await (tx || db)(TableName.OrgMembership).where(filter).update(data).returning("*");
|
||||||
|
return membership;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Update org memberships" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMembershipById = async (id: string, orgId: string, tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const [membership] = await (tx || db)(TableName.OrgMembership).where({ id, orgId }).delete().returning("*");
|
||||||
|
return membership;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Delete org membership" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findMembership = async (
|
||||||
|
filter: TFindFilter<TOrgMemberships>,
|
||||||
|
{ offset, limit, sort, tx }: TFindOpt<TOrgMemberships> = {}
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db)(TableName.OrgMembership)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
.where(buildFindFilter(filter))
|
||||||
|
.join(TableName.Users, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
|
||||||
|
.select(selectAllTableCols(TableName.OrgMembership), db.ref("email").withSchema(TableName.Users));
|
||||||
|
if (limit) void query.limit(limit);
|
||||||
|
if (offset) void query.offset(offset);
|
||||||
|
if (sort) {
|
||||||
|
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
|
||||||
|
}
|
||||||
|
const res = await query;
|
||||||
|
return res;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Find one" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return withTransaction(db, {
|
||||||
|
...orgOrm,
|
||||||
|
findOrgByProjectId,
|
||||||
|
findAllOrgMembers,
|
||||||
|
findOrgById,
|
||||||
|
findAllOrgsByUserId,
|
||||||
|
create,
|
||||||
|
updateById,
|
||||||
|
deleteById,
|
||||||
|
findMembership,
|
||||||
|
createMembership,
|
||||||
|
updateMembershipById,
|
||||||
|
deleteMembershipById,
|
||||||
|
updateMembership
|
||||||
|
});
|
||||||
|
};
|
||||||
113
backend/src/services/org/org-role-service.ts
Normal file
113
backend/src/services/org/org-role-service.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { packRules } from "@casl/ability/extra";
|
||||||
|
|
||||||
|
import { TOrgRolesInsert, TOrgRolesUpdate } from "@app/db/schemas";
|
||||||
|
import {
|
||||||
|
orgAdminPermissions,
|
||||||
|
orgMemberPermissions,
|
||||||
|
orgNoAccessPermissions,
|
||||||
|
OrgPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TOrgRoleDALFactory } from "./org-role-dal";
|
||||||
|
|
||||||
|
type TOrgRoleServiceFactoryDep = {
|
||||||
|
orgRoleDAL: TOrgRoleDALFactory;
|
||||||
|
permissionService: TPermissionServiceFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOrgRoleServiceFactory = ReturnType<typeof orgRoleServiceFactory>;
|
||||||
|
|
||||||
|
export const orgRoleServiceFactory = ({ orgRoleDAL, permissionService }: TOrgRoleServiceFactoryDep) => {
|
||||||
|
const createRole = async (userId: string, orgId: string, data: Omit<TOrgRolesInsert, "orgId">, actorOrgScope?: string) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Role);
|
||||||
|
const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId });
|
||||||
|
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
|
||||||
|
const role = await orgRoleDAL.create({
|
||||||
|
...data,
|
||||||
|
orgId,
|
||||||
|
permissions: JSON.stringify(data.permissions)
|
||||||
|
});
|
||||||
|
return role;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRole = async (userId: string, orgId: string, roleId: string, data: Omit<TOrgRolesUpdate, "orgId">, actorOrgScope?: string) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Role);
|
||||||
|
if (data?.slug) {
|
||||||
|
const existingRole = await orgRoleDAL.findOne({ slug: data.slug, orgId });
|
||||||
|
if (existingRole && existingRole.id !== roleId)
|
||||||
|
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
|
||||||
|
}
|
||||||
|
const [updatedRole] = await orgRoleDAL.update(
|
||||||
|
{ id: roleId, orgId },
|
||||||
|
{ ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined }
|
||||||
|
);
|
||||||
|
if (!updateRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
||||||
|
return updatedRole;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteRole = async (userId: string, orgId: string, roleId: string, actorOrgScope?: string) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Role);
|
||||||
|
const [deletedRole] = await orgRoleDAL.delete({ id: roleId, orgId });
|
||||||
|
if (!deleteRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
||||||
|
|
||||||
|
return deletedRole;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listRoles = async (userId: string, orgId: string, actorOrgScope?: string) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||||
|
const customRoles = await orgRoleDAL.find({ orgId });
|
||||||
|
const roles = [
|
||||||
|
{
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
|
||||||
|
orgId,
|
||||||
|
name: "Admin",
|
||||||
|
slug: "admin",
|
||||||
|
description: "Complete administration access over the organization",
|
||||||
|
permissions: packRules(orgAdminPermissions.rules),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
|
||||||
|
orgId,
|
||||||
|
name: "Member",
|
||||||
|
slug: "member",
|
||||||
|
description: "Non-administrative role in an organization",
|
||||||
|
permissions: packRules(orgMemberPermissions.rules),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b10d49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
|
||||||
|
orgId,
|
||||||
|
name: "No Access",
|
||||||
|
slug: "no-access",
|
||||||
|
description: "No access to any resources in the organization",
|
||||||
|
permissions: packRules(orgNoAccessPermissions.rules),
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
...(customRoles || []).map(({ permissions, ...data }) => ({
|
||||||
|
...data,
|
||||||
|
permissions
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserPermission = async (userId: string, orgId: string, actorOrgScope?: string) => {
|
||||||
|
const { permission, membership } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
return { permissions: packRules(permission.rules), membership };
|
||||||
|
};
|
||||||
|
|
||||||
|
return { createRole, updateRole, deleteRole, listRoles, getUserPermission };
|
||||||
|
};
|
||||||
457
backend/src/services/org/org-service.ts
Normal file
457
backend/src/services/org/org-service.ts
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
|
||||||
|
import { OrgMembershipRole, OrgMembershipStatus } from "@app/db/schemas";
|
||||||
|
import { TProjects } from "@app/db/schemas/projects";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||||
|
import { generateSymmetricKey, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
|
|
||||||
|
import { ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
|
||||||
|
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
|
import { TokenType } from "../auth-token/auth-token-types";
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
|
||||||
|
import { TOrgBotDALFactory } from "./org-bot-dal";
|
||||||
|
import { TOrgDALFactory } from "./org-dal";
|
||||||
|
import { TOrgRoleDALFactory } from "./org-role-dal";
|
||||||
|
import {
|
||||||
|
TDeleteOrgMembershipDTO,
|
||||||
|
TFindAllWorkspacesDTO,
|
||||||
|
TInviteUserToOrgDTO,
|
||||||
|
TUpdateOrgMembershipDTO,
|
||||||
|
TVerifyUserToOrgDTO
|
||||||
|
} from "./org-types";
|
||||||
|
|
||||||
|
type TOrgServiceFactoryDep = {
|
||||||
|
orgDAL: TOrgDALFactory;
|
||||||
|
orgBotDAL: TOrgBotDALFactory;
|
||||||
|
orgRoleDAL: TOrgRoleDALFactory;
|
||||||
|
userDAL: TUserDALFactory;
|
||||||
|
projectDAL: TProjectDALFactory;
|
||||||
|
incidentContactDAL: TIncidentContactsDALFactory;
|
||||||
|
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne">;
|
||||||
|
smtpService: TSmtpService;
|
||||||
|
tokenService: TAuthTokenServiceFactory;
|
||||||
|
permissionService: TPermissionServiceFactory;
|
||||||
|
licenseService: Pick<
|
||||||
|
TLicenseServiceFactory,
|
||||||
|
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
|
||||||
|
|
||||||
|
export const orgServiceFactory = ({
|
||||||
|
orgDAL,
|
||||||
|
userDAL,
|
||||||
|
orgRoleDAL,
|
||||||
|
incidentContactDAL,
|
||||||
|
permissionService,
|
||||||
|
smtpService,
|
||||||
|
projectDAL,
|
||||||
|
tokenService,
|
||||||
|
orgBotDAL,
|
||||||
|
licenseService,
|
||||||
|
samlConfigDAL
|
||||||
|
}: TOrgServiceFactoryDep) => {
|
||||||
|
/*
|
||||||
|
* Get organization details by the organization id
|
||||||
|
* */
|
||||||
|
const findOrganizationById = async (userId: string, orgId: string, actorOrgScope?: string) => {
|
||||||
|
await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
|
||||||
|
return org;
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* Get all organization a user part of
|
||||||
|
* */
|
||||||
|
const findAllOrganizationOfUser = async (userId: string) => {
|
||||||
|
const orgs = await orgDAL.findAllOrgsByUserId(userId);
|
||||||
|
return orgs;
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* Get all workspace members
|
||||||
|
* */
|
||||||
|
const findAllOrgMembers = async (userId: string, orgId: string, actorOrgScope?: string) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
|
const members = await orgDAL.findAllOrgMembers(orgId);
|
||||||
|
return members;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAllWorkspaces = async ({ actor, actorId, actorOrgScope, orgId }: TFindAllWorkspacesDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
|
||||||
|
|
||||||
|
const organizationWorkspaceIds = new Set((await projectDAL.find({ orgId })).map((workspace) => workspace.id));
|
||||||
|
|
||||||
|
let workspaces: (TProjects & { organization: string } & {
|
||||||
|
environments: {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
}[];
|
||||||
|
})[];
|
||||||
|
|
||||||
|
if (actor === ActorType.USER) {
|
||||||
|
workspaces = await projectDAL.findAllProjects(actorId);
|
||||||
|
} else if (actor === ActorType.IDENTITY) {
|
||||||
|
workspaces = await projectDAL.findAllProjectsByIdentity(actorId);
|
||||||
|
} else {
|
||||||
|
throw new BadRequestError({ message: "Invalid actor type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspaces.filter((workspace) => organizationWorkspaceIds.has(workspace.id));
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Update organization settings
|
||||||
|
* */
|
||||||
|
const updateOrgName = async (userId: string, orgId: string, name: string, actorOrgScope?: string) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||||
|
const org = await orgDAL.updateById(orgId, { name });
|
||||||
|
if (!org) throw new BadRequestError({ name: "Org not found", message: "Organization not found" });
|
||||||
|
return org;
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* Create organization
|
||||||
|
* */
|
||||||
|
const createOrganization = async (userId: string, userEmail: string, orgName: string) => {
|
||||||
|
const { privateKey, publicKey } = generateAsymmetricKeyPair();
|
||||||
|
const key = generateSymmetricKey();
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedPrivateKey,
|
||||||
|
iv: privateKeyIV,
|
||||||
|
tag: privateKeyTag,
|
||||||
|
encoding: privateKeyKeyEncoding,
|
||||||
|
algorithm: privateKeyAlgorithm
|
||||||
|
} = infisicalSymmetricEncypt(privateKey);
|
||||||
|
const {
|
||||||
|
ciphertext: encryptedSymmetricKey,
|
||||||
|
iv: symmetricKeyIV,
|
||||||
|
tag: symmetricKeyTag,
|
||||||
|
encoding: symmetricKeyKeyEncoding,
|
||||||
|
algorithm: symmetricKeyAlgorithm
|
||||||
|
} = infisicalSymmetricEncypt(key);
|
||||||
|
|
||||||
|
const customerId = await licenseService.generateOrgCustomerId(orgName, userEmail);
|
||||||
|
const organization = await orgDAL.transaction(async (tx) => {
|
||||||
|
// akhilmhdh: for now this is auto created. in future we can input from user and for previous users just modifiy
|
||||||
|
const org = await orgDAL.create(
|
||||||
|
{ name: orgName, customerId, slug: slugify(`${orgName}-${alphaNumericNanoId(4)}`) },
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await orgDAL.createMembership(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
orgId: org.id,
|
||||||
|
role: OrgMembershipRole.Admin,
|
||||||
|
status: OrgMembershipStatus.Accepted
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await orgBotDAL.create(
|
||||||
|
{
|
||||||
|
name: org.name,
|
||||||
|
publicKey,
|
||||||
|
privateKeyIV,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
symmetricKeyIV,
|
||||||
|
symmetricKeyTag,
|
||||||
|
encryptedSymmetricKey,
|
||||||
|
symmetricKeyAlgorithm,
|
||||||
|
orgId: org.id,
|
||||||
|
privateKeyTag,
|
||||||
|
privateKeyAlgorithm,
|
||||||
|
privateKeyKeyEncoding,
|
||||||
|
symmetricKeyKeyEncoding
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return org;
|
||||||
|
});
|
||||||
|
|
||||||
|
return organization;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Delete organization by id
|
||||||
|
* */
|
||||||
|
const deleteOrganizationById = async (userId: string, orgId: string, actorOrgScope?: string) => {
|
||||||
|
const { membership } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
if ((membership.role as OrgMembershipRole) !== OrgMembershipRole.Admin)
|
||||||
|
throw new UnauthorizedError({ name: "Delete org by id", message: "Not an admin" });
|
||||||
|
|
||||||
|
const organization = await orgDAL.deleteById(orgId);
|
||||||
|
if (organization.customerId) {
|
||||||
|
await licenseService.removeOrgCustomer(organization.customerId);
|
||||||
|
}
|
||||||
|
return organization;
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* Org membership management
|
||||||
|
* Not another service because it has close ties with how an org works doesn't make sense to seperate them
|
||||||
|
* */
|
||||||
|
const updateOrgMembership = async ({ role, orgId, userId, membershipId, actorOrgScope }: TUpdateOrgMembershipDTO) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
|
const isCustomRole = !Object.values(OrgMembershipRole).includes(role as OrgMembershipRole);
|
||||||
|
if (isCustomRole) {
|
||||||
|
const customRole = await orgRoleDAL.findOne({ slug: role, orgId });
|
||||||
|
if (!customRole) throw new BadRequestError({ name: "Update membership", message: "Role not found" });
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (!plan?.rbac)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
||||||
|
});
|
||||||
|
|
||||||
|
const [membership] = await orgDAL.updateMembership(
|
||||||
|
{ id: membershipId, orgId },
|
||||||
|
{
|
||||||
|
role: OrgMembershipRole.Custom,
|
||||||
|
roleId: customRole.id
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [membership] = await orgDAL.updateMembership({ id: membershipId, orgId }, { role, roleId: null });
|
||||||
|
return membership;
|
||||||
|
};
|
||||||
|
/*
|
||||||
|
* Invite user to organization
|
||||||
|
*/
|
||||||
|
const inviteUserToOrganization = async ({ orgId, userId, inviteeEmail, actorOrgScope }: TInviteUserToOrgDTO) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
|
const samlCfg = await samlConfigDAL.findOne({ orgId });
|
||||||
|
if (samlCfg && samlCfg.isActive) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to invite member due to SAML SSO configured for organization"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (plan.memberLimit !== null && plan.membersUsed >= plan.memberLimit) {
|
||||||
|
// case: limit imposed on number of members allowed
|
||||||
|
// case: number of members used exceeds the number of members allowed
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const invitee = await orgDAL.transaction(async (tx) => {
|
||||||
|
const inviteeUser = await userDAL.findUserByEmail(inviteeEmail, tx);
|
||||||
|
if (inviteeUser) {
|
||||||
|
// if user already exist means its already part of infisical
|
||||||
|
// Thus the signup flow is not needed anymore
|
||||||
|
const [inviteeMembership] = await orgDAL.findMembership({ orgId, userId: inviteeUser.id }, { tx });
|
||||||
|
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to invite an existing member of org",
|
||||||
|
name: "Invite user to org"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inviteeMembership) {
|
||||||
|
await orgDAL.createMembership(
|
||||||
|
{
|
||||||
|
userId: inviteeUser.id,
|
||||||
|
inviteEmail: inviteeEmail,
|
||||||
|
orgId,
|
||||||
|
role: OrgMembershipRole.Member,
|
||||||
|
status: OrgMembershipStatus.Invited
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return inviteeUser;
|
||||||
|
}
|
||||||
|
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
|
||||||
|
if (isEmailInvalid) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Provided a disposable email",
|
||||||
|
name: "Org invite"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// not invited before
|
||||||
|
const user = await userDAL.create(
|
||||||
|
{
|
||||||
|
email: inviteeEmail,
|
||||||
|
isAccepted: false,
|
||||||
|
authMethods: [AuthMethod.EMAIL]
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await orgDAL.createMembership(
|
||||||
|
{
|
||||||
|
inviteEmail: inviteeEmail,
|
||||||
|
orgId,
|
||||||
|
userId: user.id,
|
||||||
|
role: OrgMembershipRole.Member,
|
||||||
|
status: OrgMembershipStatus.Invited
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||||
|
userId: invitee.id,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
const user = await userDAL.findById(userId);
|
||||||
|
const appCfg = getConfig();
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.OrgInvite,
|
||||||
|
subjectLine: "Infisical organization invitation",
|
||||||
|
recipients: [inviteeEmail],
|
||||||
|
substitutions: {
|
||||||
|
inviterFirstName: user.firstName,
|
||||||
|
inviterEmail: user.email,
|
||||||
|
organizationName: org?.name,
|
||||||
|
email: inviteeEmail,
|
||||||
|
organizationId: org?.id.toString(),
|
||||||
|
token,
|
||||||
|
callback_url: `${appCfg.SITE_URL}/signupinvite`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||||
|
if (!appCfg.isSmtpConfigured) {
|
||||||
|
return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organization invitation step 2: Verify that code [code] was sent to email [email] as part of
|
||||||
|
* magic link and issue a temporary signup token for user to complete setting up their account
|
||||||
|
*/
|
||||||
|
const verifyUserToOrg = async ({ orgId, email, code }: TVerifyUserToOrgDTO) => {
|
||||||
|
const user = await userDAL.findUserByEmail(email);
|
||||||
|
if (!user) {
|
||||||
|
throw new BadRequestError({ message: "Invalid request", name: "Verify user to org" });
|
||||||
|
}
|
||||||
|
const [orgMembership] = await orgDAL.findMembership({
|
||||||
|
userId: user.id,
|
||||||
|
status: OrgMembershipStatus.Invited,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
if (!orgMembership)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to find invitation",
|
||||||
|
name: "Verify user to org"
|
||||||
|
});
|
||||||
|
|
||||||
|
await tokenService.validateTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
|
||||||
|
userId: user.id,
|
||||||
|
orgId: orgMembership.orgId,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user.isAccepted) {
|
||||||
|
// this means user has already completed signup process
|
||||||
|
// isAccepted is set true when keys are exchanged
|
||||||
|
await orgDAL.updateMembershipById(orgMembership.id, {
|
||||||
|
orgId,
|
||||||
|
status: OrgMembershipStatus.Accepted
|
||||||
|
});
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||||
|
return { user };
|
||||||
|
}
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
||||||
|
userId: user.id
|
||||||
|
},
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn: appCfg.JWT_SIGNUP_LIFETIME
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { token, user };
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteOrgMembership = async ({ orgId, userId, membershipId, actorOrgScope }: TDeleteOrgMembershipDTO) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Member);
|
||||||
|
|
||||||
|
const membership = await orgDAL.deleteMembershipById(membershipId, orgId);
|
||||||
|
|
||||||
|
await licenseService.updateSubscriptionOrgMemberCount(orgId);
|
||||||
|
return membership;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* CRUD operations of incident contacts
|
||||||
|
* */
|
||||||
|
const findIncidentContacts = async (userId: string, orgId: string, actorOrgScope?: string) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.IncidentAccount);
|
||||||
|
const incidentContacts = await incidentContactDAL.findByOrgId(orgId);
|
||||||
|
return incidentContacts;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createIncidentContact = async (userId: string, orgId: string, email: string, actorOrgScope?: string) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.IncidentAccount);
|
||||||
|
const doesIncidentContactExist = await incidentContactDAL.findOne(orgId, { email });
|
||||||
|
if (doesIncidentContactExist) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Incident contact already exist",
|
||||||
|
name: "Incident contact exist"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const incidentContact = await incidentContactDAL.create(orgId, email);
|
||||||
|
return incidentContact;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteIncidentContact = async (userId: string, orgId: string, id: string, actorOrgScope?: string) => {
|
||||||
|
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.IncidentAccount);
|
||||||
|
|
||||||
|
const incidentContact = await incidentContactDAL.deleteById(id, orgId);
|
||||||
|
return incidentContact;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
findOrganizationById,
|
||||||
|
findAllOrgMembers,
|
||||||
|
findAllOrganizationOfUser,
|
||||||
|
inviteUserToOrganization,
|
||||||
|
verifyUserToOrg,
|
||||||
|
updateOrgName,
|
||||||
|
createOrganization,
|
||||||
|
deleteOrganizationById,
|
||||||
|
deleteOrgMembership,
|
||||||
|
findAllWorkspaces,
|
||||||
|
updateOrgMembership,
|
||||||
|
// incident contacts
|
||||||
|
findIncidentContacts,
|
||||||
|
createIncidentContact,
|
||||||
|
deleteIncidentContact
|
||||||
|
};
|
||||||
|
};
|
||||||
36
backend/src/services/org/org-types.ts
Normal file
36
backend/src/services/org/org-types.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
|
export type TUpdateOrgMembershipDTO = {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
membershipId: string;
|
||||||
|
role: string;
|
||||||
|
actorOrgScope?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteOrgMembershipDTO = {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
membershipId: string;
|
||||||
|
actorOrgScope?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TInviteUserToOrgDTO = {
|
||||||
|
userId: string;
|
||||||
|
orgId: string;
|
||||||
|
actorOrgScope?: string;
|
||||||
|
inviteeEmail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TVerifyUserToOrgDTO = {
|
||||||
|
email: string;
|
||||||
|
orgId: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TFindAllWorkspacesDTO = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgScope?: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
156
backend/src/services/project/project-service.ts
Normal file
156
backend/src/services/project/project-service.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { createSecretBlindIndex } from "@app/lib/crypto";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
|
||||||
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||||
|
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||||
|
import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal";
|
||||||
|
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
|
import { TProjectDALFactory } from "./project-dal";
|
||||||
|
import { TCreateProjectDTO, TDeleteProjectDTO, TGetProjectDTO } from "./project-types";
|
||||||
|
|
||||||
|
export const DEFAULT_PROJECT_ENVS = [
|
||||||
|
{ name: "Development", slug: "dev" },
|
||||||
|
{ name: "Staging", slug: "staging" },
|
||||||
|
{ name: "Production", slug: "prod" }
|
||||||
|
];
|
||||||
|
|
||||||
|
type TProjectServiceFactoryDep = {
|
||||||
|
projectDAL: TProjectDALFactory;
|
||||||
|
folderDAL: Pick<TSecretFolderDALFactory, "insertMany">;
|
||||||
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany">;
|
||||||
|
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "create">;
|
||||||
|
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "create">;
|
||||||
|
permissionService: TPermissionServiceFactory;
|
||||||
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
||||||
|
|
||||||
|
export const projectServiceFactory = ({
|
||||||
|
projectDAL,
|
||||||
|
permissionService,
|
||||||
|
folderDAL,
|
||||||
|
secretBlindIndexDAL,
|
||||||
|
projectMembershipDAL,
|
||||||
|
projectEnvDAL,
|
||||||
|
licenseService
|
||||||
|
}: TProjectServiceFactoryDep) => {
|
||||||
|
/*
|
||||||
|
* Create workspace. Make user the admin
|
||||||
|
* */
|
||||||
|
const createProject = async ({ orgId, actor, actorId, actorOrgScope, workspaceName }: TCreateProjectDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorOrgScope);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const blindIndex = createSecretBlindIndex(appCfg.ROOT_ENCRYPTION_KEY, appCfg.ENCRYPTION_KEY);
|
||||||
|
|
||||||
|
const plan = await licenseService.getPlan(orgId);
|
||||||
|
if (plan.workspaceLimit !== null && plan.workspacesUsed >= plan.workspaceLimit) {
|
||||||
|
// case: limit imposed on number of workspaces allowed
|
||||||
|
// case: number of workspaces used exceeds the number of workspaces allowed
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProject = projectDAL.transaction(async (tx) => {
|
||||||
|
const project = await projectDAL.create(
|
||||||
|
{ name: workspaceName, orgId, slug: slugify(`${workspaceName}-${alphaNumericNanoId(4)}`) },
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
// set user as admin member for proeject
|
||||||
|
await projectMembershipDAL.create(
|
||||||
|
{
|
||||||
|
userId: actorId,
|
||||||
|
role: ProjectMembershipRole.Admin,
|
||||||
|
projectId: project.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
// generate the blind index for project
|
||||||
|
await secretBlindIndexDAL.create(
|
||||||
|
{
|
||||||
|
projectId: project.id,
|
||||||
|
keyEncoding: blindIndex.keyEncoding,
|
||||||
|
saltIV: blindIndex.iv,
|
||||||
|
saltTag: blindIndex.tag,
|
||||||
|
algorithm: blindIndex.algorithm,
|
||||||
|
encryptedSaltCipherText: blindIndex.ciphertext
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
// set default environments and root folder for provided environments
|
||||||
|
const envs = await projectEnvDAL.insertMany(
|
||||||
|
DEFAULT_PROJECT_ENVS.map((el, i) => ({ ...el, projectId: project.id, position: i + 1 })),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
await folderDAL.insertMany(
|
||||||
|
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
// _id for backward compat
|
||||||
|
return { ...project, environments: envs, _id: project.id };
|
||||||
|
});
|
||||||
|
|
||||||
|
return newProject;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteProject = async ({ actor, actorId, projectId }: TDeleteProjectDTO) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
|
||||||
|
|
||||||
|
const deletedProject = await projectDAL.deleteById(projectId);
|
||||||
|
return deletedProject;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjects = async (actorId: string) => {
|
||||||
|
const workspaces = await projectDAL.findAllProjects(actorId);
|
||||||
|
return workspaces;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAProject = async ({ actorId, projectId, actor }: TGetProjectDTO) => {
|
||||||
|
await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
return projectDAL.findProjectById(projectId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAutoCapitalization = async ({
|
||||||
|
projectId,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
autoCapitalization
|
||||||
|
}: TGetProjectDTO & { autoCapitalization: boolean }) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
const updatedProject = await projectDAL.updateById(projectId, { autoCapitalization });
|
||||||
|
return updatedProject;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateName = async ({ projectId, actor, actorId, name }: TGetProjectDTO & { name: string }) => {
|
||||||
|
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
|
||||||
|
|
||||||
|
const updatedProject = await projectDAL.updateById(projectId, { name });
|
||||||
|
return updatedProject;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createProject,
|
||||||
|
deleteProject,
|
||||||
|
getProjects,
|
||||||
|
getAProject,
|
||||||
|
toggleAutoCapitalization,
|
||||||
|
updateName
|
||||||
|
};
|
||||||
|
};
|
||||||
21
backend/src/services/project/project-types.ts
Normal file
21
backend/src/services/project/project-types.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
|
export type TCreateProjectDTO = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorOrgScope?: string;
|
||||||
|
orgId: string;
|
||||||
|
workspaceName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteProjectDTO = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetProjectDTO = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
116
backend/src/services/super-admin/super-admin-service.ts
Normal file
116
backend/src/services/super-admin/super-admin-service.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { TSuperAdmin, TSuperAdminUpdate } from "@app/db/schemas";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TAuthLoginFactory } from "../auth/auth-login-service";
|
||||||
|
import { AuthMethod } from "../auth/auth-type";
|
||||||
|
import { TOrgServiceFactory } from "../org/org-service";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
|
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
||||||
|
import { TAdminSignUpDTO } from "./super-admin-types";
|
||||||
|
|
||||||
|
type TSuperAdminServiceFactoryDep = {
|
||||||
|
serverCfgDAL: TSuperAdminDALFactory;
|
||||||
|
userDAL: TUserDALFactory;
|
||||||
|
authService: Pick<TAuthLoginFactory, "generateUserTokens">;
|
||||||
|
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
|
||||||
|
|
||||||
|
let serverCfg: Readonly<TSuperAdmin>;
|
||||||
|
export const getServerCfg = () => {
|
||||||
|
if (!serverCfg) throw new BadRequestError({ name: "Get server cfg", message: "Server cfg not initialized" });
|
||||||
|
return serverCfg;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const superAdminServiceFactory = ({
|
||||||
|
serverCfgDAL,
|
||||||
|
userDAL,
|
||||||
|
authService,
|
||||||
|
orgService
|
||||||
|
}: TSuperAdminServiceFactoryDep) => {
|
||||||
|
const initServerCfg = async () => {
|
||||||
|
serverCfg = await serverCfgDAL.findOne({});
|
||||||
|
if (!serverCfg) {
|
||||||
|
const newCfg = await serverCfgDAL.create({ initialized: false, allowSignUp: true });
|
||||||
|
serverCfg = newCfg;
|
||||||
|
return newCfg;
|
||||||
|
}
|
||||||
|
return serverCfg;
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateServerCfg = async (data: TSuperAdminUpdate) => {
|
||||||
|
const cfg = await serverCfgDAL.updateById(serverCfg.id, data);
|
||||||
|
serverCfg = cfg;
|
||||||
|
Object.freeze(serverCfg);
|
||||||
|
return cfg;
|
||||||
|
};
|
||||||
|
|
||||||
|
const adminSignUp = async ({
|
||||||
|
lastName,
|
||||||
|
firstName,
|
||||||
|
salt,
|
||||||
|
email,
|
||||||
|
verifier,
|
||||||
|
publicKey,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
ip,
|
||||||
|
userAgent
|
||||||
|
}: TAdminSignUpDTO) => {
|
||||||
|
const existingUser = await userDAL.findOne({ email });
|
||||||
|
if (existingUser) throw new BadRequestError({ name: "Admin sign up", message: "User already exist" });
|
||||||
|
|
||||||
|
const userInfo = await userDAL.transaction(async (tx) => {
|
||||||
|
const newUser = await userDAL.create(
|
||||||
|
{
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
email,
|
||||||
|
superAdmin: true,
|
||||||
|
isAccepted: true,
|
||||||
|
authMethods: [AuthMethod.EMAIL]
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
const userEnc = await userDAL.createUserEncryption(
|
||||||
|
{
|
||||||
|
salt,
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
iv: encryptedPrivateKeyIV,
|
||||||
|
tag: encryptedPrivateKeyTag,
|
||||||
|
verifier,
|
||||||
|
userId: newUser.id
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
return { user: newUser, enc: userEnc };
|
||||||
|
});
|
||||||
|
await orgService.createOrganization(userInfo.user.id, userInfo.user.email, "Admin Org");
|
||||||
|
|
||||||
|
await updateServerCfg({ initialized: true });
|
||||||
|
const token = await authService.generateUserTokens({
|
||||||
|
user: userInfo.user,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
organizationId: undefined
|
||||||
|
});
|
||||||
|
// TODO(akhilmhdh-pg): telemetry service
|
||||||
|
return { token, user: userInfo };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
initServerCfg,
|
||||||
|
updateServerCfg,
|
||||||
|
adminSignUp
|
||||||
|
};
|
||||||
|
};
|
||||||
88
frontend/src/components/signup/InitialSignupStep.tsx
Normal file
88
frontend/src/components/signup/InitialSignupStep.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button } from "../v2";
|
||||||
|
|
||||||
|
export default function InitialSignupStep({
|
||||||
|
setIsSignupWithEmail
|
||||||
|
}: {
|
||||||
|
setIsSignupWithEmail: (value: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full flex-col items-center justify-center">
|
||||||
|
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||||
|
{t("signup.initial-title")}
|
||||||
|
</h1>
|
||||||
|
<div className="w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="solid"
|
||||||
|
onClick={() => {
|
||||||
|
window.open("/api/v1/sso/redirect/google");
|
||||||
|
window.close();
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
|
||||||
|
className="mx-0 h-12 w-full"
|
||||||
|
>
|
||||||
|
{t("signup.continue-with-google")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
window.open("/api/v1/sso/redirect/github");
|
||||||
|
window.close();
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
|
||||||
|
className="mx-0 h-12 w-full"
|
||||||
|
>
|
||||||
|
Continue with GitHub
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md lg:w-1/6">
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
window.open("/api/v1/sso/redirect/gitlab");
|
||||||
|
window.close();
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
|
||||||
|
className="mx-0 h-12 w-full"
|
||||||
|
>
|
||||||
|
Continue with GitLab
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 w-1/4 min-w-[20rem] rounded-md text-center lg:w-1/6">
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
setIsSignupWithEmail(true);
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faEnvelope} className="mr-2" />}
|
||||||
|
className="mx-0 h-12 w-full"
|
||||||
|
>
|
||||||
|
Continue with Email
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 w-1/4 min-w-[20rem] px-8 text-center text-xs text-bunker-400 lg:w-1/6">
|
||||||
|
{t("signup.create-policy")}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex flex-row text-xs text-bunker-400">
|
||||||
|
<Link href="/login">
|
||||||
|
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||||
|
{t("signup.already-have-account")}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
249
frontend/src/views/Login/components/InitialStep/InitialStep.tsx
Normal file
249
frontend/src/views/Login/components/InitialStep/InitialStep.tsx
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import { FormEvent, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
|
||||||
|
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
import Error from "@app/components/basic/Error";
|
||||||
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
|
import attemptCliLogin from "@app/components/utilities/attemptCliLogin";
|
||||||
|
import attemptLogin from "@app/components/utilities/attemptLogin";
|
||||||
|
import { Button, Input } from "@app/components/v2";
|
||||||
|
import { useServerConfig } from "@app/context";
|
||||||
|
|
||||||
|
import { navigateUserToOrg } from "../../Login.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setStep: (step: number) => void;
|
||||||
|
email: string;
|
||||||
|
setEmail: (email: string) => void;
|
||||||
|
password: string;
|
||||||
|
setPassword: (email: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: Props) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [loginError, setLoginError] = useState(false);
|
||||||
|
const { config } = useServerConfig();
|
||||||
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
const handleLogin = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
try {
|
||||||
|
if (!email || !password) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
if (queryParams && queryParams.get("callback_port")) {
|
||||||
|
const callbackPort = queryParams.get("callback_port");
|
||||||
|
|
||||||
|
// attemptCliLogin
|
||||||
|
const isCliLoginSuccessful = await attemptCliLogin({
|
||||||
|
email: email.toLowerCase(),
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isCliLoginSuccessful && isCliLoginSuccessful.success) {
|
||||||
|
if (isCliLoginSuccessful.mfaEnabled) {
|
||||||
|
// case: login requires MFA step
|
||||||
|
setStep(1);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// case: login was successful
|
||||||
|
const cliUrl = `http://127.0.0.1:${callbackPort}/`;
|
||||||
|
|
||||||
|
// send request to server endpoint
|
||||||
|
const instance = axios.create();
|
||||||
|
await instance.post(cliUrl, { ...isCliLoginSuccessful.loginResponse });
|
||||||
|
|
||||||
|
// cli page
|
||||||
|
router.push("/cli-redirect");
|
||||||
|
|
||||||
|
// on success, router.push to cli Login Successful page
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const isLoginSuccessful = await attemptLogin({
|
||||||
|
email: email.toLowerCase(),
|
||||||
|
password
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoginSuccessful && isLoginSuccessful.success) {
|
||||||
|
// case: login was successful
|
||||||
|
|
||||||
|
if (isLoginSuccessful.mfaEnabled) {
|
||||||
|
// case: login requires MFA step
|
||||||
|
setStep(1);
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigateUserToOrg(router);
|
||||||
|
|
||||||
|
// case: login does not require MFA step
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully logged in",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setLoginError(true);
|
||||||
|
createNotification({
|
||||||
|
text: "Login unsuccessful. Double-check your credentials and try again.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleLogin}
|
||||||
|
className="mx-auto flex w-full flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
<h1 className="mb-8 bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
|
||||||
|
Login to Infisical
|
||||||
|
</h1>
|
||||||
|
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
const callbackPort = queryParams.get("callback_port");
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
`/api/v1/sso/redirect/google${callbackPort ? `?callback_port=${callbackPort}` : ""}`
|
||||||
|
);
|
||||||
|
window.close();
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faGoogle} className="mr-2" />}
|
||||||
|
className="mx-0 h-10 w-full"
|
||||||
|
>
|
||||||
|
{t("login.continue-with-google")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
const callbackPort = queryParams.get("callback_port");
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
`/api/v1/sso/redirect/github${callbackPort ? `?callback_port=${callbackPort}` : ""}`
|
||||||
|
);
|
||||||
|
|
||||||
|
window.close();
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faGithub} className="mr-2" />}
|
||||||
|
className="mx-0 h-10 w-full"
|
||||||
|
>
|
||||||
|
Continue with GitHub
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
const callbackPort = queryParams.get("callback_port");
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
`/api/v1/sso/redirect/gitlab${callbackPort ? `?callback_port=${callbackPort}` : ""}`
|
||||||
|
);
|
||||||
|
|
||||||
|
window.close();
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faGitlab} className="mr-2" />}
|
||||||
|
className="mx-0 h-10 w-full"
|
||||||
|
>
|
||||||
|
Continue with GitLab
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
|
setStep(2);
|
||||||
|
}}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
|
||||||
|
className="mx-0 h-10 w-full"
|
||||||
|
>
|
||||||
|
Continue with SAML SSO
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">
|
||||||
|
<div className="w-full border-t border-mineshaft-400/60" />
|
||||||
|
<span className="mx-2 text-xs text-mineshaft-200">or</span>
|
||||||
|
<div className="w-full border-t border-mineshaft-400/60" />
|
||||||
|
</div>
|
||||||
|
<div className="w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||||
|
<Input
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
type="email"
|
||||||
|
placeholder="Enter your email..."
|
||||||
|
isRequired
|
||||||
|
autoComplete="username"
|
||||||
|
className="h-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||||
|
<Input
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password..."
|
||||||
|
isRequired
|
||||||
|
autoComplete="current-password"
|
||||||
|
id="current-password"
|
||||||
|
className="select:-webkit-autofill:focus h-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="sm"
|
||||||
|
isFullWidth
|
||||||
|
className="h-10"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="solid"
|
||||||
|
isLoading={isLoading}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
Continue with Email{" "}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{!isLoading && loginError && <Error text={t("login.error-login") ?? ""} />}
|
||||||
|
{config.allowSignUp ? (
|
||||||
|
<div className="mt-6 flex flex-row text-sm text-bunker-400">
|
||||||
|
<Link href="/signup">
|
||||||
|
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||||
|
Don't have an acount yet? {t("login.create-account")}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
|
<div className="mt-2 flex flex-row text-sm text-bunker-400">
|
||||||
|
<Link href="/verify-email">
|
||||||
|
<span className="cursor-pointer duration-200 hover:text-bunker-200 hover:underline hover:decoration-primary-700 hover:underline-offset-4">
|
||||||
|
Forgot password? Recover your account
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { Button, Input } from "@app/components/v2";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
setStep: (step: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SAMLSSOStep = ({
|
||||||
|
setStep
|
||||||
|
}: Props) => {
|
||||||
|
const [ssoIdentifier, setSSOIdentifier] = useState("");
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const queryParams = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
const handleSubmission = (e:React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const callbackPort = queryParams.get("callback_port");
|
||||||
|
window.open(`/api/v1/sso/redirect/saml2/organizations/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-md md:px-6">
|
||||||
|
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
|
||||||
|
What's your organization slug?
|
||||||
|
</p>
|
||||||
|
<form onSubmit={handleSubmission}>
|
||||||
|
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
|
||||||
|
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
|
||||||
|
<Input
|
||||||
|
value={ssoIdentifier}
|
||||||
|
onChange={(e) => setSSOIdentifier(e.target.value)}
|
||||||
|
type="text"
|
||||||
|
placeholder="acme-123"
|
||||||
|
isRequired
|
||||||
|
autoComplete="email"
|
||||||
|
id="email"
|
||||||
|
className="h-12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[20rem] md:min-w-[22rem] text-center rounded-md mt-4'>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
isFullWidth
|
||||||
|
className="h-14"
|
||||||
|
>
|
||||||
|
Continue with SAML
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div className="flex flex-row items-center justify-center mt-4">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setStep(0);
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
|
className="text-bunker-300 text-sm hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
|
||||||
|
>
|
||||||
|
{t("login.other-option")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user