Compare commits

..

19 Commits

Author SHA1 Message Date
09887a7405 Update ConfiguredIntegrationItem.tsx 2024-09-16 23:05:38 +04:00
38ee3a005e Requested changes 2024-09-16 22:26:36 +04:00
74653e7ed1 Minor ui improvements 2024-09-16 13:56:23 +04:00
8a0b1bb427 Update IntegrationAuditLogsSection.tsx 2024-09-15 20:34:08 +04:00
1f6faadf81 Cleanup 2024-09-15 20:24:23 +04:00
8f3b7e1698 feat: audit logs event metadata & remapping support 2024-09-15 20:01:43 +04:00
24c460c695 feat: integration details page 2024-09-15 20:00:43 +04:00
8acceab1e7 fix: updated last used to be considered last success sync 2024-09-15 19:57:56 +04:00
d60aba9339 fix: added missing integration metadata attributes 2024-09-15 19:57:36 +04:00
3a228f7521 feat: improved audit logs 2024-09-15 19:57:02 +04:00
3f7ac0f142 feat: integration synced log event 2024-09-15 19:52:43 +04:00
63cf535ebb feat: platform-level actor for logs 2024-09-15 19:52:13 +04:00
69a2a46c47 Update organization-router.ts 2024-09-15 19:51:54 +04:00
d081077273 feat: integration sync logs 2024-09-15 19:51:38 +04:00
75034f9350 feat: more expendable audit logs 2024-09-15 19:50:03 +04:00
eacd7b0c6a feat: made audit logs more searchable with better filters 2024-09-15 19:49:35 +04:00
5bad77083c feat: more expendable audit logs 2024-09-15 19:49:07 +04:00
1025759efb Feat: Integration Audit Logs 2024-09-13 21:00:47 +04:00
5e5ab29ab9 Feat: Integration UI improvements 2024-09-12 13:09:00 +04:00
124 changed files with 2346 additions and 3279 deletions

View File

@ -72,3 +72,6 @@ PLAIN_API_KEY=
PLAIN_WISH_LABEL_IDS= PLAIN_WISH_LABEL_IDS=
SSL_CLIENT_CERTIFICATE_HEADER_KEY= SSL_CLIENT_CERTIFICATE_HEADER_KEY=
WORKFLOW_SLACK_CLIENT_ID=
WORKFLOW_SLACK_CLIENT_SECRET=

1
.gitignore vendored
View File

@ -63,7 +63,6 @@ yarn-error.log*
# Editor specific # Editor specific
.vscode/* .vscode/*
.idea/*
frontend-build frontend-build

File diff suppressed because one or more lines are too long

View File

@ -1,85 +0,0 @@
import { Knex } from "knex";
import { CertKeyUsage } from "@app/services/certificate/certificate-types";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
// Certificate template
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.CertificateTemplate, (tb) => {
if (!hasKeyUsagesCol) {
tb.specificType("keyUsages", "text[]");
}
if (!hasExtendedKeyUsagesCol) {
tb.specificType("extendedKeyUsages", "text[]");
}
});
if (!hasKeyUsagesCol) {
await knex(TableName.CertificateTemplate).update({
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]
});
}
if (!hasExtendedKeyUsagesCol) {
await knex(TableName.CertificateTemplate).update({
extendedKeyUsages: []
});
}
// Certificate
const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages");
const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.Certificate, (tb) => {
if (!doesCertTableHaveKeyUsages) {
tb.specificType("keyUsages", "text[]");
}
if (!doesCertTableHaveExtendedKeyUsages) {
tb.specificType("extendedKeyUsages", "text[]");
}
});
if (!doesCertTableHaveKeyUsages) {
await knex(TableName.Certificate).update({
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]
});
}
if (!doesCertTableHaveExtendedKeyUsages) {
await knex(TableName.Certificate).update({
extendedKeyUsages: []
});
}
}
export async function down(knex: Knex): Promise<void> {
// Certificate Template
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.CertificateTemplate, (t) => {
if (hasKeyUsagesCol) {
t.dropColumn("keyUsages");
}
if (hasExtendedKeyUsagesCol) {
t.dropColumn("extendedKeyUsages");
}
});
// Certificate
const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages");
const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages");
await knex.schema.alterTable(TableName.Certificate, (t) => {
if (doesCertTableHaveKeyUsages) {
t.dropColumn("keyUsages");
}
if (doesCertTableHaveExtendedKeyUsages) {
t.dropColumn("extendedKeyUsages");
}
});
}

View File

@ -16,9 +16,7 @@ export const CertificateTemplatesSchema = z.object({
subjectAlternativeName: z.string(), subjectAlternativeName: z.string(),
ttl: z.string(), ttl: z.string(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date()
keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional()
}); });
export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>; export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>;

View File

@ -22,9 +22,7 @@ export const CertificatesSchema = z.object({
revocationReason: z.number().nullable().optional(), revocationReason: z.number().nullable().optional(),
altNames: z.string().default("").nullable().optional(), altNames: z.string().default("").nullable().optional(),
caCertId: z.string().uuid(), caCertId: z.string().uuid(),
certificateTemplateId: z.string().uuid().nullable().optional(), certificateTemplateId: z.string().uuid().nullable().optional()
keyUsages: z.string().array().nullable().optional(),
extendedKeyUsages: z.string().array().nullable().optional()
}); });
export type TCertificates = z.infer<typeof CertificatesSchema>; export type TCertificates = z.infer<typeof CertificatesSchema>;

View File

@ -11,30 +11,6 @@ export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: {
description: "Get CRL in DER format (deprecated)",
params: z.object({
crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
}),
response: {
200: z.instanceof(Buffer)
}
},
handler: async (req, res) => {
const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
res.header("Content-Type", "application/pkix-crl");
return Buffer.from(crl);
}
});
server.route({
method: "GET",
url: "/:crlId/der",
config: {
rateLimit: readLimit
},
schema: { schema: {
description: "Get CRL in DER format", description: "Get CRL in DER format",
params: z.object({ params: z.object({

View File

@ -101,7 +101,6 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
message: "Slug must be a valid" message: "Slug must be a valid"
}), }),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional()
}), }),
response: { response: {

View File

@ -146,12 +146,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId: req.params.workspaceId, actor: req.permission.type,
...req.query,
endDate: req.query.endDate, filter: {
startDate: req.query.startDate || getLastMidnightDateISO(), ...req.query,
auditLogActor: req.query.actor, projectId: req.params.workspaceId,
actor: req.permission.type endDate: req.query.endDate,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor,
eventType: req.query.eventType ? [req.query.eventType] : undefined
}
}); });
return { auditLogs }; return { auditLogs };
} }

View File

@ -100,20 +100,9 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
async (req, profile, cb) => { async (req, profile, cb) => {
try { try {
if (!profile) throw new BadRequestError({ message: "Missing profile" }); if (!profile) throw new BadRequestError({ message: "Missing profile" });
const email = const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
profile?.email ??
// entra sends data in this format
(profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"] as string) ??
(profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\
const firstName = (profile.firstName ?? if (!email || !profile.firstName) {
// entra sends data in this format
profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName"]) as string;
const lastName =
profile.lastName ?? profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/lastName"];
if (!email || !firstName) {
logger.info( logger.info(
{ {
err: new Error("Invalid saml request. Missing email or first name"), err: new Error("Invalid saml request. Missing email or first name"),
@ -121,13 +110,14 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
}, },
`email: ${email} firstName: ${profile.firstName as string}` `email: ${email} firstName: ${profile.firstName as string}`
); );
throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
} }
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({ const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
externalId: profile.nameID, externalId: profile.nameID,
email, email,
firstName, firstName: profile.firstName as string,
lastName: lastName as string, lastName: profile.lastName as string,
relayState: (req.body as { RelayState?: string }).RelayState, relayState: (req.body as { RelayState?: string }).RelayState,
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string, authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string

View File

@ -6,6 +6,9 @@ import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex"; import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
import { ActorType } from "@app/services/auth/auth-type";
import { EventType } from "./audit-log-types";
export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>; export type TAuditLogDALFactory = ReturnType<typeof auditLogDALFactory>;
@ -25,7 +28,24 @@ export const auditLogDALFactory = (db: TDbClient) => {
const auditLogOrm = ormify(db, TableName.AuditLog); const auditLogOrm = ormify(db, TableName.AuditLog);
const find = async ( const find = async (
{ orgId, projectId, userAgentType, startDate, endDate, limit = 20, offset = 0, actor, eventType }: TFindQuery, {
orgId,
projectId,
userAgentType,
startDate,
endDate,
limit = 20,
offset = 0,
actorId,
actorType,
eventType,
eventMetadata
}: Omit<TFindQuery, "actor" | "eventType"> & {
actorId?: string;
actorType?: ActorType;
eventType?: EventType[];
eventMetadata?: Record<string, string>;
},
tx?: Knex tx?: Knex
) => { ) => {
try { try {
@ -34,7 +54,6 @@ export const auditLogDALFactory = (db: TDbClient) => {
stripUndefinedInWhere({ stripUndefinedInWhere({
projectId, projectId,
[`${TableName.AuditLog}.orgId`]: orgId, [`${TableName.AuditLog}.orgId`]: orgId,
eventType,
userAgentType userAgentType
}) })
) )
@ -52,8 +71,22 @@ export const auditLogDALFactory = (db: TDbClient) => {
.offset(offset) .offset(offset)
.orderBy(`${TableName.AuditLog}.createdAt`, "desc"); .orderBy(`${TableName.AuditLog}.createdAt`, "desc");
if (actor) { if (actorId) {
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actor]); void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actorId]);
}
if (eventMetadata && Object.keys(eventMetadata).length) {
Object.entries(eventMetadata).forEach(([key, value]) => {
void sqlQuery.whereRaw(`"eventMetadata"->>'${key}' = ?`, [value]);
});
}
if (actorType) {
void sqlQuery.where("actor", actorType);
}
if (eventType?.length) {
void sqlQuery.whereIn("eventType", eventType);
} }
if (startDate) { if (startDate) {

View File

@ -23,25 +23,12 @@ export const auditLogServiceFactory = ({
auditLogQueue, auditLogQueue,
permissionService permissionService
}: TAuditLogServiceFactoryDep) => { }: TAuditLogServiceFactoryDep) => {
const listAuditLogs = async ({ const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
userAgentType, if (filter.projectId) {
eventType,
offset,
limit,
endDate,
startDate,
actor,
actorId,
actorOrgId,
actorAuthMethod,
projectId,
auditLogActor
}: TListProjectAuditLogDTO) => {
if (projectId) {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
actorId, actorId,
projectId, filter.projectId,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
); );
@ -65,14 +52,16 @@ export const auditLogServiceFactory = ({
// If project ID is not provided, then we need to return all the audit logs for the organization itself. // If project ID is not provided, then we need to return all the audit logs for the organization itself.
const auditLogs = await auditLogDAL.find({ const auditLogs = await auditLogDAL.find({
startDate, startDate: filter.startDate,
endDate, endDate: filter.endDate,
limit, limit: filter.limit,
offset, offset: filter.offset,
eventType, eventType: filter.eventType,
userAgentType, userAgentType: filter.userAgentType,
actor: auditLogActor, actorId: filter.auditLogActorId,
...(projectId ? { projectId } : { orgId: actorOrgId }) actorType: filter.actorType,
eventMetadata: filter.eventMetadata,
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
}); });
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({ return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({

View File

@ -5,19 +5,23 @@ import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types"; import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
export type TListProjectAuditLogDTO = { export type TListProjectAuditLogDTO = {
auditLogActor?: string; filter: {
projectId?: string; userAgentType?: UserAgentType;
eventType?: string; eventType?: EventType[];
startDate?: string; offset?: number;
endDate?: string; limit: number;
userAgentType?: string; endDate?: string;
limit?: number; startDate?: string;
offset?: number; projectId?: string;
auditLogActorId?: string;
actorType?: ActorType;
eventMetadata?: Record<string, string>;
};
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TCreateAuditLogDTO = { export type TCreateAuditLogDTO = {
event: Event; event: Event;
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor; actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor;
orgId?: string; orgId?: string;
projectId?: string; projectId?: string;
} & BaseAuthData; } & BaseAuthData;
@ -177,7 +181,8 @@ export enum EventType {
UPDATE_SLACK_INTEGRATION = "update-slack-integration", UPDATE_SLACK_INTEGRATION = "update-slack-integration",
DELETE_SLACK_INTEGRATION = "delete-slack-integration", DELETE_SLACK_INTEGRATION = "delete-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config", GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config" UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
INTEGRATION_SYNCED = "integration-synced"
} }
interface UserActorMetadata { interface UserActorMetadata {
@ -198,6 +203,8 @@ interface IdentityActorMetadata {
interface ScimClientActorMetadata {} interface ScimClientActorMetadata {}
interface PlatformActorMetadata {}
export interface UserActor { export interface UserActor {
type: ActorType.USER; type: ActorType.USER;
metadata: UserActorMetadata; metadata: UserActorMetadata;
@ -208,6 +215,11 @@ export interface ServiceActor {
metadata: ServiceActorMetadata; metadata: ServiceActorMetadata;
} }
export interface PlatformActor {
type: ActorType.PLATFORM;
metadata: PlatformActorMetadata;
}
export interface IdentityActor { export interface IdentityActor {
type: ActorType.IDENTITY; type: ActorType.IDENTITY;
metadata: IdentityActorMetadata; metadata: IdentityActorMetadata;
@ -218,7 +230,7 @@ export interface ScimClientActor {
metadata: ScimClientActorMetadata; metadata: ScimClientActorMetadata;
} }
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor; export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
@ -1518,6 +1530,16 @@ interface GetProjectSlackConfig {
id: string; id: string;
}; };
} }
interface IntegrationSyncedEvent {
type: EventType.INTEGRATION_SYNCED;
metadata: {
integrationId: string;
lastSyncJobId: string;
lastUsed: Date;
syncMessage: string;
isSynced: boolean;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
@ -1657,4 +1679,5 @@ export type Event =
| DeleteSlackIntegration | DeleteSlackIntegration
| GetSlackIntegration | GetSlackIntegration
| UpdateProjectSlackConfig | UpdateProjectSlackConfig
| GetProjectSlackConfig; | GetProjectSlackConfig
| IntegrationSyncedEvent;

View File

@ -1,7 +1,6 @@
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability"; import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
import { conditionsMatcher } from "@app/lib/casl"; import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError } from "@app/lib/errors";
export enum ProjectPermissionActions { export enum ProjectPermissionActions {
Read = "read", Read = "read",
@ -76,125 +75,117 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms];
export const fullProjectPermissionSet: [ProjectPermissionActions, ProjectPermissionSub][] = [
[ProjectPermissionActions.Read, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Create, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets],
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval],
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation],
[ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback],
[ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback],
[ProjectPermissionActions.Read, ProjectPermissionSub.Member],
[ProjectPermissionActions.Create, ProjectPermissionSub.Member],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Member],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Member],
[ProjectPermissionActions.Read, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Create, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Groups],
[ProjectPermissionActions.Read, ProjectPermissionSub.Role],
[ProjectPermissionActions.Create, ProjectPermissionSub.Role],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Role],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Role],
[ProjectPermissionActions.Read, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Create, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations],
[ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks],
[ProjectPermissionActions.Read, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Create, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Identity],
[ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens],
[ProjectPermissionActions.Read, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Create, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Settings],
[ProjectPermissionActions.Read, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Create, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Environments],
[ProjectPermissionActions.Read, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Create, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Tags],
[ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs],
[ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList],
[ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList],
[ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList],
[ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList],
// double check if all CRUD are needed for CA and Certificates
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities],
[ProjectPermissionActions.Read, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Create, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates],
[ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates],
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts],
[ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Project],
[ProjectPermissionActions.Delete, ProjectPermissionSub.Project],
[ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]
];
const buildAdminPermissionRules = () => { const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
// Admins get full access to everything can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
fullProjectPermissionSet.forEach((permission) => { can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
const [action, subject] = permission; can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(action, subject); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
});
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretRotation);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Role);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Integrations);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Webhooks);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Create, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Create, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Create, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.IpAllowList);
// double check if all CRUD are needed for CA and Certificates
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Create, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Certificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Create, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.CertificateTemplates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiAlerts);
can(ProjectPermissionActions.Read, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Create, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.PkiCollections);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Project);
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Kms);
return rules; return rules;
}; };
@ -381,31 +372,4 @@ export const isAtLeastAsPrivilegedWorkspace = (
return set1.size >= set2.size; return set1.size >= set2.size;
}; };
/*
* Case: The user requests to create a role with permissions that are not valid and not supposed to be used ever.
* If we don't check for this, we can run into issues where functions like the `isAtLeastAsPrivileged` will not work as expected, because we compare the size of each permission set.
* If the permission set contains invalid permissions, the size will be different, and result in incorrect results.
*/
export const validateProjectPermissions = (permissions: unknown) => {
const parsedPermissions =
typeof permissions === "string" ? (JSON.parse(permissions) as string[]) : (permissions as string[]);
const flattenedPermissions = [...parsedPermissions];
for (const perm of flattenedPermissions) {
const [action, subject] = perm;
if (
!fullProjectPermissionSet.find(
(currentPermission) => currentPermission[0] === action && currentPermission[1] === subject
)
) {
throw new BadRequestError({
message: `Permission action ${action} on subject ${subject} is not valid`,
name: "Create Role"
});
}
}
};
/* eslint-enable */ /* eslint-enable */

View File

