mirror of
https://github.com/Infisical/infisical.git
synced 2025-09-06 06:00:42 +00:00
Compare commits
99 Commits
daniel/inv
...
v0.85.0-po
Author | SHA1 | Date | |
---|---|---|---|
|
d6b7045461 | ||
|
bd9c9ea1f4 | ||
|
5740d2b4e4 | ||
|
09887a7405 | ||
|
38ee3a005e | ||
|
10e7999334 | ||
|
8c458588ab | ||
|
2381a2e4ba | ||
|
9ef8812205 | ||
|
37a204e49e | ||
|
11927f341a | ||
|
6fc17a4964 | ||
|
eb00232db6 | ||
|
4fd245e493 | ||
|
d92c57d051 | ||
|
beaef1feb0 | ||
|
033fd5e7a4 | ||
|
f49f3c926c | ||
|
280d44f1e5 | ||
|
4eea0dc544 | ||
|
8a33f1a591 | ||
|
74653e7ed1 | ||
|
56ff11d63f | ||
|
1ecce285f0 | ||
|
b5c9b6a1bd | ||
|
e12ac6c07e | ||
|
8a0b1bb427 | ||
|
1f6faadf81 | ||
|
8f3b7e1698 | ||
|
24c460c695 | ||
|
8acceab1e7 | ||
|
d60aba9339 | ||
|
3a228f7521 | ||
|
3f7ac0f142 | ||
|
63cf535ebb | ||
|
69a2a46c47 | ||
|
d081077273 | ||
|
75034f9350 | ||
|
eacd7b0c6a | ||
|
5bad77083c | ||
|
ea480c222b | ||
|
1fb644af4a | ||
|
a6f4a95821 | ||
|
8578208f2d | ||
|
fc4189ba0f | ||
|
b9ecf42fb6 | ||
|
008e18638f | ||
|
ac3b9c25dd | ||
|
f4997dec12 | ||
|
fcf405c630 | ||
|
efc6876260 | ||
|
1025759efb | ||
|
8bab6d87bb | ||
|
39a49f12f5 | ||
|
cfd841ea08 | ||
|
4d67c03e3e | ||
|
8826bc5d60 | ||
|
03fdce67f1 | ||
|
72f3f7980e | ||
|
f1aa2fbd84 | ||
|
217de6250f | ||
|
f742bd01d9 | ||
|
3fe53d5183 | ||
|
a5f5f803df | ||
|
c37e3ba635 | ||
|
55279e5e41 | ||
|
88fb37e8c6 | ||
|
6271dcc25d | ||
|
0f7faa6bfe | ||
|
4ace339d5b | ||
|
e8c0d1ece9 | ||
|
bb1977976c | ||
|
bb3da75870 | ||
|
088e888560 | ||
|
180241fdf0 | ||
|
93f27a7ee8 | ||
|
ed3bc8dd27 | ||
|
8dc4809ec8 | ||
|
a55d64e430 | ||
|
02d54da74a | ||
|
d660168700 | ||
|
1c75fc84f0 | ||
|
f63da87c7f | ||
|
53b9fe2dec | ||
|
87dc0eed7e | ||
|
f2dd6f94a4 | ||
|
ac26ae3893 | ||
|
4c65e9910a | ||
|
5e5ab29ab9 | ||
|
5150c102e6 | ||
|
a79087670e | ||
|
ce9b66ef14 | ||
|
bfa533e9d2 | ||
|
a8759e7410 | ||
|
16182a9d1d | ||
|
c1f61f2db4 | ||
|
4e6b289e1b | ||
|
6fab7d9507 | ||
|
1c749c84f2 |
@@ -72,6 +72,3 @@ PLAIN_API_KEY=
|
||||
PLAIN_WISH_LABEL_IDS=
|
||||
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY=
|
||||
|
||||
WORKFLOW_SLACK_CLIENT_ID=
|
||||
WORKFLOW_SLACK_CLIENT_SECRET=
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -63,6 +63,7 @@ yarn-error.log*
|
||||
|
||||
# Editor specific
|
||||
.vscode/*
|
||||
.idea/*
|
||||
|
||||
frontend-build
|
||||
|
||||
|
@@ -0,0 +1,85 @@
|
||||
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");
|
||||
}
|
||||
});
|
||||
}
|
@@ -16,7 +16,9 @@ export const CertificateTemplatesSchema = z.object({
|
||||
subjectAlternativeName: z.string(),
|
||||
ttl: z.string(),
|
||||
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>;
|
||||
|
@@ -22,7 +22,9 @@ export const CertificatesSchema = z.object({
|
||||
revocationReason: z.number().nullable().optional(),
|
||||
altNames: z.string().default("").nullable().optional(),
|
||||
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>;
|
||||
|
@@ -11,6 +11,30 @@ export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
|
||||
config: {
|
||||
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: {
|
||||
description: "Get CRL in DER format",
|
||||
params: z.object({
|
||||
|
@@ -101,6 +101,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
message: "Slug must be a valid"
|
||||
}),
|
||||
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()
|
||||
}),
|
||||
response: {
|
||||
|
@@ -146,12 +146,16 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId: req.params.workspaceId,
|
||||
...req.query,
|
||||
endDate: req.query.endDate,
|
||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||
auditLogActor: req.query.actor,
|
||||
actor: req.permission.type
|
||||
actor: req.permission.type,
|
||||
|
||||
filter: {
|
||||
...req.query,
|
||||
projectId: req.params.workspaceId,
|
||||
endDate: req.query.endDate,
|
||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||
auditLogActorId: req.query.actor,
|
||||
eventType: req.query.eventType ? [req.query.eventType] : undefined
|
||||
}
|
||||
});
|
||||
return { auditLogs };
|
||||
}
|
||||
|
@@ -100,9 +100,20 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
async (req, profile, cb) => {
|
||||
try {
|
||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||
const email = profile?.email ?? (profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved
|
||||
const email =
|
||||
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\
|
||||
|
||||
if (!email || !profile.firstName) {
|
||||
const firstName = (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(
|
||||
{
|
||||
err: new Error("Invalid saml request. Missing email or first name"),
|
||||
@@ -110,14 +121,13 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
`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({
|
||||
externalId: profile.nameID,
|
||||
email,
|
||||
firstName: profile.firstName as string,
|
||||
lastName: profile.lastName as string,
|
||||
firstName,
|
||||
lastName: lastName as string,
|
||||
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
||||
|
@@ -6,6 +6,9 @@ import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, stripUndefinedInWhere } from "@app/lib/knex";
|
||||
import { logger } from "@app/lib/logger";
|
||||
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>;
|
||||
|
||||
@@ -25,7 +28,24 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
const auditLogOrm = ormify(db, TableName.AuditLog);
|
||||
|
||||
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
|
||||
) => {
|
||||
try {
|
||||
@@ -34,7 +54,6 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
stripUndefinedInWhere({
|
||||
projectId,
|
||||
[`${TableName.AuditLog}.orgId`]: orgId,
|
||||
eventType,
|
||||
userAgentType
|
||||
})
|
||||
)
|
||||
@@ -52,8 +71,22 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
.offset(offset)
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc");
|
||||
|
||||
if (actor) {
|
||||
void sqlQuery.whereRaw(`"actorMetadata"->>'userId' = ?`, [actor]);
|
||||
if (actorId) {
|
||||
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) {
|
||||
|
@@ -23,25 +23,12 @@ export const auditLogServiceFactory = ({
|
||||
auditLogQueue,
|
||||
permissionService
|
||||
}: TAuditLogServiceFactoryDep) => {
|
||||
const listAuditLogs = async ({
|
||||
userAgentType,
|
||||
eventType,
|
||||
offset,
|
||||
limit,
|
||||
endDate,
|
||||
startDate,
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
projectId,
|
||||
auditLogActor
|
||||
}: TListProjectAuditLogDTO) => {
|
||||
if (projectId) {
|
||||
const listAuditLogs = async ({ actorAuthMethod, actorId, actorOrgId, actor, filter }: TListProjectAuditLogDTO) => {
|
||||
if (filter.projectId) {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
filter.projectId,
|
||||
actorAuthMethod,
|
||||
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.
|
||||
|
||||
const auditLogs = await auditLogDAL.find({
|
||||
startDate,
|
||||
endDate,
|
||||
limit,
|
||||
offset,
|
||||
eventType,
|
||||
userAgentType,
|
||||
actor: auditLogActor,
|
||||
...(projectId ? { projectId } : { orgId: actorOrgId })
|
||||
startDate: filter.startDate,
|
||||
endDate: filter.endDate,
|
||||
limit: filter.limit,
|
||||
offset: filter.offset,
|
||||
eventType: filter.eventType,
|
||||
userAgentType: filter.userAgentType,
|
||||
actorId: filter.auditLogActorId,
|
||||
actorType: filter.actorType,
|
||||
eventMetadata: filter.eventMetadata,
|
||||
...(filter.projectId ? { projectId: filter.projectId } : { orgId: actorOrgId })
|
||||
});
|
||||
|
||||
return auditLogs.map(({ eventType: logEventType, actor: eActor, actorMetadata, eventMetadata, ...el }) => ({
|
||||
|
@@ -5,19 +5,23 @@ import { TIdentityTrustedIp } from "@app/services/identity/identity-types";
|
||||
import { PkiItemType } from "@app/services/pki-collection/pki-collection-types";
|
||||
|
||||
export type TListProjectAuditLogDTO = {
|
||||
auditLogActor?: string;
|
||||
projectId?: string;
|
||||
eventType?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
userAgentType?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
filter: {
|
||||
userAgentType?: UserAgentType;
|
||||
eventType?: EventType[];
|
||||
offset?: number;
|
||||
limit: number;
|
||||
endDate?: string;
|
||||
startDate?: string;
|
||||
projectId?: string;
|
||||
auditLogActorId?: string;
|
||||
actorType?: ActorType;
|
||||
eventMetadata?: Record<string, string>;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TCreateAuditLogDTO = {
|
||||
event: Event;
|
||||
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor;
|
||||
actor: UserActor | IdentityActor | ServiceActor | ScimClientActor | PlatformActor;
|
||||
orgId?: string;
|
||||
projectId?: string;
|
||||
} & BaseAuthData;
|
||||
@@ -177,7 +181,8 @@ export enum EventType {
|
||||
UPDATE_SLACK_INTEGRATION = "update-slack-integration",
|
||||
DELETE_SLACK_INTEGRATION = "delete-slack-integration",
|
||||
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 {
|
||||
@@ -198,6 +203,8 @@ interface IdentityActorMetadata {
|
||||
|
||||
interface ScimClientActorMetadata {}
|
||||
|
||||
interface PlatformActorMetadata {}
|
||||
|
||||
export interface UserActor {
|
||||
type: ActorType.USER;
|
||||
metadata: UserActorMetadata;
|
||||
@@ -208,6 +215,11 @@ export interface ServiceActor {
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export interface PlatformActor {
|
||||
type: ActorType.PLATFORM;
|
||||
metadata: PlatformActorMetadata;
|
||||
}
|
||||
|
||||
export interface IdentityActor {
|
||||
type: ActorType.IDENTITY;
|
||||
metadata: IdentityActorMetadata;
|
||||
@@ -218,7 +230,7 @@ export interface ScimClientActor {
|
||||
metadata: ScimClientActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor;
|
||||
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
@@ -1518,6 +1530,16 @@ interface GetProjectSlackConfig {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
interface IntegrationSyncedEvent {
|
||||
type: EventType.INTEGRATION_SYNCED;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
lastSyncJobId: string;
|
||||
lastUsed: Date;
|
||||
syncMessage: string;
|
||||
isSynced: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
@@ -1657,4 +1679,5 @@ export type Event =
|
||||
| DeleteSlackIntegration
|
||||
| GetSlackIntegration
|
||||
| UpdateProjectSlackConfig
|
||||
| GetProjectSlackConfig;
|
||||
| GetProjectSlackConfig
|
||||
| IntegrationSyncedEvent;
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { AbilityBuilder, createMongoAbility, ForcedSubject, MongoAbility } from "@casl/ability";
|
||||
|
||||
import { conditionsMatcher } from "@app/lib/casl";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
export enum ProjectPermissionActions {
|
||||
Read = "read",
|
||||
@@ -75,117 +76,125 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback]
|
||||
| [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 { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
|
||||
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);
|
||||
// Admins get full access to everything
|
||||
fullProjectPermissionSet.forEach((permission) => {
|
||||
const [action, subject] = permission;
|
||||
can(action, subject);
|
||||
});
|
||||
|
||||
return rules;
|
||||
};
|
||||
@@ -372,4 +381,31 @@ export const isAtLeastAsPrivilegedWorkspace = (
|
||||
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 */
|
||||
|
@@ -363,7 +363,12 @@ export const ORGANIZATIONS = {
|
||||
membershipId: "The ID of the membership to delete."
|
||||
},
|
||||
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: {
|
||||
organizationId: "The ID of the organization to get projects from."
|
||||
@@ -472,7 +477,12 @@ export const PROJECT_USERS = {
|
||||
|
||||
export const PROJECT_IDENTITIES = {
|
||||
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: {
|
||||
identityId: "The ID of the identity to get the membership for.",
|
||||
@@ -1073,6 +1083,10 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
certificateChain: "The certificate chain of the CA",
|
||||
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: {
|
||||
caId: "The ID of the CA to get the CA certificates for",
|
||||
certificate: "The certificate body of the CA certificate",
|
||||
@@ -1112,11 +1126,15 @@ export const CERTIFICATE_AUTHORITIES = {
|
||||
issuingCaCertificate: "The certificate of the issuing CA",
|
||||
certificateChain: "The certificate chain 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: {
|
||||
caId: "The ID of the CA to issue the certificate from",
|
||||
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",
|
||||
friendlyName: "A friendly name for the certificate",
|
||||
commonName: "The common name (CN) for the certificate",
|
||||
@@ -1166,7 +1184,10 @@ export const CERTIFICATE_TEMPLATES = {
|
||||
name: "The name of the template",
|
||||
commonName: "The regular expression string to use for validating common 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: {
|
||||
certificateTemplateId: "The ID of the certificate template to get"
|
||||
@@ -1178,7 +1199,11 @@ export const CERTIFICATE_TEMPLATES = {
|
||||
name: "The updated name of the template",
|
||||
commonName: "The updated regular expression string for validating common 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: {
|
||||
certificateTemplateId: "The ID of the certificate template to delete"
|
||||
|
@@ -147,8 +147,8 @@ const envSchema = z
|
||||
PLAIN_WISH_LABEL_IDS: zpStr(z.string().optional()),
|
||||
DISABLE_AUDIT_LOG_GENERATION: zodStrBool.default("false"),
|
||||
SSL_CLIENT_CERTIFICATE_HEADER_KEY: zpStr(z.string().optional()).default("x-ssl-client-cert"),
|
||||
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string()).optional(),
|
||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string()).optional()
|
||||
WORKFLOW_SLACK_CLIENT_ID: zpStr(z.string().optional()),
|
||||
WORKFLOW_SLACK_CLIENT_SECRET: zpStr(z.string().optional())
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
|
@@ -52,3 +52,8 @@ export enum SecretSharingAccessType {
|
||||
Anyone = "anyone",
|
||||
Organization = "organization"
|
||||
}
|
||||
|
||||
export enum OrderByDirection {
|
||||
ASC = "asc",
|
||||
DESC = "desc"
|
||||
}
|
||||
|
@@ -91,6 +91,8 @@ export type TQueueJobTypes = {
|
||||
[QueueName.IntegrationSync]: {
|
||||
name: QueueJobs.IntegrationSync;
|
||||
payload: {
|
||||
isManual?: boolean;
|
||||
actorId?: string;
|
||||
projectId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
|
@@ -493,7 +493,6 @@ export const registerRoutes = async (
|
||||
orgRoleDAL,
|
||||
permissionService,
|
||||
orgDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
incidentContactDAL,
|
||||
tokenService,
|
||||
@@ -811,6 +810,8 @@ export const registerRoutes = async (
|
||||
projectEnvDAL,
|
||||
webhookDAL,
|
||||
orgDAL,
|
||||
auditLogService,
|
||||
userDAL,
|
||||
projectMembershipDAL,
|
||||
smtpService,
|
||||
projectDAL,
|
||||
|
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -7,7 +8,7 @@ import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
|
||||
import {
|
||||
validateAltNamesField,
|
||||
@@ -139,6 +140,33 @@ 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({
|
||||
method: "PATCH",
|
||||
url: "/:caId",
|
||||
@@ -573,7 +601,9 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
||||
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(
|
||||
(data) => {
|
||||
@@ -653,7 +683,9 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
||||
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(
|
||||
(data) => {
|
||||
|
@@ -7,7 +7,7 @@ import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CrlReason } from "@app/services/certificate/certificate-types";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
validateAltNamesField,
|
||||
validateCaDateField
|
||||
@@ -86,7 +86,17 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
||||
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(
|
||||
(data) => {
|
||||
@@ -177,7 +187,17 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
||||
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(
|
||||
(data) => {
|
||||
|
@@ -7,6 +7,7 @@ import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||
|
||||
@@ -74,7 +75,19 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
ttl: z
|
||||
.string()
|
||||
.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: {
|
||||
200: sanitizedCertificateTemplate
|
||||
@@ -130,7 +143,13 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
||||
.string()
|
||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||
.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({
|
||||
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
|
||||
|
@@ -246,12 +246,13 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
description: true
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||
}).array()
|
||||
}).array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identities = await server.services.identity.listOrgIdentities({
|
||||
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -259,7 +260,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
||||
orgId: req.query.orgId
|
||||
});
|
||||
|
||||
return { identities };
|
||||
return { identities: identityMemberships, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -4,7 +4,7 @@ import { IntegrationsSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { INTEGRATION } from "@app/lib/api-docs";
|
||||
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 { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
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({
|
||||
method: "DELETE",
|
||||
url: "/:integrationId",
|
||||
|
@@ -14,7 +14,7 @@ import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
||||
import { getLastMidnightDateISO } from "@app/lib/fn";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -74,8 +74,35 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
description: "Get all audit logs for an organization",
|
||||
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),
|
||||
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),
|
||||
endDate: z.string().datetime().optional().describe(AUDIT_LOGS.EXPORT.endDate),
|
||||
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]),
|
||||
handler: async (req) => {
|
||||
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,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
...req.query,
|
||||
endDate: req.query.endDate,
|
||||
startDate: req.query.startDate || getLastMidnightDateISO(),
|
||||
auditLogActor: req.query.actor,
|
||||
actor: req.permission.type
|
||||
});
|
||||
return { auditLogs };
|
||||
|
@@ -2,9 +2,11 @@ import { z } from "zod";
|
||||
|
||||
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { OrgIdentityOrderBy } from "@app/services/identity/identity-types";
|
||||
|
||||
export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -24,6 +26,27 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
||||
params: z.object({
|
||||
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: {
|
||||
200: z.object({
|
||||
identityMemberships: IdentityOrgMembershipsSchema.merge(
|
||||
@@ -37,20 +60,26 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
||||
}).optional(),
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||
})
|
||||
).array()
|
||||
).array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityMemberships = await server.services.identity.listOrgIdentities({
|
||||
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
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 };
|
||||
return { identityMemberships, totalCount };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -7,11 +7,13 @@ import {
|
||||
ProjectMembershipRole,
|
||||
ProjectUserMembershipRolesSchema
|
||||
} from "@app/db/schemas";
|
||||
import { PROJECT_IDENTITIES } from "@app/lib/api-docs";
|
||||
import { ORGANIZATIONS, PROJECT_IDENTITIES } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-project-types";
|
||||
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
|
||||
|
||||
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||
@@ -214,6 +216,32 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
||||
params: z.object({
|
||||
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: {
|
||||
200: z.object({
|
||||
identityMemberships: z
|
||||
@@ -239,19 +267,25 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
|
||||
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
||||
})
|
||||
.array()
|
||||
.array(),
|
||||
totalCount: z.number()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const identityMemberships = await server.services.identityProject.listProjectIdentities({
|
||||
const { identityMemberships, totalCount } = await server.services.identityProject.listProjectIdentities({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
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 };
|
||||
return { identityMemberships, totalCount };
|
||||
}
|
||||
});
|
||||
|
||||
|
@@ -34,6 +34,7 @@ export enum AuthMode {
|
||||
}
|
||||
|
||||
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
|
||||
SERVICE = "service",
|
||||
IDENTITY = "identity",
|
||||
|
@@ -15,7 +15,7 @@ import {
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
export const createSerialNumber = () => {
|
||||
const randomBytes = crypto.randomBytes(32);
|
||||
const randomBytes = crypto.randomBytes(20);
|
||||
randomBytes[0] &= 0x7f; // ensure the first bit is 0
|
||||
return randomBytes.toString("hex");
|
||||
};
|
||||
|
@@ -19,7 +19,13 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { CertKeyAlgorithm, CertStatus } from "../certificate/certificate-types";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertExtendedKeyUsageOIDToName,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyUsage,
|
||||
CertStatus
|
||||
} from "../certificate/certificate-types";
|
||||
import { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
|
||||
import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||
@@ -762,6 +768,39 @@ 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
|
||||
*/
|
||||
@@ -776,6 +815,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
notAfter,
|
||||
maxPathLength
|
||||
}: TSignIntermediateDTO) => {
|
||||
const appCfg = getConfig();
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||
|
||||
@@ -850,7 +890,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||
}
|
||||
|
||||
const { caPrivateKey } = await getCaCredentials({
|
||||
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
@@ -859,6 +899,11 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
|
||||
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({
|
||||
serialNumber,
|
||||
subject: csrObj.subject,
|
||||
@@ -878,7 +923,11 @@ export const certificateAuthorityServiceFactory = ({
|
||||
),
|
||||
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
|
||||
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)
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
@@ -1052,7 +1101,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
}: TIssueCertFromCaDTO) => {
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
@@ -1168,16 +1219,70 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||
const appCfg = getConfig();
|
||||
|
||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.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[] = [
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||
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: {
|
||||
type: "email" | "dns";
|
||||
value: string;
|
||||
@@ -1259,7 +1364,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
altNames,
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: selectedExtendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -1308,6 +1415,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
* Note: CSR is generated externally and submitted to Infisical.
|
||||
*/
|
||||
const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
|
||||
const appCfg = getConfig();
|
||||
let ca: TCertificateAuthorities | undefined;
|
||||
let certificateTemplate: TCertificateTemplates | undefined;
|
||||
|
||||
@@ -1321,7 +1429,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
altNames,
|
||||
ttl,
|
||||
notBefore,
|
||||
notAfter
|
||||
notAfter,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
} = dto;
|
||||
|
||||
let collectionId = pkiCollectionId;
|
||||
@@ -1432,7 +1542,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
message: "A common name (CN) is required in the CSR or as a parameter to this endpoint"
|
||||
});
|
||||
|
||||
const { caPrivateKey } = await getCaCredentials({
|
||||
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
@@ -1440,13 +1550,115 @@ export const certificateAuthorityServiceFactory = ({
|
||||
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[] = [
|
||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
||||
new x509.BasicConstraintsExtension(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 altNamesArray: {
|
||||
type: "email" | "dns";
|
||||
@@ -1542,7 +1754,9 @@ export const certificateAuthorityServiceFactory = ({
|
||||
altNames: altNamesFromCsr || altNames,
|
||||
serialNumber,
|
||||
notBefore: notBeforeDate,
|
||||
notAfter: notAfterDate
|
||||
notAfter: notAfterDate,
|
||||
keyUsages: selectedKeyUsages,
|
||||
extendedKeyUsages: selectedExtendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -1628,6 +1842,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
renewCaCert,
|
||||
getCaCerts,
|
||||
getCaCert,
|
||||
getCaCertById,
|
||||
signIntermediate,
|
||||
importCertToCa,
|
||||
issueCertFromCa,
|
||||
|
@@ -4,7 +4,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||
import { CertKeyAlgorithm } from "../certificate/certificate-types";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types";
|
||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||
@@ -97,6 +97,8 @@ export type TIssueCertFromCaDTO = {
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TSignCertFromCaDTO =
|
||||
@@ -112,6 +114,8 @@ export type TSignCertFromCaDTO =
|
||||
ttl?: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
}
|
||||
| ({
|
||||
isInternal: false;
|
||||
@@ -125,6 +129,8 @@ export type TSignCertFromCaDTO =
|
||||
ttl: string;
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">);
|
||||
|
||||
export type TGetCaCertificateTemplatesDTO = {
|
||||
|
@@ -9,7 +9,9 @@ export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
|
||||
commonName: true,
|
||||
subjectAlternativeName: true,
|
||||
pkiCollectionId: true,
|
||||
ttl: true
|
||||
ttl: true,
|
||||
keyUsages: true,
|
||||
extendedKeyUsages: true
|
||||
}).merge(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
|
@@ -57,7 +57,9 @@ export const certificateTemplateServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
}: TCreateCertTemplateDTO) => {
|
||||
const ca = await certificateAuthorityDAL.findById(caId);
|
||||
if (!ca) {
|
||||
@@ -86,7 +88,9 @@ export const certificateTemplateServiceFactory = ({
|
||||
name,
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
ttl
|
||||
ttl,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -113,7 +117,9 @@ export const certificateTemplateServiceFactory = ({
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
}: TUpdateCertTemplateDTO) => {
|
||||
const certTemplate = await certificateTemplateDAL.getById(id);
|
||||
if (!certTemplate) {
|
||||
@@ -153,7 +159,9 @@ export const certificateTemplateServiceFactory = ({
|
||||
commonName,
|
||||
subjectAlternativeName,
|
||||
name,
|
||||
ttl
|
||||
ttl,
|
||||
keyUsages,
|
||||
extendedKeyUsages
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
|
||||
export type TCreateCertTemplateDTO = {
|
||||
caId: string;
|
||||
@@ -7,6 +8,8 @@ export type TCreateCertTemplateDTO = {
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateCertTemplateDTO = {
|
||||
@@ -17,6 +20,8 @@ export type TUpdateCertTemplateDTO = {
|
||||
commonName?: string;
|
||||
subjectAlternativeName?: string;
|
||||
ttl?: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetCertTemplateDTO = {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export enum CertStatus {
|
||||
@@ -12,6 +14,36 @@ export enum CertKeyAlgorithm {
|
||||
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 {
|
||||
UNSPECIFIED = "UNSPECIFIED",
|
||||
KEY_COMPROMISE = "KEY_COMPROMISE",
|
||||
|
@@ -152,7 +152,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
`${TableName.ProjectRoles}.id`
|
||||
)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.GroupProjectMembership),
|
||||
db.ref("id").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("isGhost").withSchema(TableName.Users),
|
||||
db.ref("username").withSchema(TableName.Users),
|
||||
db.ref("email").withSchema(TableName.Users),
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TableName, TIdentities } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { ormify, selectAllTableCols, 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>;
|
||||
|
||||
@@ -107,12 +109,45 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findByProjectId = async (projectId: string, filter: { identityId?: string } = {}, tx?: Knex) => {
|
||||
const findByProjectId = async (
|
||||
projectId: string,
|
||||
filter: { identityId?: string } & Pick<
|
||||
TListProjectIdentityDTO,
|
||||
"limit" | "offset" | "search" | "orderBy" | "orderDirection"
|
||||
> = {},
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.IdentityProjectMembership)
|
||||
// TODO: scott - optimize, there's redundancy here with project membership and the below query
|
||||
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)
|
||||
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.join(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
|
||||
.join<TIdentities, TIdentities>(fetchIdentitySubquery, (bd) => {
|
||||
bd.on(`${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`);
|
||||
})
|
||||
.where((qb) => {
|
||||
if (filter.identityId) {
|
||||
void qb.where("identityId", filter.identityId);
|
||||
@@ -154,6 +189,19 @@ export const identityProjectDALFactory = (db: TDbClient) => {
|
||||
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({
|
||||
data: docs,
|
||||
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({
|
||||
@@ -208,9 +256,37 @@ 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 {
|
||||
...identityProjectOrm,
|
||||
findByIdentityId,
|
||||
findByProjectId
|
||||
findByProjectId,
|
||||
getCountByProjectId
|
||||
};
|
||||
};
|
||||
|
@@ -268,7 +268,12 @@ export const identityProjectServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
actorOrgId,
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search
|
||||
}: TListProjectIdentityDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
@@ -279,8 +284,17 @@ export const identityProjectServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
||||
|
||||
const identityMemberships = await identityProjectDAL.findByProjectId(projectId);
|
||||
return identityMemberships;
|
||||
const identityMemberships = await identityProjectDAL.findByProjectId(projectId, {
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search
|
||||
});
|
||||
|
||||
const totalCount = await identityProjectDAL.getCountByProjectId(projectId, { search });
|
||||
|
||||
return { identityMemberships, totalCount };
|
||||
};
|
||||
|
||||
const getProjectIdentityByIdentityId = async ({
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||
|
||||
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||
|
||||
@@ -40,8 +40,18 @@ export type TDeleteProjectIdentityDTO = {
|
||||
identityId: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TListProjectIdentityDTO = TProjectPermission;
|
||||
export type TListProjectIdentityDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: ProjectIdentityOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetProjectIdentityByIdentityIdDTO = {
|
||||
identityId: string;
|
||||
} & TProjectPermission;
|
||||
|
||||
export enum ProjectIdentityOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
|
||||
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
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>;
|
||||
|
||||
@@ -27,9 +29,20 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const find = async (filter: Partial<TIdentityOrgMemberships>, tx?: Knex) => {
|
||||
const find = async (
|
||||
{
|
||||
limit,
|
||||
offset = 0,
|
||||
orderBy,
|
||||
orderDirection = OrderByDirection.ASC,
|
||||
search,
|
||||
...filter
|
||||
}: Partial<TIdentityOrgMemberships> &
|
||||
Pick<TListOrgIdentitiesByOrgIdDTO, "offset" | "limit" | "orderBy" | "orderDirection" | "search">,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const docs = await (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||
.where(filter)
|
||||
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
||||
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||
@@ -44,6 +57,30 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
.select(db.ref("id").as("identityId").withSchema(TableName.Identity))
|
||||
.select(db.ref("name").as("identityName").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(
|
||||
({
|
||||
crId,
|
||||
@@ -79,5 +116,27 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
return { ...identityOrgOrm, find, findOne };
|
||||
const countAllOrgIdentities = async (
|
||||
{ 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 };
|
||||
};
|
||||
|
@@ -6,7 +6,6 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||
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 { ActorType } from "../auth/auth-type";
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
TCreateIdentityDTO,
|
||||
TDeleteIdentityDTO,
|
||||
TGetIdentityByIdDTO,
|
||||
TListOrgIdentitiesByOrgIdDTO,
|
||||
TListProjectIdentitiesByIdentityIdDTO,
|
||||
TUpdateIdentityDTO
|
||||
} from "./identity-types";
|
||||
@@ -58,7 +58,8 @@ export const identityServiceFactory = ({
|
||||
if (!hasRequiredPriviledges) throw new BadRequestError({ message: "Failed to create a more privileged identity" });
|
||||
|
||||
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
|
||||
throw new BadRequestError({
|
||||
message: "Failed to create identity due to identity limit reached. Upgrade plan to create more identities."
|
||||
@@ -195,14 +196,36 @@ export const identityServiceFactory = ({
|
||||
return { ...deletedIdentity, orgId: identityOrgMembership.orgId };
|
||||
};
|
||||
|
||||
const listOrgIdentities = async ({ orgId, actor, actorId, actorAuthMethod, actorOrgId }: TOrgPermission) => {
|
||||
const listOrgIdentities = async ({
|
||||
orgId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
limit,
|
||||
offset,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search
|
||||
}: TListOrgIdentitiesByOrgIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
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 ({
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { IPType } from "@app/lib/ip";
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateIdentityDTO = {
|
||||
role: string;
|
||||
@@ -29,3 +29,16 @@ export interface TIdentityTrustedIp {
|
||||
export type TListProjectIdentitiesByIdentityIdDTO = {
|
||||
identityId: string;
|
||||
} & Omit<TOrgPermission, "orgId">;
|
||||
|
||||
export type TListOrgIdentitiesByOrgIdDTO = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
orderBy?: OrgIdentityOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export enum OrgIdentityOrderBy {
|
||||
Name = "name",
|
||||
Role = "role"
|
||||
}
|
||||
|
@@ -242,37 +242,12 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
|
||||
};
|
||||
}
|
||||
|
||||
const octokit = new Octokit({
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
||||
const repos = (await new Octokit({
|
||||
auth: accessToken
|
||||
});
|
||||
|
||||
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();
|
||||
}).paginate("GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}", {
|
||||
per_page: 100
|
||||
})) as GitHubApp[];
|
||||
|
||||
const apps = repos
|
||||
.filter((a: GitHubApp) => a.permissions.admin === true)
|
||||
|
@@ -207,6 +207,12 @@ 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(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
|
||||
{
|
||||
@@ -237,6 +243,12 @@ const syncSecretsGCPSecretManager = async ({
|
||||
}
|
||||
);
|
||||
} 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(
|
||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
|
||||
{
|
||||
|
@@ -2,7 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
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 { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||
@@ -19,6 +19,7 @@ import { TIntegrationDALFactory } from "./integration-dal";
|
||||
import {
|
||||
TCreateIntegrationDTO,
|
||||
TDeleteIntegrationDTO,
|
||||
TGetIntegrationDTO,
|
||||
TSyncIntegrationDTO,
|
||||
TUpdateIntegrationDTO
|
||||
} from "./integration-types";
|
||||
@@ -180,6 +181,27 @@ export const integrationServiceFactory = ({
|
||||
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 ({
|
||||
actorId,
|
||||
id,
|
||||
@@ -276,6 +298,8 @@ export const integrationServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
|
||||
await secretQueueService.syncIntegrations({
|
||||
isManual: true,
|
||||
actorId,
|
||||
environment: integration.environment.slug,
|
||||
secretPath: integration.secretPath,
|
||||
projectId: integration.projectId
|
||||
@@ -289,6 +313,7 @@ export const integrationServiceFactory = ({
|
||||
updateIntegration,
|
||||
deleteIntegration,
|
||||
listIntegrationByProject,
|
||||
getIntegration,
|
||||
syncIntegration
|
||||
};
|
||||
};
|
||||
|
@@ -39,6 +39,10 @@ export type TCreateIntegrationDTO = {
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TGetIntegrationDTO = {
|
||||
id: string;
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
||||
export type TUpdateIntegrationDTO = {
|
||||
id: string;
|
||||
app?: string;
|
||||
|
@@ -17,7 +17,6 @@ import {
|
||||
} from "@app/db/schemas";
|
||||
import { TProjects } from "@app/db/schemas/projects";
|
||||
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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
@@ -90,7 +89,6 @@ type TOrgServiceFactoryDep = {
|
||||
>;
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
|
||||
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
|
||||
};
|
||||
@@ -116,7 +114,6 @@ export const orgServiceFactory = ({
|
||||
licenseService,
|
||||
projectRoleDAL,
|
||||
samlConfigDAL,
|
||||
userGroupMembershipDAL,
|
||||
projectBotDAL,
|
||||
projectUserMembershipRoleDAL
|
||||
}: TOrgServiceFactoryDep) => {
|
||||
@@ -458,7 +455,6 @@ export const orgServiceFactory = ({
|
||||
const appCfg = getConfig();
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
|
||||
const org = await orgDAL.findOrgById(orgId);
|
||||
|
||||
@@ -476,19 +472,20 @@ export const orgServiceFactory = ({
|
||||
});
|
||||
}
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
|
||||
if (plan?.slug !== "enterprise" && plan?.memberLimit && plan.membersUsed >= plan.memberLimit) {
|
||||
// limit imposed on number of members allowed / number of members used exceeds the number of members allowed
|
||||
throw new BadRequestError({
|
||||
message: "Failed to invite member due to member limit reached. Upgrade plan to invite more members."
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
throw new BadRequestError({
|
||||
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);
|
||||
if (isCustomOrgRole) {
|
||||
if (!plan?.rbac)
|
||||
@@ -583,6 +580,8 @@ export const orgServiceFactory = ({
|
||||
|
||||
// if there exist no org membership we set is as given by the request
|
||||
if (!inviteeMembership) {
|
||||
// as its used by project invite also
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||
let roleId;
|
||||
const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug;
|
||||
if (isCustomOrgRole) {
|
||||
@@ -616,7 +615,6 @@ export const orgServiceFactory = ({
|
||||
}
|
||||
|
||||
const userIds = users.map(({ id }) => id);
|
||||
const usernames = users.map((el) => el.username);
|
||||
const userEncryptionKeys = await userDAL.findUserEncKeyByUserIdsBatch({ userIds }, tx);
|
||||
// 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));
|
||||
@@ -643,12 +641,10 @@ export const orgServiceFactory = ({
|
||||
{ tx }
|
||||
);
|
||||
const existingMembersGroupByUserId = groupBy(existingMembers, (i) => i.userId);
|
||||
const userIdsToExcludeAsPartOfGroup = new Set(
|
||||
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernames, projectId, tx)
|
||||
);
|
||||
const userWithEncryptionKeyInvitedToProject = userEncryptionKeys.filter(
|
||||
(user) => !existingMembersGroupByUserId?.[user.userId] && !userIdsToExcludeAsPartOfGroup.has(user.userId)
|
||||
(user) => !existingMembersGroupByUserId?.[user.userId]
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!userWithEncryptionKeyInvitedToProject.length) continue;
|
||||
|
||||
|
@@ -90,15 +90,20 @@ export const projectMembershipServiceFactory = ({
|
||||
// projectMembers[0].project
|
||||
if (includeGroupMembers) {
|
||||
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
|
||||
|
||||
const allMembers = [
|
||||
...projectMembers.map((m) => ({ ...m, isGroupMember: false })),
|
||||
...groupMembers.map((m) => ({ ...m, isGroupMember: true }))
|
||||
];
|
||||
|
||||
// Ensure the userId is unique
|
||||
const membersIds = new Set(allMembers.map((entity) => entity.user.id));
|
||||
const uniqueMembers = allMembers.filter((entity) => membersIds.has(entity.user.id));
|
||||
const uniqueMembers: typeof allMembers = [];
|
||||
const addedUserIds = new Set<string>();
|
||||
allMembers.forEach((member) => {
|
||||
if (!addedUserIds.has(member.user.id)) {
|
||||
uniqueMembers.push(member);
|
||||
addedUserIds.add(member.user.id);
|
||||
}
|
||||
});
|
||||
|
||||
return uniqueMembers;
|
||||
}
|
||||
|
@@ -7,7 +7,8 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub
|
||||
ProjectPermissionSub,
|
||||
validateProjectPermissions
|
||||
} from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
@@ -56,6 +57,9 @@ export const projectRoleServiceFactory = ({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Role);
|
||||
const existingRole = await projectRoleDAL.findOne({ slug: data.slug, projectId });
|
||||
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
|
||||
|
||||
validateProjectPermissions(data.permissions);
|
||||
|
||||
const role = await projectRoleDAL.create({
|
||||
...data,
|
||||
projectId
|
||||
@@ -120,6 +124,11 @@ export const projectRoleServiceFactory = ({
|
||||
if (existingRole && existingRole.id !== roleId)
|
||||
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
|
||||
}
|
||||
|
||||
if (data.permissions) {
|
||||
validateProjectPermissions(data.permissions);
|
||||
}
|
||||
|
||||
const [updatedRole] = await projectRoleDAL.update(
|
||||
{ id: roleId, projectId },
|
||||
{
|
||||
|
@@ -2,6 +2,8 @@
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
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 { TSecretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-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 { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TIntegrationDALFactory } from "../integration/integration-dal";
|
||||
import { TIntegrationAuthDALFactory } from "../integration-auth/integration-auth-dal";
|
||||
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 { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TWebhookDALFactory } from "../webhook/webhook-dal";
|
||||
import { fnTriggerWebhook } from "../webhook/webhook-fns";
|
||||
import { TSecretDALFactory } from "./secret-dal";
|
||||
@@ -71,6 +75,7 @@ type TSecretQueueFactoryDep = {
|
||||
secretVersionDAL: TSecretVersionDALFactory;
|
||||
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
|
||||
secretTagDAL: TSecretTagDALFactory;
|
||||
userDAL: Pick<TUserDALFactory, "findById">;
|
||||
secretVersionTagDAL: TSecretVersionTagDALFactory;
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||
secretV2BridgeDAL: TSecretV2BridgeDALFactory;
|
||||
@@ -81,6 +86,7 @@ type TSecretQueueFactoryDep = {
|
||||
snapshotDAL: Pick<TSnapshotDALFactory, "findNSecretV1SnapshotByFolderId" | "deleteSnapshotsAboveLimit">;
|
||||
snapshotSecretV2BridgeDAL: Pick<TSnapshotSecretV2DALFactory, "insertMany" | "batchInsert">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export type TGetSecrets = {
|
||||
@@ -106,6 +112,7 @@ export const secretQueueFactory = ({
|
||||
secretDAL,
|
||||
secretImportDAL,
|
||||
folderDAL,
|
||||
userDAL,
|
||||
webhookDAL,
|
||||
projectEnvDAL,
|
||||
orgDAL,
|
||||
@@ -125,7 +132,8 @@ export const secretQueueFactory = ({
|
||||
snapshotDAL,
|
||||
snapshotSecretV2BridgeDAL,
|
||||
secretApprovalRequestDAL,
|
||||
keyStore
|
||||
keyStore,
|
||||
auditLogService
|
||||
}: TSecretQueueFactoryDep) => {
|
||||
const removeSecretReminder = async (dto: TRemoveSecretReminderDTO) => {
|
||||
const appCfg = getConfig();
|
||||
@@ -430,7 +438,9 @@ export const secretQueueFactory = ({
|
||||
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, {
|
||||
attempts: 3,
|
||||
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) {
|
||||
await replicateSecrets({
|
||||
_deDupeReplicationQueue: deDupeReplicationQueue,
|
||||
@@ -544,7 +554,7 @@ export const secretQueueFactory = ({
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
try {
|
||||
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, {
|
||||
lastSyncJobId: job.id,
|
||||
lastUsed: new Date(),
|
||||
@@ -794,9 +843,23 @@ export const secretQueueFactory = ({
|
||||
(err instanceof AxiosError ? JSON.stringify(err?.response?.data) : (err as Error)?.message) ||
|
||||
"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, {
|
||||
lastSyncJobId: job.id,
|
||||
lastUsed: new Date(),
|
||||
syncMessage: message,
|
||||
isSynced: false
|
||||
});
|
||||
|
@@ -2,3 +2,7 @@
|
||||
title: "Create"
|
||||
openapi: "POST /api/v1/workspace/{projectSlug}/roles"
|
||||
---
|
||||
|
||||
<Note>
|
||||
You can read more about the permissions field in the [permissions documentation](/internals/permissions).
|
||||
</Note>
|
@@ -60,6 +60,8 @@ 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.
|
||||
- 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.
|
||||
- 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 title="Creating a certificate">
|
||||
To create a certificate, head to your Project > Internal PKI > Certificates and press **Issue** under the Certificates section.
|
||||
@@ -76,13 +78,16 @@ 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`.
|
||||
- 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.
|
||||
|
||||
- Key Usage: The key usage extension of the certificate.
|
||||
- Extended Key Usage: The extended key usage extension of the certificate.
|
||||
|
||||
<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**
|
||||
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.
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
<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**.
|
||||
@@ -105,7 +110,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.
|
||||
|
||||
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
|
||||
|
||||
```bash Request
|
||||
@@ -132,6 +137,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
||||
ttl: "...",
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
<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,
|
||||
@@ -164,7 +170,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
||||
<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**
|
||||
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.
|
||||
</Note>
|
||||
|
||||
@@ -197,6 +203,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
||||
serialNumber: "..."
|
||||
}
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
</Tab>
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 379 KiB After Width: | Height: | Size: 399 KiB |
Binary file not shown.
Before Width: | Height: | Size: 517 KiB After Width: | Height: | Size: 518 KiB |
83
docs/internals/permissions.mdx
Normal file
83
docs/internals/permissions.mdx
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
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>
|
@@ -696,7 +696,12 @@
|
||||
{
|
||||
"group": "Audit Logs",
|
||||
"pages": ["api-reference/endpoints/audit-logs/export-audit-log"]
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Infisical PKI",
|
||||
"pages": [
|
||||
{
|
||||
"group": "Certificate Authorities",
|
||||
"pages": [
|
||||
@@ -764,6 +769,7 @@
|
||||
"group": "Internals",
|
||||
"pages": [
|
||||
"internals/overview",
|
||||
"internals/permissions",
|
||||
"internals/components",
|
||||
"internals/flows",
|
||||
"internals/security",
|
||||
|
83
frontend/src/components/v2/ComboBox/ComboBox.tsx
Normal file
83
frontend/src/components/v2/ComboBox/ComboBox.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
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>
|
||||
);
|
||||
};
|
1
frontend/src/components/v2/ComboBox/index.tsx
Normal file
1
frontend/src/components/v2/ComboBox/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { ComboBox } from "./ComboBox";
|
@@ -11,6 +11,7 @@ type Props = {
|
||||
isDisabled?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
autoCapitalization?: boolean;
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
const inputVariants = cva(
|
||||
@@ -71,6 +72,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
containerClassName,
|
||||
isRounded = true,
|
||||
isFullWidth = true,
|
||||
isDisabled,
|
||||
@@ -94,7 +96,15 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={inputParentContainerVariants({ isRounded, isError, isFullWidth, variant })}>
|
||||
<div
|
||||
className={inputParentContainerVariants({
|
||||
isRounded,
|
||||
isError,
|
||||
isFullWidth,
|
||||
variant,
|
||||
className: containerClassName
|
||||
})}
|
||||
>
|
||||
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
|
||||
<input
|
||||
{...props}
|
||||
|
@@ -11,13 +11,24 @@ export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
||||
title?: ReactNode;
|
||||
subTitle?: ReactNode;
|
||||
footerContent?: ReactNode;
|
||||
bodyClassName?: string;
|
||||
onClose?: () => void;
|
||||
overlayClassName?: string;
|
||||
};
|
||||
|
||||
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
(
|
||||
{ children, title, subTitle, className, overlayClassName, footerContent, onClose, ...props },
|
||||
{
|
||||
children,
|
||||
title,
|
||||
subTitle,
|
||||
className,
|
||||
overlayClassName,
|
||||
footerContent,
|
||||
bodyClassName,
|
||||
onClose,
|
||||
...props
|
||||
},
|
||||
forwardedRef
|
||||
) => (
|
||||
<DialogPrimitive.Portal>
|
||||
@@ -35,7 +46,10 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
style={{ maxHeight: "90%" }}
|
||||
>
|
||||
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
|
||||
<CardBody className="overflow-y-auto overflow-x-hidden" style={{ maxHeight: "90%" }}>
|
||||
<CardBody
|
||||
className={twMerge("overflow-y-auto overflow-x-hidden", bodyClassName)}
|
||||
style={{ maxHeight: "90%" }}
|
||||
>
|
||||
{children}
|
||||
</CardBody>
|
||||
{footerContent && <CardFooter>{footerContent}</CardFooter>}
|
||||
|
@@ -40,6 +40,8 @@ export const Pagination = ({
|
||||
const upperLimit = Math.ceil(count / perPage);
|
||||
const nextPageNumber = Math.min(upperLimit, page + 1);
|
||||
const canGoNext = page + 1 <= upperLimit;
|
||||
const canGoFirst = page > 1;
|
||||
const canGoLast = page < upperLimit;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -50,7 +52,7 @@ export const Pagination = ({
|
||||
>
|
||||
<div className="mr-6 flex items-center space-x-2">
|
||||
<div className="text-xs">
|
||||
{(page - 1) * perPage} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
|
||||
{(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
|
||||
</div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -73,6 +75,16 @@ export const Pagination = ({
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<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
|
||||
variant="plain"
|
||||
ariaLabel="pagination-prev"
|
||||
@@ -89,6 +101,16 @@ export const Pagination = ({
|
||||
>
|
||||
<FontAwesomeIcon className="text-xs" icon={faChevronRight} />
|
||||
</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>
|
||||
);
|
||||
|
@@ -50,8 +50,8 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={twMerge(
|
||||
`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-200`,
|
||||
`inline-flex items-center justify-between rounded-md border border-mineshaft-600
|
||||
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`,
|
||||
className,
|
||||
isDisabled && "cursor-not-allowed opacity-50"
|
||||
)}
|
||||
|
@@ -25,6 +25,21 @@ export const UserProvider = ({ children }: Props): JSX.Element => {
|
||||
};
|
||||
}, [data, isLoading]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-center justify-center bg-bunker-800">
|
||||
<img
|
||||
src="/images/loading/loading.gif"
|
||||
height={70}
|
||||
width={120}
|
||||
decoding="async"
|
||||
loading="lazy"
|
||||
alt="infisical loading indicator"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <UserContext.Provider value={value}>{children}</UserContext.Provider>;
|
||||
};
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { createContext, ReactNode, useContext, useMemo } from "react";
|
||||
import { createContext, ReactNode, useContext, useEffect, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import { Workspace } from "@app/hooks/api/workspace/types";
|
||||
|
||||
@@ -31,6 +32,34 @@ export const WorkspaceProvider = ({ children }: Props): JSX.Element => {
|
||||
};
|
||||
}, [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>;
|
||||
};
|
||||
|
||||
|
@@ -79,7 +79,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
||||
[EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
|
||||
"Update certificate template EST 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 } = {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
export enum ActorType {
|
||||
PLATFORM = "platform",
|
||||
USER = "user",
|
||||
SERVICE = "service",
|
||||
IDENTITY = "identity"
|
||||
@@ -91,5 +92,6 @@ export enum EventType {
|
||||
UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
|
||||
GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-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"
|
||||
}
|
||||
|
@@ -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 { Actor, AuditLog, AuditLogFilters } from "./types";
|
||||
import { Actor, AuditLog, TGetAuditLogsFilter } from "./types";
|
||||
|
||||
export const auditLogKeys = {
|
||||
getAuditLogs: (workspaceId: string | null, filters: AuditLogFilters) =>
|
||||
getAuditLogs: (workspaceId: string | null, filters: TGetAuditLogsFilter) =>
|
||||
[{ workspaceId, filters }, "audit-logs"] as const,
|
||||
getAuditLogActorFilterOpts: (workspaceId: string) =>
|
||||
[{ 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({
|
||||
queryKey: auditLogKeys.getAuditLogs(workspaceId, filters),
|
||||
queryKey: auditLogKeys.getAuditLogs(projectId, filters),
|
||||
queryFn: async ({ pageParam }) => {
|
||||
const auditLogEndpoint = workspaceId
|
||||
? `/api/v1/workspace/${workspaceId}/audit-logs`
|
||||
: "/api/v1/organization/audit-logs";
|
||||
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(auditLogEndpoint, {
|
||||
params: {
|
||||
...filters,
|
||||
offset: pageParam,
|
||||
startDate: filters?.startDate?.toISOString(),
|
||||
endDate: filters?.endDate?.toISOString()
|
||||
const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
|
||||
"/api/v1/organization/audit-logs",
|
||||
{
|
||||
params: {
|
||||
...filters,
|
||||
offset: pageParam,
|
||||
startDate: filters?.startDate?.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;
|
||||
},
|
||||
getNextPageParam: (lastPage, pages) =>
|
||||
lastPage.length !== 0 ? pages.length * filters.limit : undefined
|
||||
lastPage.length !== 0 ? pages.length * filters.limit : undefined,
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -3,6 +3,17 @@ import { IdentityTrustedIp } from "../identities/types";
|
||||
import { PkiItemType } from "../pkiCollections/constants";
|
||||
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 {
|
||||
userId: string;
|
||||
email: string;
|
||||
@@ -33,7 +44,13 @@ export interface IdentityActor {
|
||||
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 {
|
||||
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 =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -838,7 +871,8 @@ export type Event =
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig
|
||||
| UpdateProjectSlackConfig
|
||||
| GetProjectSlackConfig;
|
||||
| GetProjectSlackConfig
|
||||
| IntegrationSyncedEvent;
|
||||
|
||||
export type AuditLog = {
|
||||
id: string;
|
||||
@@ -856,12 +890,3 @@ export type AuditLog = {
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type AuditLogFilters = {
|
||||
eventType?: EventType;
|
||||
userAgentType?: UserAgentType;
|
||||
actor?: string;
|
||||
limit: number;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
};
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { CertKeyAlgorithm } from "../certificates/enums";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificates/enums";
|
||||
import { CaRenewalType, CaStatus, CaType } from "./enums";
|
||||
|
||||
export type TCertificateAuthority = {
|
||||
@@ -91,6 +91,8 @@ export type TCreateCertificateDTO = {
|
||||
ttl: string; // string compatible with ms
|
||||
notBefore?: string;
|
||||
notAfter?: string;
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
};
|
||||
|
||||
export type TCreateCertificateResponse = {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificates/enums";
|
||||
|
||||
export type TCertificateTemplate = {
|
||||
id: string;
|
||||
caId: string;
|
||||
@@ -8,6 +10,8 @@ export type TCertificateTemplate = {
|
||||
commonName: string;
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
};
|
||||
|
||||
export type TCreateCertificateTemplateDTO = {
|
||||
@@ -18,6 +22,8 @@ export type TCreateCertificateTemplateDTO = {
|
||||
subjectAlternativeName: string;
|
||||
ttl: string;
|
||||
projectId: string;
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
};
|
||||
|
||||
export type TUpdateCertificateTemplateDTO = {
|
||||
@@ -29,6 +35,8 @@ export type TUpdateCertificateTemplateDTO = {
|
||||
subjectAlternativeName?: string;
|
||||
ttl?: string;
|
||||
projectId: string;
|
||||
keyUsages?: CertKeyUsage[];
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
};
|
||||
|
||||
export type TDeleteCertificateTemplateDTO = {
|
||||
|
@@ -1,4 +1,10 @@
|
||||
import { CertKeyAlgorithm, CertStatus, CrlReason } from "./enums";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertKeyAlgorithm,
|
||||
CertKeyUsage,
|
||||
CertStatus,
|
||||
CrlReason
|
||||
} from "./enums";
|
||||
|
||||
export const certStatusToNameMap: { [K in CertStatus]: string } = {
|
||||
[CertStatus.ACTIVE]: "Active",
|
||||
@@ -69,3 +75,24 @@ export const crlReasons = [
|
||||
},
|
||||
{ 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;
|
||||
|
@@ -22,3 +22,24 @@ export enum CrlReason {
|
||||
PRIVILEGE_WITHDRAWN = "PRIVILEGE_WITHDRAWN",
|
||||
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"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { CertStatus } from "./enums";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage, CertStatus } from "./enums";
|
||||
|
||||
export type TCertificate = {
|
||||
id: string;
|
||||
@@ -11,6 +11,8 @@ export type TCertificate = {
|
||||
serialNumber: string;
|
||||
notBefore: string;
|
||||
notAfter: string;
|
||||
keyUsages: CertKeyUsage[];
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
};
|
||||
|
||||
export type TDeleteCertDTO = {
|
||||
|
4
frontend/src/hooks/api/generic/types.ts
Normal file
4
frontend/src/hooks/api/generic/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum OrderByDirection {
|
||||
ASC = "asc",
|
||||
DESC = "desc"
|
||||
}
|
@@ -469,3 +469,8 @@ export type RevokeTokenDTO = {
|
||||
export type RevokeTokenRes = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type TProjectIdentitiesList = {
|
||||
identityMemberships: IdentityMembership[];
|
||||
totalCount: number;
|
||||
};
|
||||
|
@@ -1 +1,6 @@
|
||||
export { useCreateIntegration, useDeleteIntegration, useGetCloudIntegrations } from "./queries";
|
||||
export {
|
||||
useCreateIntegration,
|
||||
useDeleteIntegration,
|
||||
useGetCloudIntegrations,
|
||||
useGetIntegration
|
||||
} from "./queries";
|
||||
|
@@ -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 { apiRequest } from "@app/config/request";
|
||||
|
||||
import { workspaceKeys } from "../workspace";
|
||||
import { TCloudIntegration } from "./types";
|
||||
import { TCloudIntegration, TIntegrationWithEnv } from "./types";
|
||||
|
||||
export const integrationQueryKeys = {
|
||||
getIntegrations: () => ["integrations"] as const
|
||||
getIntegrations: () => ["integrations"] as const,
|
||||
getIntegration: (id: string) => ["integration", id] as const
|
||||
};
|
||||
|
||||
const fetchIntegrations = async () => {
|
||||
@@ -18,6 +19,14 @@ const fetchIntegrations = async () => {
|
||||
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 = () =>
|
||||
useQuery({
|
||||
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 = () => {
|
||||
return useMutation<{}, {}, { id: string; workspaceId: string; lastUsed: string }>({
|
||||
mutationFn: ({ id }) => apiRequest.post(`/api/v1/integration/${id}/sync`),
|
||||
|
@@ -36,14 +36,34 @@ export type TIntegration = {
|
||||
metadata?: {
|
||||
githubVisibility?: string;
|
||||
githubVisibilityRepoIds?: string[];
|
||||
shouldAutoRedeploy?: boolean;
|
||||
secretAWSTag?: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
|
||||
kmsKeyId?: string;
|
||||
secretSuffix?: string;
|
||||
secretPrefix?: string;
|
||||
syncBehavior?: IntegrationSyncBehavior;
|
||||
mappingBehavior?: IntegrationMappingBehavior;
|
||||
scope: string;
|
||||
org: string;
|
||||
project: string;
|
||||
environment: string;
|
||||
|
||||
shouldDisableDelete?: boolean;
|
||||
shouldMaskSecrets?: boolean;
|
||||
shouldProtectSecrets?: boolean;
|
||||
shouldEnableDelete?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type TIntegrationWithEnv = TIntegration & {
|
||||
environment: {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -1,19 +1,22 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
|
||||
import { TGroupOrgMembership } from "../groups/types";
|
||||
import { IdentityMembershipOrg } from "../identities/types";
|
||||
import {
|
||||
BillingDetails,
|
||||
Invoice,
|
||||
License,
|
||||
Organization,
|
||||
OrgIdentityOrderBy,
|
||||
OrgPlanTable,
|
||||
PlanBillingInfo,
|
||||
PmtMethod,
|
||||
ProductsTable,
|
||||
TaxID,
|
||||
TListOrgIdentitiesDTO,
|
||||
TOrgIdentitiesList,
|
||||
UpdateOrgDTO
|
||||
} from "./types";
|
||||
|
||||
@@ -30,6 +33,12 @@ export const organizationKeys = {
|
||||
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const,
|
||||
getOrgIdentityMemberships: (orgId: string) =>
|
||||
[{ 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
|
||||
};
|
||||
|
||||
@@ -360,19 +369,51 @@ export const useGetOrgLicenses = (organizationId: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetIdentityMembershipOrgs = (organizationId: string) => {
|
||||
export const useGetIdentityMembershipOrgs = (
|
||||
{
|
||||
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({
|
||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId),
|
||||
queryKey: organizationKeys.getOrgIdentityMembershipsWithParams({
|
||||
organizationId,
|
||||
offset,
|
||||
limit,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { identityMemberships }
|
||||
} = await apiRequest.get<{ identityMemberships: IdentityMembershipOrg[] }>(
|
||||
`/api/v2/organizations/${organizationId}/identity-memberships`
|
||||
const { data } = await apiRequest.get<TOrgIdentitiesList>(
|
||||
`/api/v2/organizations/${organizationId}/identity-memberships`,
|
||||
{ params }
|
||||
);
|
||||
|
||||
return identityMemberships;
|
||||
return data;
|
||||
},
|
||||
enabled: true
|
||||
enabled: true,
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -1,3 +1,6 @@
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { IdentityMembershipOrg } from "@app/hooks/api/identities/types";
|
||||
|
||||
export type Organization = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -102,3 +105,22 @@ export type ProductsTable = {
|
||||
head: ProductsTableHead[];
|
||||
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"
|
||||
}
|
||||
|
@@ -163,27 +163,29 @@ export const useGetImportedSecretsAllEnvs = ({
|
||||
queryFn: () => fetchImportedSecrets(projectId, env, path).catch(() => []),
|
||||
enabled: Boolean(projectId) && Boolean(env),
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
select: (data: TImportedSecrets[]) => {
|
||||
return data.map((el) => ({
|
||||
environment: el.environment,
|
||||
secretPath: el.secretPath,
|
||||
environmentInfo: el.environmentInfo,
|
||||
folderId: el.folderId,
|
||||
secrets: el.secrets.map((encSecret) => {
|
||||
return {
|
||||
id: encSecret.id,
|
||||
env: encSecret.environment,
|
||||
key: encSecret.secretKey,
|
||||
value: encSecret.secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: encSecret.secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt,
|
||||
version: encSecret.version
|
||||
};
|
||||
})
|
||||
}));
|
||||
}
|
||||
select: useCallback(
|
||||
(data: Awaited<ReturnType<typeof fetchImportedSecrets>>) =>
|
||||
data.map((el) => ({
|
||||
environment: el.environment,
|
||||
secretPath: el.secretPath,
|
||||
environmentInfo: el.environmentInfo,
|
||||
folderId: el.folderId,
|
||||
secrets: el.secrets.map((encSecret) => {
|
||||
return {
|
||||
id: encSecret.id,
|
||||
env: encSecret.environment,
|
||||
key: encSecret.secretKey,
|
||||
value: encSecret.secretValue,
|
||||
tags: encSecret.tags,
|
||||
comment: encSecret.secretComment,
|
||||
createdAt: encSecret.createdAt,
|
||||
updatedAt: encSecret.updatedAt,
|
||||
version: encSecret.version
|
||||
};
|
||||
})
|
||||
})),
|
||||
[]
|
||||
)
|
||||
}))
|
||||
});
|
||||
|
||||
|
@@ -108,7 +108,7 @@ export const useGetProjectSecrets = ({
|
||||
// wait for all values to be available
|
||||
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
|
||||
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
|
||||
queryFn: async () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
||||
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
||||
onError: (error) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
const serverResponse = error.response?.data as { message: string };
|
||||
@@ -119,7 +119,10 @@ export const useGetProjectSecrets = ({
|
||||
});
|
||||
}
|
||||
},
|
||||
select: ({ secrets }) => mergePersonalSecrets(secrets)
|
||||
select: useCallback(
|
||||
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => mergePersonalSecrets(data.secrets),
|
||||
[]
|
||||
)
|
||||
});
|
||||
|
||||
export const useGetProjectSecretsAllEnv = ({
|
||||
@@ -131,7 +134,11 @@ export const useGetProjectSecretsAllEnv = ({
|
||||
|
||||
const secrets = useQueries({
|
||||
queries: envs.map((environment) => ({
|
||||
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
|
||||
queryKey: secretKeys.getProjectSecret({
|
||||
workspaceId,
|
||||
environment,
|
||||
secretPath
|
||||
}),
|
||||
enabled: Boolean(workspaceId && environment),
|
||||
onError: (error: unknown) => {
|
||||
if (axios.isAxiosError(error) && !isErrorHandled) {
|
||||
@@ -147,12 +154,17 @@ export const useGetProjectSecretsAllEnv = ({
|
||||
setIsErrorHandled.on();
|
||||
}
|
||||
},
|
||||
queryFn: async () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
||||
select: (el: SecretV3RawResponse) =>
|
||||
mergePersonalSecrets(el.secrets).reduce<Record<string, SecretV3RawSanitized>>(
|
||||
(prev, curr) => ({ ...prev, [curr.key]: curr }),
|
||||
{}
|
||||
)
|
||||
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
||||
staleTime: 60 * 1000,
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
select: useCallback(
|
||||
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) =>
|
||||
mergePersonalSecrets(data.secrets).reduce<Record<string, SecretV3RawSanitized>>(
|
||||
(prev, curr) => ({ ...prev, [curr.key]: curr }),
|
||||
{}
|
||||
),
|
||||
[]
|
||||
)
|
||||
}))
|
||||
});
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
|
||||
import { CaStatus } from "../ca/enums";
|
||||
import { TCertificateAuthority } from "../ca/types";
|
||||
@@ -8,7 +9,7 @@ import { TCertificate } from "../certificates/types";
|
||||
import { TCertificateTemplate } from "../certificateTemplates/types";
|
||||
import { TGroupMembership } from "../groups/types";
|
||||
import { identitiesKeys } from "../identities/queries";
|
||||
import { IdentityMembership } from "../identities/types";
|
||||
import { TProjectIdentitiesList } from "../identities/types";
|
||||
import { IntegrationAuth } from "../integrationAuth/types";
|
||||
import { TIntegration } from "../integrations/types";
|
||||
import { TPkiAlert } from "../pkiAlerts/types";
|
||||
@@ -24,8 +25,10 @@ import {
|
||||
DeleteEnvironmentDTO,
|
||||
DeleteWorkspaceDTO,
|
||||
NameWorkspaceSecretsDTO,
|
||||
ProjectIdentityOrderBy,
|
||||
RenameWorkspaceDTO,
|
||||
TGetUpgradeProjectStatusDTO,
|
||||
TListProjectIdentitiesDTO,
|
||||
ToggleAutoCapitalizationDTO,
|
||||
TUpdateWorkspaceIdentityRoleDTO,
|
||||
TUpdateWorkspaceUserRoleDTO,
|
||||
@@ -484,18 +487,51 @@ export const useDeleteIdentityFromWorkspace = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetWorkspaceIdentityMemberships = (workspaceId: string) => {
|
||||
export const useGetWorkspaceIdentityMemberships = (
|
||||
{
|
||||
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({
|
||||
queryKey: workspaceKeys.getWorkspaceIdentityMemberships(workspaceId),
|
||||
queryKey: workspaceKeys.getWorkspaceIdentityMembershipsWithParams({
|
||||
workspaceId,
|
||||
offset,
|
||||
limit,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search
|
||||
}),
|
||||
queryFn: async () => {
|
||||
const {
|
||||
data: { identityMemberships }
|
||||
} = await apiRequest.get<{ identityMemberships: IdentityMembership[] }>(
|
||||
`/api/v2/workspace/${workspaceId}/identity-memberships`
|
||||
const params = new URLSearchParams({
|
||||
offset: String(offset),
|
||||
limit: String(limit),
|
||||
orderBy: String(orderBy),
|
||||
orderDirection: String(orderDirection),
|
||||
search: String(search)
|
||||
});
|
||||
|
||||
const { data } = await apiRequest.get<TProjectIdentitiesList>(
|
||||
`/api/v2/workspace/${workspaceId}/identity-memberships`,
|
||||
{ params }
|
||||
);
|
||||
return identityMemberships;
|
||||
return data;
|
||||
},
|
||||
enabled: true
|
||||
enabled: true,
|
||||
...options
|
||||
});
|
||||
};
|
||||
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { TListProjectIdentitiesDTO } from "@app/hooks/api/workspace/types";
|
||||
|
||||
import type { CaStatus } from "../ca";
|
||||
|
||||
export const workspaceKeys = {
|
||||
@@ -15,6 +17,12 @@ export const workspaceKeys = {
|
||||
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
|
||||
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
||||
[{ 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) =>
|
||||
[{ workspaceId }, "workspace-groups"] as const,
|
||||
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
|
||||
import { TProjectRole } from "../roles/types";
|
||||
|
||||
export enum ProjectVersion {
|
||||
@@ -141,3 +143,16 @@ export type TUpdateWorkspaceGroupRoleDTO = {
|
||||
}
|
||||
)[];
|
||||
};
|
||||
|
||||
export type TListProjectIdentitiesDTO = {
|
||||
workspaceId: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
orderBy?: ProjectIdentityOrderBy;
|
||||
orderDirection?: OrderByDirection;
|
||||
search?: string;
|
||||
};
|
||||
|
||||
export enum ProjectIdentityOrderBy {
|
||||
Name = "name"
|
||||
}
|
||||
|
@@ -104,6 +104,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
const [tagKey, setTagKey] = useState("");
|
||||
const [tagValue, setTagValue] = useState("");
|
||||
const [kmsKeyId, setKmsKeyId] = useState("");
|
||||
const [secretPrefix, setSecretPrefix] = useState("");
|
||||
|
||||
// const [path, setPath] = useState('');
|
||||
// const [pathErrorText, setPathErrorText] = useState('');
|
||||
@@ -165,6 +166,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
]
|
||||
}
|
||||
: {}),
|
||||
...(secretPrefix && { secretPrefix }),
|
||||
...(kmsKeyId && { kmsKeyId }),
|
||||
mappingBehavior: selectedMappingBehavior
|
||||
}
|
||||
@@ -325,7 +327,7 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
</Switch>
|
||||
</div>
|
||||
{shouldTag && (
|
||||
<div className="mt-4">
|
||||
<div className="mt-4 flex justify-between">
|
||||
<FormControl label="Tag Key">
|
||||
<Input
|
||||
placeholder="managed-by"
|
||||
@@ -342,10 +344,20 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
</FormControl>
|
||||
</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">
|
||||
<Select
|
||||
value={kmsKeyId}
|
||||
onValueChange={(e) => {
|
||||
if (e === "no-keys") return;
|
||||
setKmsKeyId(e);
|
||||
}}
|
||||
className="w-full border border-mineshaft-500"
|
||||
@@ -363,7 +375,9 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
<SelectItem isDisabled value="no-keys" key="no-keys">
|
||||
No KMS keys available
|
||||
</SelectItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
23
frontend/src/pages/integrations/details/[integrationId].tsx
Normal file
23
frontend/src/pages/integrations/details/[integrationId].tsx
Normal 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;
|
@@ -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;
|
||||
};
|
@@ -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;
|
||||
};
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
);
|
||||
};
|
@@ -0,0 +1 @@
|
||||
export { IntegrationDetailsPage } from "./IntegrationDetailsPage";
|
@@ -1,6 +1,10 @@
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
faArrowRight,
|
||||
faCalendarCheck,
|
||||
faEllipsis,
|
||||
faRefresh,
|
||||
faWarning,
|
||||
faXmark
|
||||
@@ -10,7 +14,7 @@ import { format } from "date-fns";
|
||||
import { integrationSlugNameMapping } from "public/data/frequentConstants";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormLabel, IconButton, Tag, Tooltip } from "@app/components/v2";
|
||||
import { Badge, FormLabel, IconButton, Tooltip } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||
import { IntegrationMappingBehavior } from "@app/hooks/api/integrations/types";
|
||||
import { TIntegration } from "@app/hooks/api/types";
|
||||
@@ -28,9 +32,12 @@ export const ConfiguredIntegrationItem = ({
|
||||
onRemoveIntegration,
|
||||
onManualSyncIntegration
|
||||
}: IProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="max-w-8xl flex justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3"
|
||||
className="max-w-8xl flex cursor-pointer justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-3 transition-all hover:bg-mineshaft-700"
|
||||
onClick={() => router.push(`/integrations/details/${integration.id}`)}
|
||||
key={`integration-${integration?.id.toString()}`}
|
||||
>
|
||||
<div className="flex">
|
||||
@@ -168,9 +175,9 @@ export const ConfiguredIntegrationItem = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-[1.5rem] flex cursor-default">
|
||||
<div className="mt-[1.5rem] flex cursor-default space-x-3">
|
||||
{integration.isSynced != null && integration.lastUsed != null && (
|
||||
<Tag key={integration.id} className={integration.isSynced ? "bg-green-800" : "bg-red/80"}>
|
||||
<Badge variant={integration.isSynced ? "success" : "danger"} key={integration.id}>
|
||||
<Tooltip
|
||||
center
|
||||
className="max-w-xs whitespace-normal break-words"
|
||||
@@ -178,7 +185,7 @@ export const ConfiguredIntegrationItem = ({
|
||||
<div className="flex max-h-[10rem] flex-col overflow-auto ">
|
||||
<div className="flex self-start">
|
||||
<FontAwesomeIcon icon={faCalendarCheck} className="pt-0.5 pr-2 text-sm" />
|
||||
<div className="text-sm">Last sync</div>
|
||||
<div className="text-sm">Last successful sync</div>
|
||||
</div>
|
||||
<div className="pl-5 text-left text-xs">
|
||||
{format(new Date(integration.lastUsed), "yyyy-MM-dd, hh:mm aaa")}
|
||||
@@ -195,43 +202,62 @@ export const ConfiguredIntegrationItem = ({
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="flex items-center space-x-2 text-white">
|
||||
<div className="flex h-full items-center space-x-2">
|
||||
<div>{integration.isSynced ? "Synced" : "Not synced"}</div>
|
||||
{!integration.isSynced && <FontAwesomeIcon icon={faWarning} />}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Tag>
|
||||
</Badge>
|
||||
)}
|
||||
<div className="mr-1 flex items-end opacity-80 duration-200 hover:opacity-100">
|
||||
<div className="space-x-1.5">
|
||||
<Tooltip className="text-center" content="Manually sync integration secrets">
|
||||
<Button
|
||||
onClick={() => onManualSyncIntegration()}
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onManualSyncIntegration();
|
||||
}}
|
||||
ariaLabel="sync"
|
||||
colorSchema="primary"
|
||||
variant="star"
|
||||
className="max-w-[2.5rem] border-none bg-mineshaft-500"
|
||||
>
|
||||
<FontAwesomeIcon icon={faRefresh} className="px-1 text-bunker-200" />
|
||||
</Button>
|
||||
<FontAwesomeIcon icon={faRefresh} className="px-1" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Integrations}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<div className="flex items-end opacity-80 duration-200 hover:opacity-100">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={ProjectPermissionSub.Integrations}
|
||||
>
|
||||
{(isAllowed: boolean) => (
|
||||
<Tooltip content="Remove Integration">
|
||||
<IconButton
|
||||
onClick={() => onRemoveIntegration()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveIntegration();
|
||||
}}
|
||||
ariaLabel="delete"
|
||||
isDisabled={!isAllowed}
|
||||
colorSchema="danger"
|
||||
variant="star"
|
||||
className="max-w-[2.5rem] border-none bg-mineshaft-500"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="px-0.5" />
|
||||
<FontAwesomeIcon icon={faXmark} className="px-1" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
<Tooltip content="View details">
|
||||
<IconButton
|
||||
ariaLabel="delete"
|
||||
colorSchema="primary"
|
||||
variant="star"
|
||||
className="max-w-[2.5rem] border-none bg-mineshaft-500"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsis} className="px-1" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -43,11 +43,6 @@ export const useNavigateToSelectOrganization = () => {
|
||||
await navigateUserToOrg(router, config.defaultAuthOrgId);
|
||||
}
|
||||
|
||||
let localOrgId = localStorage.getItem("orgData.id")
|
||||
if(!cliCallbackPort && localOrgId != null){
|
||||
await navigateUserToOrg(router, localOrgId);
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries(userKeys.getUser);
|
||||
let redirectTo = "/login/select-organization";
|
||||
|
||||
|
@@ -42,6 +42,8 @@ export const IdentitySection = withPermission(
|
||||
? subscription.identitiesUsed < subscription.identityLimit
|
||||
: true;
|
||||
|
||||
const isEnterprise = subscription?.slug === "enterprise"
|
||||
|
||||
const onDeleteIdentitySubmit = async (identityId: string) => {
|
||||
try {
|
||||
await deleteMutateAsync({
|
||||
@@ -93,7 +95,7 @@ export const IdentitySection = withPermission(
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => {
|
||||
if (!isMoreIdentitiesAllowed) {
|
||||
if (!isMoreIdentitiesAllowed && !isEnterprise) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description: "You can add more identities if you upgrade your Infisical plan."
|
||||
});
|
||||
|
@@ -1,5 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { faEllipsis, faServer } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faArrowDown,
|
||||
faArrowUp,
|
||||
faEllipsis,
|
||||
faMagnifyingGlass,
|
||||
faServer
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
@@ -11,8 +18,12 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Input,
|
||||
Pagination,
|
||||
Select,
|
||||
SelectItem,
|
||||
Spinner,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
@@ -23,7 +34,10 @@ import {
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||
import { useDebounce } from "@app/hooks";
|
||||
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||
import { OrgIdentityOrderBy } from "@app/hooks/api/organization/types";
|
||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||
|
||||
type Props = {
|
||||
@@ -36,22 +50,60 @@ type Props = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
const INIT_PER_PAGE = 20;
|
||||
|
||||
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
const router = useRouter();
|
||||
const { currentOrg } = useOrganization();
|
||||
const orgId = currentOrg?.id || "";
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
||||
const [orderBy, setOrderBy] = useState(OrgIdentityOrderBy.Name);
|
||||
const [search, setSearch] = useState("");
|
||||
const debouncedSearch = useDebounce(search);
|
||||
|
||||
const organizationId = currentOrg?.id || "";
|
||||
|
||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentity();
|
||||
const { data, isLoading } = useGetIdentityMembershipOrgs(orgId);
|
||||
|
||||
const { data: roles } = useGetOrgRoles(orgId);
|
||||
const offset = (page - 1) * perPage;
|
||||
const { data, isLoading, isFetching } = useGetIdentityMembershipOrgs(
|
||||
{
|
||||
organizationId,
|
||||
offset,
|
||||
limit: perPage,
|
||||
orderDirection,
|
||||
orderBy,
|
||||
search: debouncedSearch
|
||||
},
|
||||
{ keepPreviousData: true }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// reset page if no longer valid
|
||||
if (data && data.totalCount < offset) setPage(1);
|
||||
}, [data?.totalCount]);
|
||||
|
||||
const { data: roles } = useGetOrgRoles(organizationId);
|
||||
|
||||
const handleSort = (column: OrgIdentityOrderBy) => {
|
||||
if (column === orderBy) {
|
||||
setOrderDirection((prev) =>
|
||||
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setOrderBy(column);
|
||||
setOrderDirection(OrderByDirection.ASC);
|
||||
};
|
||||
|
||||
const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => {
|
||||
try {
|
||||
await updateMutateAsync({
|
||||
identityId,
|
||||
role,
|
||||
organizationId: orgId
|
||||
organizationId
|
||||
});
|
||||
|
||||
createNotification({
|
||||
@@ -71,124 +123,180 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Role</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={4} innerKey="org-identities" />}
|
||||
{!isLoading &&
|
||||
data?.map(({ identity: { id, name }, role, customRole }) => {
|
||||
return (
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`identity-${id}`}
|
||||
onClick={() => router.push(`/org/${orgId}/identities/${id}`)}
|
||||
>
|
||||
<Td>{name}</Td>
|
||||
<Td>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<Select
|
||||
value={role === "custom" ? (customRole?.slug as string) : role}
|
||||
isDisabled={!isAllowed}
|
||||
className="w-40 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
handleChangeRole({
|
||||
identityId: id,
|
||||
role: selectedRole
|
||||
})
|
||||
}
|
||||
>
|
||||
{(roles || []).map(({ slug, name: roleName }) => (
|
||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||
{roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/org/${orgId}/identities/${id}`);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Edit Identity
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteIdentity", {
|
||||
identityId: id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Identity
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{!isLoading && data && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={4}>
|
||||
<EmptyState
|
||||
title="No identities have been created in this organization"
|
||||
icon={faServer}
|
||||
/>
|
||||
</Td>
|
||||
<div>
|
||||
<Input
|
||||
containerClassName="mb-4"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||
placeholder="Search identities by name..."
|
||||
/>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr className="h-14">
|
||||
<Th className="w-1/2">
|
||||
<div className="flex items-center">
|
||||
Name
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Name ? "" : "opacity-30"}`}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(OrgIdentityOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
orderDirection === OrderByDirection.DESC &&
|
||||
orderBy === OrgIdentityOrderBy.Name
|
||||
? faArrowUp
|
||||
: faArrowDown
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Role
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Role ? "" : "opacity-30"}`}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(OrgIdentityOrderBy.Role)}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={
|
||||
orderDirection === OrderByDirection.DESC &&
|
||||
orderBy === OrgIdentityOrderBy.Role
|
||||
? faArrowUp
|
||||
: faArrowDown
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={3} innerKey="org-identities" />}
|
||||
{!isLoading &&
|
||||
data?.identityMemberships.map(({ identity: { id, name }, role, customRole }) => {
|
||||
return (
|
||||
<Tr
|
||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
key={`identity-${id}`}
|
||||
onClick={() => router.push(`/org/${organizationId}/identities/${id}`)}
|
||||
>
|
||||
<Td>{name}</Td>
|
||||
<Td>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => {
|
||||
return (
|
||||
<Select
|
||||
value={role === "custom" ? (customRole?.slug as string) : role}
|
||||
isDisabled={!isAllowed}
|
||||
className="w-40 bg-mineshaft-600"
|
||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
||||
onValueChange={(selectedRole) =>
|
||||
handleChangeRole({
|
||||
identityId: id,
|
||||
role: selectedRole
|
||||
})
|
||||
}
|
||||
>
|
||||
{(roles || []).map(({ slug, name: roleName }) => (
|
||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
||||
{roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}}
|
||||
</OrgPermissionCan>
|
||||
</Td>
|
||||
<Td>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||
<div className="flex justify-center hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="p-1">
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Edit}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
router.push(`/org/${organizationId}/identities/${id}`);
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Edit Identity
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Delete}
|
||||
a={OrgPermissionSubjects.Identity}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem
|
||||
className={twMerge(
|
||||
isAllowed
|
||||
? "hover:!bg-red-500 hover:!text-white"
|
||||
: "pointer-events-none cursor-not-allowed opacity-50"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteIdentity", {
|
||||
identityId: id,
|
||||
name
|
||||
});
|
||||
}}
|
||||
disabled={!isAllowed}
|
||||
>
|
||||
Delete Identity
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isLoading && data && data.totalCount > INIT_PER_PAGE && (
|
||||
<Pagination
|
||||
count={data.totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && data && data?.identityMemberships.length === 0 && (
|
||||
<EmptyState
|
||||
title={
|
||||
debouncedSearch.trim().length > 0
|
||||
? "No identities match search filter"
|
||||
: "No identities have been created in this organization"
|
||||
}
|
||||
icon={faServer}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -51,6 +51,8 @@ export const OrgMembersSection = () => {
|
||||
? subscription.identitiesUsed < subscription.identityLimit
|
||||
: true;
|
||||
|
||||
const isEnterprise = subscription?.slug === "enterprise";
|
||||
|
||||
const handleAddMemberModal = () => {
|
||||
if (currentOrg?.authEnforced) {
|
||||
createNotification({
|
||||
@@ -60,7 +62,7 @@ export const OrgMembersSection = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isMoreUsersAllowed || !isMoreIdentitiesAllowed) {
|
||||
if ((!isMoreUsersAllowed || !isMoreIdentitiesAllowed) && !isEnterprise) {
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
description: "You can add more members if you upgrade your Infisical plan."
|
||||
});
|
||||
|
@@ -42,7 +42,9 @@ export const UserAuditLogsSection = withPermission(
|
||||
<LogsSection
|
||||
showFilters={showFilter}
|
||||
filterClassName="bg-mineshaft-900 static"
|
||||
presetActor={orgMembership.user.id}
|
||||
presets={{
|
||||
actorId: orgMembership.user.id
|
||||
}}
|
||||
isOrgAuditLogs
|
||||
/>
|
||||
</div>
|
||||
|
@@ -1,14 +1,29 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { useState } from "react";
|
||||
import { Control, Controller, UseFormReset } from "react-hook-form";
|
||||
import { faFilterCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Control, Controller, UseFormReset, UseFormWatch } from "react-hook-form";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faChevronDown,
|
||||
faFilterCircleXmark
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { Button, DatePicker, FormControl, Select, SelectItem } from "@app/components/v2";
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useGetAuditLogActorFilterOpts } from "@app/hooks/api";
|
||||
import { eventToNameMap, userAgentTTypeoNameMap } from "@app/hooks/api/auditLogs/constants";
|
||||
import { ActorType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { ActorType, EventType } from "@app/hooks/api/auditLogs/enums";
|
||||
import { Actor } from "@app/hooks/api/auditLogs/types";
|
||||
|
||||
import { AuditLogFilterFormData } from "./types";
|
||||
@@ -20,13 +35,17 @@ const userAgentTypes = Object.entries(userAgentTTypeoNameMap).map(([value, label
|
||||
}));
|
||||
|
||||
type Props = {
|
||||
presetActor?: string;
|
||||
presets?: {
|
||||
actorId?: string;
|
||||
eventType?: EventType[];
|
||||
};
|
||||
className?: string;
|
||||
control: Control<AuditLogFilterFormData>;
|
||||
reset: UseFormReset<AuditLogFilterFormData>;
|
||||
watch: UseFormWatch<AuditLogFilterFormData>;
|
||||
};
|
||||
|
||||
export const LogsFilter = ({ presetActor, className, control, reset }: Props) => {
|
||||
export const LogsFilter = ({ presets, className, control, reset, watch }: Props) => {
|
||||
const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
|
||||
const [isEndDatePickerOpen, setIsEndDatePickerOpen] = useState(false);
|
||||
|
||||
@@ -71,6 +90,8 @@ export const LogsFilter = ({ presetActor, className, control, reset }: Props) =>
|
||||
}
|
||||
};
|
||||
|
||||
const selectedEventTypes = watch("eventType") as EventType[] | undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
@@ -82,29 +103,69 @@ export const LogsFilter = ({ presetActor, className, control, reset }: Props) =>
|
||||
<Controller
|
||||
control={control}
|
||||
name="eventType"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Event"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
className="w-40"
|
||||
>
|
||||
<Select
|
||||
{...(field.value ? { value: field.value } : { placeholder: "Select" })}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full border border-mineshaft-500 bg-mineshaft-700 text-mineshaft-100"
|
||||
>
|
||||
{eventTypes.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
render={({ field }) => (
|
||||
<FormControl label="Events">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-500 bg-mineshaft-700 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
|
||||
{selectedEventTypes?.length === 1
|
||||
? eventTypes.find((eventType) => eventType.value === selectedEventTypes[0])
|
||||
?.label
|
||||
: selectedEventTypes?.length === 0
|
||||
? "Select event types"
|
||||
: `${selectedEventTypes?.length} events selected`}
|
||||
<FontAwesomeIcon icon={faChevronDown} className="ml-2 text-xs" />
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="z-[100] max-h-80 overflow-hidden">
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{eventTypes && eventTypes.length > 0 ? (
|
||||
eventTypes.map((eventType) => {
|
||||
const isSelected = selectedEventTypes?.includes(
|
||||
eventType.value as EventType
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onSelect={(event) => eventTypes.length > 1 && event.preventDefault()}
|
||||
onClick={() => {
|
||||
if (selectedEventTypes?.includes(eventType.value as EventType)) {
|
||||
field.onChange(
|
||||
selectedEventTypes?.filter((e: string) => e !== eventType.value)
|
||||
);
|
||||
} else {
|
||||
field.onChange([...(selectedEventTypes || []), eventType.value]);
|
||||
}
|
||||
}}
|
||||
key={`event-type-${eventType.value}`}
|
||||
icon={
|
||||
isSelected ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faCheckCircle}
|
||||
className="pr-0.5 text-primary"
|
||||
/>
|
||||
) : (
|
||||
<div className="pl-[1.01rem]" />
|
||||
)
|
||||
}
|
||||
iconPos="left"
|
||||
className="w-[28.4rem] text-sm"
|
||||
>
|
||||
{eventType.label}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!isLoading && data && data.length > 0 && !presetActor && (
|
||||
|
||||
{!isLoading && data && data.length > 0 && !presets?.actorId && (
|
||||
<Controller
|
||||
control={control}
|
||||
name="actor"
|
||||
@@ -207,8 +268,8 @@ export const LogsFilter = ({ presetActor, className, control, reset }: Props) =>
|
||||
leftIcon={<FontAwesomeIcon icon={faFilterCircleXmark} />}
|
||||
onClick={() =>
|
||||
reset({
|
||||
eventType: undefined,
|
||||
actor: presetActor,
|
||||
eventType: presets?.eventType || [],
|
||||
actor: presets?.actorId,
|
||||
userAgentType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user