Merge remote-tracking branch 'origin/main' into misc/operator-namespace-installation

This commit is contained in:
Sheen Capadngan
2024-12-13 03:49:10 +08:00
166 changed files with 3949 additions and 1830 deletions
+1 -10
View File
@@ -14,15 +14,6 @@
<a href="https://infisical.com/careers">Hiring (Remote/SF)</a> <a href="https://infisical.com/careers">Hiring (Remote/SF)</a>
</h4> </h4>
<p align="center">
<a href="https://infisical.com/docs/self-hosting/deployment-options/aws-ec2">
<img src=".github/images/deploy-to-aws.png" width="137" />
</a>
<a href="https://infisical.com/docs/self-hosting/deployment-options/digital-ocean-marketplace" alt="Deploy to DigitalOcean">
<img width="200" alt="Deploy to DO" src="https://www.deploytodo.com/do-btn-blue.svg"/>
</a>
</p>
<h4 align="center"> <h4 align="center">
<a href="https://github.com/Infisical/infisical/blob/main/LICENSE"> <a href="https://github.com/Infisical/infisical/blob/main/LICENSE">
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Infisical is released under the MIT license." /> <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="Infisical is released under the MIT license." />
@@ -75,7 +66,7 @@ We're on a mission to make security tooling more accessible to everyone, not jus
### Key Management (KMS): ### Key Management (KMS):
- **[Cryptograhic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API. - **[Cryptographic Keys](https://infisical.com/docs/documentation/platform/kms)**: Centrally manage keys across projects through a user-friendly interface or via the API.
- **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data. - **[Encrypt and Decrypt Data](https://infisical.com/docs/documentation/platform/kms#guide-to-encrypting-data)**: Use symmetric keys to encrypt and decrypt data.
### General Platform: ### General Platform:
+2 -2
View File
@@ -53,13 +53,13 @@ export default {
extension: "ts" extension: "ts"
}); });
const smtp = mockSmtpServer(); const smtp = mockSmtpServer();
const queue = queueServiceFactory(cfg.REDIS_URL, cfg.DB_CONNECTION_URI); const queue = queueServiceFactory(cfg.REDIS_URL, { dbConnectionUrl: cfg.DB_CONNECTION_URI });
const keyStore = keyStoreFactory(cfg.REDIS_URL); const keyStore = keyStoreFactory(cfg.REDIS_URL);
const hsmModule = initializeHsmModule(); const hsmModule = initializeHsmModule();
hsmModule.initialize(); hsmModule.initialize();
const server = await main({ db, smtp, logger, queue, keyStore, hsmModule: hsmModule.getModule() }); const server = await main({ db, smtp, logger, queue, keyStore, hsmModule: hsmModule.getModule(), redis });
// @ts-expect-error type // @ts-expect-error type
globalThis.testServer = server; globalThis.testServer = server;
-29
View File
@@ -49,7 +49,6 @@
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.1", "@slack/oauth": "^3.0.1",
"@slack/web-api": "^7.3.4", "@slack/web-api": "^7.3.4",
"@team-plain/typescript-sdk": "^4.6.1",
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.31.2", "argon2": "^0.31.2",
@@ -5678,14 +5677,6 @@
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }
}, },
"node_modules/@graphql-typed-document-node/core": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz",
"integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==",
"peerDependencies": {
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/@grpc/grpc-js": { "node_modules/@grpc/grpc-js": {
"version": "1.12.2", "version": "1.12.2",
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz",
@@ -9970,18 +9961,6 @@
"optional": true, "optional": true,
"peer": true "peer": true
}, },
"node_modules/@team-plain/typescript-sdk": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/@team-plain/typescript-sdk/-/typescript-sdk-4.6.1.tgz",
"integrity": "sha512-Uy9QJXu9U7bJb6WXL9sArGk7FXPpzdqBd6q8tAF1vexTm8fbTJRqcikTKxGtZmNADt+C2SapH3cApM4oHpO4lQ==",
"dependencies": {
"@graphql-typed-document-node/core": "^3.2.0",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"graphql": "^16.6.0",
"zod": "3.22.4"
}
},
"node_modules/@techteamer/ocsp": { "node_modules/@techteamer/ocsp": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz",
@@ -15180,14 +15159,6 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true "dev": true
}, },
"node_modules/graphql": {
"version": "16.9.0",
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
"integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==",
"engines": {
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
}
},
"node_modules/gtoken": { "node_modules/gtoken": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
-1
View File
@@ -157,7 +157,6 @@
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@slack/oauth": "^3.0.1", "@slack/oauth": "^3.0.1",
"@slack/web-api": "^7.3.4", "@slack/web-api": "^7.3.4",
"@team-plain/typescript-sdk": "^4.6.1",
"@ucast/mongo2js": "^1.3.4", "@ucast/mongo2js": "^1.3.4",
"ajv": "^8.12.0", "ajv": "^8.12.0",
"argon2": "^0.31.2", "argon2": "^0.31.2",
+1 -1
View File
@@ -2,6 +2,6 @@ import "@fastify/request-context";
declare module "@fastify/request-context" { declare module "@fastify/request-context" {
interface RequestContextData { interface RequestContextData {
requestId: string; reqId: string;
} }
} }
+7
View File
@@ -1,5 +1,7 @@
import "fastify"; import "fastify";
import { Redis } from "ioredis";
import { TUsers } from "@app/db/schemas"; import { TUsers } from "@app/db/schemas";
import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service"; import { TAccessApprovalPolicyServiceFactory } from "@app/ee/services/access-approval-policy/access-approval-policy-service";
import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service"; import { TAccessApprovalRequestServiceFactory } from "@app/ee/services/access-approval-request/access-approval-request-service";
@@ -87,6 +89,10 @@ import { TWebhookServiceFactory } from "@app/services/webhook/webhook-service";
import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service"; import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integration/workflow-integration-service";
declare module "fastify" { declare module "fastify" {
interface Session {
callbackPort: string;
}
interface FastifyRequest { interface FastifyRequest {
realIp: string; realIp: string;
// used for mfa session authentication // used for mfa session authentication
@@ -115,6 +121,7 @@ declare module "fastify" {
} }
interface FastifyInstance { interface FastifyInstance {
redis: Redis;
services: { services: {
login: TAuthLoginFactory; login: TAuthLoginFactory;
password: TAuthPasswordFactory; password: TAuthPasswordFactory;
@@ -0,0 +1,59 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
const hasAccessApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn(
TableName.AccessApprovalPolicy,
"deletedAt"
);
const hasSecretApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn(
TableName.SecretApprovalPolicy,
"deletedAt"
);
if (!hasAccessApprovalPolicyDeletedAtColumn) {
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
t.timestamp("deletedAt");
});
}
if (!hasSecretApprovalPolicyDeletedAtColumn) {
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
t.timestamp("deletedAt");
});
}
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
t.dropForeign(["privilegeId"]);
// Add the new foreign key constraint with ON DELETE SET NULL
t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege).onDelete("SET NULL");
});
}
export async function down(knex: Knex): Promise<void> {
const hasAccessApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn(
TableName.AccessApprovalPolicy,
"deletedAt"
);
const hasSecretApprovalPolicyDeletedAtColumn = await knex.schema.hasColumn(
TableName.SecretApprovalPolicy,
"deletedAt"
);
if (hasAccessApprovalPolicyDeletedAtColumn) {
await knex.schema.alterTable(TableName.AccessApprovalPolicy, (t) => {
t.dropColumn("deletedAt");
});
}
if (hasSecretApprovalPolicyDeletedAtColumn) {
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
t.dropColumn("deletedAt");
});
}
await knex.schema.alterTable(TableName.AccessApprovalRequest, (t) => {
t.dropForeign(["privilegeId"]);
t.foreign("privilegeId").references("id").inTable(TableName.ProjectUserAdditionalPrivilege).onDelete("CASCADE");
});
}
@@ -15,7 +15,8 @@ export const AccessApprovalPoliciesSchema = z.object({
envId: z.string().uuid(), envId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
enforcementLevel: z.string().default("hard") enforcementLevel: z.string().default("hard"),
deletedAt: z.date().nullable().optional()
}); });
export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>; export type TAccessApprovalPolicies = z.infer<typeof AccessApprovalPoliciesSchema>;
@@ -15,7 +15,8 @@ export const SecretApprovalPoliciesSchema = z.object({
envId: z.string().uuid(), envId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
enforcementLevel: z.string().default("hard") enforcementLevel: z.string().default("hard"),
deletedAt: z.date().nullable().optional()
}); });
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>; export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
@@ -109,7 +109,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
approvers: z.string().array(), approvers: z.string().array(),
secretPath: z.string().nullish(), secretPath: z.string().nullish(),
envId: z.string(), envId: z.string(),
enforcementLevel: z.string() enforcementLevel: z.string(),
deletedAt: z.date().nullish()
}), }),
reviewers: z reviewers: z
.object({ .object({
@@ -1,4 +1,3 @@
import slugify from "@sindresorhus/slugify";
import ms from "ms"; import ms from "ms";
import { z } from "zod"; import { z } from "zod";
@@ -8,6 +7,7 @@ import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates"; import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -48,15 +48,7 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
.nullable(), .nullable(),
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash), path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1), environmentSlug: z.string().describe(DYNAMIC_SECRETS.CREATE.environmentSlug).min(1),
name: z name: slugSchema({ min: 1, max: 64, field: "Name" }).describe(DYNAMIC_SECRETS.CREATE.name)
.string()
.describe(DYNAMIC_SECRETS.CREATE.name)
.min(1)
.toLowerCase()
.max(64)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
}), }),
response: { response: {
200: z.object({ 200: z.object({
+8 -20
View File
@@ -1,8 +1,9 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas"; import { GroupsSchema, OrgMembershipRole, UsersSchema } from "@app/db/schemas";
import { EFilterReturnedUsers } from "@app/ee/services/group/group-types";
import { GROUPS } from "@app/lib/api-docs"; import { GROUPS } from "@app/lib/api-docs";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -14,15 +15,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
body: z.object({ body: z.object({
name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name), name: z.string().trim().min(1).max(50).describe(GROUPS.CREATE.name),
slug: z slug: slugSchema({ min: 5, max: 36 }).optional().describe(GROUPS.CREATE.slug),
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(GROUPS.CREATE.slug),
role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role) role: z.string().trim().min(1).default(OrgMembershipRole.NoAccess).describe(GROUPS.CREATE.role)
}), }),
response: { response: {
@@ -100,14 +93,7 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
body: z body: z
.object({ .object({
name: z.string().trim().min(1).describe(GROUPS.UPDATE.name), name: z.string().trim().min(1).describe(GROUPS.UPDATE.name),
slug: z slug: slugSchema({ min: 5, max: 36 }).describe(GROUPS.UPDATE.slug),
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(GROUPS.UPDATE.slug),
role: z.string().trim().min(1).describe(GROUPS.UPDATE.role) role: z.string().trim().min(1).describe(GROUPS.UPDATE.role)
}) })
.partial(), .partial(),
@@ -166,7 +152,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset), offset: z.coerce.number().min(0).max(100).default(0).describe(GROUPS.LIST_USERS.offset),
limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit), limit: z.coerce.number().min(1).max(100).default(10).describe(GROUPS.LIST_USERS.limit),
username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username), username: z.string().trim().optional().describe(GROUPS.LIST_USERS.username),
search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search) search: z.string().trim().optional().describe(GROUPS.LIST_USERS.search),
filter: z.nativeEnum(EFilterReturnedUsers).optional().describe(GROUPS.LIST_USERS.filterUsers)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -179,7 +166,8 @@ export const registerGroupRouter = async (server: FastifyZodProvider) => {
}) })
.merge( .merge(
z.object({ z.object({
isPartOfGroup: z.boolean() isPartOfGroup: z.boolean(),
joinedGroupAt: z.date().nullable()
}) })
) )
.array(), .array(),
@@ -8,6 +8,7 @@ import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { UnauthorizedError } from "@app/lib/errors"; import { UnauthorizedError } from "@app/lib/errors";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { import {
ProjectPermissionSchema, ProjectPermissionSchema,
@@ -33,17 +34,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
body: z.object({ body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId), identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug), projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
slug: z slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionSchema.array() permissions: ProjectPermissionSchema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions) .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.optional(), .optional(),
@@ -77,7 +68,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
...req.body, ...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), slug: req.body.slug ?? slugify(alphaNumericNanoId(12)),
isTemporary: false, isTemporary: false,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts // @ts-ignore-error this is valid ts
@@ -103,17 +94,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
body: z.object({ body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId), identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId),
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug), projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug),
slug: z slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionSchema.array() permissions: ProjectPermissionSchema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions) .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.optional(), .optional(),
@@ -159,7 +140,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
...req.body, ...req.body,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)), slug: req.body.slug ?? slugify(alphaNumericNanoId(12)),
isTemporary: true, isTemporary: true,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts // @ts-ignore-error this is valid ts
@@ -189,16 +170,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug), projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug),
privilegeDetails: z privilegeDetails: z
.object({ .object({
slug: z slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions), permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe( privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission
+1 -3
View File
@@ -9,7 +9,6 @@
import { Authenticator, Strategy } from "@fastify/passport"; import { Authenticator, Strategy } from "@fastify/passport";
import fastifySession from "@fastify/session"; import fastifySession from "@fastify/session";
import RedisStore from "connect-redis"; import RedisStore from "connect-redis";
import { Redis } from "ioredis";
import { z } from "zod"; import { z } from "zod";
import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs"; import { OidcConfigsSchema } from "@app/db/schemas/oidc-configs";
@@ -21,7 +20,6 @@ import { AuthMode } from "@app/services/auth/auth-type";
export const registerOidcRouter = async (server: FastifyZodProvider) => { export const registerOidcRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig(); const appCfg = getConfig();
const redis = new Redis(appCfg.REDIS_URL);
const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" }); const passport = new Authenticator({ key: "oidc", userProperty: "passportUser" });
/* /*
@@ -30,7 +28,7 @@ export const registerOidcRouter = async (server: FastifyZodProvider) => {
- Fastify session <> Redis structure is based on the ff: https://github.com/fastify/session/blob/master/examples/redis.js - Fastify session <> Redis structure is based on the ff: https://github.com/fastify/session/blob/master/examples/redis.js
*/ */
const redisStore = new RedisStore({ const redisStore = new RedisStore({
client: redis, client: server.redis,
prefix: "oidc-session:", prefix: "oidc-session:",
ttl: 600 // 10 minutes ttl: 600 // 10 minutes
}); });
+7 -18
View File
@@ -1,8 +1,8 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -18,17 +18,10 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
organizationId: z.string().trim() organizationId: z.string().trim()
}), }),
body: z.object({ body: z.object({
slug: z slug: slugSchema({ min: 1, max: 64 }).refine(
.string()
.min(1)
.trim()
.refine(
(val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole), (val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
"Please choose a different slug, the slug you have entered is reserved" "Please choose a different slug, the slug you have entered is reserved"
) ),
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
}),
name: z.string().trim(), name: z.string().trim(),
description: z.string().trim().optional(), description: z.string().trim().optional(),
permissions: z.any().array() permissions: z.any().array()
@@ -94,17 +87,13 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
roleId: z.string().trim() roleId: z.string().trim()
}), }),
body: z.object({ body: z.object({
slug: z // TODO: Switch to slugSchema after verifying correct methods with Akhil - Omar 11/24
.string() slug: slugSchema({ min: 1, max: 64 })
.trim()
.optional()
.refine( .refine(
(val) => typeof val !== "undefined" && !Object.keys(OrgMembershipRole).includes(val), (val) => !Object.keys(OrgMembershipRole).includes(val),
"Please choose a different slug, the slug you have entered is reserved." "Please choose a different slug, the slug you have entered is reserved."
) )
.refine((val) => typeof val === "undefined" || slugify(val) === val, { .optional(),
message: "Slug must be a valid"
}),
name: z.string().trim().optional(), name: z.string().trim().optional(),
description: z.string().trim().optional(), description: z.string().trim().optional(),
permissions: z.any().array().optional() permissions: z.any().array().optional()
@@ -1,5 +1,4 @@
import { packRules } from "@casl/ability/extra"; import { packRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
@@ -9,6 +8,7 @@ import {
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs"; import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas"; import { SanitizedRoleSchemaV1 } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -32,18 +32,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug) projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
}), }),
body: z.object({ body: z.object({
slug: z slug: slugSchema({ max: 64 })
.string()
.toLowerCase()
.trim()
.min(1)
.refine( .refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved" "Please choose a different slug, the slug you have entered is reserved"
) )
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
.describe(PROJECT_ROLE.CREATE.slug), .describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description), description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
@@ -94,21 +87,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId) roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
}), }),
body: z.object({ body: z.object({
slug: z slug: slugSchema({ max: 64 })
.string()
.toLowerCase()
.trim()
.optional()
.describe(PROJECT_ROLE.UPDATE.slug)
.refine( .refine(
(val) => (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
typeof val === "undefined" ||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved" "Please choose a different slug, the slug you have entered is reserved"
) )
.refine((val) => typeof val === "undefined" || slugify(val) === val, { .describe(PROJECT_ROLE.UPDATE.slug)
message: "Slug must be a valid" .optional(),
}),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description), description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() permissions: ProjectPermissionV1Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
@@ -1,4 +1,3 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
@@ -8,22 +7,13 @@ import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-tem
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns"; import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import { ProjectTemplates } from "@app/lib/api-docs"; import { ProjectTemplates } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission"; import { UnpackedPermissionSchema } from "@app/server/routes/santizedSchemas/permission";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
const MAX_JSON_SIZE_LIMIT_IN_BYTES = 32_768; const MAX_JSON_SIZE_LIMIT_IN_BYTES = 32_768;
const SlugSchema = z
.string()
.trim()
.min(1)
.max(32)
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Must be valid slug format"
});
const isReservedRoleSlug = (slug: string) => const isReservedRoleSlug = (slug: string) =>
Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
@@ -34,14 +24,14 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
roles: z roles: z
.object({ .object({
name: z.string().trim().min(1), name: z.string().trim().min(1),
slug: SlugSchema, slug: slugSchema(),
permissions: UnpackedPermissionSchema.array() permissions: UnpackedPermissionSchema.array()
}) })
.array(), .array(),
environments: z environments: z
.object({ .object({
name: z.string().trim().min(1), name: z.string().trim().min(1),
slug: SlugSchema, slug: slugSchema(),
position: z.number().min(1) position: z.number().min(1)
}) })
.array() .array()
@@ -50,7 +40,7 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
const ProjectTemplateRolesSchema = z const ProjectTemplateRolesSchema = z
.object({ .object({
name: z.string().trim().min(1), name: z.string().trim().min(1),
slug: SlugSchema, slug: slugSchema(),
permissions: ProjectPermissionV2Schema.array() permissions: ProjectPermissionV2Schema.array()
}) })
.array() .array()
@@ -78,7 +68,7 @@ const ProjectTemplateRolesSchema = z
const ProjectTemplateEnvironmentsSchema = z const ProjectTemplateEnvironmentsSchema = z
.object({ .object({
name: z.string().trim().min(1), name: z.string().trim().min(1),
slug: SlugSchema, slug: slugSchema(),
position: z.number().min(1) position: z.number().min(1)
}) })
.array() .array()
@@ -188,9 +178,11 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
schema: { schema: {
description: "Create a project template.", description: "Create a project template.",
body: z.object({ body: z.object({
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), { name: slugSchema({ field: "name" })
.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.` message: `The requested project template name is reserved.`
}).describe(ProjectTemplates.CREATE.name), })
.describe(ProjectTemplates.CREATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description), description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles), roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe( environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
@@ -230,7 +222,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
description: "Update a project template.", description: "Update a project template.",
params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }), params: z.object({ templateId: z.string().uuid().describe(ProjectTemplates.UPDATE.templateId) }),
body: z.object({ body: z.object({
name: SlugSchema.refine((val) => !isInfisicalProjectTemplate(val), { name: slugSchema({ field: "name" })
.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.` message: `The requested project template name is reserved.`
}) })
.optional() .optional()
@@ -52,7 +52,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
}) })
.array(), .array(),
secretPath: z.string().optional().nullable(), secretPath: z.string().optional().nullable(),
enforcementLevel: z.string() enforcementLevel: z.string(),
deletedAt: z.date().nullish()
}), }),
committerUser: approvalRequestUser, committerUser: approvalRequestUser,
commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(), commits: z.object({ op: z.string(), secretId: z.string().nullable().optional() }).array(),
@@ -260,7 +261,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
approvals: z.number(), approvals: z.number(),
approvers: approvalRequestUser.array(), approvers: approvalRequestUser.array(),
secretPath: z.string().optional().nullable(), secretPath: z.string().optional().nullable(),
enforcementLevel: z.string() enforcementLevel: z.string(),
deletedAt: z.date().nullish()
}), }),
environment: z.string(), environment: z.string(),
statusChangedByUser: approvalRequestUser.optional(), statusChangedByUser: approvalRequestUser.optional(),
@@ -7,6 +7,7 @@ import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/pr
import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/user-additional-privilege"; import { SanitizedUserProjectAdditionalPrivilegeSchema } from "@app/server/routes/santizedSchemas/user-additional-privilege";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -21,17 +22,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
schema: { schema: {
body: z.object({ body: z.object({
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
slug: z slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
.string()
.min(1)
.max(60)
.trim()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ z.object({
@@ -87,15 +78,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
}), }),
body: z body: z
.object({ .object({
slug: z slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
.string()
.max(60)
.trim()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array() permissions: ProjectPermissionV2Schema.array()
.optional() .optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
@@ -7,6 +7,7 @@ import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-p
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege"; import { SanitizedIdentityPrivilegeSchema } from "@app/server/routes/santizedSchemas/identitiy-additional-privilege";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -28,17 +29,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
body: z.object({ body: z.object({
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId), identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId), projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
slug: z slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission), permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ z.object({
@@ -100,16 +91,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.id) id: z.string().trim().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.id)
}), }),
body: z.object({ body: z.object({
slug: z slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
.string()
.min(1)
.max(60)
.trim()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array() permissions: ProjectPermissionV2Schema.array()
.optional() .optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission), .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission),
@@ -1,11 +1,11 @@
import { packRules } from "@casl/ability/extra"; import { packRules } from "@casl/ability/extra";
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs"; import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -29,18 +29,11 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId) projectId: z.string().trim().describe(PROJECT_ROLE.CREATE.projectId)
}), }),
body: z.object({ body: z.object({
slug: z slug: slugSchema({ min: 1, max: 64 })
.string()
.toLowerCase()
.trim()
.min(1)
.refine( .refine(
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole), (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved" "Please choose a different slug, the slug you have entered is reserved"
) )
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid"
})
.describe(PROJECT_ROLE.CREATE.slug), .describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description), description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
@@ -90,21 +83,13 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId) roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
}), }),
body: z.object({ body: z.object({
slug: z slug: slugSchema({ min: 1, max: 64 })
.string()
.toLowerCase()
.trim()
.optional()
.describe(PROJECT_ROLE.UPDATE.slug)
.refine( .refine(
(val) => (val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
typeof val === "undefined" ||
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
"Please choose a different slug, the slug you have entered is reserved" "Please choose a different slug, the slug you have entered is reserved"
) )
.refine((val) => typeof val === "undefined" || slugify(val) === val, { .optional()
message: "Slug must be a valid" .describe(PROJECT_ROLE.UPDATE.slug),
}),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description), description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
@@ -139,5 +139,10 @@ export const accessApprovalPolicyDALFactory = (db: TDbClient) => {
} }
}; };
return { ...accessApprovalPolicyOrm, find, findById }; const softDeleteById = async (policyId: string, tx?: Knex) => {
const softDeletedPolicy = await accessApprovalPolicyOrm.updateById(policyId, { deletedAt: new Date() }, tx);
return softDeletedPolicy;
};
return { ...accessApprovalPolicyOrm, find, findById, softDeleteById };
}; };
@@ -8,7 +8,11 @@ import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { TUserDALFactory } from "@app/services/user/user-dal"; import { TUserDALFactory } from "@app/services/user/user-dal";
import { TAccessApprovalRequestDALFactory } from "../access-approval-request/access-approval-request-dal";
import { TAccessApprovalRequestReviewerDALFactory } from "../access-approval-request/access-approval-request-reviewer-dal";
import { ApprovalStatus } from "../access-approval-request/access-approval-request-types";
import { TGroupDALFactory } from "../group/group-dal"; import { TGroupDALFactory } from "../group/group-dal";
import { TProjectUserAdditionalPrivilegeDALFactory } from "../project-user-additional-privilege/project-user-additional-privilege-dal";
import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal"; import { TAccessApprovalPolicyApproverDALFactory } from "./access-approval-policy-approver-dal";
import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal"; import { TAccessApprovalPolicyDALFactory } from "./access-approval-policy-dal";
import { import {
@@ -21,7 +25,7 @@ import {
TUpdateAccessApprovalPolicy TUpdateAccessApprovalPolicy
} from "./access-approval-policy-types"; } from "./access-approval-policy-types";
type TSecretApprovalPolicyServiceFactoryDep = { type TAccessApprovalPolicyServiceFactoryDep = {
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory; accessApprovalPolicyDAL: TAccessApprovalPolicyDALFactory;
@@ -30,6 +34,9 @@ type TSecretApprovalPolicyServiceFactoryDep = {
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find">;
groupDAL: TGroupDALFactory; groupDAL: TGroupDALFactory;
userDAL: Pick<TUserDALFactory, "find">; userDAL: Pick<TUserDALFactory, "find">;
accessApprovalRequestDAL: Pick<TAccessApprovalRequestDALFactory, "update" | "find">;
additionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
accessApprovalRequestReviewerDAL: Pick<TAccessApprovalRequestReviewerDALFactory, "update">;
}; };
export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>; export type TAccessApprovalPolicyServiceFactory = ReturnType<typeof accessApprovalPolicyServiceFactory>;
@@ -41,8 +48,11 @@ export const accessApprovalPolicyServiceFactory = ({
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
projectDAL, projectDAL,
userDAL userDAL,
}: TSecretApprovalPolicyServiceFactoryDep) => { accessApprovalRequestDAL,
additionalPrivilegeDAL,
accessApprovalRequestReviewerDAL
}: TAccessApprovalPolicyServiceFactoryDep) => {
const createAccessApprovalPolicy = async ({ const createAccessApprovalPolicy = async ({
name, name,
actor, actor,
@@ -189,7 +199,7 @@ export const accessApprovalPolicyServiceFactory = ({
); );
// ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); // ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id }); const accessApprovalPolicies = await accessApprovalPolicyDAL.find({ projectId: project.id, deletedAt: null });
return accessApprovalPolicies; return accessApprovalPolicies;
}; };
@@ -326,7 +336,29 @@ export const accessApprovalPolicyServiceFactory = ({
ProjectPermissionSub.SecretApproval ProjectPermissionSub.SecretApproval
); );
await accessApprovalPolicyDAL.deleteById(policyId); await accessApprovalPolicyDAL.transaction(async (tx) => {
await accessApprovalPolicyDAL.softDeleteById(policyId, tx);
const allAccessApprovalRequests = await accessApprovalRequestDAL.find({ policyId });
if (allAccessApprovalRequests.length) {
const accessApprovalRequestsIds = allAccessApprovalRequests.map((request) => request.id);
const privilegeIdsArray = allAccessApprovalRequests
.map((request) => request.privilegeId)
.filter((id): id is string => id != null);
if (privilegeIdsArray.length) {
await additionalPrivilegeDAL.delete({ $in: { id: privilegeIdsArray } }, tx);
}
await accessApprovalRequestReviewerDAL.update(
{ $in: { id: accessApprovalRequestsIds }, status: ApprovalStatus.PENDING },
{ status: ApprovalStatus.REJECTED },
tx
);
}
});
return policy; return policy;
}; };
@@ -356,7 +388,11 @@ export const accessApprovalPolicyServiceFactory = ({
const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug }); const environment = await projectEnvDAL.findOne({ projectId: project.id, slug: envSlug });
if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` }); if (!environment) throw new NotFoundError({ message: `Environment with slug '${envSlug}' not found` });
const policies = await accessApprovalPolicyDAL.find({ envId: environment.id, projectId: project.id }); const policies = await accessApprovalPolicyDAL.find({
envId: environment.id,
projectId: project.id,
deletedAt: null
});
if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` }); if (!policies) throw new NotFoundError({ message: `No policies found in environment with slug '${envSlug}'` });
return { count: policies.length }; return { count: policies.length };
@@ -61,7 +61,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"), db.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), db.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"), db.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId") db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
) )
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover)) .select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
@@ -118,7 +119,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
approvals: doc.policyApprovals, approvals: doc.policyApprovals,
secretPath: doc.policySecretPath, secretPath: doc.policySecretPath,
enforcementLevel: doc.policyEnforcementLevel, enforcementLevel: doc.policyEnforcementLevel,
envId: doc.policyEnvId envId: doc.policyEnvId,
deletedAt: doc.policyDeletedAt
}, },
requestedByUser: { requestedByUser: {
userId: doc.requestedByUserId, userId: doc.requestedByUserId,
@@ -141,7 +143,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
} }
: null, : null,
isApproved: !!doc.privilegeId isApproved: !!doc.policyDeletedAt || !!doc.privilegeId
}), }),
childrenMapper: [ childrenMapper: [
{ {
@@ -252,7 +254,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("slug").withSchema(TableName.Environment).as("environment"), tx.ref("slug").withSchema(TableName.Environment).as("environment"),
tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"), tx.ref("secretPath").withSchema(TableName.AccessApprovalPolicy).as("policySecretPath"),
tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"), tx.ref("enforcementLevel").withSchema(TableName.AccessApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals") tx.ref("approvals").withSchema(TableName.AccessApprovalPolicy).as("policyApprovals"),
tx.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt")
); );
const findById = async (id: string, tx?: Knex) => { const findById = async (id: string, tx?: Knex) => {
@@ -271,7 +274,8 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
name: el.policyName, name: el.policyName,
approvals: el.policyApprovals, approvals: el.policyApprovals,
secretPath: el.policySecretPath, secretPath: el.policySecretPath,
enforcementLevel: el.policyEnforcementLevel enforcementLevel: el.policyEnforcementLevel,
deletedAt: el.policyDeletedAt
}, },
requestedByUser: { requestedByUser: {
userId: el.requestedByUserId, userId: el.requestedByUserId,
@@ -363,6 +367,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient) => {
) )
.where(`${TableName.Environment}.projectId`, projectId) .where(`${TableName.Environment}.projectId`, projectId)
.where(`${TableName.AccessApprovalPolicy}.deletedAt`, null)
.select(selectAllTableCols(TableName.AccessApprovalRequest)) .select(selectAllTableCols(TableName.AccessApprovalRequest))
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus")) .select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId")); .select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"));
@@ -130,6 +130,9 @@ export const accessApprovalRequestServiceFactory = ({
message: `No policy in environment with slug '${environment.slug}' and with secret path '${secretPath}' was found.` message: `No policy in environment with slug '${environment.slug}' and with secret path '${secretPath}' was found.`
}); });
} }
if (policy.deletedAt) {
throw new BadRequestError({ message: "The policy linked to this request has been deleted" });
}
const approverIds: string[] = []; const approverIds: string[] = [];
const approverGroupIds: string[] = []; const approverGroupIds: string[] = [];
@@ -309,6 +312,12 @@ export const accessApprovalRequestServiceFactory = ({
} }
const { policy } = accessApprovalRequest; const { policy } = accessApprovalRequest;
if (policy.deletedAt) {
throw new BadRequestError({
message: "The policy associated with this access request has been deleted."
});
}
const { membership, hasRole } = await permissionService.getProjectPermission( const { membership, hasRole } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
@@ -37,7 +37,7 @@ export const auditLogQueueServiceFactory = async ({
const appCfg = getConfig(); const appCfg = getConfig();
const pushToLog = async (data: TCreateAuditLogDTO) => { const pushToLog = async (data: TCreateAuditLogDTO) => {
if (appCfg.USE_PG_QUEUE) { if (appCfg.USE_PG_QUEUE && appCfg.SHOULD_INIT_PG_QUEUE) {
await queueService.queuePg<QueueName.AuditLog>(QueueJobs.AuditLog, data, { await queueService.queuePg<QueueName.AuditLog>(QueueJobs.AuditLog, data, {
retryLimit: 10, retryLimit: 10,
retryBackoff: true retryBackoff: true
@@ -52,6 +52,7 @@ export const auditLogQueueServiceFactory = async ({
} }
}; };
if (appCfg.SHOULD_INIT_PG_QUEUE) {
await queueService.startPg<QueueName.AuditLog>( await queueService.startPg<QueueName.AuditLog>(
QueueJobs.AuditLog, QueueJobs.AuditLog,
async ([job]) => { async ([job]) => {
@@ -142,6 +143,7 @@ export const auditLogQueueServiceFactory = async ({
pollingIntervalSeconds: 0.5 pollingIntervalSeconds: 0.5
} }
); );
}
queueService.start(QueueName.AuditLog, async (job) => { queueService.start(QueueName.AuditLog, async (job) => {
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data; const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
@@ -60,6 +60,7 @@ export enum EventType {
DELETE_SECRETS = "delete-secrets", DELETE_SECRETS = "delete-secrets",
GET_WORKSPACE_KEY = "get-workspace-key", GET_WORKSPACE_KEY = "get-workspace-key",
AUTHORIZE_INTEGRATION = "authorize-integration", AUTHORIZE_INTEGRATION = "authorize-integration",
UPDATE_INTEGRATION_AUTH = "update-integration-auth",
UNAUTHORIZE_INTEGRATION = "unauthorize-integration", UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
CREATE_INTEGRATION = "create-integration", CREATE_INTEGRATION = "create-integration",
DELETE_INTEGRATION = "delete-integration", DELETE_INTEGRATION = "delete-integration",
@@ -357,6 +358,13 @@ interface AuthorizeIntegrationEvent {
}; };
} }
interface UpdateIntegrationAuthEvent {
type: EventType.UPDATE_INTEGRATION_AUTH;
metadata: {
integration: string;
};
}
interface UnauthorizeIntegrationEvent { interface UnauthorizeIntegrationEvent {
type: EventType.UNAUTHORIZE_INTEGRATION; type: EventType.UNAUTHORIZE_INTEGRATION;
metadata: { metadata: {
@@ -1680,6 +1688,7 @@ export type Event =
| DeleteSecretBatchEvent | DeleteSecretBatchEvent
| GetWorkspaceKeyEvent | GetWorkspaceKeyEvent
| AuthorizeIntegrationEvent | AuthorizeIntegrationEvent
| UpdateIntegrationAuthEvent
| UnauthorizeIntegrationEvent | UnauthorizeIntegrationEvent
| CreateIntegrationEvent | CreateIntegrationEvent
| DeleteIntegrationEvent | DeleteIntegrationEvent
+28 -3
View File
@@ -5,6 +5,8 @@ import { TableName, TGroups } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex"; import { buildFindFilter, ormify, selectAllTableCols, TFindFilter, TFindOpt } from "@app/lib/knex";
import { EFilterReturnedUsers } from "./group-types";
export type TGroupDALFactory = ReturnType<typeof groupDALFactory>; export type TGroupDALFactory = ReturnType<typeof groupDALFactory>;
export const groupDALFactory = (db: TDbClient) => { export const groupDALFactory = (db: TDbClient) => {
@@ -66,7 +68,8 @@ export const groupDALFactory = (db: TDbClient) => {
offset = 0, offset = 0,
limit, limit,
username, // depreciated in favor of search username, // depreciated in favor of search
search search,
filter
}: { }: {
orgId: string; orgId: string;
groupId: string; groupId: string;
@@ -74,6 +77,7 @@ export const groupDALFactory = (db: TDbClient) => {
limit?: number; limit?: number;
username?: string; username?: string;
search?: string; search?: string;
filter?: EFilterReturnedUsers;
}) => { }) => {
try { try {
const query = db const query = db
@@ -90,6 +94,7 @@ export const groupDALFactory = (db: TDbClient) => {
.select( .select(
db.ref("id").withSchema(TableName.OrgMembership), db.ref("id").withSchema(TableName.OrgMembership),
db.ref("groupId").withSchema(TableName.UserGroupMembership), db.ref("groupId").withSchema(TableName.UserGroupMembership),
db.ref("createdAt").withSchema(TableName.UserGroupMembership).as("joinedGroupAt"),
db.ref("email").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users), db.ref("firstName").withSchema(TableName.Users),
@@ -111,17 +116,37 @@ export const groupDALFactory = (db: TDbClient) => {
void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`); void query.andWhere(`${TableName.Users}.username`, "ilike", `%${username}%`);
} }
switch (filter) {
case EFilterReturnedUsers.EXISTING_MEMBERS:
void query.andWhere(`${TableName.UserGroupMembership}.createdAt`, "is not", null);
break;
case EFilterReturnedUsers.NON_MEMBERS:
void query.andWhere(`${TableName.UserGroupMembership}.createdAt`, "is", null);
break;
default:
break;
}
const members = await query; const members = await query;
return { return {
members: members.map( members: members.map(
({ email, username: memberUsername, firstName, lastName, userId, groupId: memberGroupId }) => ({ ({
email,
username: memberUsername,
firstName,
lastName,
userId,
groupId: memberGroupId,
joinedGroupAt
}) => ({
id: userId, id: userId,
email, email,
username: memberUsername, username: memberUsername,
firstName, firstName,
lastName, lastName,
isPartOfGroup: !!memberGroupId isPartOfGroup: !!memberGroupId,
joinedGroupAt
}) })
), ),
// @ts-expect-error col select is raw and not strongly typed // @ts-expect-error col select is raw and not strongly typed
@@ -222,7 +222,8 @@ export const groupServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId,
search search,
filter
}: TListGroupUsersDTO) => { }: TListGroupUsersDTO) => {
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" }); if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
@@ -251,7 +252,8 @@ export const groupServiceFactory = ({
offset, offset,
limit, limit,
username, username,
search search,
filter
}); });
return { users: members, totalCount }; return { users: members, totalCount };
@@ -283,8 +285,8 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group // check if user has broader or equal to privileges than group
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission); const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPriviledges) if (!hasRequiredPrivileges)
throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" }); throw new ForbiddenRequestError({ message: "Failed to add user to more privileged group" });
const user = await userDAL.findOne({ username }); const user = await userDAL.findOne({ username });
@@ -338,8 +340,8 @@ export const groupServiceFactory = ({
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId); const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
// check if user has broader or equal to privileges than group // check if user has broader or equal to privileges than group
const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, groupRolePermission); const hasRequiredPrivileges = isAtLeastAsPrivileged(permission, groupRolePermission);
if (!hasRequiredPriviledges) if (!hasRequiredPrivileges)
throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" }); throw new ForbiddenRequestError({ message: "Failed to delete user from more privileged group" });
const user = await userDAL.findOne({ username }); const user = await userDAL.findOne({ username });
@@ -39,6 +39,7 @@ export type TListGroupUsersDTO = {
limit: number; limit: number;
username?: string; username?: string;
search?: string; search?: string;
filter?: EFilterReturnedUsers;
} & TGenericPermission; } & TGenericPermission;
export type TAddUserToGroupDTO = { export type TAddUserToGroupDTO = {
@@ -101,3 +102,8 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = {
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex; tx?: Knex;
}; };
export enum EFilterReturnedUsers {
EXISTING_MEMBERS = "existingMembers",
NON_MEMBERS = "nonMembers"
}
@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { packRules } from "@casl/ability/extra"; import { packRules } from "@casl/ability/extra";
import ms from "ms"; import ms from "ms";
@@ -62,7 +62,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityId, identityId,
@@ -139,7 +142,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityProjectMembership.identityId, identityProjectMembership.identityId,
@@ -216,7 +222,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
const { permission: identityRolePermission } = await permissionService.getProjectPermission( const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityProjectMembership.identityId, identityProjectMembership.identityId,
@@ -258,7 +267,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
return { return {
...identityPrivilege, ...identityPrivilege,
@@ -289,7 +301,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug, slug,
@@ -321,7 +336,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
);
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find( const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find(
{ {
@@ -1,4 +1,4 @@
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability"; import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import ms from "ms"; import ms from "ms";
@@ -69,7 +69,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityId, identityId,
@@ -146,7 +150,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission( const { permission: targetIdentityPermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
@@ -241,7 +249,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: identityRolePermission } = await permissionService.getProjectPermission( const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityProjectMembership.identityId, identityProjectMembership.identityId,
@@ -294,7 +306,10 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({
slug, slug,
@@ -333,7 +348,11 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({
projectMembershipId: identityProjectMembership.id projectMembershipId: identityProjectMembership.id
@@ -82,6 +82,10 @@ export type SecretImportSubjectFields = {
secretPath: string; secretPath: string;
}; };
export type IdentityManagementSubjectFields = {
identityId: string;
};
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionActions, ProjectPermissionActions,
@@ -121,7 +125,10 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.Identity] | [
ProjectPermissionActions,
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
@@ -213,6 +220,21 @@ const SecretConditionV2Schema = z
}) })
.partial(); .partial();
const IdentityManagementConditionSchema = z
.object({
identityId: z.union([
z.string(),
z
.object({
[PermissionConditionOperators.$EQ]: PermissionConditionSchema[PermissionConditionOperators.$EQ],
[PermissionConditionOperators.$NEQ]: PermissionConditionSchema[PermissionConditionOperators.$NEQ],
[PermissionConditionOperators.$IN]: PermissionConditionSchema[PermissionConditionOperators.$IN]
})
.partial()
])
})
.partial();
const GeneralPermissionSchema = [ const GeneralPermissionSchema = [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.SecretApproval).describe("The entity this permission pertains to."),
@@ -262,12 +284,6 @@ const GeneralPermissionSchema = [
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.ServiceTokens).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.ServiceTokens).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
@@ -373,6 +389,12 @@ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
)
}),
...GeneralPermissionSchema ...GeneralPermissionSchema
]); ]);
@@ -417,6 +439,16 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
"When specified, only matching conditions will be allowed to access given resource." "When specified, only matching conditions will be allowed to access given resource."
).optional() ).optional()
}), }),
z.object({
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
"Describe what action an entity can take."
),
conditions: IdentityManagementConditionSchema.describe(
"When specified, only matching conditions will be allowed to access given resource."
).optional()
}),
...GeneralPermissionSchema ...GeneralPermissionSchema
]); ]);
@@ -697,26 +729,26 @@ export const buildServiceTokenProjectPermission = (
[ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach( [ProjectPermissionSub.Secrets, ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretFolders].forEach(
(subject) => { (subject) => {
if (canWrite) { if (canWrite) {
can(ProjectPermissionActions.Edit, subject, {
// TODO: @Akhi // TODO: @Akhi
// @ts-expect-error type // @ts-expect-error type
can(ProjectPermissionActions.Edit, subject, {
secretPath: { $glob: secretPath }, secretPath: { $glob: secretPath },
environment environment
}); });
// @ts-expect-error type
can(ProjectPermissionActions.Create, subject, { can(ProjectPermissionActions.Create, subject, {
// @ts-expect-error type
secretPath: { $glob: secretPath }, secretPath: { $glob: secretPath },
environment environment
}); });
// @ts-expect-error type
can(ProjectPermissionActions.Delete, subject, { can(ProjectPermissionActions.Delete, subject, {
// @ts-expect-error type
secretPath: { $glob: secretPath }, secretPath: { $glob: secretPath },
environment environment
}); });
} }
if (canRead) { if (canRead) {
// @ts-expect-error type
can(ProjectPermissionActions.Read, subject, { can(ProjectPermissionActions.Read, subject, {
// @ts-expect-error type
secretPath: { $glob: secretPath }, secretPath: { $glob: secretPath },
environment environment
}); });
@@ -177,5 +177,10 @@ export const secretApprovalPolicyDALFactory = (db: TDbClient) => {
} }
}; };
return { ...secretApprovalPolicyOrm, findById, find }; const softDeleteById = async (policyId: string, tx?: Knex) => {
const softDeletedPolicy = await secretApprovalPolicyOrm.updateById(policyId, { deletedAt: new Date() }, tx);
return softDeletedPolicy;
};
return { ...secretApprovalPolicyOrm, findById, find, softDeleteById };
}; };
@@ -11,6 +11,8 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { ApproverType } from "../access-approval-policy/access-approval-policy-types"; import { ApproverType } from "../access-approval-policy/access-approval-policy-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TSecretApprovalRequestDALFactory } from "../secret-approval-request/secret-approval-request-dal";
import { RequestState } from "../secret-approval-request/secret-approval-request-types";
import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal"; import { TSecretApprovalPolicyApproverDALFactory } from "./secret-approval-policy-approver-dal";
import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "./secret-approval-policy-dal";
import { import {
@@ -34,6 +36,7 @@ type TSecretApprovalPolicyServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "find">; userDAL: Pick<TUserDALFactory, "find">;
secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory; secretApprovalPolicyApproverDAL: TSecretApprovalPolicyApproverDALFactory;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
secretApprovalRequestDAL: Pick<TSecretApprovalRequestDALFactory, "update">;
}; };
export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>; export type TSecretApprovalPolicyServiceFactory = ReturnType<typeof secretApprovalPolicyServiceFactory>;
@@ -44,7 +47,8 @@ export const secretApprovalPolicyServiceFactory = ({
secretApprovalPolicyApproverDAL, secretApprovalPolicyApproverDAL,
projectEnvDAL, projectEnvDAL,
userDAL, userDAL,
licenseService licenseService,
secretApprovalRequestDAL
}: TSecretApprovalPolicyServiceFactoryDep) => { }: TSecretApprovalPolicyServiceFactoryDep) => {
const createSecretApprovalPolicy = async ({ const createSecretApprovalPolicy = async ({
name, name,
@@ -301,8 +305,16 @@ export const secretApprovalPolicyServiceFactory = ({
}); });
} }
await secretApprovalPolicyDAL.deleteById(secretPolicyId); const deletedPolicy = await secretApprovalPolicyDAL.transaction(async (tx) => {
return sapPolicy; await secretApprovalRequestDAL.update(
{ policyId: secretPolicyId, status: RequestState.Open },
{ status: RequestState.Closed },
tx
);
const updatedPolicy = await secretApprovalPolicyDAL.softDeleteById(secretPolicyId, tx);
return updatedPolicy;
});
return { ...deletedPolicy, projectId: sapPolicy.projectId, environment: sapPolicy.environment };
}; };
const getSecretApprovalPolicyByProjectId = async ({ const getSecretApprovalPolicyByProjectId = async ({
@@ -321,7 +333,7 @@ export const secretApprovalPolicyServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
const sapPolicies = await secretApprovalPolicyDAL.find({ projectId }); const sapPolicies = await secretApprovalPolicyDAL.find({ projectId, deletedAt: null });
return sapPolicies; return sapPolicies;
}; };
@@ -334,7 +346,7 @@ export const secretApprovalPolicyServiceFactory = ({
}); });
} }
const policies = await secretApprovalPolicyDAL.find({ envId: env.id }); const policies = await secretApprovalPolicyDAL.find({ envId: env.id, deletedAt: null });
if (!policies.length) return; if (!policies.length) return;
// this will filter policies either without scoped to secret path or the one that matches with secret path // this will filter policies either without scoped to secret path or the one that matches with secret path
const policiesFilteredByPath = policies.filter( const policiesFilteredByPath = policies.filter(
@@ -111,7 +111,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"), tx.ref("secretPath").withSchema(TableName.SecretApprovalPolicy).as("policySecretPath"),
tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"), tx.ref("envId").withSchema(TableName.SecretApprovalPolicy).as("policyEnvId"),
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"), tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals") tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt")
); );
const findById = async (id: string, tx?: Knex) => { const findById = async (id: string, tx?: Knex) => {
@@ -147,7 +148,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
approvals: el.policyApprovals, approvals: el.policyApprovals,
secretPath: el.policySecretPath, secretPath: el.policySecretPath,
enforcementLevel: el.policyEnforcementLevel, enforcementLevel: el.policyEnforcementLevel,
envId: el.policyEnvId envId: el.policyEnvId,
deletedAt: el.policyDeletedAt
} }
}), }),
childrenMapper: [ childrenMapper: [
@@ -222,6 +224,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
`${TableName.SecretApprovalRequest}.policyId`, `${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicyApprover}.policyId` `${TableName.SecretApprovalPolicyApprover}.policyId`
) )
.join(
TableName.SecretApprovalPolicy,
`${TableName.SecretApprovalRequest}.policyId`,
`${TableName.SecretApprovalPolicy}.id`
)
.where({ projectId }) .where({ projectId })
.andWhere( .andWhere(
(bd) => (bd) =>
@@ -229,6 +236,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
.where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId) .where(`${TableName.SecretApprovalPolicyApprover}.approverUserId`, userId)
.orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId) .orWhere(`${TableName.SecretApprovalRequest}.committerUserId`, userId)
) )
.andWhere((bd) => void bd.where(`${TableName.SecretApprovalPolicy}.deletedAt`, null))
.select("status", `${TableName.SecretApprovalRequest}.id`) .select("status", `${TableName.SecretApprovalRequest}.id`)
.groupBy(`${TableName.SecretApprovalRequest}.id`, "status") .groupBy(`${TableName.SecretApprovalRequest}.id`, "status")
.count("status") .count("status")
@@ -232,10 +232,10 @@ export const secretApprovalRequestServiceFactory = ({
type: KmsDataKey.SecretManager, type: KmsDataKey.SecretManager,
projectId projectId
}); });
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2( const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestIdBridgeSecretV2(
secretApprovalRequest.id secretApprovalRequest.id
); );
secrets = encrypedSecrets.map((el) => ({ secrets = encryptedSecrets.map((el) => ({
...el, ...el,
secretKey: el.key, secretKey: el.key,
id: el.id, id: el.id,
@@ -274,8 +274,8 @@ export const secretApprovalRequestServiceFactory = ({
})); }));
} else { } else {
if (!botKey) throw new NotFoundError({ message: `Project bot key not found`, name: "BotKeyNotFound" }); // CLI depends on this error message. TODO(daniel): Make API check for name BotKeyNotFound instead of message if (!botKey) throw new NotFoundError({ message: `Project bot key not found`, name: "BotKeyNotFound" }); // CLI depends on this error message. TODO(daniel): Make API check for name BotKeyNotFound instead of message
const encrypedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id); const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
secrets = encrypedSecrets.map((el) => ({ secrets = encryptedSecrets.map((el) => ({
...el, ...el,
...decryptSecretWithBot(el, botKey), ...decryptSecretWithBot(el, botKey),
secret: el.secret secret: el.secret
@@ -323,6 +323,12 @@ export const secretApprovalRequestServiceFactory = ({
} }
const { policy } = secretApprovalRequest; const { policy } = secretApprovalRequest;
if (policy.deletedAt) {
throw new BadRequestError({
message: "The policy associated with this secret approval request has been deleted."
});
}
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,
actorId, actorId,
@@ -383,6 +389,12 @@ export const secretApprovalRequestServiceFactory = ({
} }
const { policy } = secretApprovalRequest; const { policy } = secretApprovalRequest;
if (policy.deletedAt) {
throw new BadRequestError({
message: "The policy associated with this secret approval request has been deleted."
});
}
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,
actorId, actorId,
@@ -433,6 +445,12 @@ export const secretApprovalRequestServiceFactory = ({
} }
const { policy, folderId, projectId } = secretApprovalRequest; const { policy, folderId, projectId } = secretApprovalRequest;
if (policy.deletedAt) {
throw new BadRequestError({
message: "The policy associated with this secret approval request has been deleted."
});
}
const { hasRole } = await permissionService.getProjectPermission( const { hasRole } = await permissionService.getProjectPermission(
ActorType.USER, ActorType.USER,
actorId, actorId,
+8 -1
View File
@@ -19,7 +19,9 @@ export const GROUPS = {
offset: "The offset to start from. If you enter 10, it will start from the 10th user.", offset: "The offset to start from. If you enter 10, it will start from the 10th user.",
limit: "The number of users to return.", limit: "The number of users to return.",
username: "The username to search for.", username: "The username to search for.",
search: "The text string that user email or name will be filtered by." search: "The text string that user email or name will be filtered by.",
filterUsers:
"Whether to filter the list of returned users. 'existingMembers' will only return existing users in the group, 'nonMembers' will only return users not in the group, undefined will return all users in the organization."
}, },
ADD_USER: { ADD_USER: {
id: "The ID of the group to add the user to.", id: "The ID of the group to add the user to.",
@@ -1032,6 +1034,9 @@ export const INTEGRATION_AUTH = {
DELETE_BY_ID: { DELETE_BY_ID: {
integrationAuthId: "The ID of integration authentication object to delete." integrationAuthId: "The ID of integration authentication object to delete."
}, },
UPDATE_BY_ID: {
integrationAuthId: "The ID of integration authentication object to update."
},
CREATE_ACCESS_TOKEN: { CREATE_ACCESS_TOKEN: {
workspaceId: "The ID of the project to create the integration auth for.", workspaceId: "The ID of the project to create the integration auth for.",
integration: "The slug of integration for the auth object.", integration: "The slug of integration for the auth object.",
@@ -1088,11 +1093,13 @@ export const INTEGRATION = {
}, },
UPDATE: { UPDATE: {
integrationId: "The ID of the integration object.", integrationId: "The ID of the integration object.",
region: "AWS region to sync secrets to.",
app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
appId: appId:
"The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", "The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.",
isActive: "Whether the integration should be active or disabled.", isActive: "Whether the integration should be active or disabled.",
secretPath: "The path of the secrets to sync secrets from.", secretPath: "The path of the secrets to sync secrets from.",
path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault.",
owner: "External integration providers service entity owner. Used in Github.", owner: "External integration providers service entity owner. Used in Github.",
targetEnvironment: targetEnvironment:
"The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.", "The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.",
+3 -3
View File
@@ -166,8 +166,7 @@ const envSchema = z
OTEL_COLLECTOR_BASIC_AUTH_PASSWORD: zpStr(z.string().optional()), OTEL_COLLECTOR_BASIC_AUTH_PASSWORD: zpStr(z.string().optional()),
OTEL_EXPORT_TYPE: z.enum(["prometheus", "otlp"]).optional(), OTEL_EXPORT_TYPE: z.enum(["prometheus", "otlp"]).optional(),
PLAIN_API_KEY: zpStr(z.string().optional()), PYLON_API_KEY: zpStr(z.string().optional()),
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"), DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"), SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()), WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
@@ -180,7 +179,8 @@ const envSchema = z
HSM_KEY_LABEL: zpStr(z.string().optional()), HSM_KEY_LABEL: zpStr(z.string().optional()),
HSM_SLOT: z.coerce.number().optional().default(0), HSM_SLOT: z.coerce.number().optional().default(0),
USE_PG_QUEUE: zodStrBool.default("false") USE_PG_QUEUE: zodStrBool.default("false"),
SHOULD_INIT_PG_QUEUE: zodStrBool.default("false")
}) })
// To ensure that basic encryption is always possible. // To ensure that basic encryption is always possible.
.refine( .refine(
+6 -6
View File
@@ -89,9 +89,9 @@ const redactedKeys = [
const UNKNOWN_REQUEST_ID = "UNKNOWN_REQUEST_ID"; const UNKNOWN_REQUEST_ID = "UNKNOWN_REQUEST_ID";
const extractRequestId = () => { const extractReqId = () => {
try { try {
return requestContext.get("requestId") || UNKNOWN_REQUEST_ID; return requestContext.get("reqId") || UNKNOWN_REQUEST_ID;
} catch (err) { } catch (err) {
console.log("failed to get request context", err); console.log("failed to get request context", err);
return UNKNOWN_REQUEST_ID; return UNKNOWN_REQUEST_ID;
@@ -133,22 +133,22 @@ export const initLogger = async () => {
const wrapLogger = (originalLogger: Logger): CustomLogger => { const wrapLogger = (originalLogger: Logger): CustomLogger => {
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.info = (obj: unknown, msg?: string, ...args: any[]) => { originalLogger.info = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).info(obj, msg, ...args); return originalLogger.child({ reqId: extractReqId() }).info(obj, msg, ...args);
}; };
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.error = (obj: unknown, msg?: string, ...args: any[]) => { originalLogger.error = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).error(obj, msg, ...args); return originalLogger.child({ reqId: extractReqId() }).error(obj, msg, ...args);
}; };
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.warn = (obj: unknown, msg?: string, ...args: any[]) => { originalLogger.warn = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).warn(obj, msg, ...args); return originalLogger.child({ reqId: extractReqId() }).warn(obj, msg, ...args);
}; };
// eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any // eslint-disable-next-line no-param-reassign, @typescript-eslint/no-explicit-any
originalLogger.debug = (obj: unknown, msg?: string, ...args: any[]) => { originalLogger.debug = (obj: unknown, msg?: string, ...args: any[]) => {
return originalLogger.child({ requestId: extractRequestId() }).debug(obj, msg, ...args); return originalLogger.child({ reqId: extractReqId() }).debug(obj, msg, ...args);
}; };
return originalLogger; return originalLogger;
+8 -2
View File
@@ -1,6 +1,7 @@
import "./lib/telemetry/instrumentation"; import "./lib/telemetry/instrumentation";
import dotenv from "dotenv"; import dotenv from "dotenv";
import { Redis } from "ioredis";
import path from "path"; import path from "path";
import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns"; import { initializeHsmModule } from "@app/ee/services/hsm/hsm-fns";
@@ -56,15 +57,20 @@ const run = async () => {
const smtp = smtpServiceFactory(formatSmtpConfig()); const smtp = smtpServiceFactory(formatSmtpConfig());
const queue = queueServiceFactory(appCfg.REDIS_URL, appCfg.DB_CONNECTION_URI); const queue = queueServiceFactory(appCfg.REDIS_URL, {
dbConnectionUrl: appCfg.DB_CONNECTION_URI,
dbRootCert: appCfg.DB_ROOT_CERT
});
await queue.initialize(); await queue.initialize();
const keyStore = keyStoreFactory(appCfg.REDIS_URL); const keyStore = keyStoreFactory(appCfg.REDIS_URL);
const redis = new Redis(appCfg.REDIS_URL);
const hsmModule = initializeHsmModule(); const hsmModule = initializeHsmModule();
hsmModule.initialize(); hsmModule.initialize();
const server = await main({ db, auditLogDb, hsmModule: hsmModule.getModule(), smtp, logger, queue, keyStore }); const server = await main({ db, auditLogDb, hsmModule: hsmModule.getModule(), smtp, logger, queue, keyStore, redis });
const bootstrap = await bootstrapCheck({ db }); const bootstrap = await bootstrapCheck({ db });
// eslint-disable-next-line // eslint-disable-next-line
+16 -2
View File
@@ -8,6 +8,7 @@ import {
TScanFullRepoEventPayload, TScanFullRepoEventPayload,
TScanPushEventPayload TScanPushEventPayload
} from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types"; } from "@app/ee/services/secret-scanning/secret-scanning-queue/secret-scanning-queue-types";
import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { import {
TFailedIntegrationSyncEmailsPayload, TFailedIntegrationSyncEmailsPayload,
@@ -186,7 +187,10 @@ export type TQueueJobTypes = {
}; };
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
export const queueServiceFactory = (redisUrl: string, dbConnectionUrl: string) => { export const queueServiceFactory = (
redisUrl: string,
{ dbConnectionUrl, dbRootCert }: { dbConnectionUrl: string; dbRootCert?: string }
) => {
const connection = new Redis(redisUrl, { maxRetriesPerRequest: null }); const connection = new Redis(redisUrl, { maxRetriesPerRequest: null });
const queueContainer = {} as Record< const queueContainer = {} as Record<
QueueName, QueueName,
@@ -197,7 +201,13 @@ export const queueServiceFactory = (redisUrl: string, dbConnectionUrl: string) =
connectionString: dbConnectionUrl, connectionString: dbConnectionUrl,
archiveCompletedAfterSeconds: 60, archiveCompletedAfterSeconds: 60,
archiveFailedAfterSeconds: 1000, // we want to keep failed jobs for a longer time so that it can be retried archiveFailedAfterSeconds: 1000, // we want to keep failed jobs for a longer time so that it can be retried
deleteAfterSeconds: 30 deleteAfterSeconds: 30,
ssl: dbRootCert
? {
rejectUnauthorized: true,
ca: Buffer.from(dbRootCert, "base64").toString("ascii")
}
: false
}); });
const queueContainerPg = {} as Record<QueueJobs, boolean>; const queueContainerPg = {} as Record<QueueJobs, boolean>;
@@ -208,11 +218,15 @@ export const queueServiceFactory = (redisUrl: string, dbConnectionUrl: string) =
>; >;
const initialize = async () => { const initialize = async () => {
const appCfg = getConfig();
if (appCfg.SHOULD_INIT_PG_QUEUE) {
logger.info("Initializing pg-queue...");
await pgBoss.start(); await pgBoss.start();
pgBoss.on("error", (error) => { pgBoss.on("error", (error) => {
logger.error(error, "pg-queue error"); logger.error(error, "pg-queue error");
}); });
}
}; };
const start = <T extends QueueName>( const start = <T extends QueueName>(
+7 -4
View File
@@ -12,6 +12,7 @@ import type { FastifyRateLimitOptions } from "@fastify/rate-limit";
import ratelimiter from "@fastify/rate-limit"; import ratelimiter from "@fastify/rate-limit";
import { fastifyRequestContext } from "@fastify/request-context"; import { fastifyRequestContext } from "@fastify/request-context";
import fastify from "fastify"; import fastify from "fastify";
import { Redis } from "ioredis";
import { Knex } from "knex"; import { Knex } from "knex";
import { HsmModule } from "@app/ee/services/hsm/hsm-types"; import { HsmModule } from "@app/ee/services/hsm/hsm-types";
@@ -41,10 +42,11 @@ type TMain = {
queue: TQueueServiceFactory; queue: TQueueServiceFactory;
keyStore: TKeyStoreFactory; keyStore: TKeyStoreFactory;
hsmModule: HsmModule; hsmModule: HsmModule;
redis: Redis;
}; };
// Run the server! // Run the server!
export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore }: TMain) => { export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, keyStore, redis }: TMain) => {
const appCfg = getConfig(); const appCfg = getConfig();
const server = fastify({ const server = fastify({
@@ -60,6 +62,7 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
server.setValidatorCompiler(validatorCompiler); server.setValidatorCompiler(validatorCompiler);
server.setSerializerCompiler(serializerCompiler); server.setSerializerCompiler(serializerCompiler);
server.decorate("redis", redis);
server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => { server.addContentTypeParser("application/scim+json", { parseAs: "string" }, (_, body, done) => {
try { try {
const strBody = body instanceof Buffer ? body.toString() : body; const strBody = body instanceof Buffer ? body.toString() : body;
@@ -109,9 +112,9 @@ export const main = async ({ db, hsmModule, auditLogDb, smtp, logger, queue, key
await server.register(maintenanceMode); await server.register(maintenanceMode);
await server.register(fastifyRequestContext, { await server.register(fastifyRequestContext, {
defaultStoreValues: (request) => ({ defaultStoreValues: (req) => ({
requestId: request.id, reqId: req.id,
log: request.log.child({ requestId: request.id }) log: req.log.child({ reqId: req.id })
}) })
}); });
+23
View File
@@ -0,0 +1,23 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod";
interface SlugSchemaInputs {
min?: number;
max?: number;
field?: string;
}
export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInputs = {}) => {
return z
.string()
.trim()
.min(min, {
message: `${field} field must be at least ${min} lowercase character${min === 1 ? "" : "s"}`
})
.max(max, {
message: `${field} field must be at most ${max} lowercase character${max === 1 ? "" : "s"}`
})
.refine((v) => slugify(v, { lowercase: true }) === v, {
message: `${field} field can only contain lowercase letters, numbers, and hyphens`
});
};
+13 -13
View File
@@ -40,42 +40,42 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
if (error instanceof BadRequestError) { if (error instanceof BadRequestError) {
void res void res
.status(HttpStatusCodes.BadRequest) .status(HttpStatusCodes.BadRequest)
.send({ requestId: req.id, statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name }); .send({ reqId: req.id, statusCode: HttpStatusCodes.BadRequest, message: error.message, error: error.name });
} else if (error instanceof NotFoundError) { } else if (error instanceof NotFoundError) {
void res void res
.status(HttpStatusCodes.NotFound) .status(HttpStatusCodes.NotFound)
.send({ requestId: req.id, statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name }); .send({ reqId: req.id, statusCode: HttpStatusCodes.NotFound, message: error.message, error: error.name });
} else if (error instanceof UnauthorizedError) { } else if (error instanceof UnauthorizedError) {
void res.status(HttpStatusCodes.Unauthorized).send({ void res.status(HttpStatusCodes.Unauthorized).send({
requestId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.Unauthorized, statusCode: HttpStatusCodes.Unauthorized,
message: error.message, message: error.message,
error: error.name error: error.name
}); });
} else if (error instanceof DatabaseError || error instanceof InternalServerError) { } else if (error instanceof DatabaseError || error instanceof InternalServerError) {
void res.status(HttpStatusCodes.InternalServerError).send({ void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.InternalServerError, statusCode: HttpStatusCodes.InternalServerError,
message: "Something went wrong", message: "Something went wrong",
error: error.name error: error.name
}); });
} else if (error instanceof GatewayTimeoutError) { } else if (error instanceof GatewayTimeoutError) {
void res.status(HttpStatusCodes.GatewayTimeout).send({ void res.status(HttpStatusCodes.GatewayTimeout).send({
requestId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.GatewayTimeout, statusCode: HttpStatusCodes.GatewayTimeout,
message: error.message, message: error.message,
error: error.name error: error.name
}); });
} else if (error instanceof ZodError) { } else if (error instanceof ZodError) {
void res.status(HttpStatusCodes.UnprocessableContent).send({ void res.status(HttpStatusCodes.UnprocessableContent).send({
requestId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.UnprocessableContent, statusCode: HttpStatusCodes.UnprocessableContent,
error: "ValidationFailure", error: "ValidationFailure",
message: error.issues message: error.issues
}); });
} else if (error instanceof ForbiddenError) { } else if (error instanceof ForbiddenError) {
void res.status(HttpStatusCodes.Forbidden).send({ void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.Forbidden, statusCode: HttpStatusCodes.Forbidden,
error: "PermissionDenied", error: "PermissionDenied",
message: `You are not allowed to ${error.action} on ${error.subjectType}`, message: `You are not allowed to ${error.action} on ${error.subjectType}`,
@@ -88,28 +88,28 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
}); });
} else if (error instanceof ForbiddenRequestError) { } else if (error instanceof ForbiddenRequestError) {
void res.status(HttpStatusCodes.Forbidden).send({ void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.Forbidden, statusCode: HttpStatusCodes.Forbidden,
message: error.message, message: error.message,
error: error.name error: error.name
}); });
} else if (error instanceof RateLimitError) { } else if (error instanceof RateLimitError) {
void res.status(HttpStatusCodes.TooManyRequests).send({ void res.status(HttpStatusCodes.TooManyRequests).send({
requestId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.TooManyRequests, statusCode: HttpStatusCodes.TooManyRequests,
message: error.message, message: error.message,
error: error.name error: error.name
}); });
} else if (error instanceof ScimRequestError) { } else if (error instanceof ScimRequestError) {
void res.status(error.status).send({ void res.status(error.status).send({
requestId: req.id, reqId: req.id,
schemas: error.schemas, schemas: error.schemas,
status: error.status, status: error.status,
detail: error.detail detail: error.detail
}); });
} else if (error instanceof OidcAuthError) { } else if (error instanceof OidcAuthError) {
void res.status(HttpStatusCodes.InternalServerError).send({ void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.InternalServerError, statusCode: HttpStatusCodes.InternalServerError,
message: error.message, message: error.message,
error: error.name error: error.name
@@ -128,14 +128,14 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
} }
void res.status(HttpStatusCodes.Forbidden).send({ void res.status(HttpStatusCodes.Forbidden).send({
requestId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.Forbidden, statusCode: HttpStatusCodes.Forbidden,
error: "TokenError", error: "TokenError",
message: errorMessage message: errorMessage
}); });
} else { } else {
void res.status(HttpStatusCodes.InternalServerError).send({ void res.status(HttpStatusCodes.InternalServerError).send({
requestId: req.id, reqId: req.id,
statusCode: HttpStatusCodes.InternalServerError, statusCode: HttpStatusCodes.InternalServerError,
error: "InternalServerError", error: "InternalServerError",
message: "Something went wrong" message: "Something went wrong"
+8 -3
View File
@@ -414,7 +414,8 @@ export const registerRoutes = async (
permissionService, permissionService,
secretApprovalPolicyDAL, secretApprovalPolicyDAL,
licenseService, licenseService,
userDAL userDAL,
secretApprovalRequestDAL
}); });
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgMembershipDAL });
@@ -994,7 +995,10 @@ export const registerRoutes = async (
projectEnvDAL, projectEnvDAL,
projectMembershipDAL, projectMembershipDAL,
projectDAL, projectDAL,
userDAL userDAL,
accessApprovalRequestDAL,
additionalPrivilegeDAL: projectUserAdditionalPrivilegeDAL,
accessApprovalRequestReviewerDAL
}); });
const accessApprovalRequestService = accessApprovalRequestServiceFactory({ const accessApprovalRequestService = accessApprovalRequestServiceFactory({
@@ -1238,7 +1242,8 @@ export const registerRoutes = async (
}); });
const userEngagementService = userEngagementServiceFactory({ const userEngagementService = userEngagementServiceFactory({
userDAL userDAL,
orgDAL
}); });
const slackService = slackServiceFactory({ const slackService = slackServiceFactory({
@@ -30,25 +30,25 @@ export const integrationAuthPubSchema = IntegrationAuthsSchema.pick({
export const DefaultResponseErrorsSchema = { export const DefaultResponseErrorsSchema = {
400: z.object({ 400: z.object({
requestId: z.string(), reqId: z.string(),
statusCode: z.literal(400), statusCode: z.literal(400),
message: z.string(), message: z.string(),
error: z.string() error: z.string()
}), }),
404: z.object({ 404: z.object({
requestId: z.string(), reqId: z.string(),
statusCode: z.literal(404), statusCode: z.literal(404),
message: z.string(), message: z.string(),
error: z.string() error: z.string()
}), }),
401: z.object({ 401: z.object({
requestId: z.string(), reqId: z.string(),
statusCode: z.literal(401), statusCode: z.literal(401),
message: z.string(), message: z.string(),
error: z.string() error: z.string()
}), }),
403: z.object({ 403: z.object({
requestId: z.string(), reqId: z.string(),
statusCode: z.literal(403), statusCode: z.literal(403),
message: z.string(), message: z.string(),
details: z.any().optional(), details: z.any().optional(),
@@ -56,13 +56,13 @@ export const DefaultResponseErrorsSchema = {
}), }),
// Zod errors return a message of varying shapes and sizes, so z.any() is used here // Zod errors return a message of varying shapes and sizes, so z.any() is used here
422: z.object({ 422: z.object({
requestId: z.string(), reqId: z.string(),
statusCode: z.literal(422), statusCode: z.literal(422),
message: z.any(), message: z.any(),
error: z.string() error: z.string()
}), }),
500: z.object({ 500: z.object({
requestId: z.string(), reqId: z.string(),
statusCode: z.literal(500), statusCode: z.literal(500),
message: z.string(), message: z.string(),
error: z.string() error: z.string()
+2 -10
View File
@@ -1,4 +1,3 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas"; import { InternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
@@ -8,19 +7,12 @@ import { getBase64SizeInBytes, isBase64 } from "@app/lib/base64";
import { SymmetricEncryption } from "@app/lib/crypto/cipher"; import { SymmetricEncryption } from "@app/lib/crypto/cipher";
import { OrderByDirection } from "@app/lib/types"; import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CmekOrderBy } from "@app/services/cmek/cmek-types"; import { CmekOrderBy } from "@app/services/cmek/cmek-types";
const keyNameSchema = z const keyNameSchema = slugSchema({ min: 1, max: 32, field: "Name" });
.string()
.trim()
.min(1)
.max(32)
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Name must be slug friendly"
});
const keyDescriptionSchema = z.string().trim().max(500).optional(); const keyDescriptionSchema = z.string().trim().max(500).optional();
const base64Schema = z.string().superRefine((val, ctx) => { const base64Schema = z.string().superRefine((val, ctx) => {
@@ -1,9 +1,9 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { ExternalGroupOrgRoleMappingsSchema } from "@app/db/schemas/external-group-org-role-mappings"; import { ExternalGroupOrgRoleMappingsSchema } from "@app/db/schemas/external-group-org-role-mappings";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -48,13 +48,7 @@ export const registerExternalGroupOrgRoleMappingRouter = async (server: FastifyZ
mappings: z mappings: z
.object({ .object({
groupName: z.string().trim().min(1), groupName: z.string().trim().min(1),
roleSlug: z roleSlug: slugSchema({ max: 64 })
.string()
.min(1)
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Role must be a valid slug"
})
}) })
.array() .array()
}), }),
@@ -6,6 +6,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { OctopusDeployScope } from "@app/services/integration-auth/integration-auth-types"; import { OctopusDeployScope } from "@app/services/integration-auth/integration-auth-types";
import { Integrations } from "@app/services/integration-auth/integration-list";
import { integrationAuthPubSchema } from "../sanitizedSchemas"; import { integrationAuthPubSchema } from "../sanitizedSchemas";
@@ -82,6 +83,67 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
} }
}); });
server.route({
method: "PATCH",
url: "/:integrationAuthId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Update the integration authentication object required for syncing secrets.",
security: [
{
bearerAuth: []
}
],
params: z.object({
integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.UPDATE_BY_ID.integrationAuthId)
}),
body: z.object({
integration: z.nativeEnum(Integrations).optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.integration),
accessId: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessId),
accessToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessToken),
awsAssumeIamRoleArn: z
.string()
.url()
.trim()
.optional()
.describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.awsAssumeIamRoleArn),
url: z.string().url().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.url),
namespace: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.namespace),
refreshToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.refreshToken)
}),
response: {
200: z.object({
integrationAuth: integrationAuthPubSchema
})
}
},
handler: async (req) => {
const integrationAuth = await server.services.integrationAuth.updateIntegrationAuth({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
integrationAuthId: req.params.integrationAuthId,
...req.body
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: integrationAuth.projectId,
event: {
type: EventType.UPDATE_INTEGRATION_AUTH,
metadata: {
integration: integrationAuth.integration
}
}
});
return { integrationAuth };
}
});
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/", url: "/",
@@ -141,7 +141,9 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
targetEnvironment: z.string().trim().optional().describe(INTEGRATION.UPDATE.targetEnvironment), targetEnvironment: z.string().trim().optional().describe(INTEGRATION.UPDATE.targetEnvironment),
owner: z.string().trim().optional().describe(INTEGRATION.UPDATE.owner), owner: z.string().trim().optional().describe(INTEGRATION.UPDATE.owner),
environment: z.string().trim().optional().describe(INTEGRATION.UPDATE.environment), environment: z.string().trim().optional().describe(INTEGRATION.UPDATE.environment),
metadata: IntegrationMetadataSchema.optional() path: z.string().trim().optional().describe(INTEGRATION.UPDATE.path),
metadata: IntegrationMetadataSchema.optional(),
region: z.string().trim().optional().describe(INTEGRATION.UPDATE.region)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -1,4 +1,3 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { import {
@@ -14,6 +13,7 @@ import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-t
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs"; import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn"; import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type"; import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
@@ -243,22 +243,10 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
params: z.object({ organizationId: z.string().trim() }), params: z.object({ organizationId: z.string().trim() }),
body: z.object({ body: z.object({
name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(), name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
slug: z slug: slugSchema({ max: 64 }).optional(),
.string()
.trim()
.max(64, { message: "Slug must be 64 or fewer characters" })
.regex(/^[a-zA-Z0-9-]+$/, "Slug must only contain alphanumeric characters or hyphens")
.optional(),
authEnforced: z.boolean().optional(), authEnforced: z.boolean().optional(),
scimEnabled: z.boolean().optional(), scimEnabled: z.boolean().optional(),
defaultMembershipRoleSlug: z defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
.string()
.min(1)
.trim()
.refine((v) => slugify(v) === v, {
message: "Membership role must be a valid slug"
})
.optional(),
enforceMfa: z.boolean().optional(), enforceMfa: z.boolean().optional(),
selectedMfaMethod: z.nativeEnum(MfaMethod).optional() selectedMfaMethod: z.nativeEnum(MfaMethod).optional()
}), }),
@@ -1,10 +1,10 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { ProjectEnvironmentsSchema } from "@app/db/schemas"; import { ProjectEnvironmentsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ENVIRONMENTS } from "@app/lib/api-docs"; import { ENVIRONMENTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -124,13 +124,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
name: z.string().trim().describe(ENVIRONMENTS.CREATE.name), name: z.string().trim().describe(ENVIRONMENTS.CREATE.name),
position: z.number().min(1).optional().describe(ENVIRONMENTS.CREATE.position), position: z.number().min(1).optional().describe(ENVIRONMENTS.CREATE.position),
slug: z slug: slugSchema({ max: 64 }).describe(ENVIRONMENTS.CREATE.slug)
.string()
.trim()
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(ENVIRONMENTS.CREATE.slug)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -188,14 +182,7 @@ export const registerProjectEnvRouter = async (server: FastifyZodProvider) => {
id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id) id: z.string().trim().describe(ENVIRONMENTS.UPDATE.id)
}), }),
body: z.object({ body: z.object({
slug: z slug: slugSchema({ max: 64 }).optional().describe(ENVIRONMENTS.UPDATE.slug),
.string()
.trim()
.optional()
.refine((v) => !v || slugify(v) === v, {
message: "Slug must be a valid slug"
})
.describe(ENVIRONMENTS.UPDATE.slug),
name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name), name: z.string().trim().optional().describe(ENVIRONMENTS.UPDATE.name),
position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position) position: z.number().optional().describe(ENVIRONMENTS.UPDATE.position)
}), }),
@@ -1,9 +1,9 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { SecretTagsSchema } from "@app/db/schemas"; import { SecretTagsSchema } from "@app/db/schemas";
import { SECRET_TAGS } from "@app/lib/api-docs"; import { SECRET_TAGS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -111,14 +111,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId) projectId: z.string().trim().describe(SECRET_TAGS.CREATE.projectId)
}), }),
body: z.object({ body: z.object({
slug: z slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.CREATE.slug),
.string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.CREATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim().describe(SECRET_TAGS.CREATE.color) color: z.string().trim().describe(SECRET_TAGS.CREATE.color)
}), }),
response: { response: {
@@ -153,14 +146,7 @@ export const registerSecretTagRouter = async (server: FastifyZodProvider) => {
tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId) tagId: z.string().trim().describe(SECRET_TAGS.UPDATE.tagId)
}), }),
body: z.object({ body: z.object({
slug: z slug: slugSchema({ max: 64 }).describe(SECRET_TAGS.UPDATE.slug),
.string()
.toLowerCase()
.trim()
.describe(SECRET_TAGS.UPDATE.slug)
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim().describe(SECRET_TAGS.UPDATE.color) color: z.string().trim().describe(SECRET_TAGS.UPDATE.color)
}), }),
response: { response: {
+3 -14
View File
@@ -1,10 +1,10 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { SlackIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas"; import { SlackIntegrationsSchema, WorkflowIntegrationsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -35,12 +35,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
} }
], ],
querystring: z.object({ querystring: z.object({
slug: z slug: slugSchema({ max: 64 }),
.string()
.trim()
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
}),
description: z.string().optional() description: z.string().optional()
}), }),
response: { response: {
@@ -288,13 +283,7 @@ export const registerSlackRouter = async (server: FastifyZodProvider) => {
id: z.string() id: z.string()
}), }),
body: z.object({ body: z.object({
slug: z slug: slugSchema({ max: 64 }).optional(),
.string()
.trim()
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional(),
description: z.string().optional() description: z.string().optional()
}), }),
response: { response: {
+77 -23
View File
@@ -8,6 +8,7 @@
import { Authenticator } from "@fastify/passport"; import { Authenticator } from "@fastify/passport";
import fastifySession from "@fastify/session"; import fastifySession from "@fastify/session";
import RedisStore from "connect-redis";
import { Strategy as GitHubStrategy } from "passport-github"; import { Strategy as GitHubStrategy } from "passport-github";
import { Strategy as GitLabStrategy } from "passport-gitlab2"; import { Strategy as GitLabStrategy } from "passport-gitlab2";
import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import { Strategy as GoogleStrategy } from "passport-google-oauth20";
@@ -23,8 +24,22 @@ import { OrgAuthMethod } from "@app/services/org/org-types";
export const registerSsoRouter = async (server: FastifyZodProvider) => { export const registerSsoRouter = async (server: FastifyZodProvider) => {
const appCfg = getConfig(); const appCfg = getConfig();
const passport = new Authenticator({ key: "sso", userProperty: "passportUser" }); const passport = new Authenticator({ key: "sso", userProperty: "passportUser" });
await server.register(fastifySession, { secret: appCfg.COOKIE_SECRET_SIGN_KEY }); const redisStore = new RedisStore({
client: server.redis,
prefix: "oauth-session:",
ttl: 600 // 10 minutes
});
await server.register(fastifySession, {
secret: appCfg.COOKIE_SECRET_SIGN_KEY,
store: redisStore,
cookie: {
secure: appCfg.HTTPS_ENABLED,
sameSite: "lax" // we want cookies to be sent to Infisical in redirects originating from IDP server
}
});
await server.register(passport.initialize()); await server.register(passport.initialize());
await server.register(passport.secureSession()); await server.register(passport.secureSession());
// passport oauth strategy for Google // passport oauth strategy for Google
@@ -37,11 +52,15 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientID: appCfg.CLIENT_ID_GOOGLE_LOGIN as string, clientID: appCfg.CLIENT_ID_GOOGLE_LOGIN as string,
clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string, clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/google`, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/google`,
scope: ["profile", " email"] scope: ["profile", " email"],
state: true
}, },
// eslint-disable-next-line // eslint-disable-next-line
async (req, _accessToken, _refreshToken, profile, cb) => { async (req, _accessToken, _refreshToken, profile, cb) => {
try { try {
// @ts-expect-error this is because this is express type and not fastify
const callbackPort = req.session.get("callbackPort");
const email = profile?.emails?.[0]?.value; const email = profile?.emails?.[0]?.value;
if (!email) if (!email)
throw new NotFoundError({ throw new NotFoundError({
@@ -54,7 +73,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
firstName: profile?.name?.givenName || "", firstName: profile?.name?.givenName || "",
lastName: profile?.name?.familyName || "", lastName: profile?.name?.familyName || "",
authMethod: AuthMethod.GOOGLE, authMethod: AuthMethod.GOOGLE,
callbackPort: req.query.state as string callbackPort
}); });
cb(null, { isUserCompleted, providerAuthToken }); cb(null, { isUserCompleted, providerAuthToken });
} catch (error) { } catch (error) {
@@ -76,10 +95,14 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientID: appCfg.CLIENT_ID_GITHUB_LOGIN as string, clientID: appCfg.CLIENT_ID_GITHUB_LOGIN as string,
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN as string, clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN as string,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`,
scope: ["user:email"] scope: ["user:email"],
// akhilmhdh: because the ts type for this is outdated by the maintainer
state: true as unknown as string
}, },
// eslint-disable-next-line // eslint-disable-next-line
async (req, accessToken, _refreshToken, profile, cb) => { async (req, accessToken, _refreshToken, profile, cb) => {
// @ts-expect-error this is because this is express type and not fastify
const callbackPort = req.session.get("callbackPort");
try { try {
const ghEmails = await fetchGithubEmails(accessToken); const ghEmails = await fetchGithubEmails(accessToken);
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0]; const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
@@ -88,7 +111,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
firstName: profile.displayName, firstName: profile.displayName,
lastName: "", lastName: "",
authMethod: AuthMethod.GITHUB, authMethod: AuthMethod.GITHUB,
callbackPort: req.query.state as string callbackPort
}); });
return cb(null, { isUserCompleted, providerAuthToken }); return cb(null, { isUserCompleted, providerAuthToken });
} catch (error) { } catch (error) {
@@ -112,17 +135,20 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientID: appCfg.CLIENT_ID_GITLAB_LOGIN, clientID: appCfg.CLIENT_ID_GITLAB_LOGIN,
clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN, clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/gitlab`, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/gitlab`,
baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL,
state: true
}, },
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => { async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
try { try {
const callbackPort = req.session.get("callbackPort");
const email = profile.emails[0].value; const email = profile.emails[0].value;
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
email, email,
firstName: profile.displayName, firstName: profile.displayName,
lastName: "", lastName: "",
authMethod: AuthMethod.GITLAB, authMethod: AuthMethod.GITLAB,
callbackPort: req.query.state as string callbackPort
}); });
return cb(null, { isUserCompleted, providerAuthToken }); return cb(null, { isUserCompleted, providerAuthToken });
@@ -143,17 +169,24 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
callback_port: z.string().optional() callback_port: z.string().optional()
}) })
}, },
preValidation: (req, res) => preValidation: [
( async (req, res) => {
const { callback_port: callbackPort } = req.query;
// ensure fresh session state per login attempt
await req.session.regenerate();
if (callbackPort) {
req.session.set("callbackPort", callbackPort);
}
return (
passport.authenticate("google", { passport.authenticate("google", {
scope: ["profile", "email"], scope: ["profile", "email"],
session: false,
state: req.query.callback_port,
authInfo: false authInfo: false
// this is due to zod type difference // this is due to zod type difference
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any }) as any
)(req, res), )(req, res);
}
],
handler: () => {} handler: () => {}
}); });
@@ -166,7 +199,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
authInfo: false authInfo: false
// this is due to zod type difference // this is due to zod type difference
}) as never, }) as never,
handler: (req, res) => { handler: async (req, res) => {
await req.session.destroy();
if (req.passportUser.isUserCompleted) { if (req.passportUser.isUserCompleted) {
return res.redirect( return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
@@ -186,15 +220,24 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
callback_port: z.string().optional() callback_port: z.string().optional()
}) })
}, },
preValidation: (req, res) => preValidation: [
( async (req, res) => {
const { callback_port: callbackPort } = req.query;
// ensure fresh session state per login attempt
await req.session.regenerate();
if (callbackPort) {
req.session.set("callbackPort", callbackPort);
}
return (
passport.authenticate("github", { passport.authenticate("github", {
session: false, session: false,
state: req.query.callback_port,
authInfo: false authInfo: false
// this is due to zod type difference // this is due to zod type difference
}) as any }) as any
)(req, res), )(req, res);
}
],
handler: () => {} handler: () => {}
}); });
@@ -245,7 +288,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
authInfo: false authInfo: false
// this is due to zod type difference // this is due to zod type difference
}) as any, }) as any,
handler: (req, res) => { handler: async (req, res) => {
await req.session.destroy();
if (req.passportUser.isUserCompleted) { if (req.passportUser.isUserCompleted) {
return res.redirect( return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
@@ -265,16 +309,25 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
callback_port: z.string().optional() callback_port: z.string().optional()
}) })
}, },
preValidation: (req, res) => preValidation: [
( async (req, res) => {
const { callback_port: callbackPort } = req.query;
// ensure fresh session state per login attempt
await req.session.regenerate();
if (callbackPort) {
req.session.set("callbackPort", callbackPort);
}
return (
passport.authenticate("gitlab", { passport.authenticate("gitlab", {
session: false, session: false,
state: req.query.callback_port,
authInfo: false authInfo: false
// this is due to zod type difference // this is due to zod type difference
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any }) as any
)(req, res), )(req, res);
}
],
handler: () => {} handler: () => {}
}); });
@@ -288,7 +341,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
// this is due to zod type difference // this is due to zod type difference
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any, }) as any,
handler: (req, res) => { handler: async (req, res) => {
await req.session.destroy();
if (req.passportUser.isUserCompleted) { if (req.passportUser.isUserCompleted) {
return res.redirect( return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}`
@@ -21,7 +21,7 @@ export const registerUserEngagementRouter = async (server: FastifyZodProvider) =
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
return server.services.userEngagement.createUserWish(req.permission.id, req.body.text); return server.services.userEngagement.createUserWish(req.permission.id, req.permission.orgId, req.body.text);
} }
}); });
}; };
+8 -28
View File
@@ -1,4 +1,3 @@
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { import {
@@ -12,6 +11,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { slugSchema } from "@app/server/lib/schemas";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@@ -27,14 +27,6 @@ const projectWithEnv = SanitizedProjectSchema.extend({
environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array() environments: z.object({ name: z.string(), slug: z.string(), id: z.string() }).array()
}); });
const slugSchema = z
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be at least 5 character but no more than 36"
});
export const registerProjectRouter = async (server: FastifyZodProvider) => { export const registerProjectRouter = async (server: FastifyZodProvider) => {
/* Get project key */ /* Get project key */
server.route({ server.route({
@@ -162,21 +154,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
body: z.object({ body: z.object({
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName), projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription), projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription),
slug: z slug: slugSchema({ min: 5, max: 36 }).optional().describe(PROJECTS.CREATE.slug),
.string()
.min(5)
.max(36)
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.describe(PROJECTS.CREATE.slug),
kmsKeyId: z.string().optional(), kmsKeyId: z.string().optional(),
template: z template: slugSchema({ field: "Template Name", max: 64 })
.string()
.refine((v) => slugify(v) === v, {
message: "Template name must be in slug format"
})
.optional() .optional()
.default(InfisicalProjectTemplate.Default) .default(InfisicalProjectTemplate.Default)
.describe(PROJECTS.CREATE.template) .describe(PROJECTS.CREATE.template)
@@ -244,7 +224,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
} }
], ],
params: z.object({ params: z.object({
slug: slugSchema.describe("The slug of the project to delete.") slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to delete.")
}), }),
response: { response: {
200: SanitizedProjectSchema 200: SanitizedProjectSchema
@@ -278,7 +258,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
params: z.object({ params: z.object({
slug: slugSchema.describe("The slug of the project to get.") slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to get.")
}), }),
response: { response: {
200: projectWithEnv 200: projectWithEnv
@@ -311,7 +291,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
params: z.object({ params: z.object({
slug: slugSchema.describe("The slug of the project to update.") slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to update.")
}), }),
body: z.object({ body: z.object({
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name), name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
@@ -354,7 +334,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
params: z.object({ params: z.object({
slug: slugSchema.describe(PROJECTS.LIST_CAS.slug) slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CAS.slug)
}), }),
querystring: z.object({ querystring: z.object({
status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status), status: z.enum([CaStatus.ACTIVE, CaStatus.PENDING_CERTIFICATE]).optional().describe(PROJECTS.LIST_CAS.status),
@@ -395,7 +375,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
params: z.object({ params: z.object({
slug: slugSchema.describe(PROJECTS.LIST_CERTIFICATES.slug) slug: slugSchema({ min: 5, max: 36 }).describe(PROJECTS.LIST_CERTIFICATES.slug)
}), }),
querystring: z.object({ querystring: z.object({
friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName), friendlyName: z.string().optional().describe(PROJECTS.LIST_CERTIFICATES.friendlyName),
@@ -1,4 +1,4 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { ProjectMembershipRole } from "@app/db/schemas"; import { ProjectMembershipRole } from "@app/db/schemas";
@@ -61,7 +61,12 @@ export const identityProjectServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Identity, {
identityId
})
);
const existingIdentity = await identityProjectDAL.findOne({ identityId, projectId }); const existingIdentity = await identityProjectDAL.findOne({ identityId, projectId });
if (existingIdentity) if (existingIdentity)
@@ -161,7 +166,10 @@ export const identityProjectServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Identity, { identityId })
);
const projectIdentity = await identityProjectDAL.findOne({ identityId, projectId }); const projectIdentity = await identityProjectDAL.findOne({ identityId, projectId });
if (!projectIdentity) if (!projectIdentity)
@@ -253,7 +261,11 @@ export const identityProjectServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Identity, { identityId })
);
const { permission: identityRolePermission } = await permissionService.getProjectPermission( const { permission: identityRolePermission } = await permissionService.getProjectPermission(
ActorType.IDENTITY, ActorType.IDENTITY,
identityId, identityId,
@@ -317,7 +329,11 @@ export const identityProjectServiceFactory = ({
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Identity, { identityId })
);
const [identityMembership] = await identityProjectDAL.findByProjectId(projectId, { identityId }); const [identityMembership] = await identityProjectDAL.findByProjectId(projectId, { identityId });
if (!identityMembership) if (!identityMembership)
@@ -55,6 +55,7 @@ import {
TOctopusDeployVariableSet, TOctopusDeployVariableSet,
TSaveIntegrationAccessTokenDTO, TSaveIntegrationAccessTokenDTO,
TTeamCityBuildConfig, TTeamCityBuildConfig,
TUpdateIntegrationAuthDTO,
TVercelBranches TVercelBranches
} from "./integration-auth-types"; } from "./integration-auth-types";
import { getIntegrationOptions, Integrations, IntegrationUrls } from "./integration-list"; import { getIntegrationOptions, Integrations, IntegrationUrls } from "./integration-list";
@@ -368,6 +369,148 @@ export const integrationAuthServiceFactory = ({
return integrationAuthDAL.create(updateDoc); return integrationAuthDAL.create(updateDoc);
}; };
const updateIntegrationAuth = async ({
integrationAuthId,
refreshToken,
actorId,
integration: newIntegration,
url,
actor,
actorOrgId,
actorAuthMethod,
accessId,
namespace,
accessToken,
awsAssumeIamRoleArn
}: TUpdateIntegrationAuthDTO) => {
const integrationAuth = await integrationAuthDAL.findById(integrationAuthId);
if (!integrationAuth) {
throw new NotFoundError({ message: `Integration auth with id ${integrationAuthId} not found.` });
}
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integrationAuth.projectId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
const { projectId } = integrationAuth;
const integration = newIntegration || integrationAuth.integration;
const updateDoc: TIntegrationAuthsInsert = {
projectId,
integration,
namespace,
url,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8,
...(integration === Integrations.GCP_SECRET_MANAGER
? {
metadata: {
authMethod: "serviceAccount"
}
}
: {})
};
const { shouldUseSecretV2Bridge, botKey } = await projectBotService.getBotKey(projectId);
if (shouldUseSecretV2Bridge) {
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
if (refreshToken) {
const tokenDetails = await exchangeRefresh(
integration,
refreshToken,
url,
updateDoc.metadata as Record<string, string>
);
const refreshEncToken = secretManagerEncryptor({
plainText: Buffer.from(tokenDetails.refreshToken)
}).cipherTextBlob;
updateDoc.encryptedRefresh = refreshEncToken;
const accessEncToken = secretManagerEncryptor({
plainText: Buffer.from(tokenDetails.accessToken)
}).cipherTextBlob;
updateDoc.encryptedAccess = accessEncToken;
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt;
}
if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) {
if (accessToken) {
const accessEncToken = secretManagerEncryptor({
plainText: Buffer.from(accessToken)
}).cipherTextBlob;
updateDoc.encryptedAccess = accessEncToken;
updateDoc.encryptedAwsAssumeIamRoleArn = null;
}
if (accessId) {
const accessEncToken = secretManagerEncryptor({
plainText: Buffer.from(accessId)
}).cipherTextBlob;
updateDoc.encryptedAccessId = accessEncToken;
updateDoc.encryptedAwsAssumeIamRoleArn = null;
}
if (awsAssumeIamRoleArn) {
const awsAssumeIamRoleArnEncrypted = secretManagerEncryptor({
plainText: Buffer.from(awsAssumeIamRoleArn)
}).cipherTextBlob;
updateDoc.encryptedAwsAssumeIamRoleArn = awsAssumeIamRoleArnEncrypted;
updateDoc.encryptedAccess = null;
updateDoc.encryptedAccessId = null;
}
}
} else {
if (!botKey) throw new NotFoundError({ message: `Project bot key for project with ID '${projectId}' not found` });
if (refreshToken) {
const tokenDetails = await exchangeRefresh(
integration,
refreshToken,
url,
updateDoc.metadata as Record<string, string>
);
const refreshEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.refreshToken, botKey);
updateDoc.refreshIV = refreshEncToken.iv;
updateDoc.refreshTag = refreshEncToken.tag;
updateDoc.refreshCiphertext = refreshEncToken.ciphertext;
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(tokenDetails.accessToken, botKey);
updateDoc.accessIV = accessEncToken.iv;
updateDoc.accessTag = accessEncToken.tag;
updateDoc.accessCiphertext = accessEncToken.ciphertext;
updateDoc.accessExpiresAt = tokenDetails.accessExpiresAt;
}
if (!refreshToken && (accessId || accessToken || awsAssumeIamRoleArn)) {
if (accessToken) {
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessToken, botKey);
updateDoc.accessIV = accessEncToken.iv;
updateDoc.accessTag = accessEncToken.tag;
updateDoc.accessCiphertext = accessEncToken.ciphertext;
}
if (accessId) {
const accessEncToken = encryptSymmetric128BitHexKeyUTF8(accessId, botKey);
updateDoc.accessIdIV = accessEncToken.iv;
updateDoc.accessIdTag = accessEncToken.tag;
updateDoc.accessIdCiphertext = accessEncToken.ciphertext;
}
if (awsAssumeIamRoleArn) {
const awsAssumeIamRoleArnEnc = encryptSymmetric128BitHexKeyUTF8(awsAssumeIamRoleArn, botKey);
updateDoc.awsAssumeIamRoleArnCipherText = awsAssumeIamRoleArnEnc.ciphertext;
updateDoc.awsAssumeIamRoleArnIV = awsAssumeIamRoleArnEnc.iv;
updateDoc.awsAssumeIamRoleArnTag = awsAssumeIamRoleArnEnc.tag;
}
}
}
return integrationAuthDAL.updateById(integrationAuthId, updateDoc);
};
// helper function // helper function
const getIntegrationAccessToken = async ( const getIntegrationAccessToken = async (
integrationAuth: TIntegrationAuths, integrationAuth: TIntegrationAuths,
@@ -1615,6 +1758,7 @@ export const integrationAuthServiceFactory = ({
getIntegrationAuth, getIntegrationAuth,
oauthExchange, oauthExchange,
saveIntegrationToken, saveIntegrationToken,
updateIntegrationAuth,
deleteIntegrationAuthById, deleteIntegrationAuthById,
deleteIntegrationAuths, deleteIntegrationAuths,
getIntegrationAuthTeams, getIntegrationAuthTeams,
@@ -22,6 +22,11 @@ export type TSaveIntegrationAccessTokenDTO = {
awsAssumeIamRoleArn?: string; awsAssumeIamRoleArn?: string;
} & TProjectPermission; } & TProjectPermission;
export type TUpdateIntegrationAuthDTO = Omit<TSaveIntegrationAccessTokenDTO, "projectId" | "integration"> & {
integrationAuthId: string;
integration?: string;
};
export type TDeleteIntegrationAuthsDTO = TProjectPermission & { export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
integration: string; integration: string;
projectId: string; projectId: string;
@@ -151,7 +151,9 @@ export const integrationServiceFactory = ({
isActive, isActive,
environment, environment,
secretPath, secretPath,
metadata region,
metadata,
path
}: TUpdateIntegrationDTO) => { }: TUpdateIntegrationDTO) => {
const integration = await integrationDAL.findById(id); const integration = await integrationDAL.findById(id);
if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` }); if (!integration) throw new NotFoundError({ message: `Integration with ID '${id}' not found` });
@@ -192,7 +194,9 @@ export const integrationServiceFactory = ({
appId, appId,
targetEnvironment, targetEnvironment,
owner, owner,
region,
secretPath, secretPath,
path,
metadata: { metadata: {
...(integration.metadata as object), ...(integration.metadata as object),
...metadata ...metadata
@@ -49,6 +49,8 @@ export type TUpdateIntegrationDTO = {
appId?: string; appId?: string;
isActive?: boolean; isActive?: boolean;
secretPath?: string; secretPath?: string;
region?: string;
path?: string;
targetEnvironment?: string; targetEnvironment?: string;
owner?: string; owner?: string;
environment?: string; environment?: string;
@@ -53,6 +53,13 @@ export const smtpServiceFactory = (cfg: TSmtpConfig) => {
const smtp = createTransport(cfg); const smtp = createTransport(cfg);
const isSmtpOn = Boolean(cfg.host); const isSmtpOn = Boolean(cfg.host);
handlebars.registerHelper("emailFooter", () => {
const { SITE_URL } = getConfig();
return new handlebars.SafeString(
`<p style="font-size: 12px;">Email sent via Infisical at <a href="${SITE_URL}">${SITE_URL}</a></p>`
);
});
const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => { const sendMail = async ({ substitutions, recipients, template, subjectLine }: TSmtpSendMail) => {
const appCfg = getConfig(); const appCfg = getConfig();
const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8"); const html = await fs.readFile(path.resolve(__dirname, "./templates/", template), "utf8");
@@ -45,6 +45,8 @@
View the request and approve or deny it View the request and approve or deny it
<a href="{{approvalUrl}}">here</a>. <a href="{{approvalUrl}}">here</a>.
</p> </p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -11,8 +11,11 @@
<p>A secret approval request has been bypassed in the project "{{projectName}}".</p> <p>A secret approval request has been bypassed in the project "{{projectName}}".</p>
<p> <p>
{{requesterFullName}} ({{requesterEmail}}) has merged {{requesterFullName}}
a secret to environment {{environment}} at secret path {{secretPath}} ({{requesterEmail}}) has merged a secret to environment
{{environment}}
at secret path
{{secretPath}}
without obtaining the required approvals. without obtaining the required approvals.
</p> </p>
<p> <p>
@@ -24,5 +27,7 @@
To review this action, please visit the request panel To review this action, please visit the request panel
<a href="{{approvalUrl}}">here</a>. <a href="{{approvalUrl}}">here</a>.
</p> </p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -1,4 +1,3 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
@@ -14,6 +13,8 @@
<h2>{{code}}</h2> <h2>{{code}}</h2>
<p>The MFA code will be valid for 2 minutes.</p> <p>The MFA code will be valid for 2 minutes.</p>
<p>Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.</p> <p>Not you? Contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} immediately.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -10,6 +10,8 @@
<h2>Confirm your email address</h2> <h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started confirming your email.</p> <p>Your confirmation code is below — enter it in the browser window where you've started confirming your email.</p>
<h1>{{code}}</h1> <h1>{{code}}</h1>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -16,6 +16,7 @@
<p>Error: {{error}}</p> <p>Error: {{error}}</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -12,6 +12,8 @@
{{provider}} {{provider}}
to Infisical is in progress. The import process may take up to 30 minutes, and you will receive once the import to Infisical is in progress. The import process may take up to 30 minutes, and you will receive once the import
has finished or if it fails.</p> has finished or if it fails.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -9,6 +9,8 @@
<body> <body>
<h2>An import from {{provider}} to Infisical was successful</h2> <h2>An import from {{provider}} to Infisical was successful</h2>
<p>An import from {{provider}} was successful. Your data is now available in Infisical.</p> <p>An import from {{provider}} was successful. Your data is now available in Infisical.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -1,9 +1,8 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Incident alert: secrets potentially leaked</title> <title>Incident alert: secrets potentially leaked</title>
</head> </head>
@@ -13,9 +12,10 @@
<p>If these are production secrets, please rotate them immediately.</p> <p>If these are production secrets, please rotate them immediately.</p>
<p>Once you have taken action, be sure to update the status of the risk in your <a <p>Once you have taken action, be sure to update the status of the risk in your
href="{{siteUrl}}">Infisical <a href="{{siteUrl}}">Infisical dashboard</a>.</p>
dashboard</a>.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -26,6 +26,8 @@
{{#if syncMessage}} {{#if syncMessage}}
<p><b>Reason: </b>{{syncMessage}}</p> <p><b>Reason: </b>{{syncMessage}}</p>
{{/if}} {{/if}}
{{emailFooter}}
</body> </body>
</html> </html>
@@ -1,4 +1,3 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
@@ -13,7 +12,11 @@
<p><strong>Timestamp</strong>: {{timestamp}}</p> <p><strong>Timestamp</strong>: {{timestamp}}</p>
<p><strong>IP address</strong>: {{ip}}</p> <p><strong>IP address</strong>: {{ip}}</p>
<p><strong>User agent</strong>: {{userAgent}}</p> <p><strong>User agent</strong>: {{userAgent}}</p>
<p>If you believe that this login is suspicious, please contact {{#if isCloud}}Infisical{{else}}your administrator{{/if}} or reset your password immediately.</p> <p>If you believe that this login is suspicious, please contact
{{#if isCloud}}Infisical{{else}}your administrator{{/if}}
or reset your password immediately.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -8,9 +8,11 @@
</head> </head>
<body> <body>
<h2>Join your organization on Infisical</h2> <h2>Join your organization on Infisical</h2>
<p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}</p> <p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization named {{organizationName}}</p>
<a href="{{callback_url}}?token={{token}}{{#if metadata}}&metadata={{metadata}}{{/if}}&to={{email}}&organization_id={{organizationId}}">Join now</a> <a href="{{callback_url}}?token={{token}}{{#if metadata}}&metadata={{metadata}}{{/if}}&to={{email}}&organization_id={{organizationId}}">Click to join</a>
<h3>What is Infisical?</h3> <h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p> <p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -1,14 +1,16 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Account Recovery</title> <title>Account Recovery</title>
</head> </head>
<body> <body>
<h2>Reset your password</h2> <h2>Reset your password</h2>
<p>Someone requested a password reset.</p> <p>Someone requested a password reset.</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a> <a href="{{callback_url}}?token={{token}}&to={{email}}">Reset password</a>
<p>If you didn't initiate this request, please contact {{#if isCloud}}us immediately at [email protected].{{else}}your administrator immediately.{{/if}}</p> <p>If you didn't initiate this request, please contact
{{#if isCloud}}us immediately at [email protected].{{else}}your administrator immediately.{{/if}}</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -27,5 +27,7 @@
<p>Please take necessary actions to renew these items before they expire.</p> <p>Please take necessary actions to renew these items before they expire.</p>
<p>For more details, please log in to your Infisical account and check your PKI management section.</p> <p>For more details, please log in to your Infisical account and check your PKI management section.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -1,9 +1,8 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Organization Invitation</title> <title>Organization Invitation</title>
</head> </head>
<body> <body>
@@ -11,6 +10,9 @@
<p>You've been invited to join the Infisical organization — {{organizationName}}</p> <p>You've been invited to join the Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}">Join now</a> <a href="{{callback_url}}">Join now</a>
<h3>What is Infisical?</h3> <h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p> <p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
and configs.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -17,6 +17,8 @@
View the request and approve or deny it View the request and approve or deny it
<a href="{{approvalUrl}}">here</a>. <a href="{{approvalUrl}}">here</a>.
</p> </p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -1,9 +1,8 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Incident alert: secret leaked</title> <title>Incident alert: secret leaked</title>
</head> </head>
@@ -11,15 +10,18 @@
<h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3> <h3>Infisical has uncovered {{numberOfSecrets}} secret(s) from your recent push</h3>
<p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p> <p><a href="{{siteUrl}}/secret-scanning"><strong>View leaked secrets</strong></a></p>
<p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed <p>You are receiving this notification because one or more secret leaks have been detected in a recent commit pushed
by {{pusher_name}} ({{pusher_email}}). If by
these are test secrets, please add `infisical-scan:ignore` at the end of the line containing the secret as comment {{pusher_name}}
in the given programming. This will prevent future notifications from being sent out for those secret(s).</p> ({{pusher_email}}). If these are test secrets, please add `infisical-scan:ignore` at the end of the line
containing the secret as comment in the given programming. This will prevent future notifications from being sent
out for those secret(s).</p>
<p>If these are production secrets, please rotate them immediately.</p> <p>If these are production secrets, please rotate them immediately.</p>
<p>Once you have taken action, be sure to update the status of the risk in your <a <p>Once you have taken action, be sure to update the status of the risk in your
href="{{siteUrl}}">Infisical <a href="{{siteUrl}}">Infisical dashboard</a>.</p>
dashboard</a>.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -13,6 +13,8 @@
{{#if reminderNote}} {{#if reminderNote}}
<p>Here's the note included with the reminder: {{reminderNote}}</p> <p>Here's the note included with the reminder: {{reminderNote}}</p>
{{/if}} {{/if}}
{{emailFooter}}
</body> </body>
</html> </html>
@@ -1,9 +1,8 @@
<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge"> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Code</title> <title>Code</title>
</head> </head>
@@ -11,7 +10,10 @@
<h2>Confirm your email address</h2> <h2>Confirm your email address</h2>
<p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p> <p>Your confirmation code is below — enter it in the browser window where you've started signing up for Infisical.</p>
<h1>{{code}}</h1> <h1>{{code}}</h1>
<p>Questions about setting up Infisical? {{#if isCloud}}Email us at [email protected]{{else}}Contact your administrator{{/if}}.</p> <p>Questions about setting up Infisical?
{{#if isCloud}}Email us at [email protected]{{else}}Contact your administrator{{/if}}.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -11,6 +11,8 @@
<p>Your account has been temporarily locked due to multiple failed login attempts. </h2> <p>Your account has been temporarily locked due to multiple failed login attempts. </h2>
<a href="{{callback_url}}?token={{token}}">To unlock your account, follow the link here</a> <a href="{{callback_url}}?token={{token}}">To unlock your account, follow the link here</a>
<p>If these attempts were not made by you, reset your password immediately.</p> <p>If these attempts were not made by you, reset your password immediately.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -6,10 +6,12 @@
</head> </head>
<body> <body>
<h2>Join your team on Infisical</h2> <h2>Join your team on Infisical</h2>
<p>You have been invited to a new Infisical project — {{workspaceName}}</p> <p>You have been invited to a new Infisical project named {{workspaceName}}</p>
<a href="{{callback_url}}">Join now</a> <a href="{{callback_url}}">Click to join</a>
<h3>What is Infisical?</h3> <h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets <p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets
and configs.</p> and configs.</p>
{{emailFooter}}
</body> </body>
</html> </html>
@@ -1,87 +1,44 @@
import { PlainClient } from "@team-plain/typescript-sdk"; import axios from "axios";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { InternalServerError } from "@app/lib/errors"; import { InternalServerError } from "@app/lib/errors";
import { TOrgDALFactory } from "../org/org-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
type TUserEngagementServiceFactoryDep = { type TUserEngagementServiceFactoryDep = {
userDAL: Pick<TUserDALFactory, "findById">; userDAL: Pick<TUserDALFactory, "findById">;
orgDAL: Pick<TOrgDALFactory, "findById">;
}; };
export type TUserEngagementServiceFactory = ReturnType<typeof userEngagementServiceFactory>; export type TUserEngagementServiceFactory = ReturnType<typeof userEngagementServiceFactory>;
export const userEngagementServiceFactory = ({ userDAL }: TUserEngagementServiceFactoryDep) => { export const userEngagementServiceFactory = ({ userDAL, orgDAL }: TUserEngagementServiceFactoryDep) => {
const createUserWish = async (userId: string, text: string) => { const createUserWish = async (userId: string, orgId: string, text: string) => {
const user = await userDAL.findById(userId); const user = await userDAL.findById(userId);
const org = await orgDAL.findById(orgId);
const appCfg = getConfig(); const appCfg = getConfig();
if (!appCfg.PLAIN_API_KEY) { if (!appCfg.PYLON_API_KEY) {
throw new InternalServerError({ throw new InternalServerError({
message: "Plain is not configured." message: "Pylon is not configured."
}); });
} }
const client = new PlainClient({ const request = axios.create({
apiKey: appCfg.PLAIN_API_KEY baseURL: "https://api.usepylon.com",
}); headers: {
Authorization: `Bearer ${appCfg.PYLON_API_KEY}`
const customerUpsertRes = await client.upsertCustomer({
identifier: {
emailAddress: user.email
},
onCreate: {
fullName: `${user.firstName} ${user.lastName}`,
shortName: user.firstName,
email: {
email: user.email as string,
isVerified: user.isEmailVerified as boolean
},
externalId: user.id
},
onUpdate: {
fullName: {
value: `${user.firstName} ${user.lastName}`
},
shortName: {
value: user.firstName
},
email: {
email: user.email as string,
isVerified: user.isEmailVerified as boolean
},
externalId: {
value: user.id
}
} }
}); });
if (customerUpsertRes.error) { await request.post("/issues", {
throw new InternalServerError({ message: customerUpsertRes.error.message }); title: `New Wish From: ${user.firstName} ${user.lastName} (${org.name})`,
} body_html: text,
requester_email: user.email,
const createThreadRes = await client.createThread({ requester_name: `${user.firstName} ${user.lastName} (${org.name})`,
title: "Wish", tags: ["wish"]
customerIdentifier: {
externalId: customerUpsertRes.data.customer.externalId
},
components: [
{
componentText: {
text
}
}
],
labelTypeIds: appCfg.PLAIN_WISH_LABEL_IDS?.split(",")
}); });
if (createThreadRes.error) {
throw new InternalServerError({
message: createThreadRes.error.message
});
}
}; };
return { return {
createUserWish createUserWish
@@ -186,6 +186,10 @@ spec:
secretName: managed-secret secretName: managed-secret
secretNamespace: default secretNamespace: default
creationPolicy: "Orphan" ## Owner | Orphan creationPolicy: "Orphan" ## Owner | Orphan
# template:
# includeAllSecrets: true
# data:
# CUSTOM_KEY: "{{ .KEY.SecretPath }} {{ .KEY.Value }}"
# secretType: kubernetes.io/dockerconfigjson # secretType: kubernetes.io/dockerconfigjson
``` ```
@@ -698,6 +702,51 @@ The namespace of the managed Kubernetes secret to be created.
<Accordion title="managedSecretReference.secretType"> <Accordion title="managedSecretReference.secretType">
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
</Accordion> </Accordion>
<Accordion title="managedSecretReference.template">
Templates enable you to transform data from Infisical before storing it as a Kubernetes Secret.
</Accordion>
<Accordion title="managedSecretReference.template.includeAllSecrets">
When set to true, this option injects all secrets retrieved from Infisical into your configuration.
Secrets defined in the template will override the automatically injected secrets.
</Accordion>
<Accordion title="managedSecretReference.template.data">
Define secret keys and their corresponding templates.
Each data value uses a Golang template with access to all secrets retrieved from the specified scope.
Secrets are structured as follows:
```golang
type TemplateSecret struct {
Value string `json:"value"`
SecretPath string `json:"secretPath"`
}
```
#### Example template configuration:
```golang
managedSecretReference:
secretName: managed-secret
secretNamespace: default
template:
includeAllSecrets: true
data:
NEW_KEY: "{{ .KEY1.SecretPath }} {{ .KEY1.Value }}"
```
When you run the following command:
```bash
kubectl get secret managed-secret -o jsonpath='{.data}'
```
You'll receive Kubernetes secrets output that includes the NEW_KEY:
```bash
{... "KEY":"d29ybGQ=","NEW_KEY":"LyBoZWxsbw=="}
```
When you set `includeAllSecrets` as `false` the Kubernetes secrets outputs will be:
```bash
{"NEW_KEY":"LyBoZWxsbw=="}
```
</Accordion>
<Accordion title="managedSecretReference.creationPolicy"> <Accordion title="managedSecretReference.creationPolicy">
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
+1 -1
View File
@@ -89,7 +89,7 @@
"react-mailchimp-subscribe": "^2.1.3", "react-mailchimp-subscribe": "^2.1.3",
"react-markdown": "^8.0.3", "react-markdown": "^8.0.3",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-select": "^5.8.3", "react-select": "^5.8.1",
"react-table": "^7.8.0", "react-table": "^7.8.0",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"sanitize-html": "^2.12.1", "sanitize-html": "^2.12.1",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 23 KiB

@@ -3,7 +3,6 @@ import { Controller, useForm } from "react-hook-form";
import { faCheck } from "@fortawesome/free-solid-svg-icons"; import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -18,6 +17,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useCreateWsTag } from "@app/hooks/api"; import { useCreateWsTag } from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
export const secretTagsColors = [ export const secretTagsColors = [
{ {
@@ -88,13 +88,7 @@ type Props = {
}; };
const createTagSchema = z.object({ const createTagSchema = z.object({
slug: z slug: slugSchema({ min: 1, field: "Tag Slug" }),
.string()
.trim()
.toLowerCase()
.refine((v) => slugify(v) === v, {
message: "Invalid slug. Slug can only contain alphanumeric characters and hyphens."
}),
color: z.string().trim() color: z.string().trim()
}); });
@@ -36,7 +36,8 @@ import {
fetchOrgUsers, fetchOrgUsers,
useAddUserToWsNonE2EE, useAddUserToWsNonE2EE,
useCreateWorkspace, useCreateWorkspace,
useGetExternalKmsList useGetExternalKmsList,
useGetUserWorkspaces
} from "@app/hooks/api"; } from "@app/hooks/api";
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types"; import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates"; import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
@@ -68,6 +69,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
const { permission } = useOrgPermission(); const { permission } = useOrgPermission();
const { user } = useUser(); const { user } = useUser();
const createWs = useCreateWorkspace(); const createWs = useCreateWorkspace();
const { refetch: refetchWorkspaces } = useGetUserWorkspaces();
const addUsersToProject = useAddUserToWsNonE2EE(); const addUsersToProject = useAddUserToWsNonE2EE();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
@@ -137,8 +139,8 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
orgId: currentOrg.id orgId: currentOrg.id
}); });
} }
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
await new Promise((resolve) => setTimeout(resolve, 2_000)); await refetchWorkspaces();
createNotification({ text: "Project created", type: "success" }); createNotification({ text: "Project created", type: "success" });
reset(); reset();
@@ -33,6 +33,10 @@ export enum PermissionConditionOperators {
$GLOB = "$glob" $GLOB = "$glob"
} }
export type IdentityManagementSubjectFields = {
identityId: string;
};
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
[PermissionConditionOperators.$EQ]: "equal to", [PermissionConditionOperators.$EQ]: "equal to",
[PermissionConditionOperators.$IN]: "contains", [PermissionConditionOperators.$IN]: "contains",
@@ -151,7 +155,13 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
| [ProjectPermissionActions, ProjectPermissionSub.Identity] | [
ProjectPermissionActions,
(
| ProjectPermissionSub.Identity
| (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
)
]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
| [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.Certificates]
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
+21 -4
View File
@@ -1,14 +1,31 @@
/** Extracts the key and value from a passed in env string based on the provided delimiters. */ /** Extracts the key and value from a passed in env string based on the provided delimiters. */
export const getKeyValue = (pastedContent: string, delimiters: string[]) => { export const getKeyValue = (pastedContent: string, delimiters: string[]) => {
const foundDelimiter = delimiters.find((delimiter) => pastedContent.includes(delimiter)); if (!pastedContent) {
return { key: "", value: "" };
}
if (!foundDelimiter) { let firstDelimiterIndex = -1;
let foundDelimiter = "";
delimiters.forEach((delimiter) => {
const index = pastedContent.indexOf(delimiter);
if (index !== -1 && (firstDelimiterIndex === -1 || index < firstDelimiterIndex)) {
firstDelimiterIndex = index;
foundDelimiter = delimiter;
}
});
const hasValueAfterDelimiter = pastedContent.length > firstDelimiterIndex + foundDelimiter.length;
if (firstDelimiterIndex === -1 || !hasValueAfterDelimiter) {
return { key: pastedContent.trim(), value: "" }; return { key: pastedContent.trim(), value: "" };
} }
const [key, value] = pastedContent.split(foundDelimiter); const key = pastedContent.substring(0, firstDelimiterIndex);
const value = pastedContent.substring(firstDelimiterIndex + foundDelimiter.length);
return { return {
key: key.trim(), key: key.trim(),
value: (value ?? "").trim() value: value.trim()
}; };
}; };
@@ -26,7 +26,7 @@ export enum ApproverType{
export type Approver = { export type Approver = {
id: string; id: string;
type: ApproverType; type: ApproverType;
} };
export type TAccessApprovalRequest = { export type TAccessApprovalRequest = {
id: string; id: string;
@@ -70,6 +70,7 @@ export type TAccessApprovalRequest = {
secretPath?: string | null; secretPath?: string | null;
envId: string; envId: string;
enforcementLevel: EnforcementLevel; enforcementLevel: EnforcementLevel;
deletedAt: Date | null;
}; };
reviewers: { reviewers: {
@@ -8,6 +8,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.DELETE_SECRET]: "Delete secret", [EventType.DELETE_SECRET]: "Delete secret",
[EventType.GET_WORKSPACE_KEY]: "Read project key", [EventType.GET_WORKSPACE_KEY]: "Read project key",
[EventType.AUTHORIZE_INTEGRATION]: "Authorize integration", [EventType.AUTHORIZE_INTEGRATION]: "Authorize integration",
[EventType.UPDATE_INTEGRATION_AUTH]: "Update integration auth",
[EventType.UNAUTHORIZE_INTEGRATION]: "Unauthorize integration", [EventType.UNAUTHORIZE_INTEGRATION]: "Unauthorize integration",
[EventType.CREATE_INTEGRATION]: "Create integration", [EventType.CREATE_INTEGRATION]: "Create integration",
[EventType.DELETE_INTEGRATION]: "Delete integration", [EventType.DELETE_INTEGRATION]: "Delete integration",
@@ -23,6 +23,7 @@ export enum EventType {
DELETE_SECRET = "delete-secret", DELETE_SECRET = "delete-secret",
GET_WORKSPACE_KEY = "get-workspace-key", GET_WORKSPACE_KEY = "get-workspace-key",
AUTHORIZE_INTEGRATION = "authorize-integration", AUTHORIZE_INTEGRATION = "authorize-integration",
UPDATE_INTEGRATION_AUTH = "update-integration-auth",
UNAUTHORIZE_INTEGRATION = "unauthorize-integration", UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
CREATE_INTEGRATION = "create-integration", CREATE_INTEGRATION = "create-integration",
DELETE_INTEGRATION = "delete-integration", DELETE_INTEGRATION = "delete-integration",
+3 -4
View File
@@ -3,7 +3,6 @@ export {
useCreateGroup, useCreateGroup,
useDeleteGroup, useDeleteGroup,
useRemoveUserFromGroup, useRemoveUserFromGroup,
useUpdateGroup} from "./mutations"; useUpdateGroup
export { } from "./mutations";
useListGroupUsers export { useGetGroupById, useListGroupUsers } from "./queries";
} from "./queries";
+4 -2
View File
@@ -56,8 +56,9 @@ export const useUpdateGroup = () => {
return group; return group;
}, },
onSuccess: ({ orgId }) => { onSuccess: ({ orgId, id: groupId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId)); queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
queryClient.invalidateQueries(groupKeys.getGroupById(groupId));
} }
}); });
}; };
@@ -70,8 +71,9 @@ export const useDeleteGroup = () => {
return group; return group;
}, },
onSuccess: ({ orgId }) => { onSuccess: ({ orgId, id: groupId }) => {
queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId)); queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
queryClient.invalidateQueries(groupKeys.getGroupById(groupId));
} }
}); });
}; };
+25 -13
View File
@@ -2,7 +2,10 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { EFilterReturnedUsers, TGroup, TGroupUser } from "./types";
export const groupKeys = { export const groupKeys = {
getGroupById: (groupId: string) => [{ groupId }, "group"] as const,
allGroupUserMemberships: () => ["group-user-memberships"] as const, allGroupUserMemberships: () => ["group-user-memberships"] as const,
forGroupUserMemberships: (slug: string) => forGroupUserMemberships: (slug: string) =>
[...groupKeys.allGroupUserMemberships(), slug] as const, [...groupKeys.allGroupUserMemberships(), slug] as const,
@@ -10,22 +13,27 @@ export const groupKeys = {
slug, slug,
offset, offset,
limit, limit,
search search,
filter
}: { }: {
slug: string; slug: string;
offset: number; offset: number;
limit: number; limit: number;
search: string; search: string;
}) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, search }] as const filter?: EFilterReturnedUsers;
}) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, search, filter }] as const
}; };
type TUser = { export const useGetGroupById = (groupId: string) => {
id: string; return useQuery({
email: string; enabled: Boolean(groupId),
username: string; queryKey: groupKeys.getGroupById(groupId),
firstName: string; queryFn: async () => {
lastName: string; const { data } = await apiRequest.get<TGroup>(`/api/v1/groups/${groupId}`);
isPartOfGroup: boolean;
return { group: data };
}
});
}; };
export const useListGroupUsers = ({ export const useListGroupUsers = ({
@@ -33,20 +41,23 @@ export const useListGroupUsers = ({
groupSlug, groupSlug,
offset = 0, offset = 0,
limit = 10, limit = 10,
search search,
filter
}: { }: {
id: string; id: string;
groupSlug: string; groupSlug: string;
offset: number; offset: number;
limit: number; limit: number;
search: string; search: string;
filter?: EFilterReturnedUsers;
}) => { }) => {
return useQuery({ return useQuery({
queryKey: groupKeys.specificGroupUserMemberships({ queryKey: groupKeys.specificGroupUserMemberships({
slug: groupSlug, slug: groupSlug,
offset, offset,
limit, limit,
search search,
filter
}), }),
enabled: Boolean(groupSlug), enabled: Boolean(groupSlug),
keepPreviousData: true, keepPreviousData: true,
@@ -54,10 +65,11 @@ export const useListGroupUsers = ({
const params = new URLSearchParams({ const params = new URLSearchParams({
offset: String(offset), offset: String(offset),
limit: String(limit), limit: String(limit),
search search,
...(filter && { filter })
}); });
const { data } = await apiRequest.get<{ users: TUser[]; totalCount: number }>( const { data } = await apiRequest.get<{ users: TGroupUser[]; totalCount: number }>(
`/api/v1/groups/${id}/users`, `/api/v1/groups/${id}/users`,
{ {
params params
+16 -1
View File
@@ -11,7 +11,7 @@ export type TGroup = {
name: string; name: string;
slug: string; slug: string;
orgId: string; orgId: string;
createAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
role: string; role: string;
}; };
@@ -41,3 +41,18 @@ export type TGroupWithProjectMemberships = {
slug: string; slug: string;
orgId: string; orgId: string;
}; };
export type TGroupUser = {
id: string;
email: string;
username: string;
firstName: string;
lastName: string;
isPartOfGroup: boolean;
joinedGroupAt: Date;
};
export enum EFilterReturnedUsers {
EXISTING_MEMBERS = "existingMembers",
NON_MEMBERS = "nonMembers"
}
+3 -7
View File
@@ -1,6 +1,8 @@
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { slugSchema } from "@app/lib/schemas";
export type Kms = { export type Kms = {
id: string; id: string;
description: string; description: string;
@@ -119,13 +121,7 @@ export const ExternalKmsInputSchema = z.discriminatedUnion("type", [
]); ]);
export const AddExternalKmsSchema = z.object({ export const AddExternalKmsSchema = z.object({
name: z name: slugSchema({ min: 1, field: "Alias" }),
.string()
.trim()
.min(1)
.refine((v) => slugify(v) === v, {
message: "Alias must be a valid slug"
}),
description: z.string().trim().optional(), description: z.string().trim().optional(),
provider: ExternalKmsInputSchema provider: ExternalKmsInputSchema
}); });

Some files were not shown because too many files have changed in this diff Show More