@ -363,12 +363,7 @@ export const ORGANIZATIONS = {
membershipId: "The ID of the membership to delete." membershipId: "The ID of the membership to delete."
}, },
LIST_IDENTITY_MEMBERSHIPS: { LIST_IDENTITY_MEMBERSHIPS: {
orgId: "The ID of the organization to get identity memberships from.", orgId: "The ID of the organization to get identity memberships from."
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
orderBy: "The column to order identity memberships by.",
orderDirection: "The direction identity memberships will be sorted in.",
search: "The text string that identity membership names will be filtered by."
}, },
GET_PROJECTS: { GET_PROJECTS: {
organizationId: "The ID of the organization to get projects from." organizationId: "The ID of the organization to get projects from."
@ -477,12 +472,7 @@ export const PROJECT_USERS = {
export const PROJECT_IDENTITIES = { export const PROJECT_IDENTITIES = {
LIST_IDENTITY_MEMBERSHIPS: { LIST_IDENTITY_MEMBERSHIPS: {
projectId: "The ID of the project to get identity memberships from.", projectId: "The ID of the project to get identity memberships from."
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
limit: "The number of identity memberships to return.",
orderBy: "The column to order identity memberships by.",
orderDirection: "The direction identity memberships will be sorted in.",
search: "The text string that identity membership names will be filtered by."
}, },
GET_IDENTITY_MEMBERSHIP_BY_ID: { GET_IDENTITY_MEMBERSHIP_BY_ID: {
identityId: "The ID of the identity to get the membership for.", identityId: "The ID of the identity to get the membership for.",
@ -1083,10 +1073,6 @@ export const CERTIFICATE_AUTHORITIES = {
certificateChain: "The certificate chain of the CA", certificateChain: "The certificate chain of the CA",
serialNumber: "The serial number of the CA certificate" serialNumber: "The serial number of the CA certificate"
}, },
GET_CERT_BY_ID: {
caId: "The ID of the CA to get the CA certificate from",
caCertId: "The ID of the CA certificate to get"
},
GET_CA_CERTS: { GET_CA_CERTS: {
caId: "The ID of the CA to get the CA certificates for", caId: "The ID of the CA to get the CA certificates for",
certificate: "The certificate body of the CA certificate", certificate: "The certificate body of the CA certificate",
@ -1126,15 +1112,11 @@ export const CERTIFICATE_AUTHORITIES = {
issuingCaCertificate: "The certificate of the issuing CA", issuingCaCertificate: "The certificate of the issuing CA",
certificateChain: "The certificate chain of the issued certificate", certificateChain: "The certificate chain of the issued certificate",
privateKey: "The private key of the issued certificate", privateKey: "The private key of the issued certificate",
serialNumber: "The serial number of the issued certificate", serialNumber: "The serial number of the issued certificate"
keyUsages: "The key usage extension of the certificate",
extendedKeyUsages: "The extended key usage extension of the certificate"
}, },
SIGN_CERT: { SIGN_CERT: {
caId: "The ID of the CA to issue the certificate from", caId: "The ID of the CA to issue the certificate from",
pkiCollectionId: "The ID of the PKI collection to add the certificate to", pkiCollectionId: "The ID of the PKI collection to add the certificate to",
keyUsages: "The key usage extension of the certificate",
extendedKeyUsages: "The extended key usage extension of the certificate",
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance", csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
friendlyName: "A friendly name for the certificate", friendlyName: "A friendly name for the certificate",
commonName: "The common name (CN) for the certificate", commonName: "The common name (CN) for the certificate",
@ -1184,10 +1166,7 @@ export const CERTIFICATE_TEMPLATES = {
name: "The name of the template", name: "The name of the template",
commonName: "The regular expression string to use for validating common names", commonName: "The regular expression string to use for validating common names",
subjectAlternativeName: "The regular expression string to use for validating subject alternative names", subjectAlternativeName: "The regular expression string to use for validating subject alternative names",
ttl: "The max TTL for the template", ttl: "The max TTL for the template"
keyUsages: "The key usage constraint or default value for when template is used during certificate issuance",
extendedKeyUsages:
"The extended key usage constraint or default value for when template is used during certificate issuance"
}, },
GET: { GET: {
certificateTemplateId: "The ID of the certificate template to get" certificateTemplateId: "The ID of the certificate template to get"
@ -1199,11 +1178,7 @@ export const CERTIFICATE_TEMPLATES = {
name: "The updated name of the template", name: "The updated name of the template",
commonName: "The updated regular expression string for validating common names", commonName: "The updated regular expression string for validating common names",
subjectAlternativeName: "The updated regular expression string for validating subject alternative names", subjectAlternativeName: "The updated regular expression string for validating subject alternative names",
ttl: "The updated max TTL for the template", ttl: "The updated max TTL for the template"
keyUsages:
"The updated key usage constraint or default value for when template is used during certificate issuance",
extendedKeyUsages:
"The updated extended key usage constraint or default value for when template is used during certificate issuance"
}, },
DELETE: { DELETE: {
certificateTemplateId: "The ID of the certificate template to delete" certificateTemplateId: "The ID of the certificate template to delete"

View File

@ -147,8 +147,8 @@ const envSchema = z
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()), PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"), DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"), SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()), WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string()).optional(),
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional()) WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string()).optional()
}) })
.transform((data) => ({ .transform((data) => ({
...data, ...data,

View File

@ -52,8 +52,3 @@ export enum SecretSharingAccessType {
Anyone = "anyone", Anyone = "anyone",
Organization = "organization" Organization = "organization"
} }
export enum OrderByDirection {
ASC = "asc",
DESC = "desc"
}

View File

@ -91,6 +91,8 @@ export type TQueueJobTypes = {
[QueueName.IntegrationSync]: { [QueueName.IntegrationSync]: {
name: QueueJobs.IntegrationSync; name: QueueJobs.IntegrationSync;
payload: { payload: {
isManual?: boolean;
actorId?: string;
projectId: string; projectId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;

View File

@ -1,6 +1,5 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import fastifyPlugin from "fastify-plugin"; import fastifyPlugin from "fastify-plugin";
import { JsonWebTokenError } from "jsonwebtoken";
import { ZodError } from "zod"; import { ZodError } from "zod";
import { import {
@ -12,12 +11,6 @@ import {
UnauthorizedError UnauthorizedError
} from "@app/lib/errors"; } from "@app/lib/errors";
enum JWTErrors {
JwtExpired = "jwt expired",
JwtMalformed = "jwt malformed",
InvalidAlgorithm = "invalid algorithm"
}
export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => { export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider) => {
server.setErrorHandler((error, req, res) => { server.setErrorHandler((error, req, res) => {
req.log.error(error); req.log.error(error);
@ -43,27 +36,6 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
status: error.status, status: error.status,
detail: error.detail detail: error.detail
}); });
// Handle JWT errors and make them more human-readable for the end-user.
} else if (error instanceof JsonWebTokenError) {
const message = (() => {
if (error.message === JWTErrors.JwtExpired) {
return "Your token has expired. Please re-authenticate.";
}
if (error.message === JWTErrors.JwtMalformed) {
return "The provided access token is malformed. Please use a valid token or generate a new one and try again.";
}
if (error.message === JWTErrors.InvalidAlgorithm) {
return "The access token is signed with an invalid algorithm. Please provide a valid token and try again.";
}
return error.message;
})();
void res.status(401).send({
statusCode: 401,
error: "TokenError",
message
});
} else { } else {
void res.send(error); void res.send(error);
} }

View File

@ -493,6 +493,7 @@ export const registerRoutes = async (
orgRoleDAL, orgRoleDAL,
permissionService, permissionService,
orgDAL, orgDAL,
userGroupMembershipDAL,
projectBotDAL, projectBotDAL,
incidentContactDAL, incidentContactDAL,
tokenService, tokenService,
@ -810,6 +811,8 @@ export const registerRoutes = async (
projectEnvDAL, projectEnvDAL,
webhookDAL, webhookDAL,
orgDAL, orgDAL,
auditLogService,
userDAL,
projectMembershipDAL, projectMembershipDAL,
smtpService, smtpService,
projectDAL, projectDAL,

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import ms from "ms"; import ms from "ms";
import { z } from "zod"; import { z } from "zod";
@ -8,7 +7,7 @@ import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types"; import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types"; import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
import { import {
validateAltNamesField, validateAltNamesField,
@ -140,33 +139,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
} }
}); });
// this endpoint will be used to serve the CA certificate when a client makes a request
// against the Authority Information Access CA Issuer URL
server.route({
method: "GET",
url: "/:caId/certificates/:caCertId/der",
config: {
rateLimit: readLimit
},
schema: {
description: "Get DER-encoded certificate of CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caId),
caCertId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CERT_BY_ID.caCertId)
}),
response: {
200: z.instanceof(Buffer)
}
},
handler: async (req, res) => {
const caCert = await server.services.certificateAuthority.getCaCertById(req.params);
res.header("Content-Type", "application/pkix-cert");
return Buffer.from(caCert.rawData);
}
});
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/:caId", url: "/:caId",
@ -601,9 +573,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl), .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter), notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
keyUsages: z.nativeEnum(CertKeyUsage).array().optional(),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional()
}) })
.refine( .refine(
(data) => { (data) => {
@ -683,9 +653,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl), .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter), notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
keyUsages: z.nativeEnum(CertKeyUsage).array().optional(),
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional()
}) })
.refine( .refine(
(data) => { (data) => {

View File

@ -7,7 +7,7 @@ import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types"; import { CrlReason } from "@app/services/certificate/certificate-types";
import { import {
validateAltNamesField, validateAltNamesField,
validateCaDateField validateCaDateField
@ -86,17 +86,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl), .describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter), notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
keyUsages: z
.nativeEnum(CertKeyUsage)
.array()
.optional()
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.keyUsages),
extendedKeyUsages: z
.nativeEnum(CertExtendedKeyUsage)
.array()
.optional()
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.extendedKeyUsages)
}) })
.refine( .refine(
(data) => { (data) => {
@ -187,17 +177,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl), .describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore), notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter), notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
keyUsages: z
.nativeEnum(CertKeyUsage)
.array()
.optional()
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.keyUsages),
extendedKeyUsages: z
.nativeEnum(CertExtendedKeyUsage)
.array()
.optional()
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.extendedKeyUsages)
}) })
.refine( .refine(
(data) => { (data) => {

View File

@ -7,7 +7,6 @@ import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema"; import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators"; import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
@ -75,19 +74,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
ttl: z ttl: z
.string() .string()
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl), .describe(CERTIFICATE_TEMPLATES.CREATE.ttl)
keyUsages: z
.nativeEnum(CertKeyUsage)
.array()
.optional()
.default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT])
.describe(CERTIFICATE_TEMPLATES.CREATE.keyUsages),
extendedKeyUsages: z
.nativeEnum(CertExtendedKeyUsage)
.array()
.optional()
.default([])
.describe(CERTIFICATE_TEMPLATES.CREATE.extendedKeyUsages)
}), }),
response: { response: {
200: sanitizedCertificateTemplate 200: sanitizedCertificateTemplate
@ -143,13 +130,7 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
.string() .string()
.refine((val) => ms(val) > 0, "TTL must be a positive number") .refine((val) => ms(val) > 0, "TTL must be a positive number")
.optional() .optional()
.describe(CERTIFICATE_TEMPLATES.UPDATE.ttl), .describe(CERTIFICATE_TEMPLATES.UPDATE.ttl)
keyUsages: z.nativeEnum(CertKeyUsage).array().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.keyUsages),
extendedKeyUsages: z
.nativeEnum(CertExtendedKeyUsage)
.array()
.optional()
.describe(CERTIFICATE_TEMPLATES.UPDATE.extendedKeyUsages)
}), }),
params: z.object({ params: z.object({
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId) certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)

View File

@ -246,13 +246,12 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
description: true description: true
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
}).array(), }).array()
totalCount: z.number()
}) })
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({ const identities = await server.services.identity.listOrgIdentities({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
@ -260,7 +259,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
orgId: req.query.orgId orgId: req.query.orgId
}); });
return { identities: identityMemberships, totalCount }; return { identities };
} }
}); });

View File

@ -4,7 +4,7 @@ import { IntegrationsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { INTEGRATION } from "@app/lib/api-docs"; import { INTEGRATION } from "@app/lib/api-docs";
import { removeTrailingSlash, shake } from "@app/lib/fn"; import { removeTrailingSlash, shake } from "@app/lib/fn";
import { writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
@ -154,6 +154,48 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/:integrationId",
config: {
rateLimit: readLimit
},
schema: {
description: "Get an integration by integration id",
security: [
{
bearerAuth: []
}
],
params: z.object({
integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId)
}),
response: {
200: z.object({
integration: IntegrationsSchema.extend({
environment: z.object({
slug: z.string().trim(),
name: z.string().trim(),
id: z.string().trim()
})
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const integration = await server.services.integration.getIntegration({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
id: req.params.integrationId
});
return { integration };
}
});
server.route({ server.route({
method: "DELETE", method: "DELETE",
url: "/:integrationId", url: "/:integrationId",

View File

@ -14,7 +14,7 @@ import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
import { getLastMidnightDateISO } from "@app/lib/fn"; import { getLastMidnightDateISO } from "@app/lib/fn";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { ActorType, AuthMode } from "@app/services/auth/auth-type";
export const registerOrgRouter = async (server: FastifyZodProvider) => { export const registerOrgRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@ -74,8 +74,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
description: "Get all audit logs for an organization", description: "Get all audit logs for an organization",
querystring: z.object({ querystring: z.object({
eventType: z.nativeEnum(EventType).optional().describe(AUDIT_LOGS.EXPORT.eventType), projectId: z.string().optional(),
actorType: z.nativeEnum(ActorType).optional(),
// eventType is split with , for multiple values, we need to transform it to array
eventType: z
.string()
.optional()
.transform((val) => (val ? val.split(",") : undefined)),
userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType), userAgentType: z.nativeEnum(UserAgentType).optional().describe(AUDIT_LOGS.EXPORT.userAgentType),
eventMetadata: z
.string()
.optional()
.transform((val) => {
if (!val) {
return undefined;
}
const pairs = val.split(",");
return pairs.reduce(
(acc, pair) => {
const [key, value] = pair.split("=");
if (key && value) {
acc[key] = value;
}
return acc;
},
{} as Record<string, string>
);
}),
startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate), startDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.startDate),
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate), endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset), offset: z.coerce.number().default(0).describe(AUDIT_LOGS.EXPORT.offset),
@ -114,13 +141,19 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const auditLogs = await server.services.auditLog.listAuditLogs({ const auditLogs = await server.services.auditLog.listAuditLogs({
filter: {
...req.query,
endDate: req.query.endDate,
projectId: req.query.projectId,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActorId: req.query.actor,
actorType: req.query.actorType,
eventType: req.query.eventType as EventType[] | undefined
},
actorId: req.permission.id, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
...req.query,
endDate: req.query.endDate,
startDate: req.query.startDate || getLastMidnightDateISO(),
auditLogActor: req.query.actor,
actor: req.permission.type actor: req.permission.type
}); });
return { auditLogs }; return { auditLogs };

View File

@ -2,11 +2,9 @@ import { z } from "zod";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas"; import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
import { ORGANIZATIONS } from "@app/lib/api-docs"; import { ORGANIZATIONS } from "@app/lib/api-docs";
import { OrderByDirection } from "@app/lib/types";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { OrgIdentityOrderBy } from "@app/services/identity/identity-types";
export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => { export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@ -26,27 +24,6 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
params: z.object({ params: z.object({
orgId: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orgId) orgId: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orgId)
}), }),
querystring: z.object({
offset: z.coerce.number().min(0).default(0).describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.offset).optional(),
limit: z.coerce
.number()
.min(1)
.max(20000) // TODO: temp limit until combobox added to add identity to project modal, reduce once added
.default(100)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.limit)
.optional(),
orderBy: z
.nativeEnum(OrgIdentityOrderBy)
.default(OrgIdentityOrderBy.Name)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy)
.optional(),
orderDirection: z
.nativeEnum(OrderByDirection)
.default(OrderByDirection.ASC)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection)
.optional(),
search: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.search).optional()
}),
response: { response: {
200: z.object({ 200: z.object({
identityMemberships: IdentityOrgMembershipsSchema.merge( identityMemberships: IdentityOrgMembershipsSchema.merge(
@ -60,26 +37,20 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
}).optional(), }).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }) identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
}) })
).array(), ).array()
totalCount: z.number()
}) })
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({ const identityMemberships = await server.services.identity.listOrgIdentities({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
orgId: req.params.orgId, orgId: req.params.orgId
limit: req.query.limit,
offset: req.query.offset,
orderBy: req.query.orderBy,
orderDirection: req.query.orderDirection,
search: req.query.search
}); });
return { identityMemberships, totalCount }; return { identityMemberships };
} }
}); });
}; };

View File

@ -7,13 +7,11 @@ import {
ProjectMembershipRole, ProjectMembershipRole,
ProjectUserMembershipRolesSchema ProjectUserMembershipRolesSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs"; import { PROJECT_IDENTITIES } from "@app/lib/api-docs";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { OrderByDirection } from "@app/lib/types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-project-types";
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types"; import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas"; import { SanitizedProjectSchema } from "../sanitizedSchemas";
@ -216,32 +214,6 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
params: z.object({ params: z.object({
projectId: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.projectId) projectId: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.projectId)
}), }),
querystring: z.object({
offset: z.coerce
.number()
.min(0)
.default(0)
.describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.offset)
.optional(),
limit: z.coerce
.number()
.min(1)
.max(20000) // TODO: temp limit until combobox added to add identity to project modal, reduce once added
.default(100)
.describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.limit)
.optional(),
orderBy: z
.nativeEnum(ProjectIdentityOrderBy)
.default(ProjectIdentityOrderBy.Name)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy)
.optional(),
orderDirection: z
.nativeEnum(OrderByDirection)
.default(OrderByDirection.ASC)
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection)
.optional(),
search: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.search).optional()
}),
response: { response: {
200: z.object({ 200: z.object({
identityMemberships: z identityMemberships: z
@ -267,25 +239,19 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }), identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
project: SanitizedProjectSchema.pick({ name: true, id: true }) project: SanitizedProjectSchema.pick({ name: true, id: true })
}) })
.array(), .array()
totalCount: z.number()
}) })
} }
}, },
handler: async (req) => { handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identityProject.listProjectIdentities({ const identityMemberships = await server.services.identityProject.listProjectIdentities({
actor: req.permission.type, actor: req.permission.type,
actorId: req.permission.id, actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
projectId: req.params.projectId, projectId: req.params.projectId
limit: req.query.limit,
offset: req.query.offset,
orderBy: req.query.orderBy,
orderDirection: req.query.orderDirection,
search: req.query.search
}); });
return { identityMemberships, totalCount }; return { identityMemberships };
} }
}); });

View File

