mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-22 13:29:55 +00:00
Compare commits
36 Commits
update-aws
...
policy-del
Author | SHA1 | Date | |
---|---|---|---|
19ff045d2e | |||
aff97374a9 | |||
e8e90585ca | |||
abd9dbf714 | |||
89aed3640b | |||
5513ff7631 | |||
9fb7676739 | |||
6ac734d6c4 | |||
8044999785 | |||
be51e4372d | |||
460b545925 | |||
2f26c1930b | |||
fc9ae05f89 | |||
de22a3c56b | |||
7c4baa6fd4 | |||
f285648c95 | |||
0f04890d8f | |||
61274243e2 | |||
9366428091 | |||
62482852aa | |||
cc02c00b61 | |||
1b4bae6a84 | |||
1f0bcae0fc | |||
d7913a75c2 | |||
8ab51aba12 | |||
3d1f054b87 | |||
e33f34ceb4 | |||
af5805a5ca | |||
bcf1c49a1b | |||
84fedf8eda | |||
97755981eb | |||
8291663802 | |||
d9aed45504 | |||
8ada11edf3 | |||
4bd62aa462 | |||
b80b77ec36 |
@ -8,6 +8,9 @@ import { Lock } from "@app/lib/red-lock";
|
||||
export const mockKeyStore = (): TKeyStoreFactory => {
|
||||
const store: Record<string, string | number | Buffer> = {};
|
||||
|
||||
const getRegex = (pattern: string) =>
|
||||
new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
||||
|
||||
return {
|
||||
setItem: async (key, value) => {
|
||||
store[key] = value;
|
||||
@ -23,7 +26,7 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
||||
return 1;
|
||||
},
|
||||
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
|
||||
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
||||
const regex = getRegex(pattern);
|
||||
let totalDeleted = 0;
|
||||
const keys = Object.keys(store);
|
||||
|
||||
@ -53,6 +56,27 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
||||
incrementBy: async () => {
|
||||
return 1;
|
||||
},
|
||||
getItems: async (keys) => {
|
||||
const values = keys.map((key) => {
|
||||
const value = store[key];
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return values;
|
||||
},
|
||||
getKeysByPattern: async (pattern) => {
|
||||
const regex = getRegex(pattern);
|
||||
const keys = Object.keys(store);
|
||||
return keys.filter((key) => regex.test(key));
|
||||
},
|
||||
deleteItemsByKeyIn: async (keys) => {
|
||||
for (const key of keys) {
|
||||
delete store[key];
|
||||
}
|
||||
return keys.length;
|
||||
},
|
||||
acquireLock: () => {
|
||||
return Promise.resolve({
|
||||
release: () => {}
|
||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@ -74,6 +74,7 @@ import { TAllowedFields } from "@app/services/identity-ldap-auth/identity-ldap-a
|
||||
import { TIdentityOciAuthServiceFactory } from "@app/services/identity-oci-auth/identity-oci-auth-service";
|
||||
import { TIdentityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth/identity-oidc-auth-service";
|
||||
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||
import { TIdentityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-types";
|
||||
import { TIdentityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
|
||||
import { TIdentityUaServiceFactory } from "@app/services/identity-ua/identity-ua-service";
|
||||
import { TIntegrationServiceFactory } from "@app/services/integration/integration-service";
|
||||
@ -218,6 +219,7 @@ declare module "fastify" {
|
||||
identityKubernetesAuth: TIdentityKubernetesAuthServiceFactory;
|
||||
identityGcpAuth: TIdentityGcpAuthServiceFactory;
|
||||
identityAliCloudAuth: TIdentityAliCloudAuthServiceFactory;
|
||||
identityTlsCertAuth: TIdentityTlsCertAuthServiceFactory;
|
||||
identityAwsAuth: TIdentityAwsAuthServiceFactory;
|
||||
identityAzureAuth: TIdentityAzureAuthServiceFactory;
|
||||
identityOciAuth: TIdentityOciAuthServiceFactory;
|
||||
|
8
backend/src/@types/knex.d.ts
vendored
8
backend/src/@types/knex.d.ts
vendored
@ -164,6 +164,9 @@ import {
|
||||
TIdentityProjectMemberships,
|
||||
TIdentityProjectMembershipsInsert,
|
||||
TIdentityProjectMembershipsUpdate,
|
||||
TIdentityTlsCertAuths,
|
||||
TIdentityTlsCertAuthsInsert,
|
||||
TIdentityTlsCertAuthsUpdate,
|
||||
TIdentityTokenAuths,
|
||||
TIdentityTokenAuthsInsert,
|
||||
TIdentityTokenAuthsUpdate,
|
||||
@ -794,6 +797,11 @@ declare module "knex/types/tables" {
|
||||
TIdentityAlicloudAuthsInsert,
|
||||
TIdentityAlicloudAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityTlsCertAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityTlsCertAuths,
|
||||
TIdentityTlsCertAuthsInsert,
|
||||
TIdentityTlsCertAuthsUpdate
|
||||
>;
|
||||
[TableName.IdentityAwsAuth]: KnexOriginal.CompositeTableType<
|
||||
TIdentityAwsAuths,
|
||||
TIdentityAwsAuthsInsert,
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasTable(TableName.IdentityTlsCertAuth))) {
|
||||
await knex.schema.createTable(TableName.IdentityTlsCertAuth, (t) => {
|
||||
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||
t.bigInteger("accessTokenTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenMaxTTL").defaultTo(7200).notNullable();
|
||||
t.bigInteger("accessTokenNumUsesLimit").defaultTo(0).notNullable();
|
||||
t.jsonb("accessTokenTrustedIps").notNullable();
|
||||
t.timestamps(true, true, true);
|
||||
t.uuid("identityId").notNullable().unique();
|
||||
t.foreign("identityId").references("id").inTable(TableName.Identity).onDelete("CASCADE");
|
||||
t.string("allowedCommonNames").nullable();
|
||||
t.binary("encryptedCaCertificate").notNullable();
|
||||
});
|
||||
}
|
||||
|
||||
await createOnUpdateTrigger(knex, TableName.IdentityTlsCertAuth);
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.dropTableIfExists(TableName.IdentityTlsCertAuth);
|
||||
await dropOnUpdateTrigger(knex, TableName.IdentityTlsCertAuth);
|
||||
}
|
27
backend/src/db/schemas/identity-tls-cert-auths.ts
Normal file
27
backend/src/db/schemas/identity-tls-cert-auths.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// Code generated by automation script, DO NOT EDIT.
|
||||
// Automated by pulling database and generating zod schema
|
||||
// To update. Just run npm run generate:schema
|
||||
// Written by akhilmhdh.
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { zodBuffer } from "@app/lib/zod";
|
||||
|
||||
import { TImmutableDBKeys } from "./models";
|
||||
|
||||
export const IdentityTlsCertAuthsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
accessTokenTTL: z.coerce.number().default(7200),
|
||||
accessTokenMaxTTL: z.coerce.number().default(7200),
|
||||
accessTokenNumUsesLimit: z.coerce.number().default(0),
|
||||
accessTokenTrustedIps: z.unknown(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
identityId: z.string().uuid(),
|
||||
allowedCommonNames: z.string().nullable().optional(),
|
||||
encryptedCaCertificate: zodBuffer
|
||||
});
|
||||
|
||||
export type TIdentityTlsCertAuths = z.infer<typeof IdentityTlsCertAuthsSchema>;
|
||||
export type TIdentityTlsCertAuthsInsert = Omit<z.input<typeof IdentityTlsCertAuthsSchema>, TImmutableDBKeys>;
|
||||
export type TIdentityTlsCertAuthsUpdate = Partial<Omit<z.input<typeof IdentityTlsCertAuthsSchema>, TImmutableDBKeys>>;
|
@ -52,6 +52,7 @@ export * from "./identity-org-memberships";
|
||||
export * from "./identity-project-additional-privilege";
|
||||
export * from "./identity-project-membership-role";
|
||||
export * from "./identity-project-memberships";
|
||||
export * from "./identity-tls-cert-auths";
|
||||
export * from "./identity-token-auths";
|
||||
export * from "./identity-ua-client-secrets";
|
||||
export * from "./identity-universal-auths";
|
||||
|
@ -86,6 +86,7 @@ export enum TableName {
|
||||
IdentityOidcAuth = "identity_oidc_auths",
|
||||
IdentityJwtAuth = "identity_jwt_auths",
|
||||
IdentityLdapAuth = "identity_ldap_auths",
|
||||
IdentityTlsCertAuth = "identity_tls_cert_auths",
|
||||
IdentityOrgMembership = "identity_org_memberships",
|
||||
IdentityProjectMembership = "identity_project_memberships",
|
||||
IdentityProjectMembershipRole = "identity_project_membership_role",
|
||||
@ -251,6 +252,7 @@ export enum IdentityAuthMethod {
|
||||
ALICLOUD_AUTH = "alicloud-auth",
|
||||
AWS_AUTH = "aws-auth",
|
||||
AZURE_AUTH = "azure-auth",
|
||||
TLS_CERT_AUTH = "tls-cert-auth",
|
||||
OCI_AUTH = "oci-auth",
|
||||
OIDC_AUTH = "oidc-auth",
|
||||
JWT_AUTH = "jwt-auth",
|
||||
|
@ -60,7 +60,8 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
method: "GET",
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectSlug: z.string().trim()
|
||||
projectSlug: z.string().trim(),
|
||||
policyId: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -73,6 +74,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
handler: async (req) => {
|
||||
const { count } = await server.services.accessApprovalRequest.getCount({
|
||||
projectSlug: req.query.projectSlug,
|
||||
policyId: req.query.policyId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
|
@ -94,7 +94,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
workspaceId: z.string().trim()
|
||||
workspaceId: z.string().trim(),
|
||||
policyId: z.string().trim().optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
@ -112,7 +113,8 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
projectId: req.query.workspaceId
|
||||
projectId: req.query.workspaceId,
|
||||
policyId: req.query.policyId
|
||||
});
|
||||
return { approvals };
|
||||
}
|
||||
|
@ -80,6 +80,7 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignSshKey,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
principals: req.body.principals,
|
||||
@ -171,6 +172,7 @@ export const registerSshCertRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueSshCreds,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
principals: req.body.principals,
|
||||
|
@ -358,6 +358,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueSshHostUserCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
sshHostId: req.params.sshHostId,
|
||||
hostname: host.hostname,
|
||||
@ -427,6 +428,7 @@ export const registerSshHostRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueSshHostHostCert,
|
||||
organizationId: req.permission.orgId,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
sshHostId: req.params.sshHostId,
|
||||
|
@ -220,7 +220,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
bypassers: string[];
|
||||
}[]
|
||||
>;
|
||||
getCount: ({ projectId }: { projectId: string }) => Promise<{
|
||||
getCount: ({ projectId }: { projectId: string; policyId?: string }) => Promise<{
|
||||
pendingCount: number;
|
||||
finalizedCount: number;
|
||||
}>;
|
||||
@ -702,7 +702,7 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
}
|
||||
};
|
||||
|
||||
const getCount: TAccessApprovalRequestDALFactory["getCount"] = async ({ projectId }) => {
|
||||
const getCount: TAccessApprovalRequestDALFactory["getCount"] = async ({ projectId, policyId }) => {
|
||||
try {
|
||||
const accessRequests = await db
|
||||
.replicaNode()(TableName.AccessApprovalRequest)
|
||||
@ -723,8 +723,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
`${TableName.AccessApprovalRequest}.id`,
|
||||
`${TableName.AccessApprovalRequestReviewer}.requestId`
|
||||
)
|
||||
|
||||
.where(`${TableName.Environment}.projectId`, projectId)
|
||||
.where((qb) => {
|
||||
if (policyId) void qb.where(`${TableName.AccessApprovalPolicy}.id`, policyId);
|
||||
})
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
.select(db.ref("status").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerStatus"))
|
||||
.select(db.ref("reviewerUserId").withSchema(TableName.AccessApprovalRequestReviewer).as("reviewerUserId"))
|
||||
|
@ -560,6 +560,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
|
||||
const getCount: TAccessApprovalRequestServiceFactory["getCount"] = async ({
|
||||
projectSlug,
|
||||
policyId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorId,
|
||||
@ -580,7 +581,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
throw new ForbiddenRequestError({ message: "You are not a member of this project" });
|
||||
}
|
||||
|
||||
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id });
|
||||
const count = await accessApprovalRequestDAL.getCount({ projectId: project.id, policyId });
|
||||
|
||||
return { count };
|
||||
};
|
||||
|
@ -12,6 +12,7 @@ export type TVerifyPermission = {
|
||||
|
||||
export type TGetAccessRequestCountDTO = {
|
||||
projectSlug: string;
|
||||
policyId?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TReviewAccessRequestDTO = {
|
||||
|
@ -202,6 +202,12 @@ export enum EventType {
|
||||
REVOKE_IDENTITY_ALICLOUD_AUTH = "revoke-identity-alicloud-auth",
|
||||
GET_IDENTITY_ALICLOUD_AUTH = "get-identity-alicloud-auth",
|
||||
|
||||
LOGIN_IDENTITY_TLS_CERT_AUTH = "login-identity-tls-cert-auth",
|
||||
ADD_IDENTITY_TLS_CERT_AUTH = "add-identity-tls-cert-auth",
|
||||
UPDATE_IDENTITY_TLS_CERT_AUTH = "update-identity-tls-cert-auth",
|
||||
REVOKE_IDENTITY_TLS_CERT_AUTH = "revoke-identity-tls-cert-auth",
|
||||
GET_IDENTITY_TLS_CERT_AUTH = "get-identity-tls-cert-auth",
|
||||
|
||||
LOGIN_IDENTITY_AWS_AUTH = "login-identity-aws-auth",
|
||||
ADD_IDENTITY_AWS_AUTH = "add-identity-aws-auth",
|
||||
UPDATE_IDENTITY_AWS_AUTH = "update-identity-aws-auth",
|
||||
@ -1141,6 +1147,53 @@ interface GetIdentityAliCloudAuthEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityTlsCertAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
identityTlsCertAuthId: string;
|
||||
identityAccessTokenId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddIdentityTlsCertAuthEvent {
|
||||
type: EventType.ADD_IDENTITY_TLS_CERT_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
allowedCommonNames: string | null | undefined;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteIdentityTlsCertAuthEvent {
|
||||
type: EventType.REVOKE_IDENTITY_TLS_CERT_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateIdentityTlsCertAuthEvent {
|
||||
type: EventType.UPDATE_IDENTITY_TLS_CERT_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
allowedCommonNames: string | null | undefined;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetIdentityTlsCertAuthEvent {
|
||||
type: EventType.GET_IDENTITY_TLS_CERT_AUTH;
|
||||
metadata: {
|
||||
identityId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface LoginIdentityOciAuthEvent {
|
||||
type: EventType.LOGIN_IDENTITY_OCI_AUTH;
|
||||
metadata: {
|
||||
@ -3358,6 +3411,11 @@ export type Event =
|
||||
| UpdateIdentityAliCloudAuthEvent
|
||||
| GetIdentityAliCloudAuthEvent
|
||||
| DeleteIdentityAliCloudAuthEvent
|
||||
| LoginIdentityTlsCertAuthEvent
|
||||
| AddIdentityTlsCertAuthEvent
|
||||
| UpdateIdentityTlsCertAuthEvent
|
||||
| GetIdentityTlsCertAuthEvent
|
||||
| DeleteIdentityTlsCertAuthEvent
|
||||
| LoginIdentityOciAuthEvent
|
||||
| AddIdentityOciAuthEvent
|
||||
| UpdateIdentityOciAuthEvent
|
||||
|
@ -290,7 +290,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findProjectRequestCount = async (projectId: string, userId: string, tx?: Knex) => {
|
||||
const findProjectRequestCount = async (projectId: string, userId: string, policyId?: string, tx?: Knex) => {
|
||||
try {
|
||||
const docs = await (tx || db)
|
||||
.with(
|
||||
@ -309,6 +309,9 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalPolicy}.id`
|
||||
)
|
||||
.where({ projectId })
|
||||
.where((qb) => {
|
||||
if (policyId) void qb.where(`${TableName.SecretApprovalPolicy}.id`, policyId);
|
||||
})
|
||||
.andWhere(
|
||||
(bd) =>
|
||||
void bd
|
||||
|
@ -168,7 +168,14 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
microsoftTeamsService,
|
||||
folderCommitService
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod }: TApprovalRequestCountDTO) => {
|
||||
const requestCount = async ({
|
||||
projectId,
|
||||
policyId,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod
|
||||
}: TApprovalRequestCountDTO) => {
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
await permissionService.getProjectPermission({
|
||||
@ -180,7 +187,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.SecretManager
|
||||
});
|
||||
|
||||
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId);
|
||||
const count = await secretApprovalRequestDAL.findProjectRequestCount(projectId, actorId, policyId);
|
||||
return count;
|
||||
};
|
||||
|
||||
|
@ -84,7 +84,7 @@ export type TReviewRequestDTO = {
|
||||
comment?: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TApprovalRequestCountDTO = TProjectPermission;
|
||||
export type TApprovalRequestCountDTO = TProjectPermission & { policyId?: string };
|
||||
|
||||
export type TListApprovalsDTO = {
|
||||
projectId: string;
|
||||
|
@ -1,17 +1,11 @@
|
||||
import { ProbotOctokit } from "probot";
|
||||
|
||||
import { OrgMembershipRole, TableName } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { InternalServerError } from "@app/lib/errors";
|
||||
import { TQueueServiceFactory } from "@app/queue";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
import { TTelemetryServiceFactory } from "@app/services/telemetry/telemetry-service";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
import { TSecretScanningDALFactory } from "../secret-scanning-dal";
|
||||
import { scanContentAndGetFindings, scanFullRepoContentAndGetFindings } from "./secret-scanning-fns";
|
||||
import { SecretMatch, TScanFullRepoEventPayload, TScanPushEventPayload } from "./secret-scanning-queue-types";
|
||||
import { TScanFullRepoEventPayload, TScanPushEventPayload } from "./secret-scanning-queue-types";
|
||||
|
||||
type TSecretScanningQueueFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
@ -23,227 +17,21 @@ type TSecretScanningQueueFactoryDep = {
|
||||
|
||||
export type TSecretScanningQueueFactory = ReturnType<typeof secretScanningQueueFactory>;
|
||||
|
||||
export const secretScanningQueueFactory = ({
|
||||
queueService,
|
||||
secretScanningDAL,
|
||||
smtpService,
|
||||
telemetryService,
|
||||
orgMembershipDAL: orgMemberDAL
|
||||
}: TSecretScanningQueueFactoryDep) => {
|
||||
const startFullRepoScan = async (payload: TScanFullRepoEventPayload) => {
|
||||
await queueService.queue(QueueName.SecretFullRepoScan, QueueJobs.SecretScan, payload, {
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: {
|
||||
count: 20 // keep the most recent 20 jobs
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const secretScanningQueueFactory = (_props: TSecretScanningQueueFactoryDep) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const startFullRepoScan = async (_payload: TScanFullRepoEventPayload) => {
|
||||
throw new InternalServerError({
|
||||
message: "Secret Scanning V1 has been deprecated. Please migrate to Secret Scanning V2"
|
||||
});
|
||||
};
|
||||
|
||||
const startPushEventScan = async (payload: TScanPushEventPayload) => {
|
||||
await queueService.queue(QueueName.SecretPushEventScan, QueueJobs.SecretScan, payload, {
|
||||
attempts: 3,
|
||||
backoff: {
|
||||
type: "exponential",
|
||||
delay: 5000
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: {
|
||||
count: 20 // keep the most recent 20 jobs
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const startPushEventScan = async (_payload: TScanPushEventPayload) => {
|
||||
throw new InternalServerError({
|
||||
message: "Secret Scanning V1 has been deprecated. Please migrate to Secret Scanning V2"
|
||||
});
|
||||
};
|
||||
|
||||
const getOrgAdminEmails = async (organizationId: string) => {
|
||||
// get emails of admins
|
||||
const adminsOfWork = await orgMemberDAL.findMembership({
|
||||
[`${TableName.Organization}.id` as string]: organizationId,
|
||||
role: OrgMembershipRole.Admin
|
||||
});
|
||||
return adminsOfWork.filter((userObject) => userObject.email).map((userObject) => userObject.email as string);
|
||||
};
|
||||
|
||||
queueService.start(QueueName.SecretPushEventScan, async (job) => {
|
||||
const appCfg = getConfig();
|
||||
const { organizationId, commits, pusher, repository, installationId } = job.data;
|
||||
const [owner, repo] = repository.fullName.split("/");
|
||||
const octokit = new ProbotOctokit({
|
||||
auth: {
|
||||
appId: appCfg.SECRET_SCANNING_GIT_APP_ID,
|
||||
privateKey: appCfg.SECRET_SCANNING_PRIVATE_KEY,
|
||||
installationId
|
||||
}
|
||||
});
|
||||
const allFindingsByFingerprint: { [key: string]: SecretMatch } = {};
|
||||
|
||||
for (const commit of commits) {
|
||||
for (const filepath of [...commit.added, ...commit.modified]) {
|
||||
// eslint-disable-next-line
|
||||
const fileContentsResponse = await octokit.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: filepath
|
||||
});
|
||||
|
||||
const { data } = fileContentsResponse;
|
||||
const fileContent = Buffer.from((data as { content: string }).content, "base64").toString();
|
||||
|
||||
// eslint-disable-next-line
|
||||
const findings = await scanContentAndGetFindings(`\n${fileContent}`); // extra line to count lines correctly
|
||||
|
||||
for (const finding of findings) {
|
||||
const fingerPrintWithCommitId = `${commit.id}:${filepath}:${finding.RuleID}:${finding.StartLine}`;
|
||||
const fingerPrintWithoutCommitId = `${filepath}:${finding.RuleID}:${finding.StartLine}`;
|
||||
finding.Fingerprint = fingerPrintWithCommitId;
|
||||
finding.FingerPrintWithoutCommitId = fingerPrintWithoutCommitId;
|
||||
finding.Commit = commit.id;
|
||||
finding.File = filepath;
|
||||
finding.Author = commit.author.name;
|
||||
finding.Email = commit?.author?.email ? commit?.author?.email : "";
|
||||
|
||||
allFindingsByFingerprint[fingerPrintWithCommitId] = finding;
|
||||
}
|
||||
}
|
||||
}
|
||||
await secretScanningDAL.transaction(async (tx) => {
|
||||
if (!Object.keys(allFindingsByFingerprint).length) return;
|
||||
await secretScanningDAL.upsert(
|
||||
Object.keys(allFindingsByFingerprint).map((key) => ({
|
||||
installationId,
|
||||
email: allFindingsByFingerprint[key].Email,
|
||||
author: allFindingsByFingerprint[key].Author,
|
||||
date: allFindingsByFingerprint[key].Date,
|
||||
file: allFindingsByFingerprint[key].File,
|
||||
tags: allFindingsByFingerprint[key].Tags,
|
||||
commit: allFindingsByFingerprint[key].Commit,
|
||||
ruleID: allFindingsByFingerprint[key].RuleID,
|
||||
endLine: String(allFindingsByFingerprint[key].EndLine),
|
||||
entropy: String(allFindingsByFingerprint[key].Entropy),
|
||||
message: allFindingsByFingerprint[key].Message,
|
||||
endColumn: String(allFindingsByFingerprint[key].EndColumn),
|
||||
startLine: String(allFindingsByFingerprint[key].StartLine),
|
||||
startColumn: String(allFindingsByFingerprint[key].StartColumn),
|
||||
fingerPrintWithoutCommitId: allFindingsByFingerprint[key].FingerPrintWithoutCommitId,
|
||||
description: allFindingsByFingerprint[key].Description,
|
||||
symlinkFile: allFindingsByFingerprint[key].SymlinkFile,
|
||||
orgId: organizationId,
|
||||
pusherEmail: pusher.email,
|
||||
pusherName: pusher.name,
|
||||
repositoryFullName: repository.fullName,
|
||||
repositoryId: String(repository.id),
|
||||
fingerprint: allFindingsByFingerprint[key].Fingerprint
|
||||
})),
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
const adminEmails = await getOrgAdminEmails(organizationId);
|
||||
if (pusher?.email) {
|
||||
adminEmails.push(pusher.email);
|
||||
}
|
||||
if (Object.keys(allFindingsByFingerprint).length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.SecretLeakIncident,
|
||||
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`,
|
||||
recipients: adminEmails.filter((email) => email).map((email) => email),
|
||||
substitutions: {
|
||||
numberOfSecrets: Object.keys(allFindingsByFingerprint).length,
|
||||
pusher_email: pusher.email,
|
||||
pusher_name: pusher.name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await telemetryService.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretScannerPush,
|
||||
distinctId: repository.fullName,
|
||||
properties: {
|
||||
numberOfRisks: Object.keys(allFindingsByFingerprint).length
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
queueService.start(QueueName.SecretFullRepoScan, async (job) => {
|
||||
const appCfg = getConfig();
|
||||
const { organizationId, repository, installationId } = job.data;
|
||||
const octokit = new ProbotOctokit({
|
||||
auth: {
|
||||
appId: appCfg.SECRET_SCANNING_GIT_APP_ID,
|
||||
privateKey: appCfg.SECRET_SCANNING_PRIVATE_KEY,
|
||||
installationId
|
||||
}
|
||||
});
|
||||
|
||||
const findings = await scanFullRepoContentAndGetFindings(
|
||||
// this is because of collision of octokit in probot and github
|
||||
// eslint-disable-next-line
|
||||
octokit as any,
|
||||
installationId,
|
||||
repository.fullName
|
||||
);
|
||||
await secretScanningDAL.transaction(async (tx) => {
|
||||
if (!findings.length) return;
|
||||
// eslint-disable-next-line
|
||||
await secretScanningDAL.upsert(
|
||||
findings.map((finding) => ({
|
||||
installationId,
|
||||
email: finding.Email,
|
||||
author: finding.Author,
|
||||
date: finding.Date,
|
||||
file: finding.File,
|
||||
tags: finding.Tags,
|
||||
commit: finding.Commit,
|
||||
ruleID: finding.RuleID,
|
||||
endLine: String(finding.EndLine),
|
||||
entropy: String(finding.Entropy),
|
||||
message: finding.Message,
|
||||
endColumn: String(finding.EndColumn),
|
||||
startLine: String(finding.StartLine),
|
||||
startColumn: String(finding.StartColumn),
|
||||
fingerPrintWithoutCommitId: finding.FingerPrintWithoutCommitId,
|
||||
description: finding.Description,
|
||||
symlinkFile: finding.SymlinkFile,
|
||||
orgId: organizationId,
|
||||
repositoryFullName: repository.fullName,
|
||||
repositoryId: String(repository.id),
|
||||
fingerprint: finding.Fingerprint
|
||||
})),
|
||||
tx
|
||||
);
|
||||
});
|
||||
|
||||
const adminEmails = await getOrgAdminEmails(organizationId);
|
||||
if (findings.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.SecretLeakIncident,
|
||||
subjectLine: `Incident alert: leaked secrets found in Github repository ${repository.fullName}`,
|
||||
recipients: adminEmails.filter((email) => email).map((email) => email),
|
||||
substitutions: {
|
||||
numberOfSecrets: findings.length
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await telemetryService.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretScannerFull,
|
||||
distinctId: repository.fullName,
|
||||
properties: {
|
||||
numberOfRisks: findings.length
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
queueService.listen(QueueName.SecretPushEventScan, "failed", (job, err) => {
|
||||
logger.error(err, "Failed to secret scan on push", job?.data);
|
||||
});
|
||||
|
||||
queueService.listen(QueueName.SecretFullRepoScan, "failed", (job, err) => {
|
||||
logger.error(err, "Failed to do full repo secret scan", job?.data);
|
||||
});
|
||||
|
||||
return { startFullRepoScan, startPushEventScan };
|
||||
};
|
||||
|
@ -98,6 +98,7 @@ export const secretScanningServiceFactory = ({
|
||||
if (canUseSecretScanning(actorOrgId)) {
|
||||
await Promise.all(
|
||||
repositories.map(({ id, full_name }) =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-member-access
|
||||
secretScanningQueue.startFullRepoScan({
|
||||
organizationId: session.orgId,
|
||||
installationId,
|
||||
@ -180,6 +181,7 @@ export const secretScanningServiceFactory = ({
|
||||
if (!installationLink) return;
|
||||
|
||||
if (canUseSecretScanning(installationLink.orgId)) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-member-access
|
||||
await secretScanningQueue.startPushEventScan({
|
||||
commits,
|
||||
pusher: { name: pusher.name, email: pusher.email },
|
||||
|
@ -73,6 +73,7 @@ type TWaitTillReady = {
|
||||
export type TKeyStoreFactory = {
|
||||
setItem: (key: string, value: string | number | Buffer, prefix?: string) => Promise<"OK">;
|
||||
getItem: (key: string, prefix?: string) => Promise<string | null>;
|
||||
getItems: (keys: string[], prefix?: string) => Promise<(string | null)[]>;
|
||||
setExpiry: (key: string, expiryInSeconds: number) => Promise<number>;
|
||||
setItemWithExpiry: (
|
||||
key: string,
|
||||
@ -81,6 +82,7 @@ export type TKeyStoreFactory = {
|
||||
prefix?: string
|
||||
) => Promise<"OK">;
|
||||
deleteItem: (key: string) => Promise<number>;
|
||||
deleteItemsByKeyIn: (keys: string[]) => Promise<number>;
|
||||
deleteItems: (arg: TDeleteItems) => Promise<number>;
|
||||
incrementBy: (key: string, value: number) => Promise<number>;
|
||||
acquireLock(
|
||||
@ -89,6 +91,7 @@ export type TKeyStoreFactory = {
|
||||
settings?: Partial<Settings>
|
||||
): Promise<{ release: () => Promise<ExecutionResult> }>;
|
||||
waitTillReady: ({ key, waitingCb, keyCheckCb, waitIteration, delay, jitter }: TWaitTillReady) => Promise<void>;
|
||||
getKeysByPattern: (pattern: string, limit?: number) => Promise<string[]>;
|
||||
};
|
||||
|
||||
export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFactory => {
|
||||
@ -100,6 +103,9 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac
|
||||
|
||||
const getItem = async (key: string, prefix?: string) => redis.get(prefix ? `${prefix}:${key}` : key);
|
||||
|
||||
const getItems = async (keys: string[], prefix?: string) =>
|
||||
redis.mget(keys.map((key) => (prefix ? `${prefix}:${key}` : key)));
|
||||
|
||||
const setItemWithExpiry = async (
|
||||
key: string,
|
||||
expiryInSeconds: number | string,
|
||||
@ -109,6 +115,11 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac
|
||||
|
||||
const deleteItem = async (key: string) => redis.del(key);
|
||||
|
||||
const deleteItemsByKeyIn = async (keys: string[]) => {
|
||||
if (keys.length === 0) return 0;
|
||||
return redis.del(keys);
|
||||
};
|
||||
|
||||
const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => {
|
||||
let cursor = "0";
|
||||
let totalDeleted = 0;
|
||||
@ -164,6 +175,24 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac
|
||||
}
|
||||
};
|
||||
|
||||
const getKeysByPattern = async (pattern: string, limit?: number) => {
|
||||
let cursor = "0";
|
||||
const allKeys: string[] = [];
|
||||
|
||||
do {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000);
|
||||
cursor = nextCursor;
|
||||
allKeys.push(...keys);
|
||||
|
||||
if (limit && allKeys.length >= limit) {
|
||||
return allKeys.slice(0, limit);
|
||||
}
|
||||
} while (cursor !== "0");
|
||||
|
||||
return allKeys;
|
||||
};
|
||||
|
||||
return {
|
||||
setItem,
|
||||
getItem,
|
||||
@ -175,6 +204,9 @@ export const keyStoreFactory = (redisConfigKeys: TRedisConfigKeys): TKeyStoreFac
|
||||
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
||||
return redisLock.acquire(resources, duration, settings);
|
||||
},
|
||||
waitTillReady
|
||||
waitTillReady,
|
||||
getKeysByPattern,
|
||||
deleteItemsByKeyIn,
|
||||
getItems
|
||||
};
|
||||
};
|
||||
|
@ -8,6 +8,8 @@ import { TKeyStoreFactory } from "./keystore";
|
||||
|
||||
export const inMemoryKeyStore = (): TKeyStoreFactory => {
|
||||
const store: Record<string, string | number | Buffer> = {};
|
||||
const getRegex = (pattern: string) =>
|
||||
new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
||||
|
||||
return {
|
||||
setItem: async (key, value) => {
|
||||
@ -24,7 +26,7 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => {
|
||||
return 1;
|
||||
},
|
||||
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
|
||||
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
|
||||
const regex = getRegex(pattern);
|
||||
let totalDeleted = 0;
|
||||
const keys = Object.keys(store);
|
||||
|
||||
@ -59,6 +61,27 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => {
|
||||
release: () => {}
|
||||
}) as Promise<Lock>;
|
||||
},
|
||||
waitTillReady: async () => {}
|
||||
waitTillReady: async () => {},
|
||||
getKeysByPattern: async (pattern) => {
|
||||
const regex = getRegex(pattern);
|
||||
const keys = Object.keys(store);
|
||||
return keys.filter((key) => regex.test(key));
|
||||
},
|
||||
deleteItemsByKeyIn: async (keys) => {
|
||||
for (const key of keys) {
|
||||
delete store[key];
|
||||
}
|
||||
return keys.length;
|
||||
},
|
||||
getItems: async (keys) => {
|
||||
const values = keys.map((key) => {
|
||||
const value = store[key];
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return values;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -22,6 +22,7 @@ export enum ApiDocsTags {
|
||||
UniversalAuth = "Universal Auth",
|
||||
GcpAuth = "GCP Auth",
|
||||
AliCloudAuth = "Alibaba Cloud Auth",
|
||||
TlsCertAuth = "TLS Certificate Auth",
|
||||
AwsAuth = "AWS Auth",
|
||||
OciAuth = "OCI Auth",
|
||||
AzureAuth = "Azure Auth",
|
||||
@ -283,6 +284,38 @@ export const ALICLOUD_AUTH = {
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const TLS_CERT_AUTH = {
|
||||
LOGIN: {
|
||||
identityId: "The ID of the identity to login."
|
||||
},
|
||||
ATTACH: {
|
||||
identityId: "The ID of the identity to attach the configuration onto.",
|
||||
allowedCommonNames:
|
||||
"The comma-separated list of trusted common names that are allowed to authenticate with Infisical.",
|
||||
caCertificate: "The PEM-encoded CA certificate to validate client certificates.",
|
||||
accessTokenTTL: "The lifetime for an access token in seconds.",
|
||||
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
|
||||
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used.",
|
||||
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
|
||||
},
|
||||
UPDATE: {
|
||||
identityId: "The ID of the identity to update the auth method for.",
|
||||
allowedCommonNames:
|
||||
"The comma-separated list of trusted common names that are allowed to authenticate with Infisical.",
|
||||
caCertificate: "The PEM-encoded CA certificate to validate client certificates.",
|
||||
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
||||
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
|
||||
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
|
||||
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
|
||||
},
|
||||
RETRIEVE: {
|
||||
identityId: "The ID of the identity to retrieve the auth method for."
|
||||
},
|
||||
REVOKE: {
|
||||
identityId: "The ID of the identity to revoke the auth method for."
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const AWS_AUTH = {
|
||||
LOGIN: {
|
||||
identityId: "The ID of the identity to login.",
|
||||
|
@ -193,6 +193,9 @@ const envSchema = z
|
||||
PYLON_API_KEY: zpStr(z.string().optional()),
|
||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
|
||||
IDENTITY_TLS_CERT_AUTH_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default(
|
||||
"x-identity-tls-cert-auth-client-cert"
|
||||
),
|
||||
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()),
|
||||
ENABLE_MSSQL_SECRET_ROTATION_ENCRYPT: zodStrBool.default("true"),
|
||||
|
@ -62,7 +62,8 @@ export enum QueueName {
|
||||
SecretRotationV2 = "secret-rotation-v2",
|
||||
FolderTreeCheckpoint = "folder-tree-checkpoint",
|
||||
InvalidateCache = "invalidate-cache",
|
||||
SecretScanningV2 = "secret-scanning-v2"
|
||||
SecretScanningV2 = "secret-scanning-v2",
|
||||
TelemetryAggregatedEvents = "telemetry-aggregated-events"
|
||||
}
|
||||
|
||||
export enum QueueJobs {
|
||||
@ -101,7 +102,8 @@ export enum QueueJobs {
|
||||
SecretScanningV2DiffScan = "secret-scanning-v2-diff-scan",
|
||||
SecretScanningV2SendNotification = "secret-scanning-v2-notification",
|
||||
CaOrderCertificateForSubscriber = "ca-order-certificate-for-subscriber",
|
||||
PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal"
|
||||
PkiSubscriberDailyAutoRenewal = "pki-subscriber-daily-auto-renewal",
|
||||
TelemetryAggregatedEvents = "telemetry-aggregated-events"
|
||||
}
|
||||
|
||||
export type TQueueJobTypes = {
|
||||
@ -292,6 +294,10 @@ export type TQueueJobTypes = {
|
||||
name: QueueJobs.PkiSubscriberDailyAutoRenewal;
|
||||
payload: undefined;
|
||||
};
|
||||
[QueueName.TelemetryAggregatedEvents]: {
|
||||
name: QueueJobs.TelemetryAggregatedEvents;
|
||||
payload: undefined;
|
||||
};
|
||||
};
|
||||
|
||||
const SECRET_SCANNING_JOBS = [
|
||||
|
@ -193,6 +193,8 @@ import { identityOidcAuthServiceFactory } from "@app/services/identity-oidc-auth
|
||||
import { identityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
import { identityProjectMembershipRoleDALFactory } from "@app/services/identity-project/identity-project-membership-role-dal";
|
||||
import { identityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
|
||||
import { identityTlsCertAuthDALFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-dal";
|
||||
import { identityTlsCertAuthServiceFactory } from "@app/services/identity-tls-cert-auth/identity-tls-cert-auth-service";
|
||||
import { identityTokenAuthDALFactory } from "@app/services/identity-token-auth/identity-token-auth-dal";
|
||||
import { identityTokenAuthServiceFactory } from "@app/services/identity-token-auth/identity-token-auth-service";
|
||||
import { identityUaClientSecretDALFactory } from "@app/services/identity-ua/identity-ua-client-secret-dal";
|
||||
@ -297,7 +299,6 @@ import { injectAssumePrivilege } from "../plugins/auth/inject-assume-privilege";
|
||||
import { injectIdentity } from "../plugins/auth/inject-identity";
|
||||
import { injectPermission } from "../plugins/auth/inject-permission";
|
||||
import { injectRateLimits } from "../plugins/inject-rate-limits";
|
||||
import { registerSecretScannerGhApp } from "../plugins/secret-scanner";
|
||||
import { registerV1Routes } from "./v1";
|
||||
import { registerV2Routes } from "./v2";
|
||||
import { registerV3Routes } from "./v3";
|
||||
@ -326,7 +327,6 @@ export const registerRoutes = async (
|
||||
}
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
await server.register(registerSecretScannerGhApp, { prefix: "/ss-webhook" });
|
||||
await server.register(registerSecretScanningV2Webhooks, {
|
||||
prefix: "/secret-scanning/webhooks"
|
||||
});
|
||||
@ -386,6 +386,7 @@ export const registerRoutes = async (
|
||||
const identityKubernetesAuthDAL = identityKubernetesAuthDALFactory(db);
|
||||
const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db);
|
||||
const identityAliCloudAuthDAL = identityAliCloudAuthDALFactory(db);
|
||||
const identityTlsCertAuthDAL = identityTlsCertAuthDALFactory(db);
|
||||
const identityAwsAuthDAL = identityAwsAuthDALFactory(db);
|
||||
const identityGcpAuthDAL = identityGcpAuthDALFactory(db);
|
||||
const identityOciAuthDAL = identityOciAuthDALFactory(db);
|
||||
@ -686,7 +687,8 @@ export const registerRoutes = async (
|
||||
const telemetryQueue = telemetryQueueServiceFactory({
|
||||
keyStore,
|
||||
telemetryDAL,
|
||||
queueService
|
||||
queueService,
|
||||
telemetryService
|
||||
});
|
||||
|
||||
const invalidateCacheQueue = invalidateCacheQueueFactory({
|
||||
@ -1493,6 +1495,15 @@ export const registerRoutes = async (
|
||||
permissionService
|
||||
});
|
||||
|
||||
const identityTlsCertAuthService = identityTlsCertAuthServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityTlsCertAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
permissionService,
|
||||
kmsService
|
||||
});
|
||||
|
||||
const identityAwsAuthService = identityAwsAuthServiceFactory({
|
||||
identityAccessTokenDAL,
|
||||
identityAwsAuthDAL,
|
||||
@ -1947,6 +1958,7 @@ export const registerRoutes = async (
|
||||
identityAwsAuth: identityAwsAuthService,
|
||||
identityAzureAuth: identityAzureAuthService,
|
||||
identityOciAuth: identityOciAuthService,
|
||||
identityTlsCertAuth: identityTlsCertAuthService,
|
||||
identityOidcAuth: identityOidcAuthService,
|
||||
identityJwtAuth: identityJwtAuthService,
|
||||
identityLdapAuth: identityLdapAuthService,
|
||||
|
@ -722,6 +722,7 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.InvalidateCache,
|
||||
organizationId: req.permission.orgId,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
...req.auditLogInfo
|
||||
|
@ -692,6 +692,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
organizationId: req.permission.orgId,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
caId: ca.id,
|
||||
@ -786,6 +787,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignCert,
|
||||
organizationId: req.permission.orgId,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
caId: ca.id,
|
||||
|
@ -266,6 +266,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
caId: req.body.caId,
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
@ -442,6 +443,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
caId: req.body.caId,
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
|
@ -475,6 +475,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secretCountFromEnv,
|
||||
workspaceId: projectId,
|
||||
@ -979,6 +980,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secretCount,
|
||||
workspaceId: projectId,
|
||||
@ -1144,6 +1146,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secretCountForEnv,
|
||||
workspaceId: projectId,
|
||||
@ -1336,6 +1339,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId: projectId,
|
||||
|
@ -85,6 +85,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.MachineIdentityCreated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
orgId: req.body.organizationId,
|
||||
name: identity.name,
|
||||
|
396
backend/src/server/routes/v1/identity-tls-cert-auth-router.ts
Normal file
396
backend/src/server/routes/v1/identity-tls-cert-auth-router.ts
Normal file
@ -0,0 +1,396 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
import { IdentityTlsCertAuthsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { ApiDocsTags, TLS_CERT_AUTH } from "@app/lib/api-docs";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
|
||||
|
||||
const validateCommonNames = z
|
||||
.string()
|
||||
.min(1)
|
||||
.trim()
|
||||
.transform((el) =>
|
||||
el
|
||||
.split(",")
|
||||
.map((i) => i.trim())
|
||||
.join(",")
|
||||
);
|
||||
|
||||
const validateCaCertificate = (caCert: string) => {
|
||||
if (!caCert) return true;
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new crypto.X509Certificate(caCert);
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/login",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.TlsCertAuth],
|
||||
description: "Login with TLS Certificate Auth",
|
||||
body: z.object({
|
||||
identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
accessToken: z.string(),
|
||||
expiresIn: z.coerce.number(),
|
||||
accessTokenMaxTTL: z.coerce.number(),
|
||||
tokenType: z.literal("Bearer")
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const appCfg = getConfig();
|
||||
const clientCertificate = req.headers[appCfg.IDENTITY_TLS_CERT_AUTH_CLIENT_CERTIFICATE_HEADER_KEY];
|
||||
if (!clientCertificate) {
|
||||
throw new BadRequestError({ message: "Missing TLS certificate in header" });
|
||||
}
|
||||
|
||||
const { identityTlsCertAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
await server.services.identityTlsCertAuth.login({
|
||||
identityId: req.body.identityId,
|
||||
clientCertificate: clientCertificate as string
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: identityMembershipOrg?.orgId,
|
||||
event: {
|
||||
type: EventType.LOGIN_IDENTITY_TLS_CERT_AUTH,
|
||||
metadata: {
|
||||
identityId: identityTlsCertAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
identityTlsCertAuthId: identityTlsCertAuth.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
tokenType: "Bearer" as const,
|
||||
expiresIn: identityTlsCertAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.TlsCertAuth],
|
||||
description: "Attach TLS Certificate Auth configuration onto identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().trim().describe(TLS_CERT_AUTH.ATTACH.identityId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
allowedCommonNames: validateCommonNames
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(TLS_CERT_AUTH.ATTACH.allowedCommonNames),
|
||||
caCertificate: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(10240)
|
||||
.refine(validateCaCertificate, "Invalid CA Certificate.")
|
||||
.describe(TLS_CERT_AUTH.ATTACH.caCertificate),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
|
||||
.describe(TLS_CERT_AUTH.ATTACH.accessTokenTrustedIps),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(TLS_CERT_AUTH.ATTACH.accessTokenTTL),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(315360000)
|
||||
.default(2592000)
|
||||
.describe(TLS_CERT_AUTH.ATTACH.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.default(0)
|
||||
.describe(TLS_CERT_AUTH.ATTACH.accessTokenNumUsesLimit)
|
||||
})
|
||||
.refine(
|
||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityTlsCertAuth: IdentityTlsCertAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityTlsCertAuth = await server.services.identityTlsCertAuth.attachTlsCertAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
identityId: req.params.identityId,
|
||||
isActorSuperAdmin: isSuperAdmin(req.auth)
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.ADD_IDENTITY_TLS_CERT_AUTH,
|
||||
metadata: {
|
||||
identityId: identityTlsCertAuth.identityId,
|
||||
allowedCommonNames: identityTlsCertAuth.allowedCommonNames,
|
||||
accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityTlsCertAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityTlsCertAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "PATCH",
|
||||
url: "/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.TlsCertAuth],
|
||||
description: "Update TLS Certificate Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(TLS_CERT_AUTH.UPDATE.identityId)
|
||||
}),
|
||||
body: z
|
||||
.object({
|
||||
caCertificate: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(10240)
|
||||
.refine(validateCaCertificate, "Invalid CA Certificate.")
|
||||
.optional()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.caCertificate),
|
||||
allowedCommonNames: validateCommonNames
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.allowedCommonNames),
|
||||
accessTokenTrustedIps: z
|
||||
.object({
|
||||
ipAddress: z.string().trim()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
.optional()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.accessTokenTrustedIps),
|
||||
accessTokenTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(315360000)
|
||||
.optional()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.accessTokenTTL),
|
||||
accessTokenNumUsesLimit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.accessTokenNumUsesLimit),
|
||||
accessTokenMaxTTL: z
|
||||
.number()
|
||||
.int()
|
||||
.max(315360000)
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe(TLS_CERT_AUTH.UPDATE.accessTokenMaxTTL)
|
||||
})
|
||||
.refine(
|
||||
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||
"Access Token TTL cannot be greater than Access Token Max TTL."
|
||||
),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityTlsCertAuth: IdentityTlsCertAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityTlsCertAuth = await server.services.identityTlsCertAuth.updateTlsCertAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.body,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_IDENTITY_TLS_CERT_AUTH,
|
||||
metadata: {
|
||||
identityId: identityTlsCertAuth.identityId,
|
||||
allowedCommonNames: identityTlsCertAuth.allowedCommonNames,
|
||||
accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
|
||||
accessTokenTrustedIps: identityTlsCertAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||
accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityTlsCertAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.TlsCertAuth],
|
||||
description: "Retrieve TLS Certificate Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(TLS_CERT_AUTH.RETRIEVE.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityTlsCertAuth: IdentityTlsCertAuthsSchema.extend({
|
||||
caCertificate: z.string()
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityTlsCertAuth = await server.services.identityTlsCertAuth.getTlsCertAuth({
|
||||
identityId: req.params.identityId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.GET_IDENTITY_TLS_CERT_AUTH,
|
||||
metadata: {
|
||||
identityId: identityTlsCertAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
return { identityTlsCertAuth };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/identities/:identityId",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
schema: {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.TlsCertAuth],
|
||||
description: "Delete TLS Certificate Auth configuration on identity",
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
params: z.object({
|
||||
identityId: z.string().describe(TLS_CERT_AUTH.REVOKE.identityId)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
identityTlsCertAuth: IdentityTlsCertAuthsSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityTlsCertAuth = await server.services.identityTlsCertAuth.revokeTlsCertAuth({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
identityId: req.params.identityId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.REVOKE_IDENTITY_TLS_CERT_AUTH,
|
||||
metadata: {
|
||||
identityId: identityTlsCertAuth.identityId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { identityTlsCertAuth };
|
||||
}
|
||||
});
|
||||
};
|
@ -25,6 +25,7 @@ import { registerIdentityLdapAuthRouter } from "./identity-ldap-auth-router";
|
||||
import { registerIdentityOciAuthRouter } from "./identity-oci-auth-router";
|
||||
import { registerIdentityOidcAuthRouter } from "./identity-oidc-auth-router";
|
||||
import { registerIdentityRouter } from "./identity-router";
|
||||
import { registerIdentityTlsCertAuthRouter } from "./identity-tls-cert-auth-router";
|
||||
import { registerIdentityTokenAuthRouter } from "./identity-token-auth-router";
|
||||
import { registerIdentityUaRouter } from "./identity-universal-auth-router";
|
||||
import { registerIntegrationAuthRouter } from "./integration-auth-router";
|
||||
@ -66,6 +67,7 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
||||
await authRouter.register(registerIdentityAccessTokenRouter);
|
||||
await authRouter.register(registerIdentityAliCloudAuthRouter);
|
||||
await authRouter.register(registerIdentityAwsAuthRouter);
|
||||
await authRouter.register(registerIdentityTlsCertAuthRouter, { prefix: "/tls-cert-auth" });
|
||||
await authRouter.register(registerIdentityAzureAuthRouter);
|
||||
await authRouter.register(registerIdentityOciAuthRouter);
|
||||
await authRouter.register(registerIdentityOidcAuthRouter);
|
||||
|
@ -103,6 +103,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IntegrationCreated,
|
||||
organizationId: req.permission.orgId,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
...createIntegrationEventProperty,
|
||||
|
@ -64,6 +64,7 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.UserOrgInvitation,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
inviteeEmails: req.body.inviteeEmails,
|
||||
organizationRoleSlug: req.body.organizationRoleSlug,
|
||||
|
@ -331,6 +331,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
organizationId: req.permission.orgId,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
subscriberId: subscriber.id,
|
||||
@ -399,6 +400,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
subscriberId: subscriber.id,
|
||||
commonName: subscriber.commonName,
|
||||
@ -471,6 +473,7 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
subscriberId: subscriber.id,
|
||||
commonName: subscriber.commonName,
|
||||
|
@ -165,6 +165,7 @@ export const registerSecretRequestsRouter = async (server: FastifyZodProvider) =
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretRequestDeleted,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
secretRequestId: req.params.id,
|
||||
organizationId: req.permission.orgId,
|
||||
@ -256,6 +257,7 @@ export const registerSecretRequestsRouter = async (server: FastifyZodProvider) =
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretRequestCreated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
secretRequestId: shareRequest.id,
|
||||
organizationId: req.permission.orgId,
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
import {
|
||||
CloudflarePagesSyncSchema,
|
||||
CreateCloudflarePagesSyncSchema,
|
||||
UpdateCloudflarePagesSyncSchema
|
||||
} from "@app/services/secret-sync/cloudflare-pages/cloudflare-pages-schema";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
|
||||
import { registerSyncSecretsEndpoints } from "./secret-sync-endpoints";
|
||||
|
||||
export const registerCloudflarePagesSyncRouter = async (server: FastifyZodProvider) =>
|
||||
registerSyncSecretsEndpoints({
|
||||
|
@ -198,6 +198,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.ProjectCreated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
orgId: project.orgId,
|
||||
name: project.name,
|
||||
|
@ -333,6 +333,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId,
|
||||
@ -489,6 +490,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
organizationId: req.permission.orgId,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
@ -615,6 +617,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretCreated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: projectId,
|
||||
@ -750,6 +753,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretUpdated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: projectId,
|
||||
@ -850,6 +854,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretDeleted,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: projectId,
|
||||
@ -957,6 +962,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId: req.query.workspaceId,
|
||||
@ -1036,6 +1042,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretPulled,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: req.query.workspaceId,
|
||||
@ -1207,6 +1214,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretCreated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: req.body.workspaceId,
|
||||
@ -1396,6 +1404,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretUpdated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: req.body.workspaceId,
|
||||
@ -1519,6 +1528,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretDeleted,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
workspaceId: req.body.workspaceId,
|
||||
@ -1702,6 +1712,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretCreated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId: req.body.workspaceId,
|
||||
@ -1828,6 +1839,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretUpdated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId: req.body.workspaceId,
|
||||
@ -1946,6 +1958,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretDeleted,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId: req.body.workspaceId,
|
||||
@ -2054,6 +2067,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretCreated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId: secrets[0].workspace,
|
||||
@ -2209,6 +2223,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretUpdated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId: secrets[0].workspace,
|
||||
@ -2307,6 +2322,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SecretDeleted,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
workspaceId: secrets[0].workspace,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
@ -9,7 +10,6 @@ import {
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { CloudflareConnectionMethod } from "./cloudflare-connection-enum";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
|
||||
const accountIdCharacterValidator = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
@ -19,6 +20,7 @@ export const gcpConnectionService = (getAppConnection: TGetAppConnectionFunc) =>
|
||||
|
||||
return projects;
|
||||
} catch (error) {
|
||||
logger.error(error, "Error listing GCP secret manager projects");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
@ -45,6 +45,11 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
.leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`)
|
||||
.leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`)
|
||||
.leftJoin(
|
||||
TableName.IdentityTlsCertAuth,
|
||||
`${TableName.Identity}.id`,
|
||||
`${TableName.IdentityTlsCertAuth}.identityId`
|
||||
)
|
||||
.select(selectAllTableCols(TableName.IdentityAccessToken))
|
||||
.select(
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"),
|
||||
@ -61,6 +66,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityLdapAuth).as("accessTokenTrustedIpsLdap"),
|
||||
db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTlsCertAuth).as("accessTokenTrustedIpsTlsCert"),
|
||||
db.ref("name").withSchema(TableName.Identity)
|
||||
)
|
||||
.first();
|
||||
@ -79,7 +85,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => {
|
||||
trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc,
|
||||
trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken,
|
||||
trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt,
|
||||
trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap
|
||||
trustedIpsAccessLdapAuth: doc.accessTokenTrustedIpsLdap,
|
||||
trustedIpsAccessTlsCertAuth: doc.accessTokenTrustedIpsTlsCert
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "IdAccessTokenFindOne" });
|
||||
|
@ -201,7 +201,8 @@ export const identityAccessTokenServiceFactory = ({
|
||||
[IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth,
|
||||
[IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth,
|
||||
[IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth,
|
||||
[IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth
|
||||
[IdentityAuthMethod.LDAP_AUTH]: identityAccessToken.trustedIpsAccessLdapAuth,
|
||||
[IdentityAuthMethod.TLS_CERT_AUTH]: identityAccessToken.trustedIpsAccessTlsCertAuth
|
||||
};
|
||||
|
||||
const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod];
|
||||
|
@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify, TOrmify } from "@app/lib/knex";
|
||||
|
||||
export type TIdentityTlsCertAuthDALFactory = TOrmify<TableName.IdentityTlsCertAuth>;
|
||||
|
||||
export const identityTlsCertAuthDALFactory = (db: TDbClient) => {
|
||||
const orm = ormify(db, TableName.IdentityTlsCertAuth);
|
||||
return orm;
|
||||
};
|
@ -0,0 +1,423 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { KmsDataKey } from "../kms/kms-types";
|
||||
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
||||
import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal";
|
||||
import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types";
|
||||
|
||||
type TIdentityTlsCertAuthServiceFactoryDep = {
|
||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "create" | "delete">;
|
||||
identityTlsCertAuthDAL: Pick<
|
||||
TIdentityTlsCertAuthDALFactory,
|
||||
"findOne" | "transaction" | "create" | "updateById" | "delete"
|
||||
>;
|
||||
identityOrgMembershipDAL: Pick<TIdentityOrgDALFactory, "findOne">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
};
|
||||
|
||||
const parseSubjectDetails = (data: string) => {
|
||||
const values: Record<string, string> = {};
|
||||
data.split("\n").forEach((el) => {
|
||||
const [key, value] = el.split("=");
|
||||
values[key.trim()] = value.trim();
|
||||
});
|
||||
return values;
|
||||
};
|
||||
|
||||
export const identityTlsCertAuthServiceFactory = ({
|
||||
identityAccessTokenDAL,
|
||||
identityTlsCertAuthDAL,
|
||||
identityOrgMembershipDAL,
|
||||
licenseService,
|
||||
permissionService,
|
||||
kmsService
|
||||
}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => {
|
||||
const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => {
|
||||
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
|
||||
if (!identityTlsCertAuth) {
|
||||
throw new NotFoundError({
|
||||
message: "TLS Certificate auth method not found for identity, did you configure TLS Certificate auth?"
|
||||
});
|
||||
}
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
|
||||
identityId: identityTlsCertAuth.identityId
|
||||
});
|
||||
|
||||
if (!identityMembershipOrg) {
|
||||
throw new NotFoundError({
|
||||
message: `Identity organization membership for identity with ID '${identityTlsCertAuth.identityId}' not found`
|
||||
});
|
||||
}
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const caCertificate = decryptor({
|
||||
cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate
|
||||
}).toString();
|
||||
|
||||
const leafCertificate = extractX509CertFromChain(decodeURIComponent(clientCertificate))?.[0];
|
||||
if (!leafCertificate) {
|
||||
throw new BadRequestError({ message: "Missing client certificate" });
|
||||
}
|
||||
|
||||
const clientCertificateX509 = new crypto.X509Certificate(leafCertificate);
|
||||
const caCertificateX509 = new crypto.X509Certificate(caCertificate);
|
||||
|
||||
const isValidCertificate = clientCertificateX509.verify(caCertificateX509.publicKey);
|
||||
if (!isValidCertificate)
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: Certificate not issued by the provided CA."
|
||||
});
|
||||
|
||||
if (new Date(clientCertificateX509.validTo) < new Date()) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: Certificate has expired."
|
||||
});
|
||||
}
|
||||
|
||||
if (new Date(clientCertificateX509.validFrom) > new Date()) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: Certificate not yet valid."
|
||||
});
|
||||
}
|
||||
|
||||
const subjectDetails = parseSubjectDetails(clientCertificateX509.subject);
|
||||
if (identityTlsCertAuth.allowedCommonNames) {
|
||||
const isValidCommonName = identityTlsCertAuth.allowedCommonNames.split(",").includes(subjectDetails.CN);
|
||||
if (!isValidCommonName) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied: TLS Certificate Auth common name not allowed."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the token
|
||||
const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => {
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityTlsCertAuth.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit,
|
||||
authMethod: IdentityAuthMethod.TLS_CERT_AUTH
|
||||
},
|
||||
tx
|
||||
);
|
||||
return newToken;
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
identityId: identityTlsCertAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
Number(identityAccessToken.accessTokenTTL) === 0
|
||||
? undefined
|
||||
: {
|
||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
identityTlsCertAuth,
|
||||
accessToken,
|
||||
identityAccessToken,
|
||||
identityMembershipOrg
|
||||
};
|
||||
};
|
||||
|
||||
const attachTlsCertAuth: TIdentityTlsCertAuthServiceFactory["attachTlsCertAuth"] = async ({
|
||||
identityId,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId,
|
||||
isActorSuperAdmin,
|
||||
caCertificate,
|
||||
allowedCommonNames
|
||||
}) => {
|
||||
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to add TLS Certificate Auth to already configured identity"
|
||||
});
|
||||
}
|
||||
|
||||
if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
if (
|
||||
!plan.ipAllowlisting &&
|
||||
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||
throw new BadRequestError({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||
});
|
||||
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const identityTlsCertAuth = await identityTlsCertAuthDAL.transaction(async (tx) => {
|
||||
const doc = await identityTlsCertAuthDAL.create(
|
||||
{
|
||||
identityId: identityMembershipOrg.identityId,
|
||||
accessTokenMaxTTL,
|
||||
allowedCommonNames,
|
||||
accessTokenTTL,
|
||||
encryptedCaCertificate: encryptor({ plainText: Buffer.from(caCertificate) }).cipherTextBlob,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
},
|
||||
tx
|
||||
);
|
||||
return doc;
|
||||
});
|
||||
return { ...identityTlsCertAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const updateTlsCertAuth: TIdentityTlsCertAuthServiceFactory["updateTlsCertAuth"] = async ({
|
||||
identityId,
|
||||
caCertificate,
|
||||
allowedCommonNames,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
}) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) {
|
||||
throw new NotFoundError({
|
||||
message: "The identity does not have TLS Certificate Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
|
||||
|
||||
if (
|
||||
(accessTokenMaxTTL || identityTlsCertAuth.accessTokenMaxTTL) > 0 &&
|
||||
(accessTokenTTL || identityTlsCertAuth.accessTokenTTL) >
|
||||
(accessTokenMaxTTL || identityTlsCertAuth.accessTokenMaxTTL)
|
||||
) {
|
||||
throw new BadRequestError({ message: "Access token TTL cannot be greater than max TTL" });
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
if (
|
||||
!plan.ipAllowlisting &&
|
||||
accessTokenTrustedIp.ipAddress !== "0.0.0.0/0" &&
|
||||
accessTokenTrustedIp.ipAddress !== "::/0"
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range to access token due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
if (!isValidIpOrCidr(accessTokenTrustedIp.ipAddress))
|
||||
throw new BadRequestError({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
return extractIPDetails(accessTokenTrustedIp.ipAddress);
|
||||
});
|
||||
const { encryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
|
||||
const updatedTlsCertAuth = await identityTlsCertAuthDAL.updateById(identityTlsCertAuth.id, {
|
||||
allowedCommonNames,
|
||||
encryptedCaCertificate: caCertificate
|
||||
? encryptor({ plainText: Buffer.from(caCertificate) }).cipherTextBlob
|
||||
: undefined,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||
: undefined
|
||||
});
|
||||
|
||||
return { ...updatedTlsCertAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const getTlsCertAuth: TIdentityTlsCertAuthServiceFactory["getTlsCertAuth"] = async ({
|
||||
identityId,
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have TLS Certificate Auth attached"
|
||||
});
|
||||
}
|
||||
|
||||
const identityAuth = await identityTlsCertAuthDAL.findOne({ identityId });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
orgId: identityMembershipOrg.orgId
|
||||
});
|
||||
let caCertificate = "";
|
||||
if (identityAuth.encryptedCaCertificate) {
|
||||
caCertificate = decryptor({ cipherTextBlob: identityAuth.encryptedCaCertificate }).toString();
|
||||
}
|
||||
|
||||
return { ...identityAuth, caCertificate, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
const revokeTlsCertAuth: TIdentityTlsCertAuthServiceFactory["revokeTlsCertAuth"] = async ({
|
||||
identityId,
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}) => {
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.TLS_CERT_AUTH)) {
|
||||
throw new BadRequestError({
|
||||
message: "The identity does not have TLS Certificate auth"
|
||||
});
|
||||
}
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityMembershipOrg.identityId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke TLS Certificate auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const revokedIdentityTlsCertAuth = await identityTlsCertAuthDAL.transaction(async (tx) => {
|
||||
const deletedTlsCertAuth = await identityTlsCertAuthDAL.delete({ identityId }, tx);
|
||||
await identityAccessTokenDAL.delete({ identityId, authMethod: IdentityAuthMethod.TLS_CERT_AUTH }, tx);
|
||||
|
||||
return { ...deletedTlsCertAuth?.[0], orgId: identityMembershipOrg.orgId };
|
||||
});
|
||||
return revokedIdentityTlsCertAuth;
|
||||
};
|
||||
|
||||
return {
|
||||
login,
|
||||
attachTlsCertAuth,
|
||||
updateTlsCertAuth,
|
||||
getTlsCertAuth,
|
||||
revokeTlsCertAuth
|
||||
};
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
import { TIdentityAccessTokens, TIdentityOrgMemberships, TIdentityTlsCertAuths } from "@app/db/schemas";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TLoginTlsCertAuthDTO = {
|
||||
identityId: string;
|
||||
clientCertificate: string;
|
||||
};
|
||||
|
||||
export type TAttachTlsCertAuthDTO = {
|
||||
identityId: string;
|
||||
caCertificate: string;
|
||||
allowedCommonNames?: string | null;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: { ipAddress: string }[];
|
||||
isActorSuperAdmin?: boolean;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateTlsCertAuthDTO = {
|
||||
identityId: string;
|
||||
caCertificate?: string;
|
||||
allowedCommonNames?: string | null;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetTlsCertAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TRevokeTlsCertAuthDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TIdentityTlsCertAuthServiceFactory = {
|
||||
login: (dto: TLoginTlsCertAuthDTO) => Promise<{
|
||||
identityTlsCertAuth: TIdentityTlsCertAuths;
|
||||
accessToken: string;
|
||||
identityAccessToken: TIdentityAccessTokens;
|
||||
identityMembershipOrg: TIdentityOrgMemberships;
|
||||
}>;
|
||||
attachTlsCertAuth: (dto: TAttachTlsCertAuthDTO) => Promise<TIdentityTlsCertAuths>;
|
||||
updateTlsCertAuth: (dto: TUpdateTlsCertAuthDTO) => Promise<TIdentityTlsCertAuths>;
|
||||
revokeTlsCertAuth: (dto: TRevokeTlsCertAuthDTO) => Promise<TIdentityTlsCertAuths>;
|
||||
getTlsCertAuth: (dto: TGetTlsCertAuthDTO) => Promise<TIdentityTlsCertAuths & { caCertificate: string }>;
|
||||
};
|
@ -11,7 +11,8 @@ export const buildAuthMethods = ({
|
||||
azureId,
|
||||
tokenId,
|
||||
jwtId,
|
||||
ldapId
|
||||
ldapId,
|
||||
tlsCertId
|
||||
}: {
|
||||
uaId?: string;
|
||||
gcpId?: string;
|
||||
@ -24,6 +25,7 @@ export const buildAuthMethods = ({
|
||||
tokenId?: string;
|
||||
jwtId?: string;
|
||||
ldapId?: string;
|
||||
tlsCertId?: string;
|
||||
}) => {
|
||||
return [
|
||||
...[uaId ? IdentityAuthMethod.UNIVERSAL_AUTH : null],
|
||||
@ -36,6 +38,7 @@ export const buildAuthMethods = ({
|
||||
...[azureId ? IdentityAuthMethod.AZURE_AUTH : null],
|
||||
...[tokenId ? IdentityAuthMethod.TOKEN_AUTH : null],
|
||||
...[jwtId ? IdentityAuthMethod.JWT_AUTH : null],
|
||||
...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null]
|
||||
...[ldapId ? IdentityAuthMethod.LDAP_AUTH : null],
|
||||
...[tlsCertId ? IdentityAuthMethod.TLS_CERT_AUTH : null]
|
||||
].filter((authMethod) => authMethod) as IdentityAuthMethod[];
|
||||
};
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
TIdentityOciAuths,
|
||||
TIdentityOidcAuths,
|
||||
TIdentityOrgMemberships,
|
||||
TIdentityTlsCertAuths,
|
||||
TIdentityTokenAuths,
|
||||
TIdentityUniversalAuths,
|
||||
TOrgRoles
|
||||
@ -99,7 +100,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityLdapAuth}.identityId`
|
||||
)
|
||||
|
||||
.leftJoin<TIdentityTlsCertAuths>(
|
||||
TableName.IdentityTlsCertAuth,
|
||||
`${TableName.IdentityOrgMembership}.identityId`,
|
||||
`${TableName.IdentityTlsCertAuth}.identityId`
|
||||
)
|
||||
.select(
|
||||
selectAllTableCols(TableName.IdentityOrgMembership),
|
||||
|
||||
@ -114,6 +119,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
|
||||
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
|
||||
db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth),
|
||||
db.ref("name").withSchema(TableName.Identity),
|
||||
db.ref("hasDeleteProtection").withSchema(TableName.Identity)
|
||||
);
|
||||
@ -238,7 +244,11 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityLdapAuth}.identityId`
|
||||
)
|
||||
|
||||
.leftJoin<TIdentityTlsCertAuths>(
|
||||
TableName.IdentityTlsCertAuth,
|
||||
"paginatedIdentity.identityId",
|
||||
`${TableName.IdentityTlsCertAuth}.identityId`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema("paginatedIdentity"),
|
||||
db.ref("role").withSchema("paginatedIdentity"),
|
||||
@ -260,7 +270,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").as("azureId").withSchema(TableName.IdentityAzureAuth),
|
||||
db.ref("id").as("tokenId").withSchema(TableName.IdentityTokenAuth),
|
||||
db.ref("id").as("jwtId").withSchema(TableName.IdentityJwtAuth),
|
||||
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth)
|
||||
db.ref("id").as("ldapId").withSchema(TableName.IdentityLdapAuth),
|
||||
db.ref("id").as("tlsCertId").withSchema(TableName.IdentityTlsCertAuth)
|
||||
)
|
||||
// cr stands for custom role
|
||||
.select(db.ref("id").as("crId").withSchema(TableName.OrgRoles))
|
||||
@ -306,6 +317,7 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
azureId,
|
||||
tokenId,
|
||||
ldapId,
|
||||
tlsCertId,
|
||||
createdAt,
|
||||
updatedAt
|
||||
}) => ({
|
||||
@ -313,7 +325,6 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
roleId,
|
||||
identityId,
|
||||
id,
|
||||
|
||||
orgId,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
@ -341,7 +352,8 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
azureId,
|
||||
tokenId,
|
||||
jwtId,
|
||||
ldapId
|
||||
ldapId,
|
||||
tlsCertId
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
@ -7,13 +7,18 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { getServerCfg } from "../super-admin/super-admin-service";
|
||||
import { TTelemetryDALFactory } from "./telemetry-dal";
|
||||
import { TELEMETRY_SECRET_OPERATIONS_KEY, TELEMETRY_SECRET_PROCESSED_KEY } from "./telemetry-service";
|
||||
import {
|
||||
TELEMETRY_SECRET_OPERATIONS_KEY,
|
||||
TELEMETRY_SECRET_PROCESSED_KEY,
|
||||
TTelemetryServiceFactory
|
||||
} from "./telemetry-service";
|
||||
import { PostHogEventTypes } from "./telemetry-types";
|
||||
|
||||
type TTelemetryQueueServiceFactoryDep = {
|
||||
queueService: TQueueServiceFactory;
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "deleteItem">;
|
||||
telemetryDAL: TTelemetryDALFactory;
|
||||
telemetryService: TTelemetryServiceFactory;
|
||||
};
|
||||
|
||||
export type TTelemetryQueueServiceFactory = ReturnType<typeof telemetryQueueServiceFactory>;
|
||||
@ -21,7 +26,8 @@ export type TTelemetryQueueServiceFactory = ReturnType<typeof telemetryQueueServ
|
||||
export const telemetryQueueServiceFactory = ({
|
||||
queueService,
|
||||
keyStore,
|
||||
telemetryDAL
|
||||
telemetryDAL,
|
||||
telemetryService
|
||||
}: TTelemetryQueueServiceFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
const postHog =
|
||||
@ -48,6 +54,10 @@ export const telemetryQueueServiceFactory = ({
|
||||
await keyStore.deleteItem(TELEMETRY_SECRET_OPERATIONS_KEY);
|
||||
});
|
||||
|
||||
queueService.start(QueueName.TelemetryAggregatedEvents, async () => {
|
||||
await telemetryService.processAggregatedEvents();
|
||||
});
|
||||
|
||||
// every day at midnight a telemetry job executes on self-hosted instances
|
||||
// this sends some telemetry information like instance id secrets operated etc
|
||||
const startTelemetryCheck = async () => {
|
||||
@ -60,11 +70,26 @@ export const telemetryQueueServiceFactory = ({
|
||||
{ pattern: "0 0 * * *", utc: true },
|
||||
QueueName.TelemetryInstanceStats // just a job id
|
||||
);
|
||||
|
||||
// clear previous aggregated events job
|
||||
await queueService.stopRepeatableJob(
|
||||
QueueName.TelemetryAggregatedEvents,
|
||||
QueueJobs.TelemetryAggregatedEvents,
|
||||
{ pattern: "*/5 * * * *", utc: true },
|
||||
QueueName.TelemetryAggregatedEvents // just a job id
|
||||
);
|
||||
|
||||
if (postHog) {
|
||||
await queueService.queue(QueueName.TelemetryInstanceStats, QueueJobs.TelemetryInstanceStats, undefined, {
|
||||
jobId: QueueName.TelemetryInstanceStats,
|
||||
repeat: { pattern: "0 0 * * *", utc: true }
|
||||
});
|
||||
|
||||
// Start aggregated events job (runs every five minutes)
|
||||
await queueService.queue(QueueName.TelemetryAggregatedEvents, QueueJobs.TelemetryAggregatedEvents, undefined, {
|
||||
jobId: QueueName.TelemetryAggregatedEvents,
|
||||
repeat: { pattern: "*/5 * * * *", utc: true }
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -72,6 +97,10 @@ export const telemetryQueueServiceFactory = ({
|
||||
logger.error(err?.failedReason, `${QueueName.TelemetryInstanceStats}: failed`);
|
||||
});
|
||||
|
||||
queueService.listen(QueueName.TelemetryAggregatedEvents, "failed", (err) => {
|
||||
logger.error(err?.failedReason, `${QueueName.TelemetryAggregatedEvents}: failed`);
|
||||
});
|
||||
|
||||
return {
|
||||
startTelemetryCheck
|
||||
};
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { createHash, randomUUID } from "crypto";
|
||||
import { PostHog } from "posthog-node";
|
||||
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
@ -12,12 +13,49 @@ import { PostHogEventTypes, TPostHogEvent, TSecretModifiedEvent } from "./teleme
|
||||
export const TELEMETRY_SECRET_PROCESSED_KEY = "telemetry-secret-processed";
|
||||
export const TELEMETRY_SECRET_OPERATIONS_KEY = "telemetry-secret-operations";
|
||||
|
||||
export const POSTHOG_AGGREGATED_EVENTS = [PostHogEventTypes.SecretPulled];
|
||||
const TELEMETRY_AGGREGATED_KEY_EXP = 900; // 15mins
|
||||
|
||||
// Bucket configuration
|
||||
const TELEMETRY_BUCKET_COUNT = 30;
|
||||
const TELEMETRY_BUCKET_NAMES = Array.from(
|
||||
{ length: TELEMETRY_BUCKET_COUNT },
|
||||
(_, i) => `bucket-${i.toString().padStart(2, "0")}`
|
||||
);
|
||||
|
||||
type AggregatedEventData = Record<string, unknown>;
|
||||
type SingleEventData = {
|
||||
distinctId: string;
|
||||
event: string;
|
||||
properties: unknown;
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
export type TTelemetryServiceFactory = ReturnType<typeof telemetryServiceFactory>;
|
||||
export type TTelemetryServiceFactoryDep = {
|
||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "incrementBy">;
|
||||
keyStore: Pick<
|
||||
TKeyStoreFactory,
|
||||
"incrementBy" | "deleteItemsByKeyIn" | "setItemWithExpiry" | "getKeysByPattern" | "getItems"
|
||||
>;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getInstanceType">;
|
||||
};
|
||||
|
||||
const getBucketForDistinctId = (distinctId: string): string => {
|
||||
// Use SHA-256 hash for consistent distribution
|
||||
const hash = createHash("sha256").update(distinctId).digest("hex");
|
||||
|
||||
// Take first 8 characters and convert to number for better distribution
|
||||
const hashNumber = parseInt(hash.substring(0, 8), 16);
|
||||
const bucketIndex = hashNumber % TELEMETRY_BUCKET_COUNT;
|
||||
|
||||
return TELEMETRY_BUCKET_NAMES[bucketIndex];
|
||||
};
|
||||
|
||||
export const createTelemetryEventKey = (event: string, distinctId: string): string => {
|
||||
const bucketId = getBucketForDistinctId(distinctId);
|
||||
return `telemetry-event-${event}-${bucketId}-${distinctId}-${randomUUID()}`;
|
||||
};
|
||||
|
||||
export const telemetryServiceFactory = ({ keyStore, licenseService }: TTelemetryServiceFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@ -64,11 +102,33 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme
|
||||
const instanceType = licenseService.getInstanceType();
|
||||
// capture posthog only when its cloud or signup event happens in self-hosted
|
||||
if (instanceType === InstanceType.Cloud || event.event === PostHogEventTypes.UserSignedUp) {
|
||||
postHog.capture({
|
||||
event: event.event,
|
||||
distinctId: event.distinctId,
|
||||
properties: event.properties
|
||||
});
|
||||
if (event.organizationId) {
|
||||
try {
|
||||
postHog.groupIdentify({ groupType: "organization", groupKey: event.organizationId });
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to identify PostHog organization");
|
||||
}
|
||||
}
|
||||
if (POSTHOG_AGGREGATED_EVENTS.includes(event.event)) {
|
||||
const eventKey = createTelemetryEventKey(event.event, event.distinctId);
|
||||
await keyStore.setItemWithExpiry(
|
||||
eventKey,
|
||||
TELEMETRY_AGGREGATED_KEY_EXP,
|
||||
JSON.stringify({
|
||||
distinctId: event.distinctId,
|
||||
event: event.event,
|
||||
properties: event.properties,
|
||||
organizationId: event.organizationId
|
||||
})
|
||||
);
|
||||
} else {
|
||||
postHog.capture({
|
||||
event: event.event,
|
||||
distinctId: event.distinctId,
|
||||
properties: event.properties,
|
||||
...(event.organizationId ? { groups: { organization: event.organizationId } } : {})
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -89,6 +149,160 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme
|
||||
}
|
||||
};
|
||||
|
||||
const aggregateGroupProperties = (events: SingleEventData[]): AggregatedEventData => {
|
||||
const aggregatedData: AggregatedEventData = {};
|
||||
|
||||
// Set the total count
|
||||
aggregatedData.count = events.length;
|
||||
|
||||
events.forEach((event) => {
|
||||
if (!event.properties) return;
|
||||
|
||||
Object.entries(event.properties as Record<string, unknown>).forEach(([key, value]: [string, unknown]) => {
|
||||
if (Array.isArray(value)) {
|
||||
// For arrays, count occurrences of each item
|
||||
const existingCounts =
|
||||
aggregatedData[key] &&
|
||||
typeof aggregatedData[key] === "object" &&
|
||||
aggregatedData[key]?.constructor === Object
|
||||
? (aggregatedData[key] as Record<string, number>)
|
||||
: {};
|
||||
|
||||
value.forEach((item) => {
|
||||
const itemKey = typeof item === "object" ? JSON.stringify(item) : String(item);
|
||||
existingCounts[itemKey] = (existingCounts[itemKey] || 0) + 1;
|
||||
});
|
||||
|
||||
aggregatedData[key] = existingCounts;
|
||||
} else if (typeof value === "object" && value?.constructor === Object) {
|
||||
// For objects, count occurrences of each field value
|
||||
const existingCounts =
|
||||
aggregatedData[key] &&
|
||||
typeof aggregatedData[key] === "object" &&
|
||||
aggregatedData[key]?.constructor === Object
|
||||
? (aggregatedData[key] as Record<string, number>)
|
||||
: {};
|
||||
|
||||
if (value) {
|
||||
Object.values(value).forEach((fieldValue) => {
|
||||
const valueKey = typeof fieldValue === "object" ? JSON.stringify(fieldValue) : String(fieldValue);
|
||||
existingCounts[valueKey] = (existingCounts[valueKey] || 0) + 1;
|
||||
});
|
||||
}
|
||||
aggregatedData[key] = existingCounts;
|
||||
} else if (typeof value === "number") {
|
||||
// For numbers, add to existing sum
|
||||
aggregatedData[key] = ((aggregatedData[key] as number) || 0) + value;
|
||||
} else if (value !== undefined && value !== null) {
|
||||
// For other types (strings, booleans, etc.), count occurrences
|
||||
const stringValue = String(value);
|
||||
const existingValue = aggregatedData[key];
|
||||
|
||||
if (!existingValue) {
|
||||
aggregatedData[key] = { [stringValue]: 1 };
|
||||
} else if (existingValue && typeof existingValue === "object" && existingValue.constructor === Object) {
|
||||
const countObject = existingValue as Record<string, number>;
|
||||
countObject[stringValue] = (countObject[stringValue] || 0) + 1;
|
||||
} else {
|
||||
const oldValue = String(existingValue);
|
||||
aggregatedData[key] = {
|
||||
[oldValue]: 1,
|
||||
[stringValue]: 1
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return aggregatedData;
|
||||
};
|
||||
|
||||
const processBucketEvents = async (eventType: string, bucketId: string) => {
|
||||
if (!postHog) return 0;
|
||||
|
||||
try {
|
||||
const bucketPattern = `telemetry-event-${eventType}-${bucketId}-*`;
|
||||
const bucketKeys = await keyStore.getKeysByPattern(bucketPattern);
|
||||
|
||||
if (bucketKeys.length === 0) return 0;
|
||||
|
||||
const bucketEvents = await keyStore.getItems(bucketKeys);
|
||||
let bucketEventsParsed: SingleEventData[] = [];
|
||||
|
||||
try {
|
||||
bucketEventsParsed = bucketEvents
|
||||
.filter((event) => event !== null)
|
||||
.map((event) => JSON.parse(event as string) as SingleEventData);
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to parse bucket events for ${eventType} in ${bucketId}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const eventsGrouped = new Map<string, SingleEventData[]>();
|
||||
|
||||
bucketEventsParsed.forEach((event) => {
|
||||
const key = JSON.stringify({ id: event.distinctId, org: event.organizationId });
|
||||
if (!eventsGrouped.has(key)) {
|
||||
eventsGrouped.set(key, []);
|
||||
}
|
||||
eventsGrouped.get(key)!.push(event);
|
||||
});
|
||||
|
||||
if (eventsGrouped.size === 0) return 0;
|
||||
|
||||
for (const [eventsKey, events] of eventsGrouped) {
|
||||
const key = JSON.parse(eventsKey) as { id: string; org?: string };
|
||||
if (key.org) {
|
||||
try {
|
||||
postHog.groupIdentify({ groupType: "organization", groupKey: key.org });
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to identify PostHog organization");
|
||||
}
|
||||
}
|
||||
const properties = aggregateGroupProperties(events);
|
||||
|
||||
postHog.capture({
|
||||
event: `${eventType} aggregated`,
|
||||
distinctId: key.id,
|
||||
properties,
|
||||
...(key.org ? { groups: { organization: key.org } } : {})
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up processed data for this bucket
|
||||
await keyStore.deleteItemsByKeyIn(bucketKeys);
|
||||
|
||||
logger.info(`Processed ${bucketEventsParsed.length} events from bucket ${bucketId} for ${eventType}`);
|
||||
return bucketEventsParsed.length;
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to process bucket ${bucketId} for ${eventType}`);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const processAggregatedEvents = async () => {
|
||||
if (!postHog) return;
|
||||
|
||||
for (const eventType of POSTHOG_AGGREGATED_EVENTS) {
|
||||
let totalProcessed = 0;
|
||||
|
||||
logger.info(`Starting bucket processing for ${eventType}`);
|
||||
|
||||
// Process each bucket sequentially to control memory usage
|
||||
for (const bucketId of TELEMETRY_BUCKET_NAMES) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const processed = await processBucketEvents(eventType, bucketId);
|
||||
totalProcessed += processed;
|
||||
} catch (error) {
|
||||
logger.error(error, `Failed to process bucket ${bucketId} for ${eventType}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Completed processing ${totalProcessed} total events for ${eventType}`);
|
||||
}
|
||||
};
|
||||
|
||||
const flushAll = async () => {
|
||||
if (postHog) {
|
||||
await postHog.shutdownAsync();
|
||||
@ -98,6 +312,8 @@ To opt into telemetry, you can set "TELEMETRY_ENABLED=true" within the environme
|
||||
return {
|
||||
sendLoopsEvent,
|
||||
sendPostHogEvents,
|
||||
flushAll
|
||||
processAggregatedEvents,
|
||||
flushAll,
|
||||
getBucketForDistinctId
|
||||
};
|
||||
};
|
||||
|
@ -1,3 +1,13 @@
|
||||
import {
|
||||
IdentityActor,
|
||||
KmipClientActor,
|
||||
PlatformActor,
|
||||
ScimClientActor,
|
||||
ServiceActor,
|
||||
UnknownUserActor,
|
||||
UserActor
|
||||
} from "@app/ee/services/audit-log/audit-log-types";
|
||||
|
||||
export enum PostHogEventTypes {
|
||||
SecretPush = "secrets pushed",
|
||||
SecretPulled = "secrets pulled",
|
||||
@ -40,6 +50,14 @@ export type TSecretModifiedEvent = {
|
||||
secretPath: string;
|
||||
channel?: string;
|
||||
userAgent?: string;
|
||||
actor?:
|
||||
| UserActor
|
||||
| IdentityActor
|
||||
| ServiceActor
|
||||
| ScimClientActor
|
||||
| PlatformActor
|
||||
| UnknownUserActor
|
||||
| KmipClientActor;
|
||||
};
|
||||
};
|
||||
|
||||
@ -214,7 +232,7 @@ export type TInvalidateCacheEvent = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TPostHogEvent = { distinctId: string } & (
|
||||
export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
|
||||
| TSecretModifiedEvent
|
||||
| TAdminInitEvent
|
||||
| TUserSignedUpEvent
|
||||
|
@ -4,7 +4,7 @@ services:
|
||||
nginx:
|
||||
container_name: infisical-dev-nginx
|
||||
image: nginx
|
||||
restart: always
|
||||
restart: "always"
|
||||
ports:
|
||||
- 8080:80
|
||||
- 8443:443
|
||||
|
4
docs/api-reference/endpoints/tls-cert-auth/attach.mdx
Normal file
4
docs/api-reference/endpoints/tls-cert-auth/attach.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Attach"
|
||||
openapi: "POST /api/v1/auth/tls-cert-auth/identities/{identityId}"
|
||||
---
|
4
docs/api-reference/endpoints/tls-cert-auth/login.mdx
Normal file
4
docs/api-reference/endpoints/tls-cert-auth/login.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Login"
|
||||
openapi: "POST /api/v1/auth/tls-cert-auth/login"
|
||||
---
|
4
docs/api-reference/endpoints/tls-cert-auth/retrieve.mdx
Normal file
4
docs/api-reference/endpoints/tls-cert-auth/retrieve.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Retrieve"
|
||||
openapi: "GET /api/v1/auth/tls-cert-auth/identities/{identityId}"
|
||||
---
|
4
docs/api-reference/endpoints/tls-cert-auth/revoke.mdx
Normal file
4
docs/api-reference/endpoints/tls-cert-auth/revoke.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Revoke"
|
||||
openapi: "DELETE /api/v1/auth/tls-cert-auth/identities/{identityId}"
|
||||
---
|
4
docs/api-reference/endpoints/tls-cert-auth/update.mdx
Normal file
4
docs/api-reference/endpoints/tls-cert-auth/update.mdx
Normal file
@ -0,0 +1,4 @@
|
||||
---
|
||||
title: "Update"
|
||||
openapi: "PATCH /api/v1/auth/tls-cert-auth/identities/{identityId}"
|
||||
---
|
@ -288,6 +288,7 @@
|
||||
"documentation/platform/identities/kubernetes-auth",
|
||||
"documentation/platform/identities/oci-auth",
|
||||
"documentation/platform/identities/token-auth",
|
||||
"documentation/platform/identities/tls-cert-auth",
|
||||
"documentation/platform/identities/universal-auth",
|
||||
{
|
||||
"group": "OIDC Auth",
|
||||
@ -752,6 +753,16 @@
|
||||
"api-reference/endpoints/alicloud-auth/revoke"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "TLS Certificate Auth",
|
||||
"pages": [
|
||||
"api-reference/endpoints/tls-cert-auth/login",
|
||||
"api-reference/endpoints/tls-cert-auth/attach",
|
||||
"api-reference/endpoints/tls-cert-auth/retrieve",
|
||||
"api-reference/endpoints/tls-cert-auth/update",
|
||||
"api-reference/endpoints/tls-cert-auth/revoke"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "AWS Auth",
|
||||
"pages": [
|
||||
|
176
docs/documentation/platform/identities/tls-cert-auth.mdx
Normal file
176
docs/documentation/platform/identities/tls-cert-auth.mdx
Normal file
@ -0,0 +1,176 @@
|
||||
---
|
||||
title: TLS Certificate Auth
|
||||
description: "Learn how to authenticate with Infisical using TLS Certificate."
|
||||
---
|
||||
|
||||
**TLS Certificate Auth** is an authentication method that verifies a user's TLS Client certificate using the provided CA Certificate, allowing secure access to Infisical resources.
|
||||
|
||||
## Diagram
|
||||
|
||||
The following sequence diagram illustrates the TLS Certificate Auth workflow for authenticating users with Infisical.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Client
|
||||
participant Infisical
|
||||
|
||||
Note over Client,Client: Step 1: Setup your TLS request with the client certificate
|
||||
|
||||
Note over Client,Infisical: Step 2: Login Operation
|
||||
Client->>Infisical: Send request to /api/v1/auth/tls-cert-auth/login
|
||||
|
||||
Note over Infisical: Step 3: Request verification using CA Certificate
|
||||
|
||||
Infisical->>Client: Return short-lived access token
|
||||
|
||||
Note over Client,Infisical: Step 5: Access Infisical API with token
|
||||
Client->>Infisical: Make authenticated requests using the short-lived access token
|
||||
```
|
||||
|
||||
## Concept
|
||||
|
||||
At a high level, Infisical authenticates the client's TLS Certificate by verifying its identity and checking that it meets specific requirements (e.g., it is bound to the allowed common names) at the `/api/v1/auth/tls-cert-auth/login` endpoint. If successful, Infisical returns a short-lived access token that can be used to make authenticated requests to the Infisical API.
|
||||
|
||||
To be more specific:
|
||||
|
||||
1. The client sends a TLS request with the client certificate to Infisical at the `/api/v1/auth/tls-cert-auth/login` endpoint.
|
||||
2. Infisical verifies the incoming request using the provided CA certificate.
|
||||
3. Infisical checks the user's properties against set criteria such as Allowed Common Names.
|
||||
4. If all checks pass, Infisical returns a short-lived access token that the client can use to make authenticated requests to the Infisical API.
|
||||
|
||||
<Accordion title="TLS with Load Balancer/Proxy">
|
||||
Most of the time, the Infisical server will be behind a load balancer or
|
||||
proxy. To propagate the TLS certificate from the load balancer to the
|
||||
instance, you can configure the TLS to send the client certificate as a header
|
||||
that is set as an [environment
|
||||
variable](/self-hosting/configuration/envars#param-identity-tls-cert-auth-client-certificate-header-key).
|
||||
</Accordion>
|
||||
|
||||
## Guide
|
||||
|
||||
In the following steps, we explore how to create and use identities for your workloads and applications on TLS Certificate to
|
||||
access the Infisical API using request signing.
|
||||
|
||||
<Warning>
|
||||
**Self-Hosted Users:** Before using TLS Certificate Auth, please review the
|
||||
[Security Requirements for Self-Hosted
|
||||
Deployments](#security-requirements-for-self-hosted-deployments) section below
|
||||
to ensure proper configuration and avoid security vulnerabilities.
|
||||
</Warning>
|
||||
|
||||
### Creating an identity
|
||||
|
||||
To create an identity, head to your Organization Settings > Access Control > [Identities](https://app.infisical.com/organization/access-management?selectedTab=identities) and press **Create identity**.
|
||||
|
||||

|
||||
|
||||
When creating an identity, you specify an organization-level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > [Organization Roles](https://app.infisical.com/organization/access-management?selectedTab=roles).
|
||||
|
||||

|
||||
|
||||
Input some details for your new identity:
|
||||
|
||||
- **Name (required):** A friendly name for the identity.
|
||||
- **Role (required):** A role from the [**Organization Roles**](https://app.infisical.com/organization/access-management?selectedTab=roles) tab for the identity to assume. The organization role assigned will determine what organization-level resources this identity can have access to.
|
||||
|
||||
Once you've created an identity, you'll be redirected to a page where you can manage the identity.
|
||||
|
||||

|
||||
|
||||
Since the identity has been configured with [Universal Auth](https://infisical.com/docs/documentation/platform/identities/universal-auth) by default, you should reconfigure it to use TLS Certificate Auth instead. To do this, click the cog next to **Universal Auth** and then select **Delete** in the options dropdown.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Now create a new TLS Certificate Auth Method.
|
||||
|
||||

|
||||
|
||||
Here's some information about each field:
|
||||
|
||||
- **CA Certificate:** A PEM encoded CA Certificate used to validate incoming TLS request client certificate.
|
||||
- **Allowed Common Names:** A comma separated list of client certificate common names allowed.
|
||||
- **Access Token TTL (default is `2592000` equivalent to 30 days):** The lifetime for an access token in seconds. This value will be referenced at renewal time.
|
||||
- **Access Token Max TTL (default is `2592000` equivalent to 30 days):** The maximum lifetime for an access token in seconds. This value will be referenced at renewal time.
|
||||
- **Access Token Max Number of Uses (default is `0`):** The maximum number of times that an access token can be used; a value of `0` implies an infinite number of uses.
|
||||
- **Access Token Trusted IPs:** The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0`, allowing usage from any network address.
|
||||
|
||||
### Adding an identity to a project
|
||||
|
||||
In order to allow an identity to access project-level resources such as secrets, you must add it to the relevant projects.
|
||||
|
||||
To do this, head over to the project you want to add the identity to and navigate to Project Settings > Access Control > Machine Identities and press **Add Identity**.
|
||||
|
||||

|
||||
|
||||
Select the identity you want to add to the project and the project-level role you want it to assume. The project role given to the identity will determine what project-level resources this identity can access.
|
||||
|
||||

|
||||
|
||||
### Accessing the Infisical API with the identity
|
||||
|
||||
To access the Infisical API as the identity, you need to send a TLS request to `/api/v1/auth/tls-cert-auth/login` endpoint.
|
||||
|
||||
Below is an example of how you can authenticate with Infisical using NodeJS.
|
||||
|
||||
```javascript
|
||||
const fs = require("fs");
|
||||
const https = require("https");
|
||||
const axios = require("axios");
|
||||
|
||||
try {
|
||||
const clientCertificate = fs.readFileSync("client-cert.pem", "utf8");
|
||||
const clientKeyCertificate = fs.readFileSync("client-key.pem", "utf8");
|
||||
|
||||
const infisicalUrl = "https://app.infisical.com"; // or your self-hosted Infisical URL
|
||||
const identityId = "<your-identity-id>";
|
||||
|
||||
// Create HTTPS agent with client certificate and key
|
||||
const httpsAgent = new https.Agent({
|
||||
cert: clientCertificate,
|
||||
key: clientKeyCertificate,
|
||||
});
|
||||
|
||||
const { data } = await axios.post(
|
||||
`${infisicalUrl}/api/v1/auth/tls-cert-auth/login`,
|
||||
{
|
||||
identityId,
|
||||
},
|
||||
{
|
||||
httpsAgent: httpsAgent, // Pass the HTTPS agent with client cert
|
||||
}
|
||||
);
|
||||
|
||||
console.log("result data: ", data); // access token here
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
Each identity access token has a time-to-live (TTL) which you can infer from the response of the login operation; the default TTL is `7200` seconds, which can be adjusted.
|
||||
|
||||
If an identity access token expires, it can no longer access the Infisical API. A new access token should be obtained by performing another login operation.
|
||||
|
||||
</Note>
|
||||
|
||||
## Security Requirements for Self-Hosted Deployments
|
||||
|
||||
ALL TLS cert [login](/api-reference/endpoints/tls-cert-auth/login) requests **MUST** go through a load balancer/proxy that verifies certificate ownership:
|
||||
|
||||
- **REQUIRED:** Configure your load balancer/proxy to **require a proper TLS handshake with client certificate presentation**
|
||||
- **REQUIRED:** Ensure the load balancer **verifies the client possesses the private key** corresponding to the certificate (standard TLS behavior)
|
||||
- **NEVER** allow direct connections to Infisical for TLS cert auth - this enables header injection attacks
|
||||
- **NEVER** forward certificate headers without requiring proper TLS certificate presentation
|
||||
|
||||
### Load Balancer Configuration Examples
|
||||
|
||||
- **AWS ALB:** Use mTLS listeners which require client certificate presentation during the TLS handshake
|
||||
- **NGINX/HAProxy:** Configure SSL client certificate requirement with proper TLS handshake verification
|
||||
|
||||
<Note>
|
||||
Infisical will handle the actual certificate validation against the configured
|
||||
CA certificate and determine authentication permissions. The load balancer's
|
||||
role is to ensure certificate ownership, not certificate trust validation.
|
||||
</Note>
|
Binary file not shown.
After Width: | Height: | Size: 467 KiB |
Binary file not shown.
After Width: | Height: | Size: 327 KiB |
@ -7,9 +7,10 @@ description: "Learn how to configure a GCP Secret Manager Sync for Infisical."
|
||||
|
||||
- Set up and add secrets to [Infisical Cloud](https://app.infisical.com)
|
||||
- Create a [GCP Connection](/integrations/app-connections/gcp) with the required **Secret Sync** permissions
|
||||
- Enable **Cloud Resource Manager API** and **Secret Manager API** on your GCP project
|
||||
- Enable **Cloud Resource Manager API**, **Secret Manager API**, and **Service Usage API** on your GCP project
|
||||

|
||||

|
||||

|
||||
|
||||
<Tabs>
|
||||
<Tab title="Infisical UI">
|
||||
|
@ -32,7 +32,7 @@ Used to configure platform-specific security and operational settings
|
||||
<ParamField query="HOST" type="string" default="localhost" optional>
|
||||
Specifies the network interface Infisical will bind to when accepting incoming connections.
|
||||
|
||||
By default, Infisical binds to `localhost`, which restricts access to connections from the same machine.
|
||||
By default, Infisical binds to `localhost`, which restricts access to connections from the same machine.
|
||||
|
||||
To make the application accessible externally (e.g., for self-hosted deployments), set this to `0.0.0.0`, which tells the server to listen on all network interfaces.
|
||||
|
||||
@ -122,6 +122,7 @@ DB_READ_REPLICAS=[{"DB_CONNECTION_URI":""}]
|
||||
</ParamField>
|
||||
|
||||
### Redis
|
||||
|
||||
Redis is used for caching and background tasks. You can use either a standalone Redis instance or a Redis Sentinel setup.
|
||||
|
||||
<Tabs>
|
||||
@ -199,8 +200,17 @@ Without email configuration, Infisical's core functions like sign-up/login and s
|
||||
connection can not be encrypted then message is not sent.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SMTP_TLS_REJECT_UNAUTHORIZED" type="bool" default="true" optional>
|
||||
If this is `true`, Infisical will validate the server's SSL/TLS certificate and reject the connection if the certificate is invalid or not trusted. If set to `false`, the client will accept the server's certificate regardless of its validity, which can be useful in development or testing environments but is not recommended for production use.
|
||||
<ParamField
|
||||
query="SMTP_TLS_REJECT_UNAUTHORIZED"
|
||||
type="bool"
|
||||
default="true"
|
||||
optional
|
||||
>
|
||||
If this is `true`, Infisical will validate the server's SSL/TLS certificate
|
||||
and reject the connection if the certificate is invalid or not trusted. If set
|
||||
to `false`, the client will accept the server's certificate regardless of its
|
||||
validity, which can be useful in development or testing environments but is
|
||||
not recommended for production use.
|
||||
</ParamField>
|
||||
|
||||
<ParamField query="SMTP_CUSTOM_CA_CERT" type="string" default="none" optional>
|
||||
@ -211,6 +221,7 @@ Without email configuration, Infisical's core functions like sign-up/login and s
|
||||
Infisical highly encourages the following variables be used alongside this one for maximum security:
|
||||
- `SMTP_REQUIRE_TLS=true`
|
||||
- `SMTP_TLS_REJECT_UNAUTHORIZED=true`
|
||||
|
||||
</ParamField>
|
||||
</Accordion>
|
||||
|
||||
@ -577,6 +588,7 @@ You can configure third-party app connections for re-use across Infisical Projec
|
||||
<ParamField query="INF_APP_CONNECTION_GITHUB_RADAR_APP_WEBHOOK_SECRET" type="string" default="none" optional>
|
||||
The webhook secret configured for payload verification in the GitHub Radar App
|
||||
</ParamField>
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="GitHub OAuth Connection">
|
||||
@ -771,3 +783,14 @@ If export type is set to `otlp`, you will have to configure a value for `OTEL_EX
|
||||
<ParamField query="OTEL_COLLECTOR_BASIC_AUTH_PASSWORD" type="string">
|
||||
The password for authenticating with the telemetry collector.
|
||||
</ParamField>
|
||||
|
||||
## Identity Auth Method
|
||||
|
||||
<ParamField
|
||||
query="IDENTITY_TLS_CERT_AUTH_CLIENT_CERTIFICATE_HEADER_KEY"
|
||||
type="string"
|
||||
default="x-identity-tls-cert-auth-client-cert"
|
||||
>
|
||||
The TLS header used to propagate the client certificate from the load balancer
|
||||
to the server.
|
||||
</ParamField>
|
||||
|
@ -58,6 +58,7 @@ export const CreateSecretSyncModal = ({ onOpenChange, selectSync = null, ...prop
|
||||
}
|
||||
onPointerDownOutside={(e) => e.preventDefault()}
|
||||
className="max-w-2xl"
|
||||
bodyClassName="overflow-visible"
|
||||
subTitle={selectedSync ? undefined : "Select a third-party service to sync secrets to."}
|
||||
>
|
||||
<Content
|
||||
|
@ -1,10 +1,11 @@
|
||||
import { faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMemo } from "react";
|
||||
import { faInfoCircle, faMagnifyingGlass, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Spinner, Tooltip } from "@app/components/v2";
|
||||
import { EmptyState, Input, Pagination, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { SecretSync, useSecretSyncOptions } from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { UpgradePlanModal } from "../license/UpgradePlanModal";
|
||||
@ -19,6 +20,26 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
|
||||
initPerPage: 16
|
||||
});
|
||||
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
secretSyncOptions?.filter(
|
||||
({ name, destination }) =>
|
||||
name?.toLowerCase().includes(search.trim().toLowerCase()) ||
|
||||
destination.toLowerCase().includes(search.toLowerCase())
|
||||
) ?? [],
|
||||
[secretSyncOptions, search]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredOptions.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||
@ -29,75 +50,103 @@ export const SecretSyncSelect = ({ onSelect }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{secretSyncOptions?.map(({ destination, enterprise }) => {
|
||||
const { image, name } = SECRET_SYNC_MAP[destination];
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseSecretSyncs
|
||||
? handlePopUpOpen("upgradePlan")
|
||||
: onSelect(destination)
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
height={40}
|
||||
width={40}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search options..."
|
||||
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
|
||||
/>
|
||||
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
|
||||
{filteredOptions.slice(offset, perPage * page)?.map(({ destination, enterprise }) => {
|
||||
const { image, name } = SECRET_SYNC_MAP[destination];
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseSecretSyncs
|
||||
? handlePopUpOpen("upgradePlan")
|
||||
: onSelect(destination)
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
height={40}
|
||||
width={40}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!filteredOptions?.length && (
|
||||
<EmptyState
|
||||
className="col-span-full mt-40"
|
||||
title="No Secret Syncs match search"
|
||||
icon={faSearch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{Boolean(filteredOptions.length) && (
|
||||
<Pagination
|
||||
startAdornment={
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
className="max-w-sm py-4"
|
||||
content={
|
||||
<>
|
||||
<p className="mb-2">Infisical is constantly adding support for more services.</p>
|
||||
<p>
|
||||
{`If you don't see the third-party
|
||||
service you're looking for,`}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://infisical.com/slack"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
let us know on Slack
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://github.com/Infisical/infisical/discussions"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
make a request on GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
|
||||
<span className="text-xs">
|
||||
Don't see the third-party service you're looking for?
|
||||
</span>
|
||||
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
count={filteredOptions.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
perPageList={[16]}
|
||||
/>
|
||||
)}
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use every Secret Sync if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
className="max-w-sm py-4"
|
||||
content={
|
||||
<>
|
||||
<p className="mb-2">Infisical is constantly adding support for more services.</p>
|
||||
<p>
|
||||
{`If you don't see the third-party
|
||||
service you're looking for,`}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://infisical.com/slack"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
let us know on Slack
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://github.com/Infisical/infisical/discussions"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
make a request on GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4 hover:bg-mineshaft-900/50">
|
||||
<FontAwesomeIcon className="mt-auto text-3xl" icon={faWrench} />
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -76,7 +76,7 @@ export const GcpSyncFields = () => {
|
||||
helperText={
|
||||
<Tooltip
|
||||
className="max-w-md"
|
||||
content="Ensure that you've enabled the Secret Manager API and Cloud Resource Manager API on your GCP project. Additionally, make sure that the service account is assigned the appropriate GCP roles."
|
||||
content="Ensure that you've enabled the Secret Manager API, Cloud Resource Manager API, and Service Usage API on your GCP project. Additionally, make sure that the service account is assigned the appropriate GCP roles."
|
||||
>
|
||||
<div>
|
||||
<span>Don't see the project you're looking for?</span>{" "}
|
||||
|
@ -20,6 +20,7 @@ type Props = {
|
||||
children?: ReactNode;
|
||||
deletionMessage?: ReactNode;
|
||||
buttonColorSchema?: "danger" | "primary" | "secondary" | "gray" | null;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const DeleteActionModal = ({
|
||||
@ -34,6 +35,7 @@ export const DeleteActionModal = ({
|
||||
formContent,
|
||||
deletionMessage,
|
||||
buttonColorSchema = "danger",
|
||||
isDisabled,
|
||||
children
|
||||
}: Props): JSX.Element => {
|
||||
const [inputData, setInputData] = useState("");
|
||||
@ -70,7 +72,7 @@ export const DeleteActionModal = ({
|
||||
<Button
|
||||
className="mr-4"
|
||||
colorSchema={buttonColorSchema}
|
||||
isDisabled={!(deleteKey === inputData) || isLoading}
|
||||
isDisabled={!(deleteKey === inputData) || isLoading || isDisabled}
|
||||
onClick={onDelete}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
|
@ -163,7 +163,7 @@ const PasswordGeneratorModal = ({
|
||||
<div className="mb-6 flex flex-row justify-between gap-2">
|
||||
<Checkbox
|
||||
id="useUppercase"
|
||||
className="mr-2 data-[state=checked]:bg-primary"
|
||||
className="mr-2"
|
||||
isChecked={passwordOptions.useUppercase}
|
||||
onCheckedChange={(checked) =>
|
||||
setPasswordOptions({ ...passwordOptions, useUppercase: checked as boolean })
|
||||
@ -174,7 +174,7 @@ const PasswordGeneratorModal = ({
|
||||
|
||||
<Checkbox
|
||||
id="useLowercase"
|
||||
className="mr-2 data-[state=checked]:bg-primary"
|
||||
className="mr-2"
|
||||
isChecked={passwordOptions.useLowercase}
|
||||
onCheckedChange={(checked) =>
|
||||
setPasswordOptions({ ...passwordOptions, useLowercase: checked as boolean })
|
||||
@ -185,7 +185,7 @@ const PasswordGeneratorModal = ({
|
||||
|
||||
<Checkbox
|
||||
id="useNumbers"
|
||||
className="mr-2 data-[state=checked]:bg-primary"
|
||||
className="mr-2"
|
||||
isChecked={passwordOptions.useNumbers}
|
||||
onCheckedChange={(checked) =>
|
||||
setPasswordOptions({ ...passwordOptions, useNumbers: checked as boolean })
|
||||
@ -196,7 +196,7 @@ const PasswordGeneratorModal = ({
|
||||
|
||||
<Checkbox
|
||||
id="useSpecialChars"
|
||||
className="mr-2 data-[state=checked]:bg-primary"
|
||||
className="mr-2"
|
||||
isChecked={passwordOptions.useSpecialChars}
|
||||
onCheckedChange={(checked) =>
|
||||
setPasswordOptions({ ...passwordOptions, useSpecialChars: checked as boolean })
|
||||
|
@ -25,8 +25,8 @@ export const accessApprovalKeys = {
|
||||
requestedBy?: string,
|
||||
bypassReason?: string
|
||||
) => [{ projectSlug, envSlug, requestedBy, bypassReason }, "access-approvals-requests"] as const,
|
||||
getAccessApprovalRequestCount: (projectSlug: string) =>
|
||||
[{ projectSlug }, "access-approval-request-count"] as const
|
||||
getAccessApprovalRequestCount: (projectSlug: string, policyId?: string) =>
|
||||
[{ projectSlug }, "access-approval-request-count", ...(policyId ? [policyId] : [])] as const
|
||||
};
|
||||
|
||||
export const fetchPolicyApprovalCount = async ({
|
||||
@ -87,21 +87,22 @@ const fetchApprovalRequests = async ({
|
||||
}));
|
||||
};
|
||||
|
||||
const fetchAccessRequestsCount = async (projectSlug: string) => {
|
||||
const fetchAccessRequestsCount = async (projectSlug: string, policyId?: string) => {
|
||||
const { data } = await apiRequest.get<TAccessRequestCount>(
|
||||
"/api/v1/access-approvals/requests/count",
|
||||
{ params: { projectSlug } }
|
||||
{ params: { projectSlug, policyId } }
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const useGetAccessRequestsCount = ({
|
||||
projectSlug,
|
||||
policyId,
|
||||
options = {}
|
||||
}: TGetAccessApprovalRequestsDTO & TReactQueryOptions) =>
|
||||
useQuery({
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug),
|
||||
queryFn: () => fetchAccessRequestsCount(projectSlug),
|
||||
queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug, policyId),
|
||||
queryFn: () => fetchAccessRequestsCount(projectSlug, policyId),
|
||||
...options,
|
||||
enabled: Boolean(projectSlug) && (options?.enabled ?? true)
|
||||
});
|
||||
|
@ -147,6 +147,7 @@ export type TCreateAccessRequestDTO = {
|
||||
|
||||
export type TGetAccessApprovalRequestsDTO = {
|
||||
projectSlug: string;
|
||||
policyId?: string;
|
||||
envSlug?: string;
|
||||
authorUserId?: string;
|
||||
};
|
||||
|
@ -11,5 +11,6 @@ export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
|
||||
[IdentityAuthMethod.OCI_AUTH]: "OCI Auth",
|
||||
[IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth",
|
||||
[IdentityAuthMethod.LDAP_AUTH]: "LDAP Auth",
|
||||
[IdentityAuthMethod.JWT_AUTH]: "JWT Auth"
|
||||
[IdentityAuthMethod.JWT_AUTH]: "JWT Auth",
|
||||
[IdentityAuthMethod.TLS_CERT_AUTH]: "TLS Certificate Auth"
|
||||
};
|
||||
|
@ -9,7 +9,8 @@ export enum IdentityAuthMethod {
|
||||
OCI_AUTH = "oci-auth",
|
||||
OIDC_AUTH = "oidc-auth",
|
||||
LDAP_AUTH = "ldap-auth",
|
||||
JWT_AUTH = "jwt-auth"
|
||||
JWT_AUTH = "jwt-auth",
|
||||
TLS_CERT_AUTH = "tls-cert-auth"
|
||||
}
|
||||
|
||||
export enum IdentityJwtConfigurationType {
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
AddIdentityLdapAuthDTO,
|
||||
AddIdentityOciAuthDTO,
|
||||
AddIdentityOidcAuthDTO,
|
||||
AddIdentityTlsCertAuthDTO,
|
||||
AddIdentityTokenAuthDTO,
|
||||
AddIdentityUniversalAuthDTO,
|
||||
ClientSecretData,
|
||||
@ -32,6 +33,7 @@ import {
|
||||
DeleteIdentityLdapAuthDTO,
|
||||
DeleteIdentityOciAuthDTO,
|
||||
DeleteIdentityOidcAuthDTO,
|
||||
DeleteIdentityTlsCertAuthDTO,
|
||||
DeleteIdentityTokenAuthDTO,
|
||||
DeleteIdentityUniversalAuthClientSecretDTO,
|
||||
DeleteIdentityUniversalAuthDTO,
|
||||
@ -46,6 +48,7 @@ import {
|
||||
IdentityLdapAuth,
|
||||
IdentityOciAuth,
|
||||
IdentityOidcAuth,
|
||||
IdentityTlsCertAuth,
|
||||
IdentityTokenAuth,
|
||||
IdentityUniversalAuth,
|
||||
RevokeTokenDTO,
|
||||
@ -60,6 +63,7 @@ import {
|
||||
UpdateIdentityLdapAuthDTO,
|
||||
UpdateIdentityOciAuthDTO,
|
||||
UpdateIdentityOidcAuthDTO,
|
||||
UpdateIdentityTlsCertAuthDTO,
|
||||
UpdateIdentityTokenAuthDTO,
|
||||
UpdateIdentityUniversalAuthDTO,
|
||||
UpdateTokenIdentityTokenAuthDTO
|
||||
@ -655,6 +659,107 @@ export const useDeleteIdentityAliCloudAuth = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useAddIdentityTlsCertAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityTlsCertAuth, object, AddIdentityTlsCertAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
allowedCommonNames,
|
||||
caCertificate,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const {
|
||||
data: { identityTlsCertAuth }
|
||||
} = await apiRequest.post<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
|
||||
`/api/v1/auth/tls-cert-auth/identities/${identityId}`,
|
||||
{
|
||||
allowedCommonNames,
|
||||
caCertificate,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityTlsCertAuth;
|
||||
},
|
||||
onSuccess: (_, { identityId, organizationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityTlsCertAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityTlsCertAuth, object, UpdateIdentityTlsCertAuthDTO>({
|
||||
mutationFn: async ({
|
||||
identityId,
|
||||
allowedCommonNames,
|
||||
caCertificate,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}) => {
|
||||
const {
|
||||
data: { identityTlsCertAuth }
|
||||
} = await apiRequest.patch<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
|
||||
`/api/v1/auth/tls-cert-auth/identities/${identityId}`,
|
||||
{
|
||||
caCertificate,
|
||||
allowedCommonNames,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}
|
||||
);
|
||||
|
||||
return identityTlsCertAuth;
|
||||
},
|
||||
onSuccess: (_, { identityId, organizationId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteIdentityTlsCertAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityTlsCertAuth, object, DeleteIdentityTlsCertAuthDTO>({
|
||||
mutationFn: async ({ identityId }) => {
|
||||
const {
|
||||
data: { identityTlsCertAuth }
|
||||
} = await apiRequest.delete(`/api/v1/auth/tls-cert-auth/identities/${identityId}`);
|
||||
return identityTlsCertAuth;
|
||||
},
|
||||
onSuccess: (_, { organizationId, identityId }) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId)
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId)
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateIdentityOidcAuth = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<IdentityOidcAuth, object, UpdateIdentityOidcAuthDTO>({
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
IdentityMembershipOrg,
|
||||
IdentityOciAuth,
|
||||
IdentityOidcAuth,
|
||||
IdentityTlsCertAuth,
|
||||
IdentityTokenAuth,
|
||||
IdentityUniversalAuth,
|
||||
TSearchIdentitiesDTO
|
||||
@ -34,6 +35,8 @@ export const identitiesKeys = {
|
||||
getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
|
||||
getIdentityOidcAuth: (identityId: string) => [{ identityId }, "identity-oidc-auth"] as const,
|
||||
getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const,
|
||||
getIdentityTlsCertAuth: (identityId: string) =>
|
||||
[{ identityId }, "identity-tls-cert-auth"] as const,
|
||||
getIdentityAliCloudAuth: (identityId: string) =>
|
||||
[{ identityId }, "identity-alicloud-auth"] as const,
|
||||
getIdentityOciAuth: (identityId: string) => [{ identityId }, "identity-oci-auth"] as const,
|
||||
@ -175,6 +178,27 @@ export const useGetIdentityAwsAuth = (
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityTlsCertAuth = (
|
||||
identityId: string,
|
||||
options?: TReactQueryOptions["options"]
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: identitiesKeys.getIdentityTlsCertAuth(identityId),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { identityTlsCertAuth }
|
||||
} = await apiRequest.get<{ identityTlsCertAuth: IdentityTlsCertAuth }>(
|
||||
`/api/v1/auth/tls-cert-auth/identities/${identityId}`
|
||||
);
|
||||
return identityTlsCertAuth;
|
||||
},
|
||||
staleTime: 0,
|
||||
gcTime: 0,
|
||||
...options,
|
||||
enabled: Boolean(identityId) && (options?.enabled ?? true)
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityOciAuth = (
|
||||
identityId: string,
|
||||
options?: TReactQueryOptions["options"]
|
||||
|
@ -485,6 +485,47 @@ export type DeleteIdentityKubernetesAuthDTO = {
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
export type IdentityTlsCertAuth = {
|
||||
identityId: string;
|
||||
caCertificate: string;
|
||||
allowedCommonNames: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||
};
|
||||
|
||||
export type AddIdentityTlsCertAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
caCertificate: string;
|
||||
allowedCommonNames?: string;
|
||||
accessTokenTTL: number;
|
||||
accessTokenMaxTTL: number;
|
||||
accessTokenNumUsesLimit: number;
|
||||
accessTokenTrustedIps: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type UpdateIdentityTlsCertAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
caCertificate: string;
|
||||
allowedCommonNames?: string | null;
|
||||
accessTokenTTL?: number;
|
||||
accessTokenMaxTTL?: number;
|
||||
accessTokenNumUsesLimit?: number;
|
||||
accessTokenTrustedIps?: {
|
||||
ipAddress: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type DeleteIdentityTlsCertAuthDTO = {
|
||||
organizationId: string;
|
||||
identityId: string;
|
||||
};
|
||||
|
||||
export type CreateIdentityUniversalAuthClientSecretDTO = {
|
||||
identityId: string;
|
||||
description?: string;
|
||||
|
@ -34,9 +34,10 @@ export const secretApprovalRequestKeys = {
|
||||
] as const,
|
||||
detail: ({ id }: Omit<TGetSecretApprovalRequestDetails, "decryptKey">) =>
|
||||
[{ id }, "secret-approval-request-detail"] as const,
|
||||
count: ({ workspaceId }: TGetSecretApprovalRequestCount) => [
|
||||
count: ({ workspaceId, policyId }: TGetSecretApprovalRequestCount) => [
|
||||
{ workspaceId },
|
||||
"secret-approval-request-count"
|
||||
"secret-approval-request-count",
|
||||
...(policyId ? [policyId] : [])
|
||||
]
|
||||
};
|
||||
|
||||
@ -204,10 +205,13 @@ export const useGetSecretApprovalRequestDetails = ({
|
||||
enabled: Boolean(id) && (options?.enabled ?? true)
|
||||
});
|
||||
|
||||
const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprovalRequestCount) => {
|
||||
const fetchSecretApprovalRequestCount = async ({
|
||||
workspaceId,
|
||||
policyId
|
||||
}: TGetSecretApprovalRequestCount) => {
|
||||
const { data } = await apiRequest.get<{ approvals: TSecretApprovalRequestCount }>(
|
||||
"/api/v1/secret-approval-requests/count",
|
||||
{ params: { workspaceId } }
|
||||
{ params: { workspaceId, policyId } }
|
||||
);
|
||||
|
||||
return data.approvals;
|
||||
@ -215,6 +219,7 @@ const fetchSecretApprovalRequestCount = async ({ workspaceId }: TGetSecretApprov
|
||||
|
||||
export const useGetSecretApprovalRequestCount = ({
|
||||
workspaceId,
|
||||
policyId,
|
||||
options = {}
|
||||
}: TGetSecretApprovalRequestCount & {
|
||||
options?: Omit<
|
||||
@ -228,8 +233,8 @@ export const useGetSecretApprovalRequestCount = ({
|
||||
>;
|
||||
}) =>
|
||||
useQuery({
|
||||
queryKey: secretApprovalRequestKeys.count({ workspaceId }),
|
||||
queryKey: secretApprovalRequestKeys.count({ workspaceId, policyId }),
|
||||
refetchInterval: 15000,
|
||||
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId }),
|
||||
queryFn: () => fetchSecretApprovalRequestCount({ workspaceId, policyId }),
|
||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true)
|
||||
});
|
||||
|
@ -118,6 +118,7 @@ export type TGetSecretApprovalRequestList = {
|
||||
|
||||
export type TGetSecretApprovalRequestCount = {
|
||||
workspaceId: string;
|
||||
policyId?: string;
|
||||
};
|
||||
|
||||
export type TGetSecretApprovalRequestDetails = {
|
||||
|
@ -164,7 +164,10 @@ export const MinimizedOrgSidebar = () => {
|
||||
const handleCopyToken = async () => {
|
||||
try {
|
||||
await window.navigator.clipboard.writeText(getAuthToken());
|
||||
createNotification({ type: "success", text: "Copied current login session token to clipboard" });
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Copied current login session token to clipboard"
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({ type: "error", text: "Failed to copy user token to clipboard" });
|
||||
|
@ -446,7 +446,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isDisabled={Boolean(cert)}
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
@ -481,7 +480,6 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isDisabled={Boolean(cert)}
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
|
@ -405,7 +405,6 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
@ -439,7 +438,6 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
|
@ -408,7 +408,6 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
@ -443,7 +442,6 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
@ -477,12 +475,7 @@ export const PkiSubscriberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
errorText={error?.message}
|
||||
tooltipText="If enabled, a new certificate will be issued automatically X days before the current certificate expires."
|
||||
>
|
||||
<Checkbox
|
||||
id="enableAutoRenewal"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
>
|
||||
<Checkbox id="enableAutoRenewal" isChecked={value} onCheckedChange={onChange}>
|
||||
Enable Certificate Auto Renewal
|
||||
</Checkbox>
|
||||
</FormControl>
|
||||
|
@ -333,7 +333,6 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
@ -367,7 +366,6 @@ export const PkiTemplateForm = ({ certTemplate, handlePopUpToggle }: Props) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
|
@ -145,7 +145,6 @@ const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => {
|
||||
<Checkbox
|
||||
id={optionValue}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
onCheckedChange={(state) => {
|
||||
onChange({
|
||||
|
@ -17,6 +17,7 @@ import { IdentityKubernetesAuthForm } from "./IdentityKubernetesAuthForm";
|
||||
import { IdentityLdapAuthForm } from "./IdentityLdapAuthForm";
|
||||
import { IdentityOciAuthForm } from "./IdentityOciAuthForm";
|
||||
import { IdentityOidcAuthForm } from "./IdentityOidcAuthForm";
|
||||
import { IdentityTlsCertAuthForm } from "./IdentityTlsCertAuthForm";
|
||||
import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
|
||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||
|
||||
@ -52,6 +53,7 @@ const identityAuthMethods = [
|
||||
{ label: "OCI Auth", value: IdentityAuthMethod.OCI_AUTH },
|
||||
{ label: "OIDC Auth", value: IdentityAuthMethod.OIDC_AUTH },
|
||||
{ label: "LDAP Auth", value: IdentityAuthMethod.LDAP_AUTH },
|
||||
{ label: "TLS Certificate Auth", value: IdentityAuthMethod.TLS_CERT_AUTH },
|
||||
{
|
||||
label: "JWT Auth",
|
||||
value: IdentityAuthMethod.JWT_AUTH
|
||||
@ -123,6 +125,15 @@ export const IdentityAuthMethodModalContent = ({
|
||||
/>
|
||||
)
|
||||
},
|
||||
[IdentityAuthMethod.TLS_CERT_AUTH]: {
|
||||
render: () => (
|
||||
<IdentityTlsCertAuthForm
|
||||
identityId={identityAuthMethodData.identityId}
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
[IdentityAuthMethod.OIDC_AUTH]: {
|
||||
render: () => (
|
||||
|
@ -0,0 +1,358 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
Tabs,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import {
|
||||
useAddIdentityTlsCertAuth,
|
||||
useGetIdentityTlsCertAuth,
|
||||
useUpdateIdentityTlsCertAuth
|
||||
} from "@app/hooks/api";
|
||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
import { IdentityFormTab } from "./types";
|
||||
|
||||
const schema = z.object({
|
||||
allowedCommonNames: z.string().optional(),
|
||||
caCertificate: z.string().min(1),
|
||||
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
||||
message: "Access Token TTL cannot be greater than 315360000"
|
||||
}),
|
||||
accessTokenMaxTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
||||
message: "Access Token Max TTL cannot be greater than 315360000"
|
||||
}),
|
||||
accessTokenNumUsesLimit: z.string(),
|
||||
accessTokenTrustedIps: z
|
||||
.array(
|
||||
z.object({
|
||||
ipAddress: z.string().max(50)
|
||||
})
|
||||
)
|
||||
.min(1)
|
||||
});
|
||||
|
||||
export type FormData = z.infer<typeof schema>;
|
||||
|
||||
type Props = {
|
||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||
handlePopUpToggle: (
|
||||
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||
state?: boolean
|
||||
) => void;
|
||||
identityId?: string;
|
||||
isUpdate?: boolean;
|
||||
};
|
||||
|
||||
export const IdentityTlsCertAuthForm = ({
|
||||
handlePopUpOpen,
|
||||
handlePopUpToggle,
|
||||
identityId,
|
||||
isUpdate
|
||||
}: Props) => {
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const { mutateAsync: addMutateAsync } = useAddIdentityTlsCertAuth();
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTlsCertAuth();
|
||||
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||
|
||||
const { data } = useGetIdentityTlsCertAuth(identityId ?? "", {
|
||||
enabled: isUpdate
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
caCertificate: "",
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
fields: accessTokenTrustedIpsFields,
|
||||
append: appendAccessTokenTrustedIp,
|
||||
remove: removeAccessTokenTrustedIp
|
||||
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
reset({
|
||||
caCertificate: data.caCertificate,
|
||||
allowedCommonNames: data.allowedCommonNames || undefined,
|
||||
accessTokenTTL: String(data.accessTokenTTL),
|
||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
|
||||
({ ipAddress, prefix }: IdentityTrustedIp) => {
|
||||
return {
|
||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||
};
|
||||
}
|
||||
)
|
||||
});
|
||||
} else {
|
||||
reset({
|
||||
caCertificate: "",
|
||||
accessTokenTTL: "2592000",
|
||||
accessTokenMaxTTL: "2592000",
|
||||
accessTokenNumUsesLimit: "0",
|
||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({
|
||||
caCertificate,
|
||||
allowedCommonNames,
|
||||
accessTokenTTL,
|
||||
accessTokenMaxTTL,
|
||||
accessTokenNumUsesLimit,
|
||||
accessTokenTrustedIps
|
||||
}: FormData) => {
|
||||
try {
|
||||
if (!identityId) return;
|
||||
|
||||
if (data) {
|
||||
await updateMutateAsync({
|
||||
organizationId: orgId,
|
||||
caCertificate,
|
||||
allowedCommonNames: allowedCommonNames || null,
|
||||
identityId,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
} else {
|
||||
await addMutateAsync({
|
||||
organizationId: orgId,
|
||||
identityId,
|
||||
caCertificate,
|
||||
allowedCommonNames: allowedCommonNames || undefined,
|
||||
accessTokenTTL: Number(accessTokenTTL),
|
||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||
accessTokenTrustedIps
|
||||
});
|
||||
}
|
||||
|
||||
handlePopUpToggle("identityAuthMethod", false);
|
||||
|
||||
createNotification({
|
||||
text: `Successfully ${isUpdate ? "updated" : "configured"} auth method`,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
reset();
|
||||
} catch {
|
||||
createNotification({
|
||||
text: `Failed to ${isUpdate ? "update" : "configure"} identity`,
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||
<TabList>
|
||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={IdentityFormTab.Configuration}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="caCertificate"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="CA Certificate"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
tooltipText="A PEM-encoded CA certificate. This will be used to validate client certificate."
|
||||
>
|
||||
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="allowedCommonNames"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Allowed Common Names"
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
tooltipText="Comma separated common names allowed to authenticate against the identity. Leave empty to allow any certificate."
|
||||
>
|
||||
<Input {...field} placeholder="" type="text" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token TTL (seconds)"
|
||||
tooltipText="The lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="2592000"
|
||||
name="accessTokenMaxTTL"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max TTL (seconds)"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum lifetime for an access token in seconds. This value will be referenced at renewal time."
|
||||
>
|
||||
<Input {...field} placeholder="2592000" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue="0"
|
||||
name="accessTokenNumUsesLimit"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Access Token Max Number of Uses"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses."
|
||||
>
|
||||
<Input {...field} placeholder="0" type="number" min="0" step="1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel value={IdentityFormTab.Advanced}>
|
||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||
<Controller
|
||||
control={control}
|
||||
name={`accessTokenTrustedIps.${index}.ipAddress`}
|
||||
defaultValue="0.0.0.0/0"
|
||||
render={({ field, fieldState: { error } }) => {
|
||||
return (
|
||||
<FormControl
|
||||
className="mb-0 flex-grow"
|
||||
label={index === 0 ? "Access Token Trusted IPs" : undefined}
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
tooltipText="The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the 0.0.0.0/0, allowing usage from any network address."
|
||||
>
|
||||
<Input
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
field.onChange(e);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
placeholder="123.456.789.0"
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
removeAccessTokenTrustedIp(index);
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
className="p-3"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</div>
|
||||
))}
|
||||
<div className="my-4 ml-1">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
if (subscription?.ipAllowlisting) {
|
||||
appendAccessTokenTrustedIp({
|
||||
ipAddress: "0.0.0.0/0"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handlePopUpOpen("upgradePlan");
|
||||
}}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
size="xs"
|
||||
>
|
||||
Add IP Address
|
||||
</Button>
|
||||
</div>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isUpdate ? "Update" : "Add"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
onClick={() => handlePopUpToggle("identityAuthMethod", false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -1,11 +1,12 @@
|
||||
import { faWrench } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMemo } from "react";
|
||||
import { faInfoCircle, faMagnifyingGlass, faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { Spinner, Tooltip } from "@app/components/v2";
|
||||
import { EmptyState, Input, Pagination, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useSubscription } from "@app/context";
|
||||
import { APP_CONNECTION_MAP } from "@app/helpers/appConnections";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import { useAppConnectionOptions } from "@app/hooks/api/appConnections";
|
||||
import { AppConnection } from "@app/hooks/api/appConnections/enums";
|
||||
|
||||
@ -19,6 +20,26 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||
|
||||
const { search, setSearch, setPage, page, perPage, setPerPage, offset } = usePagination("", {
|
||||
initPerPage: 16
|
||||
});
|
||||
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
appConnectionOptions?.filter(
|
||||
({ name, app }) =>
|
||||
name?.toLowerCase().includes(search.trim().toLowerCase()) ||
|
||||
app.toLowerCase().includes(search.toLowerCase())
|
||||
) ?? [],
|
||||
[appConnectionOptions, search]
|
||||
);
|
||||
|
||||
useResetPageHelper({
|
||||
totalCount: filteredOptions.length,
|
||||
offset,
|
||||
setPage
|
||||
});
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center py-2.5">
|
||||
@ -29,86 +50,120 @@ export const AppConnectionsSelect = ({ onSelect }: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{appConnectionOptions?.map((option) => {
|
||||
const { image, name, size = 50, enterprise = false, icon } = APP_CONNECTION_MAP[option.app];
|
||||
<div className="flex flex-col gap-4">
|
||||
<Input
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search options..."
|
||||
className="bg-mineshaft-800 placeholder:text-mineshaft-400"
|
||||
/>
|
||||
<div className="grid h-[29.5rem] grid-cols-4 content-start gap-2">
|
||||
{filteredOptions.slice(offset, perPage * page)?.map((option) => {
|
||||
const {
|
||||
image,
|
||||
name,
|
||||
size = 50,
|
||||
enterprise = false,
|
||||
icon
|
||||
} = APP_CONNECTION_MAP[option.app];
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseAppConnections
|
||||
? handlePopUpOpen("upgradePlan")
|
||||
: onSelect(option.app)
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
style={{
|
||||
width: `${size}px`
|
||||
}}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
{icon && (
|
||||
<FontAwesomeIcon
|
||||
className="absolute -bottom-1.5 -right-1.5 text-primary-700"
|
||||
size="xl"
|
||||
icon={icon}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
enterprise && !subscription.enterpriseAppConnections
|
||||
? handlePopUpOpen("upgradePlan")
|
||||
: onSelect(option.app)
|
||||
}
|
||||
className="group relative flex h-28 cursor-pointer flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-700 p-4 duration-200 hover:bg-mineshaft-600"
|
||||
>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={`/images/integrations/${image}`}
|
||||
style={{
|
||||
width: `${size}px`
|
||||
}}
|
||||
className="mt-auto"
|
||||
alt={`${name} logo`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{icon && (
|
||||
<FontAwesomeIcon
|
||||
className="absolute -bottom-1.5 -right-1.5 text-primary-700"
|
||||
size="xl"
|
||||
icon={icon}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-auto max-w-xs text-center text-xs font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
{name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{!filteredOptions?.length && (
|
||||
<EmptyState
|
||||
className="col-span-full mt-40"
|
||||
title="No App Connections match search"
|
||||
icon={faSearch}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{Boolean(filteredOptions.length) && (
|
||||
<Pagination
|
||||
startAdornment={
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
className="max-w-sm py-4"
|
||||
content={
|
||||
<>
|
||||
<p className="mb-2">Infisical is constantly adding support for more services.</p>
|
||||
<p>
|
||||
{`If you don't see the third-party
|
||||
service you're looking for,`}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://infisical.com/slack"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
let us know on Slack
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://github.com/Infisical/infisical/discussions"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
make a request on GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="-ml-3 flex items-center gap-1.5 text-mineshaft-400">
|
||||
<span className="text-xs">
|
||||
Don't see the third-party service you're looking for?
|
||||
</span>
|
||||
<FontAwesomeIcon size="xs" icon={faInfoCircle} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
count={filteredOptions.length}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={setPage}
|
||||
onChangePerPage={setPerPage}
|
||||
perPageList={[16]}
|
||||
/>
|
||||
)}
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
text="You can use every App Connection if you switch to Infisical's Enterprise plan."
|
||||
/>
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
className="max-w-sm py-4"
|
||||
content={
|
||||
<>
|
||||
<p className="mb-2">Infisical is constantly adding support for more connections.</p>
|
||||
<p>
|
||||
{`If you don't see the third-party
|
||||
app you're looking for,`}{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://infisical.com/slack"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
let us know on Slack
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
target="_blank"
|
||||
className="underline hover:text-mineshaft-300"
|
||||
href="https://github.com/Infisical/infisical/discussions"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
make a request on GitHub
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="group relative flex h-28 flex-col items-center justify-center rounded-md border border-dashed border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||
<FontAwesomeIcon className="mt-auto text-xl" icon={faWrench} />
|
||||
<div className="mt-auto max-w-xs text-center text-sm font-medium text-gray-300 duration-200 group-hover:text-gray-200">
|
||||
Coming Soon
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
useDeleteIdentityLdapAuth,
|
||||
useDeleteIdentityOciAuth,
|
||||
useDeleteIdentityOidcAuth,
|
||||
useDeleteIdentityTlsCertAuth,
|
||||
useDeleteIdentityTokenAuth,
|
||||
useDeleteIdentityUniversalAuth
|
||||
} from "@app/hooks/api";
|
||||
@ -29,6 +30,7 @@ import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthC
|
||||
import { ViewIdentityLdapAuthContent } from "./ViewIdentityLdapAuthContent";
|
||||
import { ViewIdentityOciAuthContent } from "./ViewIdentityOciAuthContent";
|
||||
import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent";
|
||||
import { ViewIdentityTlsCertAuthContent } from "./ViewIdentityTlsCertAuthContent";
|
||||
import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent";
|
||||
import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent";
|
||||
|
||||
@ -63,6 +65,7 @@ export const Content = ({
|
||||
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
|
||||
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
|
||||
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
|
||||
const { mutateAsync: revokeTlsCertAuth } = useDeleteIdentityTlsCertAuth();
|
||||
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
|
||||
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
|
||||
const { mutateAsync: revokeAliCloudAuth } = useDeleteIdentityAliCloudAuth();
|
||||
@ -93,6 +96,10 @@ export const Content = ({
|
||||
revokeMethod = revokeGcpAuth;
|
||||
Component = ViewIdentityGcpAuthContent;
|
||||
break;
|
||||
case IdentityAuthMethod.TLS_CERT_AUTH:
|
||||
revokeMethod = revokeTlsCertAuth;
|
||||
Component = ViewIdentityTlsCertAuthContent;
|
||||
break;
|
||||
case IdentityAuthMethod.AWS_AUTH:
|
||||
revokeMethod = revokeAwsAuth;
|
||||
Component = ViewIdentityAwsAuthContent;
|
||||
|
@ -0,0 +1,88 @@
|
||||
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
|
||||
import { useGetIdentityTlsCertAuth } from "@app/hooks/api";
|
||||
import { IdentityTlsCertAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTlsCertAuthForm";
|
||||
|
||||
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
|
||||
import { ViewAuthMethodProps } from "./types";
|
||||
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
|
||||
|
||||
export const ViewIdentityTlsCertAuthContent = ({
|
||||
identityId,
|
||||
handlePopUpToggle,
|
||||
handlePopUpOpen,
|
||||
onDelete,
|
||||
popUp
|
||||
}: ViewAuthMethodProps) => {
|
||||
const { data, isPending } = useGetIdentityTlsCertAuth(identityId);
|
||||
|
||||
if (isPending) {
|
||||
return (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<Spinner className="text-mineshaft-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<EmptyState
|
||||
icon={faBan}
|
||||
title="Could not find TLS Certificate Auth associated with this Identity."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (popUp.identityAuthMethod.isOpen) {
|
||||
return (
|
||||
<IdentityTlsCertAuthForm
|
||||
identityId={identityId}
|
||||
isUpdate
|
||||
handlePopUpOpen={handlePopUpOpen}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewIdentityContentWrapper
|
||||
onEdit={() => handlePopUpOpen("identityAuthMethod")}
|
||||
onDelete={onDelete}
|
||||
>
|
||||
<IdentityAuthFieldDisplay label="Access Token TTL (seconds)">
|
||||
{data.accessTokenTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max TTL (seconds)">
|
||||
{data.accessTokenMaxTTL}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
|
||||
{data.accessTokenNumUsesLimit}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
|
||||
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay className="col-span-2" label="CA Certificate">
|
||||
<Tooltip
|
||||
side="right"
|
||||
className="max-w-xl p-2"
|
||||
content={<p className="break-words rounded bg-mineshaft-600 p-2">{data.caCertificate}</p>}
|
||||
>
|
||||
<div className="w-min">
|
||||
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
|
||||
<FontAwesomeIcon icon={faEye} />
|
||||
<span>Reveal</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</IdentityAuthFieldDisplay>
|
||||
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Common Names">
|
||||
{data.allowedCommonNames
|
||||
?.split(",")
|
||||
.map((cn) => cn.trim())
|
||||
.join(", ")}
|
||||
</IdentityAuthFieldDisplay>
|
||||
</ViewIdentityContentWrapper>
|
||||
);
|
||||
};
|
@ -18,6 +18,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
@ -39,6 +40,7 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { isCustomProjectRole } from "@app/helpers/roles";
|
||||
import {
|
||||
getUserTablePreference,
|
||||
PreferenceKey,
|
||||
@ -53,7 +55,8 @@ import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/R
|
||||
|
||||
enum RolesOrderBy {
|
||||
Name = "name",
|
||||
Slug = "slug"
|
||||
Slug = "slug",
|
||||
Type = "type"
|
||||
}
|
||||
|
||||
export const ProjectRoleList = () => {
|
||||
@ -98,7 +101,7 @@ export const ProjectRoleList = () => {
|
||||
setPerPage,
|
||||
setPage,
|
||||
offset
|
||||
} = usePagination<RolesOrderBy>(RolesOrderBy.Name, {
|
||||
} = usePagination<RolesOrderBy>(RolesOrderBy.Type, {
|
||||
initPerPage: getUserTablePreference("projectRolesTable", PreferenceKey.PerPage, 20)
|
||||
});
|
||||
|
||||
@ -125,6 +128,12 @@ export const ProjectRoleList = () => {
|
||||
switch (orderBy) {
|
||||
case RolesOrderBy.Slug:
|
||||
return roleOne.slug.toLowerCase().localeCompare(roleTwo.slug.toLowerCase());
|
||||
case RolesOrderBy.Type: {
|
||||
const roleOneValue = isCustomProjectRole(roleOne.slug) ? -1 : 1;
|
||||
const roleTwoValue = isCustomProjectRole(roleTwo.slug) ? -1 : 1;
|
||||
|
||||
return roleTwoValue - roleOneValue;
|
||||
}
|
||||
case RolesOrderBy.Name:
|
||||
default:
|
||||
return roleOne.name.toLowerCase().localeCompare(roleTwo.name.toLowerCase());
|
||||
@ -210,6 +219,19 @@ export const ProjectRoleList = () => {
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Type
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(RolesOrderBy.Type)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(RolesOrderBy.Type)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(RolesOrderBy.Type)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th aria-label="actions" className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
@ -237,6 +259,11 @@ export const ProjectRoleList = () => {
|
||||
>
|
||||
<Td>{name}</Td>
|
||||
<Td>{slug}</Td>
|
||||
<Td>
|
||||
<Badge className="w-min whitespace-nowrap bg-mineshaft-400/50 text-bunker-200">
|
||||
{isCustomProjectRole(slug) ? "Custom" : "Default"}
|
||||
</Badge>
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip className="max-w-sm text-center" content="Options">
|
||||
<DropdownMenu>
|
||||
|
@ -284,7 +284,6 @@ const ServiceTokenForm = () => {
|
||||
<Checkbox
|
||||
id={String(value[optionValue])}
|
||||
key={optionValue}
|
||||
className="data-[state=checked]:bg-primary"
|
||||
isChecked={value[optionValue]}
|
||||
isDisabled={optionValue === "read"}
|
||||
onCheckedChange={(state) => {
|
||||
|
@ -16,11 +16,9 @@ import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@ -54,8 +52,6 @@ import {
|
||||
} from "@app/helpers/userTablePreferences";
|
||||
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||
import {
|
||||
useDeleteAccessApprovalPolicy,
|
||||
useDeleteSecretApprovalPolicy,
|
||||
useGetSecretApprovalPolicies,
|
||||
useGetWorkspaceUsers,
|
||||
useListWorkspaceGroups
|
||||
@ -67,6 +63,7 @@ import { TAccessApprovalPolicy, Workspace } from "@app/hooks/api/types";
|
||||
|
||||
import { AccessPolicyForm } from "./components/AccessPolicyModal";
|
||||
import { ApprovalPolicyRow } from "./components/ApprovalPolicyRow";
|
||||
import { RemoveApprovalPolicyModal } from "./components/RemoveApprovalPolicyModal";
|
||||
|
||||
interface IProps {
|
||||
workspaceId: string;
|
||||
@ -122,7 +119,7 @@ const useApprovalPolicies = (permission: TProjectPermission, currentWorkspace?:
|
||||
};
|
||||
|
||||
export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([
|
||||
const { handlePopUpToggle, handlePopUpOpen, popUp } = usePopUp([
|
||||
"policyForm",
|
||||
"deletePolicy",
|
||||
"upgradePlan"
|
||||
@ -213,39 +210,6 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
setPage
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
|
||||
const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy();
|
||||
|
||||
const handleDeletePolicy = async () => {
|
||||
const { id, policyType } = popUp.deletePolicy.data as TAccessApprovalPolicy;
|
||||
if (!currentWorkspace?.slug) return;
|
||||
|
||||
try {
|
||||
if (policyType === PolicyType.ChangePolicy) {
|
||||
await deleteSecretApprovalPolicy({
|
||||
workspaceId,
|
||||
id
|
||||
});
|
||||
} else {
|
||||
await deleteAccessApprovalPolicy({
|
||||
projectSlug: currentWorkspace?.slug,
|
||||
id
|
||||
});
|
||||
}
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted policy"
|
||||
});
|
||||
handlePopUpClose("deletePolicy");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete policy"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isTableFiltered = filters.type !== null || Boolean(filters.environmentIds.length);
|
||||
|
||||
const handleSort = (column: PolicyOrderBy) => {
|
||||
@ -528,13 +492,14 @@ export const ApprovalPolicyList = ({ workspaceId }: IProps) => {
|
||||
members={members}
|
||||
editValues={popUp.policyForm.data as TAccessApprovalPolicy}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePolicy.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this policy?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePolicy", isOpen)}
|
||||
onDeleteApproved={handleDeletePolicy}
|
||||
/>
|
||||
{popUp.deletePolicy.data && (
|
||||
<RemoveApprovalPolicyModal
|
||||
isOpen={popUp.deletePolicy.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deletePolicy", isOpen)}
|
||||
policyType={popUp.deletePolicy.data.policyType}
|
||||
policyId={popUp.deletePolicy.data.id}
|
||||
/>
|
||||
)}
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
|
@ -0,0 +1,135 @@
|
||||
import { faCheck, faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { DeleteActionModal, Spinner } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import {
|
||||
useDeleteAccessApprovalPolicy,
|
||||
useDeleteSecretApprovalPolicy,
|
||||
useGetAccessRequestsCount,
|
||||
useGetSecretApprovalRequestCount
|
||||
} from "@app/hooks/api";
|
||||
import { PolicyType } from "@app/hooks/api/policies/enums";
|
||||
|
||||
type Props = {
|
||||
policyId: string;
|
||||
policyType: PolicyType;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export const RemoveApprovalPolicyModal = ({
|
||||
policyId,
|
||||
policyType,
|
||||
isOpen,
|
||||
onOpenChange
|
||||
}: Props) => {
|
||||
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
|
||||
const { mutateAsync: deleteAccessApprovalPolicy } = useDeleteAccessApprovalPolicy();
|
||||
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const handleDeletePolicy = async () => {
|
||||
try {
|
||||
if (policyType === PolicyType.ChangePolicy) {
|
||||
await deleteSecretApprovalPolicy({
|
||||
workspaceId: currentWorkspace.id,
|
||||
id: policyId
|
||||
});
|
||||
} else {
|
||||
await deleteAccessApprovalPolicy({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
id: policyId
|
||||
});
|
||||
}
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted policy"
|
||||
});
|
||||
onOpenChange(false);
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete policy"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSecretApprovalData = useGetSecretApprovalRequestCount({
|
||||
policyId,
|
||||
workspaceId: currentWorkspace.id,
|
||||
options: {
|
||||
enabled: Boolean(policyId) && policyType === PolicyType.ChangePolicy
|
||||
}
|
||||
});
|
||||
|
||||
const deleteAccessApprovalData = useGetAccessRequestsCount({
|
||||
projectSlug: currentWorkspace.slug,
|
||||
policyId,
|
||||
options: {
|
||||
enabled: Boolean(policyId) && policyType === PolicyType.AccessPolicy
|
||||
}
|
||||
});
|
||||
|
||||
let openCount: number | undefined;
|
||||
let isPending: boolean;
|
||||
if (policyType === PolicyType.ChangePolicy) {
|
||||
openCount = deleteSecretApprovalData.data?.open;
|
||||
isPending = deleteSecretApprovalData.isPending;
|
||||
} else {
|
||||
openCount = deleteAccessApprovalData.data?.pendingCount;
|
||||
isPending = deleteAccessApprovalData.isPending;
|
||||
}
|
||||
|
||||
return (
|
||||
<DeleteActionModal
|
||||
isOpen={isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this policy?"
|
||||
onChange={onOpenChange}
|
||||
onDeleteApproved={handleDeletePolicy}
|
||||
isDisabled={isPending}
|
||||
>
|
||||
{isPending ? (
|
||||
<div className="mt-4 flex w-full items-center gap-2 p-2">
|
||||
<Spinner size="xs" className="text-mineshaft-600" />
|
||||
<span className="text-sm text-mineshaft-400">Checking for open requests...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={twMerge(
|
||||
"mt-4 flex w-full items-start gap-2 rounded border p-2 text-sm",
|
||||
(openCount ?? 0) > 0
|
||||
? "border-yellow/20 bg-yellow/10 text-yellow"
|
||||
: "border-green/20 bg-green/10 text-green"
|
||||
)}
|
||||
>
|
||||
{(openCount ?? 0) > 0 ? (
|
||||
<>
|
||||
<FontAwesomeIcon className="mt-1" icon={faWarning} />
|
||||
<div className="flex flex-col">
|
||||
<span>
|
||||
This policy has {openCount} open request
|
||||
{(openCount ?? 0) > 1 ? "s" : ""}
|
||||
</span>
|
||||
<p className="text-xs text-mineshaft-200">
|
||||
Removing this policy will close all open requests.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FontAwesomeIcon className="mt-1" icon={faCheck} />
|
||||
<div className="flex flex-col">
|
||||
<span>This policy has no open requests</span>
|
||||
<p className="text-xs text-mineshaft-200">This policy is safe to remove.</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DeleteActionModal>
|
||||
);
|
||||
};
|
@ -133,7 +133,6 @@ const Folder: React.FC<FolderProps> = ({
|
||||
{!isDisabled && (
|
||||
<Checkbox
|
||||
id="folder-root"
|
||||
className="data-[state=indeterminate]:bg-secondary data-[state=checked]:bg-primary"
|
||||
isChecked={allSelected || someSelected}
|
||||
onCheckedChange={handleFolderSelect}
|
||||
isIndeterminate={someSelected && !allSelected}
|
||||
@ -167,7 +166,6 @@ const Folder: React.FC<FolderProps> = ({
|
||||
{!isDisabled && (
|
||||
<Checkbox
|
||||
id={`folder-${item.id}`}
|
||||
className="data-[state=indeterminate]:bg-secondary data-[state=checked]:bg-primary"
|
||||
isChecked={selectedItemIds.includes(item.id)}
|
||||
onCheckedChange={(checked) => onItemSelect(item, !!checked)}
|
||||
isDisabled={isDisabled}
|
||||
|
@ -42,7 +42,6 @@ export const AutoCapitalizationSection = () => {
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="autoCapitalization"
|
||||
isDisabled={!isAllowed}
|
||||
isChecked={currentWorkspace?.autoCapitalization ?? false}
|
||||
|
@ -38,7 +38,6 @@ export const DeleteProjectProtection = () => {
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="hasDeleteProtection"
|
||||
isDisabled={!isAllowed}
|
||||
isChecked={currentWorkspace?.hasDeleteProtection ?? false}
|
||||
|
@ -48,7 +48,6 @@ export const SecretSharingSection = () => {
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="secretSharing"
|
||||
isDisabled={!isAllowed || isLoading}
|
||||
isChecked={currentWorkspace?.secretSharing ?? true}
|
||||
|
@ -48,7 +48,6 @@ export const SecretSnapshotsLegacySection = () => {
|
||||
{(isAllowed) => (
|
||||
<div className="w-max">
|
||||
<Checkbox
|
||||
className="data-[state=checked]:bg-primary"
|
||||
id="showSnapshotsLegacy"
|
||||
isDisabled={!isAllowed || isLoading}
|
||||
isChecked={currentWorkspace?.showSnapshotsLegacy ?? false}
|
||||
|
Reference in New Issue
Block a user