Add org-scoped auth to org-level endpoints, add migration file for org enableAuth field

This commit is contained in:
Tuan Dang
2024-02-04 14:44:08 -08:00
parent c8633bf546
commit f138973ac7
47 changed files with 18779 additions and 15932 deletions

26839
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,75 +1,127 @@
{
"dependencies": {
"@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",
"name": "backend",
"version": "1.0.0",
"main": "src/index.js",
"description": "",
"main": "index.js",
"scripts": {
"start": "npm run build && node build/index.js",
"dev": "nodemon",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./src/json ./build",
"lint": "eslint . --ext .ts",
"lint-and-fix": "eslint . --ext .ts --fix",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
"lint-staged": "lint-staged"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Infisical/infisical-api.git"
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine",
"dev:docker": "nodemon",
"build": "rimraf dist && tsup && cp -R ./src/lib/validator/disposable_emails.txt ./dist && cp -R ./src/services/smtp/templates ./dist",
"start": "node dist/main.mjs",
"type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'",
"test:e2e": "vitest run -c vitest.e2e.config.ts",
"test:e2e-watch": "vitest -c vitest.e2e.config.ts",
"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": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/Infisical/infisical-api/issues"
},
"homepage": "https://github.com/Infisical/infisical-api#readme",
"description": "",
"devDependencies": {
"@posthog/plugin-scaffold": "^1.3.4",
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/jsonwebtoken": "^8.5.9",
"@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6",
"@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3",
"@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1",
"eslint": "^8.26.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"install": "^0.13.0",
"jest": "^29.3.1",
"nodemon": "^2.0.19",
"npm": "^8.19.3",
"prettier": "^2.7.1",
"ts-node": "^10.9.1"
"@types/bcrypt": "^5.0.2",
"@types/jmespath": "^0.15.2",
"@types/jsonwebtoken": "^9.0.5",
"@types/jsrp": "^0.2.6",
"@types/libsodium-wrappers": "^0.7.13",
"@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.9.5",
"@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3",
"@types/prompt-sync": "^4.2.3",
"@types/uuid": "^9.0.7",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-simple-import-sort": "^10.0.0",
"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
View 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">;
};
}
}

View 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");
});
}

View 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>>;

View 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;
}
});
};

View 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 };
}
});
};

View 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;
}
});
};

View 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;
}
});
};

View 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 };
};

View 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
};
};

View 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;

View 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
};
};

View 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
};
};

View 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
};
};

View 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;
};

View File

@@ -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
};
};

View 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>>;

View 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" });
}
});
});

View 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 };
}
});
});

View 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" });
};

View 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 };
}
});
};

View 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 };
}
});
};

View 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 };
}
});
};

View 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
};
}
});
};

View 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 };
}
});
};

View 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 };
}
});
};

View 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 };
}
});
};

View 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
};
}
});
};

View 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 };
}
});
};

View 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
};
};

View 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();
};

View 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
};
};

View 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;
};

View 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;
};

View 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
};
};

View 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
};
};

View 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
});
};

View 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 };
};

View 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
};
};

View 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;
};

View 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
};
};

View 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;
};

View 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
};
};

View 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>
);
}

View 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&apos;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>
);
};

View File

@@ -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&apos;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>
);
}