@ -34,6 +34,7 @@ export enum AuthMode {
} }
export enum ActorType { // would extend to AWS, Azure, ... export enum ActorType { // would extend to AWS, Azure, ...
PLATFORM = "platform", // Useful for when we want to perform logging on automated actions such as integration syncs.
USER = "user", // userIdentity USER = "user", // userIdentity
SERVICE = "service", SERVICE = "service",
IDENTITY = "identity", IDENTITY = "identity",

View File

@ -15,7 +15,7 @@ import {
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
export const createSerialNumber = () => { export const createSerialNumber = () => {
const randomBytes = crypto.randomBytes(20); const randomBytes = crypto.randomBytes(32);
randomBytes[0] &= 0x7f; // ensure the first bit is 0 randomBytes[0] &= 0x7f; // ensure the first bit is 0
return randomBytes.toString("hex"); return randomBytes.toString("hex");
}; };

View File

@ -19,13 +19,7 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
CertExtendedKeyUsage,
CertExtendedKeyUsageOIDToName,
CertKeyAlgorithm,
CertKeyUsage,
CertStatus
} from "../certificate/certificate-types";
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal"; import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns"; import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns";
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
@ -768,39 +762,6 @@ export const certificateAuthorityServiceFactory = ({
}; };
}; };
/**
* Return CA certificate object by ID
*/
const getCaCertById = async ({ caId, caCertId }: { caId: string; caCertId: string }) => {
const caCert = await certificateAuthorityCertDAL.findOne({
caId,
id: caCertId
});
if (!caCert) {
throw new NotFoundError({ message: "CA certificate not found" });
}
const ca = await certificateAuthorityDAL.findById(caId);
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const decryptedCaCert = await kmsDecryptor({
cipherTextBlob: caCert.encryptedCertificate
});
const caCertObj = new x509.X509Certificate(decryptedCaCert);
return caCertObj;
};
/** /**
* Issue certificate to be imported back in for intermediate CA * Issue certificate to be imported back in for intermediate CA
*/ */
@ -815,7 +776,6 @@ export const certificateAuthorityServiceFactory = ({
notAfter, notAfter,
maxPathLength maxPathLength
}: TSignIntermediateDTO) => { }: TSignIntermediateDTO) => {
const appCfg = getConfig();
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new BadRequestError({ message: "CA not found" });
@ -890,7 +850,7 @@ export const certificateAuthorityServiceFactory = ({
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" }); throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
} }
const { caPrivateKey, caSecret } = await getCaCredentials({ const { caPrivateKey } = await getCaCredentials({
caId: ca.id, caId: ca.id,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
@ -899,11 +859,6 @@ export const certificateAuthorityServiceFactory = ({
}); });
const serialNumber = createSerialNumber(); const serialNumber = createSerialNumber();
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
const intermediateCert = await x509.X509CertificateGenerator.create({ const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
subject: csrObj.subject, subject: csrObj.subject,
@ -923,11 +878,7 @@ export const certificateAuthorityServiceFactory = ({
), ),
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true), new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
new x509.AuthorityInfoAccessExtension({
caIssuers: new x509.GeneralName("url", caIssuerUrl)
})
] ]
}); });
@ -1101,9 +1052,7 @@ export const certificateAuthorityServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
actorOrgId, actorOrgId
keyUsages,
extendedKeyUsages
}: TIssueCertFromCaDTO) => { }: TIssueCertFromCaDTO) => {
let ca: TCertificateAuthorities | undefined; let ca: TCertificateAuthorities | undefined;
let certificateTemplate: TCertificateTemplates | undefined; let certificateTemplate: TCertificateTemplates | undefined;
@ -1219,70 +1168,16 @@ export const certificateAuthorityServiceFactory = ({
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id }); const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const appCfg = getConfig(); const appCfg = getConfig();
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`; const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
new x509.CRLDistributionPointsExtension([distributionPointUrl]), new x509.CRLDistributionPointsExtension([distributionPointUrl]),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
new x509.AuthorityInfoAccessExtension({
caIssuers: new x509.GeneralName("url", caIssuerUrl)
}),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
]; ];
// handle key usages
let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? [];
if (keyUsages === undefined && !certificateTemplate) {
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
}
if (keyUsages === undefined && certificateTemplate) {
selectedKeyUsages = (certificateTemplate.keyUsages ?? []) as CertKeyUsage[];
}
if (keyUsages?.length && certificateTemplate) {
const validKeyUsages = certificateTemplate.keyUsages || [];
if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) {
throw new BadRequestError({
message: "Invalid key usage value based on template policy"
});
}
selectedKeyUsages = keyUsages;
}
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
if (keyUsagesBitValue) {
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
}
// handle extended key usages
let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = extendedKeyUsages ?? [];
if (extendedKeyUsages === undefined && certificateTemplate) {
selectedExtendedKeyUsages = (certificateTemplate.extendedKeyUsages ?? []) as CertExtendedKeyUsage[];
}
if (extendedKeyUsages?.length && certificateTemplate) {
const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || [];
if (extendedKeyUsages.some((eku) => !validExtendedKeyUsages.includes(eku))) {
throw new BadRequestError({
message: "Invalid extended key usage value based on template policy"
});
}
selectedExtendedKeyUsages = extendedKeyUsages;
}
if (selectedExtendedKeyUsages.length) {
extensions.push(
new x509.ExtendedKeyUsageExtension(
selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]),
true
)
);
}
let altNamesArray: { let altNamesArray: {
type: "email" | "dns"; type: "email" | "dns";
value: string; value: string;
@ -1364,9 +1259,7 @@ export const certificateAuthorityServiceFactory = ({
altNames, altNames,
serialNumber, serialNumber,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: notAfterDate, notAfter: notAfterDate
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages
}, },
tx tx
); );
@ -1415,7 +1308,6 @@ export const certificateAuthorityServiceFactory = ({
* Note: CSR is generated externally and submitted to Infisical. * Note: CSR is generated externally and submitted to Infisical.
*/ */
const signCertFromCa = async (dto: TSignCertFromCaDTO) => { const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
const appCfg = getConfig();
let ca: TCertificateAuthorities | undefined; let ca: TCertificateAuthorities | undefined;
let certificateTemplate: TCertificateTemplates | undefined; let certificateTemplate: TCertificateTemplates | undefined;
@ -1429,9 +1321,7 @@ export const certificateAuthorityServiceFactory = ({
altNames, altNames,
ttl, ttl,
notBefore, notBefore,
notAfter, notAfter
keyUsages,
extendedKeyUsages
} = dto; } = dto;
let collectionId = pkiCollectionId; let collectionId = pkiCollectionId;
@ -1542,7 +1432,7 @@ export const certificateAuthorityServiceFactory = ({
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint" message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
}); });
const { caPrivateKey, caSecret } = await getCaCredentials({ const { caPrivateKey } = await getCaCredentials({
caId: ca.id, caId: ca.id,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
@ -1550,115 +1440,13 @@ export const certificateAuthorityServiceFactory = ({
kmsService kmsService
}); });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey), await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
new x509.AuthorityInfoAccessExtension({
caIssuers: new x509.GeneralName("url", caIssuerUrl)
}),
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
]; ];
// handle key usages
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
let csrKeyUsages: CertKeyUsage[] = [];
if (csrKeyUsageExtension) {
csrKeyUsages = Object.values(CertKeyUsage).filter(
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
);
}
let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? [];
if (keyUsages === undefined && !certificateTemplate) {
if (csrKeyUsageExtension) {
selectedKeyUsages = csrKeyUsages;
} else {
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
}
}
if (keyUsages === undefined && certificateTemplate) {
if (csrKeyUsageExtension) {
const validKeyUsages = certificateTemplate.keyUsages || [];
if (csrKeyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) {
throw new BadRequestError({
message: "Invalid key usage value based on template policy"
});
}
selectedKeyUsages = csrKeyUsages;
} else {
selectedKeyUsages = (certificateTemplate.keyUsages ?? []) as CertKeyUsage[];
}
}
if (keyUsages?.length && certificateTemplate) {
const validKeyUsages = certificateTemplate.keyUsages || [];
if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) {
throw new BadRequestError({
message: "Invalid key usage value based on template policy"
});
}
selectedKeyUsages = keyUsages;
}
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
if (keyUsagesBitValue) {
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
}
// handle extended key usages
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
let csrExtendedKeyUsages: CertExtendedKeyUsage[] = [];
if (csrExtendedKeyUsageExtension) {
csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
);
}
let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = extendedKeyUsages ?? [];
if (extendedKeyUsages === undefined && !certificateTemplate && csrExtendedKeyUsageExtension) {
selectedExtendedKeyUsages = csrExtendedKeyUsages;
}
if (extendedKeyUsages === undefined && certificateTemplate) {
if (csrExtendedKeyUsageExtension) {
const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || [];
if (csrExtendedKeyUsages.some((eku) => !validExtendedKeyUsages.includes(eku))) {
throw new BadRequestError({
message: "Invalid extended key usage value based on template policy"
});
}
selectedExtendedKeyUsages = csrExtendedKeyUsages;
} else {
selectedExtendedKeyUsages = (certificateTemplate.extendedKeyUsages ?? []) as CertExtendedKeyUsage[];
}
}
if (extendedKeyUsages?.length && certificateTemplate) {
const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || [];
if (extendedKeyUsages.some((keyUsage) => !validExtendedKeyUsages.includes(keyUsage))) {
throw new BadRequestError({
message: "Invalid extended key usage value based on template policy"
});
}
selectedExtendedKeyUsages = extendedKeyUsages;
}
if (selectedExtendedKeyUsages.length) {
extensions.push(
new x509.ExtendedKeyUsageExtension(
selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]),
true
)
);
}
let altNamesFromCsr: string = ""; let altNamesFromCsr: string = "";
let altNamesArray: { let altNamesArray: {
type: "email" | "dns"; type: "email" | "dns";
@ -1754,9 +1542,7 @@ export const certificateAuthorityServiceFactory = ({
altNames: altNamesFromCsr || altNames, altNames: altNamesFromCsr || altNames,
serialNumber, serialNumber,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: notAfterDate, notAfter: notAfterDate
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages
}, },
tx tx
); );
@ -1842,7 +1628,6 @@ export const certificateAuthorityServiceFactory = ({
renewCaCert, renewCaCert,
getCaCerts, getCaCerts,
getCaCert, getCaCert,
getCaCertById,
signIntermediate, signIntermediate,
importCertToCa, importCertToCa,
issueCertFromCa, issueCertFromCa,

View File

@ -4,7 +4,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types"; import { CertKeyAlgorithm } from "../certificate/certificate-types";
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal"; import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal"; import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
@ -97,8 +97,6 @@ export type TIssueCertFromCaDTO = {
ttl: string; ttl: string;
notBefore?: string; notBefore?: string;
notAfter?: string; notAfter?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TSignCertFromCaDTO = export type TSignCertFromCaDTO =
@ -114,8 +112,6 @@ export type TSignCertFromCaDTO =
ttl?: string; ttl?: string;
notBefore?: string; notBefore?: string;
notAfter?: string; notAfter?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
} }
| ({ | ({
isInternal: false; isInternal: false;
@ -129,8 +125,6 @@ export type TSignCertFromCaDTO =
ttl: string; ttl: string;
notBefore?: string; notBefore?: string;
notAfter?: string; notAfter?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
} & Omit<TProjectPermission, "projectId">); } & Omit<TProjectPermission, "projectId">);
export type TGetCaCertificateTemplatesDTO = { export type TGetCaCertificateTemplatesDTO = {

View File

@ -9,9 +9,7 @@ export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
commonName: true, commonName: true,
subjectAlternativeName: true, subjectAlternativeName: true,
pkiCollectionId: true, pkiCollectionId: true,
ttl: true, ttl: true
keyUsages: true,
extendedKeyUsages: true
}).merge( }).merge(
z.object({ z.object({
projectId: z.string(), projectId: z.string(),

View File

@ -57,9 +57,7 @@ export const certificateTemplateServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
actorOrgId, actorOrgId
keyUsages,
extendedKeyUsages
}: TCreateCertTemplateDTO) => { }: TCreateCertTemplateDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) { if (!ca) {
@ -88,9 +86,7 @@ export const certificateTemplateServiceFactory = ({
name, name,
commonName, commonName,
subjectAlternativeName, subjectAlternativeName,
ttl, ttl
keyUsages,
extendedKeyUsages
}, },
tx tx
); );
@ -117,9 +113,7 @@ export const certificateTemplateServiceFactory = ({
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actor, actor,
actorOrgId, actorOrgId
keyUsages,
extendedKeyUsages
}: TUpdateCertTemplateDTO) => { }: TUpdateCertTemplateDTO) => {
const certTemplate = await certificateTemplateDAL.getById(id); const certTemplate = await certificateTemplateDAL.getById(id);
if (!certTemplate) { if (!certTemplate) {
@ -159,9 +153,7 @@ export const certificateTemplateServiceFactory = ({
commonName, commonName,
subjectAlternativeName, subjectAlternativeName,
name, name,
ttl, ttl
keyUsages,
extendedKeyUsages
}, },
tx tx
); );

View File

@ -1,5 +1,4 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
export type TCreateCertTemplateDTO = { export type TCreateCertTemplateDTO = {
caId: string; caId: string;
@ -8,8 +7,6 @@ export type TCreateCertTemplateDTO = {
commonName: string; commonName: string;
subjectAlternativeName: string; subjectAlternativeName: string;
ttl: string; ttl: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TUpdateCertTemplateDTO = { export type TUpdateCertTemplateDTO = {
@ -20,8 +17,6 @@ export type TUpdateCertTemplateDTO = {
commonName?: string; commonName?: string;
subjectAlternativeName?: string; subjectAlternativeName?: string;
ttl?: string; ttl?: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetCertTemplateDTO = { export type TGetCertTemplateDTO = {

View File

@ -1,5 +1,3 @@
import * as x509 from "@peculiar/x509";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
export enum CertStatus { export enum CertStatus {
@ -14,36 +12,6 @@ export enum CertKeyAlgorithm {
ECDSA_P384 = "EC_secp384r1" ECDSA_P384 = "EC_secp384r1"
} }
export enum CertKeyUsage {
DIGITAL_SIGNATURE = "digitalSignature",
KEY_ENCIPHERMENT = "keyEncipherment",
NON_REPUDIATION = "nonRepudiation",
DATA_ENCIPHERMENT = "dataEncipherment",
KEY_AGREEMENT = "keyAgreement",
KEY_CERT_SIGN = "keyCertSign",
CRL_SIGN = "cRLSign",
ENCIPHER_ONLY = "encipherOnly",
DECIPHER_ONLY = "decipherOnly"
}
export enum CertExtendedKeyUsage {
CLIENT_AUTH = "clientAuth",
SERVER_AUTH = "serverAuth",
CODE_SIGNING = "codeSigning",
EMAIL_PROTECTION = "emailProtection",
TIMESTAMPING = "timeStamping",
OCSP_SIGNING = "ocspSigning"
}
export const CertExtendedKeyUsageOIDToName: Record<string, CertExtendedKeyUsage> = {
[x509.ExtendedKeyUsage.clientAuth]: CertExtendedKeyUsage.CLIENT_AUTH,
[x509.ExtendedKeyUsage.serverAuth]: CertExtendedKeyUsage.SERVER_AUTH,
[x509.ExtendedKeyUsage.codeSigning]: CertExtendedKeyUsage.CODE_SIGNING,
[x509.ExtendedKeyUsage.emailProtection]: CertExtendedKeyUsage.EMAIL_PROTECTION,
[x509.ExtendedKeyUsage.ocspSigning]: CertExtendedKeyUsage.OCSP_SIGNING,
[x509.ExtendedKeyUsage.timeStamping]: CertExtendedKeyUsage.TIMESTAMPING
};
export enum CrlReason { export enum CrlReason {
UNSPECIFIED = "UNSPECIFIED", UNSPECIFIED = "UNSPECIFIED",
KEY_COMPROMISE = "KEY_COMPROMISE", KEY_COMPROMISE = "KEY_COMPROMISE",

View File

@ -152,7 +152,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
`${TableName.ProjectRoles}.id` `${TableName.ProjectRoles}.id`
) )
.select( .select(
db.ref("id").withSchema(TableName.UserGroupMembership), db.ref("id").withSchema(TableName.GroupProjectMembership),
db.ref("isGhost").withSchema(TableName.Users), db.ref("isGhost").withSchema(TableName.Users),
db.ref("username").withSchema(TableName.Users), db.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users), db.ref("email").withSchema(TableName.Users),

View File

@ -1,11 +1,9 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TIdentities } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex"; import { ormify, sqlNestRelationships } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { ProjectIdentityOrderBy, TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types";
export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>; export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>;
@ -109,45 +107,12 @@ export const identityProjectDALFactory = (db: TDbClient) => {
} }
}; };
const findByProjectId = async ( const findByProjectId = async (projectId: string, filter: { identityId?: string } = {}, tx?: Knex) => {
projectId: string,
filter: { identityId?: string } & Pick<
TListProjectIdentityDTO,
"limit" | "offset" | "search" | "orderBy" | "orderDirection"
> = {},
tx?: Knex
) => {
try { try {
// TODO: scott - optimize, there's redundancy here with project membership and the below query const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership)
const fetchIdentitySubquery = (tx || db.replicaNode())(TableName.Identity)
.where((qb) => {
if (filter.search) {
void qb.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`);
}
})
.join(
TableName.IdentityProjectMembership,
`${TableName.IdentityProjectMembership}.identityId`,
`${TableName.Identity}.id`
)
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
.orderBy(
`${TableName.Identity}.${filter.orderBy ?? ProjectIdentityOrderBy.Name}`,
filter.orderDirection ?? OrderByDirection.ASC
)
.select(selectAllTableCols(TableName.Identity))
.as(TableName.Identity); // required for subqueries
if (filter.limit) {
void fetchIdentitySubquery.offset(filter.offset ?? 0).limit(filter.limit);
}
const query = (tx || db.replicaNode())(TableName.IdentityProjectMembership)
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId) .where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`) .join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
.join<TIdentities, TIdentities>(fetchIdentitySubquery, (bd) => { .join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
bd.on(`${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`);
})
.where((qb) => { .where((qb) => {
if (filter.identityId) { if (filter.identityId) {
void qb.where("identityId", filter.identityId); void qb.where("identityId", filter.identityId);
@ -189,19 +154,6 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("name").as("projectName").withSchema(TableName.Project) db.ref("name").as("projectName").withSchema(TableName.Project)
); );
// TODO: scott - joins seem to reorder identities so need to order again, for the sake of urgency will optimize at a later point
if (filter.orderBy) {
switch (filter.orderBy) {
case "name":
void query.orderBy(`${TableName.Identity}.${filter.orderBy}`, filter.orderDirection);
break;
default:
// do nothing
}
}
const docs = await query;
const members = sqlNestRelationships({ const members = sqlNestRelationships({
data: docs, data: docs,
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({ parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({
@ -256,37 +208,9 @@ export const identityProjectDALFactory = (db: TDbClient) => {
} }
}; };
const getCountByProjectId = async (
projectId: string,
filter: { identityId?: string } & Pick<TListProjectIdentityDTO, "search"> = {},
tx?: Knex
) => {
try {
const identities = await (tx || db.replicaNode())(TableName.IdentityProjectMembership)
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
.where((qb) => {
if (filter.identityId) {
void qb.where("identityId", filter.identityId);
}
if (filter.search) {
void qb.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`);
}
})
.count();
return Number(identities[0].count);
} catch (error) {
throw new DatabaseError({ error, name: "GetCountByProjectId" });
}
};
return { return {
...identityProjectOrm, ...identityProjectOrm,
findByIdentityId, findByIdentityId,
findByProjectId, findByProjectId
getCountByProjectId
}; };
}; };

View File

@ -268,12 +268,7 @@ export const identityProjectServiceFactory = ({
actor, actor,
actorId, actorId,
actorAuthMethod, actorAuthMethod,
actorOrgId, actorOrgId
limit,
offset,
orderBy,
orderDirection,
search
}: TListProjectIdentityDTO) => { }: TListProjectIdentityDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@ -284,17 +279,8 @@ export const identityProjectServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
const identityMemberships = await identityProjectDAL.findByProjectId(projectId, { const identityMemberships = await identityProjectDAL.findByProjectId(projectId);
limit, return identityMemberships;
offset,
orderBy,
orderDirection,
search
});
const totalCount = await identityProjectDAL.getCountByProjectId(projectId, { search });
return { identityMemberships, totalCount };
}; };
const getProjectIdentityByIdentityId = async ({ const getProjectIdentityByIdentityId = async ({

View File

@ -1,4 +1,4 @@
import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types"; import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
@ -40,18 +40,8 @@ export type TDeleteProjectIdentityDTO = {
identityId: string; identityId: string;
} & TProjectPermission; } & TProjectPermission;
export type TListProjectIdentityDTO = { export type TListProjectIdentityDTO = TProjectPermission;
limit?: number;
offset?: number;
orderBy?: ProjectIdentityOrderBy;
orderDirection?: OrderByDirection;
search?: string;
} & TProjectPermission;
export type TGetProjectIdentityByIdentityIdDTO = { export type TGetProjectIdentityByIdentityIdDTO = {
identityId: string; identityId: string;
} & TProjectPermission; } & TProjectPermission;
export enum ProjectIdentityOrderBy {
Name = "name"
}

View File

@ -4,8 +4,6 @@ import { TDbClient } from "@app/db";
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas"; import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors"; import { DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols } from "@app/lib/knex";
import { OrderByDirection } from "@app/lib/types";
import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>; export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>;
@ -29,20 +27,9 @@ export const identityOrgDALFactory = (db: TDbClient) => {
} }
}; };
const find = async ( const find = async (filter: Partial<TIdentityOrgMemberships>, tx?: Knex) => {
{
limit,
offset = 0,
orderBy,
orderDirection = OrderByDirection.ASC,
search,
...filter
}: Partial<TIdentityOrgMemberships> &
Pick<TListOrgIdentitiesByOrgIdDTO, "offset" | "limit" | "orderBy" | "orderDirection" | "search">,
tx?: Knex
) => {
try { try {
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership) const docs = await (tx || db.replicaNode())(TableName.IdentityOrgMembership)
.where(filter) .where(filter)
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`) .join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`) .leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
@ -57,30 +44,6 @@ export const identityOrgDALFactory = (db: TDbClient) => {
.select(db.ref("id").as("identityId").withSchema(TableName.Identity)) .select(db.ref("id").as("identityId").withSchema(TableName.Identity))
.select(db.ref("name").as("identityName").withSchema(TableName.Identity)) .select(db.ref("name").as("identityName").withSchema(TableName.Identity))
.select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity)); .select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity));
if (limit) {
void query.offset(offset).limit(limit);
}
if (orderBy) {
switch (orderBy) {
case "name":
void query.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection);
break;
case "role":
void query.orderBy(`${TableName.IdentityOrgMembership}.${orderBy}`, orderDirection);
break;
default:
// do nothing
}
}
if (search?.length) {
void query.whereILike(`${TableName.Identity}.name`, `%${search}%`);
}
const docs = await query;
return docs.map( return docs.map(
({ ({
crId, crId,
@ -116,27 +79,5 @@ export const identityOrgDALFactory = (db: TDbClient) => {
} }
}; };
const countAllOrgIdentities = async ( return { ...identityOrgOrm, find, findOne };
{ search, ...filter }: Partial<TIdentityOrgMemberships> & Pick<TListOrgIdentitiesByOrgIdDTO, "search">,
tx?: Knex
) => {
try {
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
.where(filter)
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
.count();
if (search?.length) {
void query.whereILike(`${TableName.Identity}.name`, `%${search}%`);
}
const identities = await query;
return Number(identities[0].count);
} catch (error) {
throw new DatabaseError({ error, name: "countAllOrgIdentities" });
}
};
return { ...identityOrgOrm, find, findOne, countAllOrgIdentities };
}; };

View File

@ -6,6 +6,7 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgPermission } from "@app/lib/types";
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
@ -15,7 +16,6 @@ import {
TCreateIdentityDTO, TCreateIdentityDTO,
TDeleteIdentityDTO, TDeleteIdentityDTO,
TGetIdentityByIdDTO, TGetIdentityByIdDTO,
TListOrgIdentitiesByOrgIdDTO,
TListProjectIdentitiesByIdentityIdDTO, TListProjectIdentitiesByIdentityIdDTO,
TUpdateIdentityDTO TUpdateIdentityDTO
} from "./identity-types"; } from "./identity-types";
@ -58,8 +58,7 @@ export const identityServiceFactory = ({
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged identity" }); if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged identity" });
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to create identity due to identity limit reached. Upgrade plan to create more identities." message: "Failed to create identity due to identity limit reached. Upgrade plan to create more identities."
@ -196,36 +195,14 @@ export const identityServiceFactory = ({
return { ...deletedIdentity, orgId: identityOrgMembership.orgId }; return { ...deletedIdentity, orgId: identityOrgMembership.orgId };
}; };
const listOrgIdentities = async ({ const listOrgIdentities = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPermission) => {
orgId,
actor,
actorId,
actorAuthMethod,
actorOrgId,
limit,
offset,
orderBy,
orderDirection,
search
}: TListOrgIdentitiesByOrgIdDTO) => {
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
const identityMemberships = await identityOrgMembershipDAL.find({ const identityMemberships = await identityOrgMembershipDAL.find({
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId, [`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId
limit,
offset,
orderBy,
orderDirection,
search
}); });
return identityMemberships;
const totalCount = await identityOrgMembershipDAL.countAllOrgIdentities({
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId,
search
});
return { identityMemberships, totalCount };
}; };
const listProjectIdentitiesByIdentityId = async ({ const listProjectIdentitiesByIdentityId = async ({

View File

@ -1,5 +1,5 @@
import { IPType } from "@app/lib/ip"; import { IPType } from "@app/lib/ip";
import { OrderByDirection, TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
export type TCreateIdentityDTO = { export type TCreateIdentityDTO = {
role: string; role: string;
@ -29,16 +29,3 @@ export interface TIdentityTrustedIp {
export type TListProjectIdentitiesByIdentityIdDTO = { export type TListProjectIdentitiesByIdentityIdDTO = {
identityId: string; identityId: string;
} & Omit<TOrgPermission, "orgId">; } & Omit<TOrgPermission, "orgId">;
export type TListOrgIdentitiesByOrgIdDTO = {
limit?: number;
offset?: number;
orderBy?: OrgIdentityOrderBy;
orderDirection?: OrderByDirection;
search?: string;
} & TOrgPermission;
export enum OrgIdentityOrderBy {
Name = "name",
Role = "role"
}

View File

@ -242,12 +242,37 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
}; };
} }
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const octokit = new Octokit({
const repos = (await new Octokit({
auth: accessToken auth: accessToken
}).paginate("GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}", { });
per_page: 100
})) as GitHubApp[]; const getAllRepos = async () => {
let repos: GitHubApp[] = [];
let page = 1;
const perPage = 100;
let hasMore = true;
while (hasMore) {
const response = await octokit.request(
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
{
per_page: perPage,
page
}
);
if ((response.data as GitHubApp[]).length > 0) {
repos = repos.concat(response.data as GitHubApp[]);
page += 1;
} else {
hasMore = false;
}
}
return repos;
};
const repos = await getAllRepos();
const apps = repos const apps = repos
.filter((a: GitHubApp) => a.permissions.admin === true) .filter((a: GitHubApp) => a.permissions.admin === true)

View File

@ -207,12 +207,6 @@ const syncSecretsGCPSecretManager = async ({
} }
); );
if (!secrets[key].value) {
logger.warn(
`syncSecretsGcpsecretManager: create secret value in gcp where [key=${key}] and integration appId [appId=${integration.appId}]`
);
}
await request.post( await request.post(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`, `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
{ {
@ -243,12 +237,6 @@ const syncSecretsGCPSecretManager = async ({
} }
); );
} else if (secrets[key].value !== res[key]) { } else if (secrets[key].value !== res[key]) {
if (!secrets[key].value) {
logger.warn(
`syncSecretsGcpsecretManager: update secret value in gcp where [key=${key}] and integration appId [appId=${integration.appId}]`
);
}
await request.post( await request.post(
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`, `${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
{ {

View File

@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal"; import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
@ -19,6 +19,7 @@ import { TIntegrationDALFactory } from "./integration-dal";
import { import {
TCreateIntegrationDTO, TCreateIntegrationDTO,
TDeleteIntegrationDTO, TDeleteIntegrationDTO,
TGetIntegrationDTO,
TSyncIntegrationDTO, TSyncIntegrationDTO,
TUpdateIntegrationDTO TUpdateIntegrationDTO
} from "./integration-types"; } from "./integration-types";
@ -180,6 +181,27 @@ export const integrationServiceFactory = ({
return updatedIntegration; return updatedIntegration;
}; };
const getIntegration = async ({ id, actor, actorAuthMethod, actorId, actorOrgId }: TGetIntegrationDTO) => {
const integration = await integrationDAL.findById(id);
const { permission } = await permissionService.getProjectPermission(
actor,
actorId,
integration?.projectId || "",
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
if (!integration) {
throw new NotFoundError({
message: "Integration not found"
});
}
return { ...integration, envId: integration.environment.id };
};
const deleteIntegration = async ({ const deleteIntegration = async ({
actorId, actorId,
id, id,
@ -276,6 +298,8 @@ export const integrationServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
await secretQueueService.syncIntegrations({ await secretQueueService.syncIntegrations({
isManual: true,
actorId,
environment: integration.environment.slug, environment: integration.environment.slug,
secretPath: integration.secretPath, secretPath: integration.secretPath,
projectId: integration.projectId projectId: integration.projectId
@ -289,6 +313,7 @@ export const integrationServiceFactory = ({
updateIntegration, updateIntegration,
deleteIntegration, deleteIntegration,
listIntegrationByProject, listIntegrationByProject,
getIntegration,
syncIntegration syncIntegration
}; };
}; };

View File

@ -39,6 +39,10 @@ export type TCreateIntegrationDTO = {
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TGetIntegrationDTO = {
id: string;
} & Omit<TProjectPermission, "projectId">;
export type TUpdateIntegrationDTO = { export type TUpdateIntegrationDTO = {
id: string; id: string;
app?: string; app?: string;

View File

@ -17,6 +17,7 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects"; import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@ -89,6 +90,7 @@ type TOrgServiceFactoryDep = {
>; >;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
}; };
@ -114,6 +116,7 @@ export const orgServiceFactory = ({
licenseService, licenseService,
projectRoleDAL, projectRoleDAL,
samlConfigDAL, samlConfigDAL,
userGroupMembershipDAL,
projectBotDAL, projectBotDAL,
projectUserMembershipRoleDAL projectUserMembershipRoleDAL
}: TOrgServiceFactoryDep) => { }: TOrgServiceFactoryDep) => {
@ -455,6 +458,7 @@ export const orgServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
const org = await orgDAL.findOrgById(orgId); const org = await orgDAL.findOrgById(orgId);
@ -472,20 +476,19 @@ export const orgServiceFactory = ({
}); });
} }
const plan = await licenseService.getPlan(orgId); const plan = await licenseService.getPlan(orgId);
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) { if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed // limit imposed on number of members allowed / number of members used exceeds the number of members allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
}); });
} }
if (plan?.slug !== "enterprise" && plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) { if (plan?.identityLimit && plan.identitiesUsed >= plan.identityLimit) {
// limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed // limit imposed on number of identities allowed / number of identities used exceeds the number of identities allowed
throw new BadRequestError({ throw new BadRequestError({
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members." message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
}); });
} }
const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole); const isCustomOrgRole = !Object.values(OrgMembershipRole).includes(organizationRoleSlug as OrgMembershipRole);
if (isCustomOrgRole) { if (isCustomOrgRole) {
if (!plan?.rbac) if (!plan?.rbac)
@ -580,8 +583,6 @@ export const orgServiceFactory = ({
// if there exist no org membership we set is as given by the request // if there exist no org membership we set is as given by the request
if (!inviteeMembership) { if (!inviteeMembership) {
// as its used by project invite also
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
let roleId; let roleId;
const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug; const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug;
if (isCustomOrgRole) { if (isCustomOrgRole) {
@ -615,6 +616,7 @@ export const orgServiceFactory = ({
} }
const userIds = users.map(({ id }) => id); const userIds = users.map(({ id }) => id);
const usernames = users.map((el) => el.username);
const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx); const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx);
// we don't need to spam with email. Thus org invitation doesn't need project invitation again // we don't need to spam with email. Thus org invitation doesn't need project invitation again
const userIdsWithOrgInvitation = new Set(mailsForOrgInvitation.map((el) => el.userId)); const userIdsWithOrgInvitation = new Set(mailsForOrgInvitation.map((el) => el.userId));
@ -641,10 +643,12 @@ export const orgServiceFactory = ({
{ tx } { tx }
); );
const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.userId); const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.userId);
const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter( const userIdsToExcludeAsPartOfGroup = new Set(
(user) => !existingMembersGroupByUserId?.[user.userId] await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernames, projectId, tx)
);
const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter(
(user) => !existingMembersGroupByUserId?.[user.userId] && !userIdsToExcludeAsPartOfGroup.has(user.userId)
); );
// eslint-disable-next-line no-continue // eslint-disable-next-line no-continue
if (!userWithEncryptionKeyInvitedToProject.length) continue; if (!userWithEncryptionKeyInvitedToProject.length) continue;

View File

@ -26,10 +26,7 @@ export const getBotKeyFnFactory = (
) => { ) => {
const getBotKeyFn = async (projectId: string) => { const getBotKeyFn = async (projectId: string) => {
const project = await projectDAL.findById(projectId); const project = await projectDAL.findById(projectId);
if (!project) if (!project) throw new BadRequestError({ message: "Project not found during bot lookup." });
throw new BadRequestError({
message: "Project not found during bot lookup. Are you sure you are using the correct project ID?"
});
if (project.version === 3) { if (project.version === 3) {
return { project, shouldUseSecretV2Bridge: true }; return { project, shouldUseSecretV2Bridge: true };

View File

@ -90,20 +90,15 @@ export const projectMembershipServiceFactory = ({
// projectMembers[0].project // projectMembers[0].project
if (includeGroupMembers) { if (includeGroupMembers) {
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId); const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
const allMembers = [ const allMembers = [
...projectMembers.map((m) => ({ ...m, isGroupMember: false })), ...projectMembers.map((m) => ({ ...m, isGroupMember: false })),
...groupMembers.map((m) => ({ ...m, isGroupMember: true })) ...groupMembers.map((m) => ({ ...m, isGroupMember: true }))
]; ];
// Ensure the userId is unique // Ensure the userId is unique
const uniqueMembers: typeof allMembers = []; const membersIds = new Set(allMembers.map((entity) => entity.user.id));
const addedUserIds = new Set<string>(); const uniqueMembers = allMembers.filter((entity) => membersIds.has(entity.user.id));
allMembers.forEach((member) => {
if (!addedUserIds.has(member.user.id)) {
uniqueMembers.push(member);
addedUserIds.add(member.user.id);
}
});
return uniqueMembers; return uniqueMembers;
} }

View File

@ -7,8 +7,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { import {
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSet, ProjectPermissionSet,
ProjectPermissionSub, ProjectPermissionSub
validateProjectPermissions
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
@ -57,9 +56,6 @@ export const projectRoleServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId }); const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId });
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" }); if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
validateProjectPermissions(data.permissions);
const role = await projectRoleDAL.create({ const role = await projectRoleDAL.create({
...data, ...data,
projectId projectId
@ -124,11 +120,6 @@ export const projectRoleServiceFactory = ({
if (existingRole && existingRole.id !== roleId) if (existingRole && existingRole.id !== roleId)
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" }); throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
} }
if (data.permissions) {
validateProjectPermissions(data.permissions);
}
const [updatedRole] = await projectRoleDAL.update( const [updatedRole] = await projectRoleDAL.update(
{ id: roleId, projectId }, { id: roleId, projectId },
{ {

View File

@ -512,11 +512,7 @@ export const secretImportServiceFactory = ({
return importedSecrets; return importedSecrets;
} }
if (!botKey) if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL }); const importedSecrets = await fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
return importedSecrets.map((el) => ({ return importedSecrets.map((el) => ({

View File

@ -832,11 +832,7 @@ export const createManySecretsRawFnFactory = ({
secretDAL secretDAL
}); });
if (!botKey) if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const inputSecrets = secrets.map((secret) => { const inputSecrets = secrets.map((secret) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secret.secretValue || "", botKey);
@ -997,11 +993,7 @@ export const updateManySecretsRawFnFactory = ({
return updatedSecrets; return updatedSecrets;
} }
if (!botKey) if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId }); const blindIndexCfg = await secretBlindIndexDAL.findOne({ projectId });
if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" }); if (!blindIndexCfg) throw new BadRequestError({ message: "Blind index not found", name: "Update secret" });

View File

@ -2,6 +2,8 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas"; import { ProjectUpgradeStatus, ProjectVersion, TSecretSnapshotSecretsV2, TSecretVersionsV2 } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { Actor, EventType } from "@app/ee/services/audit-log/audit-log-types";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal"; import { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal"; import { TSnapshotDALFactory } from "@app/ee/services/secret-snapshot/snapshot-dal";
@ -21,6 +23,7 @@ import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
import { ActorType } from "../auth/auth-type";
import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal";
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal"; import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service"; import { TIntegrationAuthServiceFactory } from "../integration-auth/integration-auth-service";
@ -40,6 +43,7 @@ import { expandSecretReferencesFactory, getAllNestedSecretReferences } from "../
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TWebhookDALFactory } from "../webhook/webhook-dal"; import { TWebhookDALFactory } from "../webhook/webhook-dal";
import { fnTriggerWebhook } from "../webhook/webhook-fns"; import { fnTriggerWebhook } from "../webhook/webhook-fns";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
@ -71,6 +75,7 @@ type TSecretQueueFactoryDep = {
secretVersionDAL: TSecretVersionDALFactory; secretVersionDAL: TSecretVersionDALFactory;
secretBlindIndexDAL: TSecretBlindIndexDALFactory; secretBlindIndexDAL: TSecretBlindIndexDALFactory;
secretTagDAL: TSecretTagDALFactory; secretTagDAL: TSecretTagDALFactory;
userDAL: Pick<TUserDALFactory, "findById">;
secretVersionTagDAL: TSecretVersionTagDALFactory; secretVersionTagDAL: TSecretVersionTagDALFactory;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: TSecretV2BridgeDALFactory; secretV2BridgeDAL: TSecretV2BridgeDALFactory;
@ -81,6 +86,7 @@ type TSecretQueueFactoryDep = {
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">; snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">; snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">; keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
}; };
export type TGetSecrets = { export type TGetSecrets = {
@ -106,6 +112,7 @@ export const secretQueueFactory = ({
secretDAL, secretDAL,
secretImportDAL, secretImportDAL,
folderDAL, folderDAL,
userDAL,
webhookDAL, webhookDAL,
projectEnvDAL, projectEnvDAL,
orgDAL, orgDAL,
@ -125,7 +132,8 @@ export const secretQueueFactory = ({
snapshotDAL, snapshotDAL,
snapshotSecretV2BridgeDAL, snapshotSecretV2BridgeDAL,
secretApprovalRequestDAL, secretApprovalRequestDAL,
keyStore keyStore,
auditLogService
}: TSecretQueueFactoryDep) => { }: TSecretQueueFactoryDep) => {
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => { const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
const appCfg = getConfig(); const appCfg = getConfig();
@ -430,7 +438,9 @@ export const secretQueueFactory = ({
return content; return content;
}; };
const syncIntegrations = async (dto: TGetSecrets & { deDupeQueue?: Record<string, boolean> }) => { const syncIntegrations = async (
dto: TGetSecrets & { isManual?: boolean; actorId?: string; deDupeQueue?: Record<string, boolean> }
) => {
await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, { await queueService.queue(QueueName.IntegrationSync, QueueJobs.IntegrationSync, dto, {
attempts: 3, attempts: 3,
delay: 1000, delay: 1000,
@ -528,7 +538,7 @@ export const secretQueueFactory = ({
} }
} }
); );
await syncIntegrations({ secretPath, projectId, environment, deDupeQueue }); await syncIntegrations({ secretPath, projectId, environment, deDupeQueue, isManual: false });
if (!excludeReplication) { if (!excludeReplication) {
await replicateSecrets({ await replicateSecrets({
_deDupeReplicationQueue: deDupeReplicationQueue, _deDupeReplicationQueue: deDupeReplicationQueue,
@ -544,7 +554,7 @@ export const secretQueueFactory = ({
}); });
queueService.start(QueueName.IntegrationSync, async (job) => { queueService.start(QueueName.IntegrationSync, async (job) => {
const { environment, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data; const { environment, actorId, isManual, projectId, secretPath, depth = 1, deDupeQueue = {} } = job.data;
if (depth > MAX_SYNC_SECRET_DEPTH) return; if (depth > MAX_SYNC_SECRET_DEPTH) return;
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
@ -693,6 +703,30 @@ export const secretQueueFactory = ({
}); });
} }
const generateActor = async (): Promise<Actor> => {
if (isManual && actorId) {
const user = await userDAL.findById(actorId);
if (!user) {
throw new Error("User not found");
}
return {
type: ActorType.USER,
metadata: {
email: user.email,
username: user.username,
userId: user.id
}
};
}
return {
type: ActorType.PLATFORM,
metadata: {}
};
};
// akhilmhdh: this try catch is for lock release // akhilmhdh: this try catch is for lock release
try { try {
const secrets = shouldUseSecretV2Bridge const secrets = shouldUseSecretV2Bridge
@ -778,6 +812,21 @@ export const secretQueueFactory = ({
} }
}); });
await auditLogService.createAuditLog({
projectId,
actor: await generateActor(),
event: {
type: EventType.INTEGRATION_SYNCED,
metadata: {
integrationId: integration.id,
isSynced: response?.isSynced ?? true,
lastSyncJobId: job?.id ?? "",
lastUsed: new Date(),
syncMessage: response?.syncMessage ?? ""
}
}
});
await integrationDAL.updateById(integration.id, { await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id, lastSyncJobId: job.id,
lastUsed: new Date(), lastUsed: new Date(),
@ -794,9 +843,23 @@ export const secretQueueFactory = ({
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) || (err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
"Unknown error occurred."; "Unknown error occurred.";
await auditLogService.createAuditLog({
projectId,
actor: await generateActor(),
event: {
type: EventType.INTEGRATION_SYNCED,
metadata: {
integrationId: integration.id,
isSynced: false,
lastSyncJobId: job?.id ?? "",
lastUsed: new Date(),
syncMessage: message
}
}
});
await integrationDAL.updateById(integration.id, { await integrationDAL.updateById(integration.id, {
lastSyncJobId: job.id, lastSyncJobId: job.id,
lastUsed: new Date(),
syncMessage: message, syncMessage: message,
isSynced: false isSynced: false
}); });

View File

@ -985,11 +985,7 @@ export const secretServiceFactory = ({
return { secrets, imports }; return { secrets, imports };
} }
if (!botKey) if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const { secrets, imports } = await getSecrets({ const { secrets, imports } = await getSecrets({
actorId, actorId,
@ -1150,10 +1146,7 @@ export const secretServiceFactory = ({
}); });
if (!botKey) if (!botKey)
throw new BadRequestError({ throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
if (expandSecretReferences) { if (expandSecretReferences) {
@ -1245,11 +1238,7 @@ export const secretServiceFactory = ({
return { secret, type: SecretProtectionType.Direct as const }; return { secret, type: SecretProtectionType.Direct as const };
} }
if (!botKey) if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretName, botKey);
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey); const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
@ -1387,11 +1376,7 @@ export const secretServiceFactory = ({
return { type: SecretProtectionType.Direct as const, secret }; return { type: SecretProtectionType.Direct as const, secret };
} }
if (!botKey) if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey); const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8(secretValue || "", botKey);
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey); const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8(secretComment || "", botKey);
@ -1513,11 +1498,7 @@ export const secretServiceFactory = ({
}); });
return { type: SecretProtectionType.Direct as const, secret }; return { type: SecretProtectionType.Direct as const, secret };
} }
if (!botKey) if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
if (policy) { if (policy) {
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
policy, policy,
@ -1617,11 +1598,7 @@ export const secretServiceFactory = ({
return { secrets, type: SecretProtectionType.Direct as const }; return { secrets, type: SecretProtectionType.Direct as const };
} }
if (!botKey) if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const sanitizedSecrets = inputSecrets.map( const sanitizedSecrets = inputSecrets.map(
({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => { ({ secretComment, secretKey, metadata, tagIds, secretValue, skipMultilineEncoding }) => {
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey); const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8(secretKey, botKey);
@ -1743,11 +1720,7 @@ export const secretServiceFactory = ({
return { type: SecretProtectionType.Direct as const, secrets }; return { type: SecretProtectionType.Direct as const, secrets };
} }
if (!botKey) if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
const sanitizedSecrets = inputSecrets.map( const sanitizedSecrets = inputSecrets.map(
({ ({
secretComment, secretComment,
@ -1875,11 +1848,7 @@ export const secretServiceFactory = ({
return { type: SecretProtectionType.Direct as const, secrets }; return { type: SecretProtectionType.Direct as const, secrets };
} }
if (!botKey) if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
throw new BadRequestError({
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
if (policy) { if (policy) {
const approval = await secretApprovalRequestService.generateSecretApprovalRequest({ const approval = await secretApprovalRequestService.generateSecretApprovalRequest({
@ -2213,10 +2182,7 @@ export const secretServiceFactory = ({
} }
if (!botKey) if (!botKey)
throw new BadRequestError({ throw new BadRequestError({ message: "Please upgrade your project first", name: "bot_not_found_error" });
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
await secretDAL.transaction(async (tx) => { await secretDAL.transaction(async (tx) => {
const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx); const secrets = await secretDAL.findAllProjectSecretValues(projectId, tx);
@ -2299,10 +2265,7 @@ export const secretServiceFactory = ({
const { botKey } = await projectBotService.getBotKey(project.id); const { botKey } = await projectBotService.getBotKey(project.id);
if (!botKey) { if (!botKey) {
throw new BadRequestError({ throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" });
message: "Project bot not found. Please upgrade your project.",
name: "bot_not_found_error"
});
} }
const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath); const sourceFolder = await folderDAL.findBySecretPath(project.id, sourceEnvironment, sourceSecretPath);

View File

@ -4,7 +4,6 @@ Copyright (c) 2023 Infisical Inc.
package cmd package cmd
import ( import (
"fmt"
"os" "os"
"strings" "strings"
@ -44,26 +43,14 @@ func init() {
rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.") rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.")
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
silent, err := cmd.Flags().GetBool("silent") silent, err := cmd.Flags().GetBool("silent")
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)
if err != nil { if err != nil {
util.HandleError(err) util.HandleError(err)
} }
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)
if !util.IsRunningInDocker() && !silent { if !util.IsRunningInDocker() && !silent {
util.CheckForUpdate() util.CheckForUpdate()
} }
loggedInDetails, err := util.GetCurrentLoggedInUserDetails()
if !silent && err == nil && loggedInDetails.IsUserLoggedIn && !loggedInDetails.LoginExpired {
token, err := util.GetInfisicalToken(cmd)
if err == nil && token != nil {
util.PrintWarning(fmt.Sprintf("Your logged-in session is being overwritten by the token provided from the %s.", token.Source))
}
}
} }
// if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment // if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment

View File

@ -160,19 +160,19 @@ var secretsSetCmd = &cobra.Command{
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }
if token == nil { if (token == nil) {
util.RequireLocalWorkspaceFile() util.RequireLocalWorkspaceFile()
} }
environmentName, _ := cmd.Flags().GetString("env") environmentName, _ := cmd.Flags().GetString("env")
if !cmd.Flags().Changed("env") { if !cmd.Flags().Changed("env") {
environmentFromWorkspace := util.GetEnvFromWorkspaceFile() environmentFromWorkspace := util.GetEnvFromWorkspaceFile()
if environmentFromWorkspace != "" { if environmentFromWorkspace != "" {
environmentName = environmentFromWorkspace environmentName = environmentFromWorkspace
} }
} }
projectId, err := cmd.Flags().GetString("projectId") projectId, err := cmd.Flags().GetString("projectId")
if err != nil { if err != nil {
util.HandleError(err, "Unable to parse flag") util.HandleError(err, "Unable to parse flag")
} }

View File

@ -63,9 +63,8 @@ type DynamicSecretLease struct {
} }
type TokenDetails struct { type TokenDetails struct {
Type string Type string
Token string Token string
Source string
} }
type SingleFolder struct { type SingleFolder struct {

View File

@ -87,15 +87,11 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro
return nil, err return nil, err
} }
var source = "--token flag"
if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable. if infisicalToken == "" { // If no flag is passed, we first check for the universal auth access token env variable.
infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME) infisicalToken = os.Getenv(INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)
source = fmt.Sprintf("%s environment variable", INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME)
if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable. if infisicalToken == "" { // If it's still empty after the first env check, we check for the service token env variable.
infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME) infisicalToken = os.Getenv(INFISICAL_TOKEN_NAME)
source = fmt.Sprintf("%s environment variable", INFISICAL_TOKEN_NAME)
} }
} }
@ -105,16 +101,14 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro
if strings.HasPrefix(infisicalToken, "st.") { if strings.HasPrefix(infisicalToken, "st.") {
return &models.TokenDetails{ return &models.TokenDetails{
Type: SERVICE_TOKEN_IDENTIFIER, Type: SERVICE_TOKEN_IDENTIFIER,
Token: infisicalToken, Token: infisicalToken,
Source: source,
}, nil }, nil
} }
return &models.TokenDetails{ return &models.TokenDetails{
Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER, Type: UNIVERSAL_AUTH_TOKEN_IDENTIFIER,
Token: infisicalToken, Token: infisicalToken,
Source: source,
}, nil }, nil
} }

View File

@ -2,7 +2,3 @@
title: "Create" title: "Create"
openapi: "POST /api/v1/workspace/{projectSlug}/roles" openapi: "POST /api/v1/workspace/{projectSlug}/roles"
--- ---
<Note>
You can read more about the permissions field in the [permissions documentation](/internals/permissions).
</Note>

View File

@ -60,8 +60,6 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
- Common Name (CN): A regular expression used to validate the common name in certificate requests. - Common Name (CN): A regular expression used to validate the common name in certificate requests.
- Alternative Names (SANs): A regular expression used to validate subject alternative names in certificate requests. - Alternative Names (SANs): A regular expression used to validate subject alternative names in certificate requests.
- TTL: The maximum Time-to-Live (TTL) for certificates issued using this template. - TTL: The maximum Time-to-Live (TTL) for certificates issued using this template.
- Key Usage: The key usage constraint or default value for certificates issued using this template.
- Extended Key Usage: The extended key usage constraint or default value for certificates issued using this template.
</Step> </Step>
<Step title="Creating a certificate"> <Step title="Creating a certificate">
To create a certificate, head to your Project > Internal PKI > Certificates and press **Issue** under the Certificates section. To create a certificate, head to your Project > Internal PKI > Certificates and press **Issue** under the Certificates section.
@ -78,16 +76,13 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
- Common Name (CN): The (common) name for the certificate like `service.acme.com`. - Common Name (CN): The (common) name for the certificate like `service.acme.com`.
- Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`. - Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`.
- TTL: The lifetime of the certificate in seconds. - TTL: The lifetime of the certificate in seconds.
- Key Usage: The key usage extension of the certificate.
- Extended Key Usage: The extended key usage extension of the certificate.
<Note> <Note>
Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None** Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None**
and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same. and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same.
That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates. That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates.
</Note> </Note>
</Step> </Step>
<Step title="Copying the certificate details"> <Step title="Copying the certificate details">
Once you have created the certificate from step 1, you'll be presented with the certificate details including the **Certificate Body**, **Certificate Chain**, and **Private Key**. Once you have created the certificate from step 1, you'll be presented with the certificate details including the **Certificate Body**, **Certificate Chain**, and **Private Key**.
@ -110,7 +105,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like .*.acme.com or perhaps that the max TTL cannot be more than 1 year. With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like .*.acme.com or perhaps that the max TTL cannot be more than 1 year.
To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint, specifying the issuing CA. To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint, specifying the issuing CA.
### Sample request ### Sample request
```bash Request ```bash Request
@ -137,7 +132,6 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
ttl: "...", ttl: "...",
} }
``` ```
</Step> </Step>
<Step title="Creating a certificate"> <Step title="Creating a certificate">
To create a certificate under the certificate template, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint, To create a certificate under the certificate template, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
@ -170,7 +164,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
<Note> <Note>
Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None** Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None**
and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same. and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same.
That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates. That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates.
</Note> </Note>
@ -203,7 +197,6 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
serialNumber: "..." serialNumber: "..."
} }
``` ```
</Step> </Step>
</Steps> </Steps>
</Tab> </Tab>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 399 KiB

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 518 KiB

After

Width:  |  Height:  |  Size: 517 KiB

View File

@ -1,83 +0,0 @@
---
title: "Permissions"
description: "Infisical's permissions system provides granular access control."
---
## Summary
The Infisical permissions system is based on a role-based access control (RBAC) model. The system allows you to define roles and assign them to users and machines. Each role has a set of permissions that define what actions a user can perform.
Permissions are built on a subject-action-object model. The subject is the resource permission is being applied to, the action is what the permission allows.
An example of a subject/action combination would be `secrets/read`. This permission allows the subject to read secrets.
Currently Infisical supports 4 actions:
1. `read`, allows the subject to read the object.
2. `create`, allows the subject to create the object.
3. `edit`, allows the subject to edit the object.
4. `delete`, allows the subject to delete the object.
Most subjects support all 4 actions, but some subjects only support a subset of actions. Please view the table below for a list of subjects and the actions they support.
## Subjects and Actions
<Tabs>
<Tab title="Project Permissions">
<Note>
Not all actions are applicable to all subjects. As an example, the `secrets-rollback` subject only supports `read`, and `create` as actions. While `secrets` support `read`, `create`, `edit`, `delete`.
</Note>
| Subject | Actions |
|-----------------------------|---------|
| `secrets` | `read`, `create`, `edit`, `delete` |
| `secret-approval` | `read`, `create`, `edit`, `delete` |
| `secret-rotation` | `read`, `create`, `edit`, `delete` |
| `secret-rollback` | `read`, `create` |
| `member` | `read`, `create`, `edit`, `delete` |
| `groups` | `read`, `create`, `edit`, `delete` |
| `role` | `read`, `create`, `edit`, `delete` |
| `integrations` | `read`, `create`, `edit`, `delete` |
| `webhooks` | `read`, `create`, `edit`, `delete` |
| `identity` | `read`, `create`, `edit`, `delete` |
| `service-tokens` | `read`, `create`, `edit`, `delete` |
| `settings` | `read`, `create`, `edit`, `delete` |
| `environments` | `read`, `create`, `edit`, `delete` |
| `tags` | `read`, `create`, `edit`, `delete` |
| `audit-logs` | `read`, `create`, `edit`, `delete` |
| `ip-allowlist` | `read`, `create`, `edit`, `delete` |
| `certificate-authorities` | `read`, `create`, `edit`, `delete` |
| `certificates` | `read`, `create`, `edit`, `delete` |
| `certificate-templates` | `read`, `create`, `edit`, `delete` |
| `pki-alerts` | `read`, `create`, `edit`, `delete` |
| `pki-collections` | `read`, `create`, `edit`, `delete` |
| `workspace` | `edit`, `delete` |
| `kms` | `edit` |
These details are especially useful if you're using the API to [create new project roles](../api-reference/endpoints/project-roles/create).
The rules outlined on this page, also apply when using our Terraform Provider to manage your Infisical project roles, or any other of our clients that manage project roles.
</Tab>
<Tab title="Organization Permissions">
<Note>
Not all actions are applicable to all subjects. As an example, the `workspace` subject only supports `read`, and `create` as actions. While `member` support `read`, `create`, `edit`, `delete`.
</Note>
| Subject | Actions |
|-----------------------------|------------------------------------|
| `workspace` | `read`, `create` |
| `role` | `read`, `create`, `edit`, `delete` |
| `member` | `read`, `create`, `edit`, `delete` |
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
| `settings` | `read`, `create`, `edit`, `delete` |
| `incident-account` | `read`, `create`, `edit`, `delete` |
| `sso` | `read`, `create`, `edit`, `delete` |
| `scim` | `read`, `create`, `edit`, `delete` |
| `ldap` | `read`, `create`, `edit`, `delete` |
| `groups` | `read`, `create`, `edit`, `delete` |
| `billing` | `read`, `create`, `edit`, `delete` |
| `identity` | `read`, `create`, `edit`, `delete` |
| `kms` | `read` |
</Tab>
</Tabs>

View File

@ -696,12 +696,7 @@
{ {
"group": "Audit Logs", "group": "Audit Logs",
"pages": ["api-reference/endpoints/audit-logs/export-audit-log"] "pages": ["api-reference/endpoints/audit-logs/export-audit-log"]
} },
]
},
{
"group": "Infisical PKI",
"pages": [
{ {
"group": "Certificate Authorities", "group": "Certificate Authorities",
"pages": [ "pages": [
@ -769,7 +764,6 @@
"group": "Internals", "group": "Internals",
"pages": [ "pages": [
"internals/overview", "internals/overview",
"internals/permissions",
"internals/components", "internals/components",
"internals/flows", "internals/flows",
"internals/security", "internals/security",

View File

@ -1,83 +0,0 @@
import { useState } from "react";
import { faCaretDown, faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Combobox, Transition } from "@headlessui/react";
import { ByComparator } from "@headlessui/react/dist/types";
import { twMerge } from "tailwind-merge";
type ComboBoxProps<T extends object> = {
value?: T;
className?: string;
items: {
value: T;
key: string;
label: string;
}[];
by: ByComparator<T>;
defaultValue?: T;
displayValue: (value: T) => string;
onSelectChange: (value: T) => void;
onFilter: (value: { value: T }, filterQuery: string) => boolean;
};
// TODO(akhilmhdh): This is a very temporary one due to limitation of present situation
// don't mind the api for now will be switched to aria later
export const ComboBox = <T extends object>({
onSelectChange,
onFilter,
displayValue,
by,
items,
...props
}: ComboBoxProps<T>) => {
const [query, setQuery] = useState("");
const filteredResult =
query === "" ? items.slice(0, 20) : items.filter((el) => onFilter(el, query)).slice(0, 20);
return (
<Combobox by={by} {...props} onChange={onSelectChange}>
<div className="relative">
<Combobox.Input
onChange={(event) => setQuery(event.target.value)}
displayValue={displayValue}
className=" inline-flex w-full items-center justify-between rounded-md bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200"
/>
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
<FontAwesomeIcon icon={faCaretDown} size="sm" aria-hidden="true" />
</Combobox.Button>
<Transition
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
afterLeave={() => setQuery("")}
>
<Combobox.Options className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md bg-mineshaft-900 py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
{filteredResult.map(({ value, key, label }) => (
<Combobox.Option
key={key}
value={value}
className={({ active }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 transition-all hover:bg-mineshaft-500 ${
active ? "text-primary" : "text-white"
}`
}
>
{({ selected }) => (
<>
{label}
{selected ? (
<div className={twMerge("absolute top-2 left-3 text-primary")}>
<FontAwesomeIcon icon={faCheck} />
</div>
) : null}
</>
)}
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</div>
</Combobox>
);
};

View File

@ -1 +0,0 @@
export { ComboBox } from "./ComboBox";

View File

@ -11,7 +11,6 @@ type Props = {
isDisabled?: boolean; isDisabled?: boolean;
isReadOnly?: boolean; isReadOnly?: boolean;
autoCapitalization?: boolean; autoCapitalization?: boolean;
containerClassName?: string;
}; };
const inputVariants = cva( const inputVariants = cva(
@ -72,7 +71,6 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
( (
{ {
className, className,
containerClassName,
isRounded = true, isRounded = true,
isFullWidth = true, isFullWidth = true,
isDisabled, isDisabled,
@ -96,15 +94,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
}; };
return ( return (
<div <div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
className={inputParentContainerVariants({
isRounded,
isError,
isFullWidth,
variant,
className: containerClassName
})}
>
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>} {leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
<input <input
{...props} {...props}

View File

@ -11,24 +11,13 @@ export type ModalContentProps = DialogPrimitive.DialogContentProps & {
title?: ReactNode; title?: ReactNode;
subTitle?: ReactNode; subTitle?: ReactNode;
footerContent?: ReactNode; footerContent?: ReactNode;
bodyClassName?: string;
onClose?: () => void; onClose?: () => void;
overlayClassName?: string; overlayClassName?: string;
}; };
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>( export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
( (
{ { children, title, subTitle, className, overlayClassName, footerContent, onClose, ...props },
children,
title,
subTitle,
className,
overlayClassName,
footerContent,
bodyClassName,
onClose,
...props
},
forwardedRef forwardedRef
) => ( ) => (
<DialogPrimitive.Portal> <DialogPrimitive.Portal>
@ -46,10 +35,7 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
style={{ maxHeight: "90%" }} style={{ maxHeight: "90%" }}
> >
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>} {title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
<CardBody <CardBody className="overflow-y-auto overflow-x-hidden" style={{ maxHeight: "90%" }}>
className={twMerge("overflow-y-auto overflow-x-hidden", bodyClassName)}
style={{ maxHeight: "90%" }}
>
{children} {children}
</CardBody> </CardBody>
{footerContent && <CardFooter>{footerContent}</CardFooter>} {footerContent && <CardFooter>{footerContent}</CardFooter>}

View File

@ -40,8 +40,6 @@ export const Pagination = ({
const upperLimit = Math.ceil(count / perPage); const upperLimit = Math.ceil(count / perPage);
const nextPageNumber = Math.min(upperLimit, page + 1); const nextPageNumber = Math.min(upperLimit, page + 1);
const canGoNext = page + 1 <= upperLimit; const canGoNext = page + 1 <= upperLimit;
const canGoFirst = page > 1;
const canGoLast = page < upperLimit;
return ( return (
<div <div
@ -52,7 +50,7 @@ export const Pagination = ({
> >
<div className="mr-6 flex items-center space-x-2"> <div className="mr-6 flex items-center space-x-2">
<div className="text-xs"> <div className="text-xs">
{(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count} {(page - 1) * perPage} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
</div> </div>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -75,16 +73,6 @@ export const Pagination = ({
</DropdownMenu> </DropdownMenu>
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<IconButton
variant="plain"
ariaLabel="pagination-first"
className="relative"
onClick={() => onChangePage(1)}
isDisabled={!canGoFirst}
>
<FontAwesomeIcon className="absolute left-2.5 top-1 text-xs" icon={faChevronLeft} />
<FontAwesomeIcon className="text-xs" icon={faChevronLeft} />
</IconButton>
<IconButton <IconButton
variant="plain" variant="plain"
ariaLabel="pagination-prev" ariaLabel="pagination-prev"
@ -101,16 +89,6 @@ export const Pagination = ({
> >
<FontAwesomeIcon className="text-xs" icon={faChevronRight} /> <FontAwesomeIcon className="text-xs" icon={faChevronRight} />
</IconButton> </IconButton>
<IconButton
variant="plain"
ariaLabel="pagination-last"
className="relative"
onClick={() => onChangePage(upperLimit)}
isDisabled={!canGoLast}
>
<FontAwesomeIcon className="absolute left-2.5 top-1 text-xs" icon={faChevronRight} />
<FontAwesomeIcon className="text-xs" icon={faChevronRight} />
</IconButton>
</div> </div>
</div> </div>
); );

View File

@ -50,8 +50,8 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={twMerge( className={twMerge(
`inline-flex items-center justify-between rounded-md border border-mineshaft-600 `inline-flex items-center justify-between rounded-md
bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-400`, bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200`,
className, className,
isDisabled && "cursor-not-allowed opacity-50" isDisabled && "cursor-not-allowed opacity-50"
)} )}

View File

@ -1,7 +1,6 @@
import { createContext, ReactNode, useContext, useEffect, useMemo } from "react"; import { createContext, ReactNode, useContext, useMemo } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { createNotification } from "@app/components/notifications";
import { useGetUserWorkspaces } from "@app/hooks/api"; import { useGetUserWorkspaces } from "@app/hooks/api";
import { Workspace } from "@app/hooks/api/workspace/types"; import { Workspace } from "@app/hooks/api/workspace/types";
@ -32,34 +31,6 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
}; };
}, [ws, workspaceId, isLoading]); }, [ws, workspaceId, isLoading]);
const shouldTriggerNoProjectAccess =
!value.isLoading &&
!value.currentWorkspace &&
router.pathname.startsWith("/project") &&
workspaceId;
// handle redirects for project-specific routes
useEffect(() => {
if (shouldTriggerNoProjectAccess) {
createNotification({
text: "You are not a member of this project.",
type: "info"
});
setTimeout(() => {
router.push("/");
}, 5000);
}
}, [shouldTriggerNoProjectAccess, router]);
if (shouldTriggerNoProjectAccess) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800 text-primary-50">
You do not have sufficient access to this project.
</div>
);
}
return <WorkspaceContext.Provider value={value}>{children}</WorkspaceContext.Provider>; return <WorkspaceContext.Provider value={value}>{children}</WorkspaceContext.Provider>;
}; };

View File

@ -79,7 +79,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]: [EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
"Update certificate template EST configuration", "Update certificate template EST configuration",
[EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration", [EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration",
[EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration" [EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration",
[EventType.INTEGRATION_SYNCED]: "Integration sync"
}; };
export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = { export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {

View File

@ -1,4 +1,5 @@
export enum ActorType { export enum ActorType {
PLATFORM = "platform",
USER = "user", USER = "user",
SERVICE = "service", SERVICE = "service",
IDENTITY = "identity" IDENTITY = "identity"
@ -91,5 +92,6 @@ export enum EventType {
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config", UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config", GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config", UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config" GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
INTEGRATION_SYNCED = "integration-synced"
} }

View File

@ -1,35 +1,58 @@
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useInfiniteQuery, UseInfiniteQueryOptions, useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { Actor, AuditLog, AuditLogFilters } from "./types"; import { Actor, AuditLog, TGetAuditLogsFilter } from "./types";
export const auditLogKeys = { export const auditLogKeys = {
getAuditLogs: (workspaceId: string | null, filters: AuditLogFilters) => getAuditLogs: (workspaceId: string | null, filters: TGetAuditLogsFilter) =>
[{ workspaceId, filters }, "audit-logs"] as const, [{ workspaceId, filters }, "audit-logs"] as const,
getAuditLogActorFilterOpts: (workspaceId: string) => getAuditLogActorFilterOpts: (workspaceId: string) =>
[{ workspaceId }, "audit-log-actor-filters"] as const [{ workspaceId }, "audit-log-actor-filters"] as const
}; };
export const useGetAuditLogs = (filters: AuditLogFilters, workspaceId: string | null) => { export const useGetAuditLogs = (
filters: TGetAuditLogsFilter,
projectId: string | null,
options: Omit<
UseInfiniteQueryOptions<
AuditLog[],
unknown,
AuditLog[],
AuditLog[],
ReturnType<typeof auditLogKeys.getAuditLogs>
>,
"queryFn" | "queryKey" | "getNextPageParam"
> = {}
) => {
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: auditLogKeys.getAuditLogs(workspaceId, filters), queryKey: auditLogKeys.getAuditLogs(projectId, filters),
queryFn: async ({ pageParam }) => { queryFn: async ({ pageParam }) => {
const auditLogEndpoint = workspaceId const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
? `/api/v1/workspace/${workspaceId}/audit-logs` "/api/v1/organization/audit-logs",
: "/api/v1/organization/audit-logs"; {
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(auditLogEndpoint, { params: {
params: { ...filters,
...filters, offset: pageParam,
offset: pageParam, startDate: filters?.startDate?.toISOString(),
startDate: filters?.startDate?.toISOString(), endDate: filters?.endDate?.toISOString(),
endDate: filters?.endDate?.toISOString() ...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
? {
eventMetadata: Object.entries(filters.eventMetadata)
.map(([key, value]) => `${key}=${value}`)
.join(",")
}
: {}),
...(filters.eventType?.length ? { eventType: filters.eventType.join(",") } : {}),
...(projectId ? { projectId } : {})
}
} }
}); );
return data.auditLogs; return data.auditLogs;
}, },
getNextPageParam: (lastPage, pages) => getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * filters.limit : undefined lastPage.length !== 0 ? pages.length * filters.limit : undefined,
...options
}); });
}; };

View File

@ -3,6 +3,17 @@ import { IdentityTrustedIp } from "../identities/types";
import { PkiItemType } from "../pkiCollections/constants"; import { PkiItemType } from "../pkiCollections/constants";
import { ActorType, EventType, UserAgentType } from "./enums"; import { ActorType, EventType, UserAgentType } from "./enums";
export type TGetAuditLogsFilter = {
eventType?: EventType[];
userAgentType?: UserAgentType;
eventMetadata?: Record<string, string>;
actorType?: ActorType;
actorId?: string; // user ID format
startDate?: Date;
endDate?: Date;
limit: number;
};
interface UserActorMetadata { interface UserActorMetadata {
userId: string; userId: string;
email: string; email: string;
@ -33,7 +44,13 @@ export interface IdentityActor {
metadata: IdentityActorMetadata; metadata: IdentityActorMetadata;
} }
export type Actor = UserActor | ServiceActor | IdentityActor; export interface PlatformActorMetadata {}
export interface PlatformActor {
type: ActorType.PLATFORM;
metadata: PlatformActorMetadata;
}
export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
@ -761,6 +778,22 @@ interface GetProjectSlackConfig {
}; };
} }
export enum IntegrationSyncedEventTrigger {
MANUAL = "manual",
AUTO = "auto"
}
interface IntegrationSyncedEvent {
type: EventType.INTEGRATION_SYNCED;
metadata: {
integrationId: string;
lastSyncJobId: string;
lastUsed: Date;
syncMessage: string;
isSynced: boolean;
};
}
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
@ -838,7 +871,8 @@ export type Event =
| CreateCertificateTemplateEstConfig | CreateCertificateTemplateEstConfig
| GetCertificateTemplateEstConfig | GetCertificateTemplateEstConfig
| UpdateProjectSlackConfig | UpdateProjectSlackConfig
| GetProjectSlackConfig; | GetProjectSlackConfig
| IntegrationSyncedEvent;
export type AuditLog = { export type AuditLog = {
id: string; id: string;
@ -856,12 +890,3 @@ export type AuditLog = {
slug: string; slug: string;
}; };
}; };
export type AuditLogFilters = {
eventType?: EventType;
userAgentType?: UserAgentType;
actor?: string;
limit: number;
startDate?: Date;
endDate?: Date;
};

View File

@ -1,4 +1,4 @@
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificates/enums"; import { CertKeyAlgorithm } from "../certificates/enums";
import { CaRenewalType, CaStatus, CaType } from "./enums"; import { CaRenewalType, CaStatus, CaType } from "./enums";
export type TCertificateAuthority = { export type TCertificateAuthority = {
@ -91,8 +91,6 @@ export type TCreateCertificateDTO = {
ttl: string; // string compatible with ms ttl: string; // string compatible with ms
notBefore?: string; notBefore?: string;
notAfter?: string; notAfter?: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
}; };
export type TCreateCertificateResponse = { export type TCreateCertificateResponse = {

View File

@ -1,5 +1,3 @@
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificates/enums";
export type TCertificateTemplate = { export type TCertificateTemplate = {
id: string; id: string;
caId: string; caId: string;
@ -10,8 +8,6 @@ export type TCertificateTemplate = {
commonName: string; commonName: string;
subjectAlternativeName: string; subjectAlternativeName: string;
ttl: string; ttl: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
}; };
export type TCreateCertificateTemplateDTO = { export type TCreateCertificateTemplateDTO = {
@ -22,8 +18,6 @@ export type TCreateCertificateTemplateDTO = {
subjectAlternativeName: string; subjectAlternativeName: string;
ttl: string; ttl: string;
projectId: string; projectId: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
}; };
export type TUpdateCertificateTemplateDTO = { export type TUpdateCertificateTemplateDTO = {
@ -35,8 +29,6 @@ export type TUpdateCertificateTemplateDTO = {
subjectAlternativeName?: string; subjectAlternativeName?: string;
ttl?: string; ttl?: string;
projectId: string; projectId: string;
keyUsages?: CertKeyUsage[];
extendedKeyUsages?: CertExtendedKeyUsage[];
}; };
export type TDeleteCertificateTemplateDTO = { export type TDeleteCertificateTemplateDTO = {

View File

@ -1,10 +1,4 @@
import { import { CertKeyAlgorithm, CertStatus, CrlReason } from "./enums";
CertExtendedKeyUsage,
CertKeyAlgorithm,
CertKeyUsage,
CertStatus,
CrlReason
} from "./enums";
export const certStatusToNameMap: { [K in CertStatus]: string } = { export const certStatusToNameMap: { [K in CertStatus]: string } = {
[CertStatus.ACTIVE]: "Active", [CertStatus.ACTIVE]: "Active",
@ -75,24 +69,3 @@ export const crlReasons = [
}, },
{ label: crlReasonToNameMap[CrlReason.A_A_COMPROMISE], value: CrlReason.A_A_COMPROMISE } { label: crlReasonToNameMap[CrlReason.A_A_COMPROMISE], value: CrlReason.A_A_COMPROMISE }
]; ];
export const KEY_USAGES_OPTIONS = [
{ value: CertKeyUsage.DIGITAL_SIGNATURE, label: "Digital Signature" },
{ value: CertKeyUsage.KEY_ENCIPHERMENT, label: "Key Encipherment" },
{ value: CertKeyUsage.NON_REPUDIATION, label: "Non Repudiation" },
{ value: CertKeyUsage.DATA_ENCIPHERMENT, label: "Data Encipherment" },
{ value: CertKeyUsage.KEY_AGREEMENT, label: "Key Agreement" },
{ value: CertKeyUsage.KEY_CERT_SIGN, label: "Certificate Sign" },
{ value: CertKeyUsage.CRL_SIGN, label: "CRL Sign" },
{ value: CertKeyUsage.ENCIPHER_ONLY, label: "Encipher Only" },
{ value: CertKeyUsage.DECIPHER_ONLY, label: "Decipher Only" }
] as const;
export const EXTENDED_KEY_USAGES_OPTIONS = [
{ value: CertExtendedKeyUsage.CLIENT_AUTH, label: "Client Auth" },
{ value: CertExtendedKeyUsage.SERVER_AUTH, label: "Server Auth" },
{ value: CertExtendedKeyUsage.EMAIL_PROTECTION, label: "Email Protection" },
{ value: CertExtendedKeyUsage.OCSP_SIGNING, label: "OCSP Signing" },
{ value: CertExtendedKeyUsage.CODE_SIGNING, label: "Code Signing" },
{ value: CertExtendedKeyUsage.TIMESTAMPING, label: "Timestamping" }
] as const;

View File

@ -22,24 +22,3 @@ export enum CrlReason {
PRIVILEGE_WITHDRAWN = "PRIVILEGE_WITHDRAWN", PRIVILEGE_WITHDRAWN = "PRIVILEGE_WITHDRAWN",
A_A_COMPROMISE = "A_A_COMPROMISE" A_A_COMPROMISE = "A_A_COMPROMISE"
} }
export enum CertKeyUsage {
DIGITAL_SIGNATURE = "digitalSignature",
KEY_ENCIPHERMENT = "keyEncipherment",
NON_REPUDIATION = "nonRepudiation",
DATA_ENCIPHERMENT = "dataEncipherment",
KEY_AGREEMENT = "keyAgreement",
KEY_CERT_SIGN = "keyCertSign",
CRL_SIGN = "cRLSign",
ENCIPHER_ONLY = "encipherOnly",
DECIPHER_ONLY = "decipherOnly"
}
export enum CertExtendedKeyUsage {
CLIENT_AUTH = "clientAuth",
SERVER_AUTH = "serverAuth",
CODE_SIGNING = "codeSigning",
EMAIL_PROTECTION = "emailProtection",
TIMESTAMPING = "timeStamping",
OCSP_SIGNING = "ocspSigning"
}

View File

@ -1,4 +1,4 @@
import { CertExtendedKeyUsage, CertKeyUsage, CertStatus } from "./enums"; import { CertStatus } from "./enums";
export type TCertificate = { export type TCertificate = {
id: string; id: string;
@ -11,8 +11,6 @@ export type TCertificate = {
serialNumber: string; serialNumber: string;
notBefore: string; notBefore: string;
notAfter: string; notAfter: string;
keyUsages: CertKeyUsage[];
extendedKeyUsages: CertExtendedKeyUsage[];
}; };
export type TDeleteCertDTO = { export type TDeleteCertDTO = {

View File

@ -1,4 +0,0 @@
export enum OrderByDirection {
ASC = "asc",
DESC = "desc"
}

View File

@ -469,8 +469,3 @@ export type RevokeTokenDTO = {
export type RevokeTokenRes = { export type RevokeTokenRes = {
message: string; message: string;
}; };
export type TProjectIdentitiesList = {
identityMemberships: IdentityMembership[];
totalCount: number;
};

View File

@ -1 +1,6 @@
export { useCreateIntegration, useDeleteIntegration, useGetCloudIntegrations } from "./queries"; export {
useCreateIntegration,
useDeleteIntegration,
useGetCloudIntegrations,
useGetIntegration
} from "./queries";

View File

@ -1,13 +1,14 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { workspaceKeys } from "../workspace"; import { workspaceKeys } from "../workspace";
import { TCloudIntegration } from "./types"; import { TCloudIntegration, TIntegrationWithEnv } from "./types";
export const integrationQueryKeys = { export const integrationQueryKeys = {
getIntegrations: () => ["integrations"] as const getIntegrations: () => ["integrations"] as const,
getIntegration: (id: string) => ["integration", id] as const
}; };
const fetchIntegrations = async () => { const fetchIntegrations = async () => {
@ -18,6 +19,14 @@ const fetchIntegrations = async () => {
return data.integrationOptions; return data.integrationOptions;
}; };
const fetchIntegration = async (id: string) => {
const { data } = await apiRequest.get<{ integration: TIntegrationWithEnv }>(
`/api/v1/integration/${id}`
);
return data.integration;
};
export const useGetCloudIntegrations = () => export const useGetCloudIntegrations = () =>
useQuery({ useQuery({
queryKey: integrationQueryKeys.getIntegrations(), queryKey: integrationQueryKeys.getIntegrations(),
@ -128,6 +137,26 @@ export const useDeleteIntegration = () => {
}); });
}; };
export const useGetIntegration = (
integrationId: string,
options?: Omit<
UseQueryOptions<
TIntegrationWithEnv,
unknown,
TIntegrationWithEnv,
ReturnType<typeof integrationQueryKeys.getIntegration>
>,
"queryFn" | "queryKey"
>
) => {
return useQuery({
...options,
enabled: Boolean(integrationId && options?.enabled === undefined ? true : options?.enabled),
queryKey: integrationQueryKeys.getIntegration(integrationId),
queryFn: () => fetchIntegration(integrationId)
});
};
export const useSyncIntegration = () => { export const useSyncIntegration = () => {
return useMutation<{}, {}, { id: string; workspaceId: string; lastUsed: string }>({ return useMutation<{}, {}, { id: string; workspaceId: string; lastUsed: string }>({
mutationFn: ({ id }) => apiRequest.post(`/api/v1/integration/${id}/sync`), mutationFn: ({ id }) => apiRequest.post(`/api/v1/integration/${id}/sync`),

View File

@ -36,14 +36,34 @@ export type TIntegration = {
metadata?: { metadata?: {
githubVisibility?: string; githubVisibility?: string;
githubVisibilityRepoIds?: string[]; githubVisibilityRepoIds?: string[];
shouldAutoRedeploy?: boolean;
secretAWSTag?: {
key: string;
value: string;
}[];
kmsKeyId?: string;
secretSuffix?: string; secretSuffix?: string;
secretPrefix?: string;
syncBehavior?: IntegrationSyncBehavior; syncBehavior?: IntegrationSyncBehavior;
mappingBehavior?: IntegrationMappingBehavior; mappingBehavior?: IntegrationMappingBehavior;
scope: string; scope: string;
org: string; org: string;
project: string; project: string;
environment: string; environment: string;
shouldDisableDelete?: boolean;
shouldMaskSecrets?: boolean;
shouldProtectSecrets?: boolean;
shouldEnableDelete?: boolean;
};
};
export type TIntegrationWithEnv = TIntegration & {
environment: {
id: string;
name: string;
slug: string;
}; };
}; };

View File

@ -1,22 +1,19 @@
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TGroupOrgMembership } from "../groups/types"; import { TGroupOrgMembership } from "../groups/types";
import { IdentityMembershipOrg } from "../identities/types";
import { import {
BillingDetails, BillingDetails,
Invoice, Invoice,
License, License,
Organization, Organization,
OrgIdentityOrderBy,
OrgPlanTable, OrgPlanTable,
PlanBillingInfo, PlanBillingInfo,
PmtMethod, PmtMethod,
ProductsTable, ProductsTable,
TaxID, TaxID,
TListOrgIdentitiesDTO,
TOrgIdentitiesList,
UpdateOrgDTO UpdateOrgDTO
} from "./types"; } from "./types";
@ -33,12 +30,6 @@ export const organizationKeys = {
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const, getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const,
getOrgIdentityMemberships: (orgId: string) => getOrgIdentityMemberships: (orgId: string) =>
[{ orgId }, "organization-identity-memberships"] as const, [{ orgId }, "organization-identity-memberships"] as const,
// allows invalidation using above key without knowing params
getOrgIdentityMembershipsWithParams: ({
organizationId: orgId,
...params
}: TListOrgIdentitiesDTO) =>
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const
}; };
@ -369,51 +360,19 @@ export const useGetOrgLicenses = (organizationId: string) => {
}); });
}; };
export const useGetIdentityMembershipOrgs = ( export const useGetIdentityMembershipOrgs = (organizationId: string) => {
{
organizationId,
offset = 0,
limit = 100,
orderBy = OrgIdentityOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search = ""
}: TListOrgIdentitiesDTO,
options?: Omit<
UseQueryOptions<
TOrgIdentitiesList,
unknown,
TOrgIdentitiesList,
ReturnType<typeof organizationKeys.getOrgIdentityMembershipsWithParams>
>,
"queryKey" | "queryFn"
>
) => {
const params = new URLSearchParams({
offset: String(offset),
limit: String(limit),
orderBy: String(orderBy),
orderDirection: String(orderDirection),
search: String(search)
});
return useQuery({ return useQuery({
queryKey: organizationKeys.getOrgIdentityMembershipsWithParams({ queryKey: organizationKeys.getOrgIdentityMemberships(organizationId),
organizationId,
offset,
limit,
orderBy,
orderDirection,
search
}),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<TOrgIdentitiesList>( const {
`/api/v2/organizations/${organizationId}/identity-memberships`, data: { identityMemberships }
{ params } } = await apiRequest.get<{ identityMemberships: IdentityMembershipOrg[] }>(
`/api/v2/organizations/${organizationId}/identity-memberships`
); );
return data; return identityMemberships;
}, },
enabled: true, enabled: true
...options
}); });
}; };

View File

@ -1,6 +1,3 @@
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { IdentityMembershipOrg } from "@app/hooks/api/identities/types";
export type Organization = { export type Organization = {
id: string; id: string;
name: string; name: string;
@ -105,22 +102,3 @@ export type ProductsTable = {
head: ProductsTableHead[]; head: ProductsTableHead[];
rows: ProductsTableRow[]; rows: ProductsTableRow[];
}; };
export type TListOrgIdentitiesDTO = {
organizationId: string;
offset?: number;
limit?: number;
orderBy?: OrgIdentityOrderBy;
orderDirection?: OrderByDirection;
search?: string;
};
export type TOrgIdentitiesList = {
identityMemberships: IdentityMembershipOrg[];
totalCount: number;
};
export enum OrgIdentityOrderBy {
Name = "name",
Role = "role"
}

View File

@ -163,29 +163,27 @@ export const useGetImportedSecretsAllEnvs = ({
queryFn: () => fetchImportedSecrets(projectId, env, path).catch(() => []), queryFn: () => fetchImportedSecrets(projectId, env, path).catch(() => []),
enabled: Boolean(projectId) && Boolean(env), enabled: Boolean(projectId) && Boolean(env),
// eslint-disable-next-line react-hooks/rules-of-hooks // eslint-disable-next-line react-hooks/rules-of-hooks
select: useCallback( select: (data: TImportedSecrets[]) => {
(data: Awaited<ReturnType<typeof fetchImportedSecrets>>) => return data.map((el) => ({
data.map((el) => ({ environment: el.environment,
environment: el.environment, secretPath: el.secretPath,
secretPath: el.secretPath, environmentInfo: el.environmentInfo,
environmentInfo: el.environmentInfo, folderId: el.folderId,
folderId: el.folderId, secrets: el.secrets.map((encSecret) => {
secrets: el.secrets.map((encSecret) => { return {
return { id: encSecret.id,
id: encSecret.id, env: encSecret.environment,
env: encSecret.environment, key: encSecret.secretKey,
key: encSecret.secretKey, value: encSecret.secretValue,
value: encSecret.secretValue, tags: encSecret.tags,
tags: encSecret.tags, comment: encSecret.secretComment,
comment: encSecret.secretComment, createdAt: encSecret.createdAt,
createdAt: encSecret.createdAt, updatedAt: encSecret.updatedAt,
updatedAt: encSecret.updatedAt, version: encSecret.version
version: encSecret.version };
}; })
}) }));
})), }
[]
)
})) }))
}); });

View File

@ -108,7 +108,7 @@ export const useGetProjectSecrets = ({
// wait for all values to be available // wait for all values to be available
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true), enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }), queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }), queryFn: async () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
onError: (error) => { onError: (error) => {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
const serverResponse = error.response?.data as { message: string }; const serverResponse = error.response?.data as { message: string };
@ -119,10 +119,7 @@ export const useGetProjectSecrets = ({
}); });
} }
}, },
select: useCallback( select: ({ secrets }) => mergePersonalSecrets(secrets)
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => mergePersonalSecrets(data.secrets),
[]
)
}); });
export const useGetProjectSecretsAllEnv = ({ export const useGetProjectSecretsAllEnv = ({
@ -134,11 +131,7 @@ export const useGetProjectSecretsAllEnv = ({
const secrets = useQueries({ const secrets = useQueries({
queries: envs.map((environment) => ({ queries: envs.map((environment) => ({
queryKey: secretKeys.getProjectSecret({ queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
workspaceId,
environment,
secretPath
}),
enabled: Boolean(workspaceId && environment), enabled: Boolean(workspaceId && environment),
onError: (error: unknown) => { onError: (error: unknown) => {
if (axios.isAxiosError(error) && !isErrorHandled) { if (axios.isAxiosError(error) && !isErrorHandled) {
@ -154,17 +147,12 @@ export const useGetProjectSecretsAllEnv = ({
setIsErrorHandled.on(); setIsErrorHandled.on();
} }
}, },
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }), queryFn: async () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
staleTime: 60 * 1000, select: (el: SecretV3RawResponse) =>
// eslint-disable-next-line react-hooks/rules-of-hooks mergePersonalSecrets(el.secrets).reduce<Record<string, SecretV3RawSanitized>>(
select: useCallback( (prev, curr) => ({ ...prev, [curr.key]: curr }),
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => {}
mergePersonalSecrets(data.secrets).reduce<Record<string, SecretV3RawSanitized>>( )
(prev, curr) => ({ ...prev, [curr.key]: curr }),
{}
),
[]
)
})) }))
}); });

View File

@ -1,7 +1,6 @@
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { CaStatus } from "../ca/enums"; import { CaStatus } from "../ca/enums";
import { TCertificateAuthority } from "../ca/types"; import { TCertificateAuthority } from "../ca/types";
@ -9,7 +8,7 @@ import { TCertificate } from "../certificates/types";
import { TCertificateTemplate } from "../certificateTemplates/types"; import { TCertificateTemplate } from "../certificateTemplates/types";
import { TGroupMembership } from "../groups/types"; import { TGroupMembership } from "../groups/types";
import { identitiesKeys } from "../identities/queries"; import { identitiesKeys } from "../identities/queries";
import { TProjectIdentitiesList } from "../identities/types"; import { IdentityMembership } from "../identities/types";
import { IntegrationAuth } from "../integrationAuth/types"; import { IntegrationAuth } from "../integrationAuth/types";
import { TIntegration } from "../integrations/types"; import { TIntegration } from "../integrations/types";
import { TPkiAlert } from "../pkiAlerts/types"; import { TPkiAlert } from "../pkiAlerts/types";
@ -25,10 +24,8 @@ import {
DeleteEnvironmentDTO, DeleteEnvironmentDTO,
DeleteWorkspaceDTO, DeleteWorkspaceDTO,
NameWorkspaceSecretsDTO, NameWorkspaceSecretsDTO,
ProjectIdentityOrderBy,
RenameWorkspaceDTO, RenameWorkspaceDTO,
TGetUpgradeProjectStatusDTO, TGetUpgradeProjectStatusDTO,
TListProjectIdentitiesDTO,
ToggleAutoCapitalizationDTO, ToggleAutoCapitalizationDTO,
TUpdateWorkspaceIdentityRoleDTO, TUpdateWorkspaceIdentityRoleDTO,
TUpdateWorkspaceUserRoleDTO, TUpdateWorkspaceUserRoleDTO,
@ -487,51 +484,18 @@ export const useDeleteIdentityFromWorkspace = () => {
}); });
}; };
export const useGetWorkspaceIdentityMemberships = ( export const useGetWorkspaceIdentityMemberships = (workspaceId: string) => {
{
workspaceId,
offset = 0,
limit = 100,
orderBy = ProjectIdentityOrderBy.Name,
orderDirection = OrderByDirection.ASC,
search = ""
}: TListProjectIdentitiesDTO,
options?: Omit<
UseQueryOptions<
TProjectIdentitiesList,
unknown,
TProjectIdentitiesList,
ReturnType<typeof workspaceKeys.getWorkspaceIdentityMembershipsWithParams>
>,
"queryKey" | "queryFn"
>
) => {
return useQuery({ return useQuery({
queryKey: workspaceKeys.getWorkspaceIdentityMembershipsWithParams({ queryKey: workspaceKeys.getWorkspaceIdentityMemberships(workspaceId),
workspaceId,
offset,
limit,
orderBy,
orderDirection,
search
}),
queryFn: async () => { queryFn: async () => {
const params = new URLSearchParams({ const {
offset: String(offset), data: { identityMemberships }
limit: String(limit), } = await apiRequest.get<{ identityMemberships: IdentityMembership[] }>(
orderBy: String(orderBy), `/api/v2/workspace/${workspaceId}/identity-memberships`
orderDirection: String(orderDirection),
search: String(search)
});
const { data } = await apiRequest.get<TProjectIdentitiesList>(
`/api/v2/workspace/${workspaceId}/identity-memberships`,
{ params }
); );
return data; return identityMemberships;
}, },
enabled: true, enabled: true
...options
}); });
}; };

View File

@ -1,5 +1,3 @@
import { TListProjectIdentitiesDTO } from "@app/hooks/api/workspace/types";
import type { CaStatus } from "../ca"; import type { CaStatus } from "../ca";
export const workspaceKeys = { export const workspaceKeys = {
@ -17,12 +15,6 @@ export const workspaceKeys = {
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const, getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
getWorkspaceIdentityMemberships: (workspaceId: string) => getWorkspaceIdentityMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-identity-memberships"] as const, [{ workspaceId }, "workspace-identity-memberships"] as const,
// allows invalidation using above key without knowing params
getWorkspaceIdentityMembershipsWithParams: ({
workspaceId,
...params
}: TListProjectIdentitiesDTO) =>
[...workspaceKeys.getWorkspaceIdentityMemberships(workspaceId), params] as const,
getWorkspaceGroupMemberships: (workspaceId: string) => getWorkspaceGroupMemberships: (workspaceId: string) =>
[{ workspaceId }, "workspace-groups"] as const, [{ workspaceId }, "workspace-groups"] as const,
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) => getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>

View File

@ -1,5 +1,3 @@
import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TProjectRole } from "../roles/types"; import { TProjectRole } from "../roles/types";
export enum ProjectVersion { export enum ProjectVersion {
@ -143,16 +141,3 @@ export type TUpdateWorkspaceGroupRoleDTO = {
} }
)[]; )[];
}; };
export type TListProjectIdentitiesDTO = {
workspaceId: string;
offset?: number;
limit?: number;
orderBy?: ProjectIdentityOrderBy;
orderDirection?: OrderByDirection;
search?: string;
};
export enum ProjectIdentityOrderBy {
Name = "name"
}

View File

@ -104,7 +104,6 @@ export default function AWSSecretManagerCreateIntegrationPage() {
const [tagKey, setTagKey] = useState(""); const [tagKey, setTagKey] = useState("");
const [tagValue, setTagValue] = useState(""); const [tagValue, setTagValue] = useState("");
const [kmsKeyId, setKmsKeyId] = useState(""); const [kmsKeyId, setKmsKeyId] = useState("");
const [secretPrefix, setSecretPrefix] = useState("");
// const [path, setPath] = useState(''); // const [path, setPath] = useState('');
// const [pathErrorText, setPathErrorText] = useState(''); // const [pathErrorText, setPathErrorText] = useState('');
@ -166,7 +165,6 @@ export default function AWSSecretManagerCreateIntegrationPage() {
] ]
} }
: {}), : {}),
...(secretPrefix && { secretPrefix }),
...(kmsKeyId && { kmsKeyId }), ...(kmsKeyId && { kmsKeyId }),
mappingBehavior: selectedMappingBehavior mappingBehavior: selectedMappingBehavior
} }
@ -327,7 +325,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
</Switch> </Switch>
</div> </div>
{shouldTag && ( {shouldTag && (
<div className="mt-4 flex justify-between"> <div className="mt-4">
<FormControl label="Tag Key"> <FormControl label="Tag Key">
<Input <Input
placeholder="managed-by" placeholder="managed-by"
@ -344,20 +342,10 @@ export default function AWSSecretManagerCreateIntegrationPage() {
</FormControl> </FormControl>
</div> </div>
)} )}
<FormControl label="Secret Prefix" className="mt-4">
<Input
value={secretPrefix}
onChange={(e) => setSecretPrefix(e.target.value)}
placeholder="INFISICAL_"
/>
</FormControl>
<FormControl label="Encryption Key" className="mt-4"> <FormControl label="Encryption Key" className="mt-4">
<Select <Select
value={kmsKeyId} value={kmsKeyId}
onValueChange={(e) => { onValueChange={(e) => {
if (e === "no-keys") return;
setKmsKeyId(e); setKmsKeyId(e);
}} }}
className="w-full border border-mineshaft-500" className="w-full border border-mineshaft-500"
@ -375,9 +363,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
); );
}) })
) : ( ) : (
<SelectItem isDisabled value="no-keys" key="no-keys"> <div />
No KMS keys available
</SelectItem>
)} )}
</Select> </Select>
</FormControl> </FormControl>

View File

@ -0,0 +1,23 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { IntegrationDetailsPage } from "@app/views/IntegrationsPage/IntegrationDetailsPage";
export default function IntegrationsDetailsPage() {
const { t } = useTranslation();
return (
<>
<Head>
<title>Integration Details | Infisical</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content="Manage your .env files in seconds" />
<meta name="og:description" content={t("integrations.description") as string} />
</Head>
<IntegrationDetailsPage />
</>
);
}
IntegrationsDetailsPage.requireAuth = true;

View File

@ -0,0 +1,120 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useRouter } from "next/router";
import { faChevronLeft, faEllipsis, faRefresh, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { twMerge } from "tailwind-merge";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Tooltip
} from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
useUser,
useWorkspace
} from "@app/context";
import { useGetIntegration } from "@app/hooks/api";
import { useSyncIntegration } from "@app/hooks/api/integrations/queries";
import { IntegrationAuditLogsSection } from "./components/IntegrationAuditLogsSection";
import { IntegrationConnectionSection } from "./components/IntegrationConnectionSection";
import { IntegrationDetailsSection } from "./components/IntegrationDetailsSection";
import { IntegrationSettingsSection } from "./components/IntegrationSettingsSection";
export const IntegrationDetailsPage = () => {
const router = useRouter();
const integrationId = router.query.integrationId as string;
const { data: integration } = useGetIntegration(integrationId, {
refetchInterval: 4000
});
const projectId = useWorkspace().currentWorkspace?.id;
const { mutateAsync: syncIntegration } = useSyncIntegration();
const { currentOrg } = useOrganization();
return integration ? (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl py-6 px-6">
<Button
variant="link"
type="submit"
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
onClick={() => {
router.push(`/integrations/${projectId}`);
}}
className="mb-4"
>
Integrations
</Button>
<div className="mb-4 flex items-center justify-between">
<p className="text-3xl font-semibold text-white">
{integrationSlugNameMapping[integration.integration]} Integration
</p>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuItem
onClick={async () => {
await syncIntegration({
id: integration.id,
lastUsed: integration.lastUsed!,
workspaceId: projectId!
});
}}
>
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faRefresh} />
Manually Sync
</div>
</DropdownMenuItem>
<OrgPermissionCan I={OrgPermissionActions.Delete} a={OrgPermissionSubjects.Member}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => {}}
disabled={!isAllowed}
>
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faTrash} />
Delete Integration
</div>
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="flex justify-center">
<div className="mr-4 w-96">
<IntegrationDetailsSection integration={integration} />
<IntegrationConnectionSection integration={integration} />
</div>
<div className="space-y-4">
<IntegrationSettingsSection integration={integration} />
<IntegrationAuditLogsSection orgId={currentOrg?.id || ""} integration={integration} />
</div>
</div>
</div>
</div>
) : null;
};

View File

@ -0,0 +1,78 @@
import Link from "next/link";
import { EmptyState } from "@app/components/v2";
import { useSubscription } from "@app/context";
import { EventType } from "@app/hooks/api/auditLogs/enums";
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
import { LogsSection } from "@app/views/Project/AuditLogsPage/components";
// Add more events if needed
const INTEGRATION_EVENTS = [EventType.INTEGRATION_SYNCED];
type Props = {
integration: TIntegrationWithEnv;
orgId: string;
};
export const IntegrationAuditLogsSection = ({ integration, orgId }: Props) => {
const { subscription, isLoading } = useSubscription();
const auditLogsRetentionDays = subscription?.auditLogsRetentionDays ?? 30;
// eslint-disable-next-line no-nested-ternary
return subscription?.auditLogs ? (
<div className="h-full w-full min-w-[51rem] rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<p className="text-lg font-semibold text-gray-200">Integration Logs</p>
<p className="text-xs text-gray-400">
Displaying audit logs from the last {auditLogsRetentionDays} days
</p>
</div>
<LogsSection
refetchInterval={4000}
remappedHeaders={{
Metadata: "Sync Status"
}}
showFilters={false}
presets={{
eventMetadata: { integrationId: integration.id },
startDate: new Date(new Date().setDate(new Date().getDate() - auditLogsRetentionDays)),
eventType: INTEGRATION_EVENTS
}}
filterClassName="bg-mineshaft-900 static"
/>
</div>
) : !isLoading ? (
<div className="h-full w-full min-w-[51rem] rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4 opacity-60">
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<p className="text-lg font-semibold text-gray-200">Integration Logs</p>
</div>
<EmptyState
className="rounded-lg"
title={
<div>
<p>
Please{" "}
<Link
href={
subscription && subscription.slug !== null
? `/org${orgId}/billing`
: "https://infisical.com/scheduledemo"
}
passHref
>
<a
className="cursor-pointer font-medium text-primary-500 transition-all hover:text-primary-600"
target="_blank"
>
upgrade your subscription
</a>
</Link>{" "}
to view integration logs
</p>
</div>
}
/>
</div>
) : null;
};

View File

@ -0,0 +1,194 @@
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { FormLabel } from "@app/components/v2";
import { IntegrationMappingBehavior, TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
type Props = {
integration: TIntegrationWithEnv;
};
export const IntegrationConnectionSection = ({ integration }: Props) => {
const specifcQoveryDetails = () => {
if (integration.integration !== "qovery") return null;
return (
<div className="flex flex-row">
<div className="flex flex-col">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Organization" />
<div className="text-sm text-mineshaft-300">{integration?.owner || "-"}</div>
</div>
<div className="flex flex-col">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Project" />
<div className="text-sm text-mineshaft-300">{integration?.targetService || "-"}</div>
</div>
<div className="flex flex-col">
<FormLabel
className="text-sm font-semibold text-mineshaft-300"
label="Target Environment"
/>
<div className="text-sm text-mineshaft-300">{integration?.targetEnvironment || "-"}</div>
</div>
</div>
);
};
const isNotAwsManagerOneToOneDetails = () => {
const isAwsSecretManagerOneToOne =
integration.integration === "aws-secret-manager" &&
integration.metadata?.mappingBehavior === IntegrationMappingBehavior.ONE_TO_ONE;
if (isAwsSecretManagerOneToOne) {
return null;
}
const formLabel = () => {
switch (integration.integration) {
case "qovery":
return integration.scope;
case "circleci":
case "terraform-cloud":
return "Project";
case "aws-secret-manager":
return "Secret";
case "aws-parameter-store":
case "rundeck":
return "Path";
case "github":
if (["github-env", "github-repo"].includes(integration.scope!)) {
return "Repository";
}
return "Organization";
default:
return "App";
}
};
const contents = () => {
switch (integration.integration) {
case "hashicorp-vault":
return `${integration.app} - path: ${integration.path}`;
case "github":
if (integration.scope === "github-org") {
return `${integration.owner}`;
}
return `${integration.owner}/${integration.app}`;
case "aws-parameter-store":
case "rundeck":
return `${integration.path}`;
default:
return `${integration.app}`;
}
};
return (
<div className="flex flex-col">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label={formLabel()} />
<div className="text-sm text-mineshaft-300">{contents()}</div>
</div>
);
};
const targetEnvironmentDetails = () => {
if (
["vercel", "netlify", "railway", "gitlab", "teamcity", "bitbucket"].includes(
integration.integration
) ||
(integration.integration === "github" && integration.scope === "github-env")
) {
return (
<div className="flex flex-col">
<FormLabel
className="text-sm font-semibold text-mineshaft-300"
label="Target Environment"
/>
<div className="text-sm text-mineshaft-300">
{integration.targetEnvironment || integration.targetEnvironmentId}
</div>
</div>
);
}
return null;
};
const generalIntegrationSpecificDetails = () => {
if (integration.integration === "checkly" && integration.targetService) {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Group" />
<div className="text-sm text-mineshaft-300">{integration.targetService}</div>
</div>
);
}
if (integration.integration === "circleci" && integration.owner) {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Organization" />
<div className="text-sm text-mineshaft-300">{integration.owner}</div>
</div>
);
}
if (integration.integration === "terraform-cloud" && integration.targetService) {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Category" />
<div className="text-sm text-mineshaft-300">{integration.targetService}</div>
</div>
);
}
if (integration.integration === "checkly" || integration.integration === "github") {
return (
<div>
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Secret Suffix" />
<div className="text-sm text-mineshaft-300">
{integration?.metadata?.secretSuffix || "-"}
</div>
</div>
);
}
return null;
};
return (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Connection</h3>
</div>
<div className="mt-4">
<FormLabel className="my-2" label="Source" />
<div className="space-y-2 rounded-lg border border-mineshaft-700 bg-mineshaft-800 p-2">
<div className="flex flex-col">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Environment" />
<div className="text-sm text-mineshaft-300">{integration.environment.name}</div>
</div>
<div className="flex flex-col">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Secret Path" />
<div className="text-sm text-mineshaft-300">{integration.secretPath}</div>
</div>
</div>
<FormLabel className="my-2" label="Destination" />
<div className="space-y-2 rounded-lg border border-mineshaft-700 bg-mineshaft-800 p-2">
<FormLabel className="text-sm font-semibold text-mineshaft-300" label="Platform" />
<div className="text-sm text-mineshaft-300">
{integrationSlugNameMapping[integration.integration]}
</div>
{specifcQoveryDetails()}
{isNotAwsManagerOneToOneDetails()}
{targetEnvironmentDetails()}
{generalIntegrationSpecificDetails()}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,69 @@
import { faCalendarCheck, faCheckCircle, faCircleXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { integrationSlugNameMapping } from "public/data/frequentConstants";
import { twMerge } from "tailwind-merge";
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
type Props = {
integration: TIntegrationWithEnv;
};
export const IntegrationDetailsSection = ({ integration }: Props) => {
return (
<div>
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Integration Details</h3>
</div>
<div className="mt-4">
<div className="space-y-3">
<div>
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">
{integrationSlugNameMapping[integration.integration]}
</p>
</div>
<div>
<p className="text-sm font-semibold text-mineshaft-300">Sync Status</p>
<div className="flex items-center">
<p
className={twMerge(
"mr-2 text-sm font-medium",
integration.isSynced ? "text-green-500" : "text-red-500"
)}
>
{integration.isSynced ? "Synced" : "Not Synced"}
</p>
<FontAwesomeIcon
size="sm"
className={twMerge(integration.isSynced ? "text-green-500" : "text-red-500")}
icon={integration.isSynced ? faCheckCircle : faCircleXmark}
/>
</div>
</div>
{integration.lastUsed && (
<div>
<p className="text-sm font-semibold text-mineshaft-300">Latest Successful Sync</p>
<div className="flex items-center gap-2 text-sm text-mineshaft-300">
{format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")}
<FontAwesomeIcon icon={faCalendarCheck} className="pt-0.5 pr-2 text-sm" />
</div>
</div>
)}
<div>
{!integration.isSynced && integration.syncMessage && (
<>
<p className="text-sm font-semibold text-mineshaft-300">Latest Sync Error</p>
<p className="text-sm text-mineshaft-300">{integration.syncMessage}</p>
</>
)}
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,89 @@
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
type Props = {
integration: TIntegrationWithEnv;
};
type Metadata = NonNullable<TIntegrationWithEnv["metadata"]>;
type MetadataKey = keyof Metadata;
type MetadataValue<K extends MetadataKey> = Metadata[K];
const metadataMappings: Record<keyof NonNullable<TIntegrationWithEnv["metadata"]>, string> = {
githubVisibility: "Github Visibility",
githubVisibilityRepoIds: "Github Visibility Repo Ids",
shouldAutoRedeploy: "Auto Redeploy Target Application When Secrets Change",
secretAWSTag: "Tags For Secrets Stored In AWS",
kmsKeyId: "AWS KMS Key ID",
secretSuffix: "Secret Suffix",
secretPrefix: "Secret Prefix",
syncBehavior: "Secrets Sync behavior",
mappingBehavior: "Secrets Mapping Behavior",
scope: "Scope",
org: "Organization",
project: "Project",
environment: "Environment",
shouldDisableDelete: "AWS Secret Deletion Disabled",
shouldMaskSecrets: "GitLab Secrets Masking Enabled",
shouldProtectSecrets: "GitLab Secret Protection Enabled",
shouldEnableDelete: "GitHub Secret Deletion Enabled"
} as const;
export const IntegrationSettingsSection = ({ integration }: Props) => {
const renderValue = <K extends MetadataKey>(key: K, value: MetadataValue<K>) => {
if (!value) return null;
// If it's a boolean, we render a generic "Yes" or "No" response.
if (typeof value === "boolean") {
return value ? "Yes" : "No";
}
// When the value is an object or array, or array of objects, we need to handle some special cases.
if (typeof value === "object") {
if (key === "secretAWSTag") {
return (value as MetadataValue<"secretAWSTag">)!.map(({ key: tagKey, value: tagValue }) => (
<p key={tagKey} className="text-sm text-gray-200">
{tagKey}={tagValue}
</p>
));
}
if (key === "githubVisibilityRepoIds") {
return value.join(", ");
}
}
if (typeof value === "string") {
return value.length ? value : "N/A";
}
if (typeof value === "number") {
return value;
}
return null;
};
if (!integration.metadata || Object.keys(integration.metadata).length === 0) {
return null;
}
// eslint-disable-next-line no-nested-ternary
return (
<div className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<p className="text-lg font-semibold text-gray-200">Integration Settings</p>
</div>
<div className="grid grid-cols-2 gap-4">
{integration.metadata &&
Object.entries(integration.metadata).map(([key, value]) => (
<div key={key} className="flex flex-col">
<p className="text-sm text-gray-400">
{metadataMappings[key as keyof typeof metadataMappings]}
</p>
<p className="text-sm text-gray-200">{renderValue(key as MetadataKey, value)}</p>
</div>
))}
</div>
</div>
);
};

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