Compare commits

...

99 Commits

Author SHA1 Message Date
Sheen
d6b7045461 Merge pull request #2450 from Infisical/fix/address-client-side-error-secret-approval-page
fix: add loading screen for user context
2024-09-19 02:59:18 +08:00
Sheen Capadngan
bd9c9ea1f4 fix: add loading screen for user context 2024-09-19 02:33:03 +08:00
Daniel Hougaard
5740d2b4e4 Merge pull request #2429 from Infisical/daniel/integration-ui-improvements
feat: integration details page with logging
2024-09-17 14:29:26 +04:00
Daniel Hougaard
09887a7405 Update ConfiguredIntegrationItem.tsx 2024-09-16 23:05:38 +04:00
Daniel Hougaard
38ee3a005e Requested changes 2024-09-16 22:26:36 +04:00
Sheen
10e7999334 Merge pull request #2439 from Infisical/misc/address-slack-env-related-error
misc: addressed slack env config validation error
2024-09-17 02:16:07 +08:00
Sheen Capadngan
8c458588ab misc: removed from .env.example 2024-09-17 01:25:16 +08:00
Sheen Capadngan
2381a2e4ba misc: addressed slack env config validation error 2024-09-17 01:19:45 +08:00
Sheen
9ef8812205 Merge pull request #2434 from Infisical/misc/added-handling-of-no-project-access
misc: added handling of no project access for redirects
2024-09-17 01:07:35 +08:00
Sheen Capadngan
37a204e49e misc: addressed review comment 2024-09-16 23:27:10 +08:00
Daniel Hougaard
11927f341a Merge pull request #2433 from Infisical/daniel/aws-sm-secrets-prefix
feat(integrations): aws secrets manager secrets prefixing support
2024-09-16 18:24:40 +04:00
Daniel Hougaard
6fc17a4964 Update license-fns.ts 2024-09-16 18:15:35 +04:00
Sheen
eb00232db6 Merge pull request #2437 from Infisical/misc/allow-direct-project-assignment-even-with-group
misc: allow direct project assignment even with group access
2024-09-16 22:04:43 +08:00
Meet Shah
4fd245e493 Merge pull request #2418 from meetcshah19/meet/allow-unlimited-users
Don't enforce max user and identity limits
2024-09-16 19:27:02 +05:30
Sheen Capadngan
d92c57d051 misc: allow direct project assignment even with group access 2024-09-16 21:35:45 +08:00
Daniel Hougaard
beaef1feb0 Merge pull request #2436 from Infisical/daniel/fix-project-role-desc-update
fix: updating role description
2024-09-16 16:47:21 +04:00
Daniel Hougaard
033fd5e7a4 fix: updating role description 2024-09-16 16:42:11 +04:00
Sheen Capadngan
f49f3c926c misc: added handling of no project access for redirects 2024-09-16 20:00:54 +08:00
Sheen
280d44f1e5 Merge pull request #2432 from Infisical/fix/addressed-group-view-issue-in-approval-creation
fix: address group view issue encountered during policy creation
2024-09-16 19:40:03 +08:00
Daniel Hougaard
4eea0dc544 fix(integrations): improved github repos fetching 2024-09-16 15:37:44 +04:00
Daniel Hougaard
8a33f1a591 feat(integrations): aws secrets manager prefix support 2024-09-16 15:36:41 +04:00
Daniel Hougaard
74653e7ed1 Minor ui improvements 2024-09-16 13:56:23 +04:00
Sheen Capadngan
56ff11d63f fix: address group view issue encountered during approval creation 2024-09-16 14:17:14 +08:00
Maidul Islam
1ecce285f0 Merge pull request #2426 from scott-ray-wilson/secret-env-access-warning
Fix: Restricted Secret Environment UI Corrections
2024-09-15 19:08:23 -04:00
Scott Wilson
b5c9b6a1bd fix: hide envs without read permission in secret main page nav header dropdown 2024-09-15 12:36:42 -07:00
Scott Wilson
e12ac6c07e fix: hide envs without read permission in the env filter dropdown 2024-09-15 12:29:24 -07:00
Daniel Hougaard
8a0b1bb427 Update IntegrationAuditLogsSection.tsx 2024-09-15 20:34:08 +04:00
Daniel Hougaard
1f6faadf81 Cleanup 2024-09-15 20:24:23 +04:00
Daniel Hougaard
8f3b7e1698 feat: audit logs event metadata & remapping support 2024-09-15 20:01:43 +04:00
Daniel Hougaard
24c460c695 feat: integration details page 2024-09-15 20:00:43 +04:00
Daniel Hougaard
8acceab1e7 fix: updated last used to be considered last success sync 2024-09-15 19:57:56 +04:00
Daniel Hougaard
d60aba9339 fix: added missing integration metadata attributes 2024-09-15 19:57:36 +04:00
Daniel Hougaard
3a228f7521 feat: improved audit logs 2024-09-15 19:57:02 +04:00
Daniel Hougaard
3f7ac0f142 feat: integration synced log event 2024-09-15 19:52:43 +04:00
Daniel Hougaard
63cf535ebb feat: platform-level actor for logs 2024-09-15 19:52:13 +04:00
Daniel Hougaard
69a2a46c47 Update organization-router.ts 2024-09-15 19:51:54 +04:00
Daniel Hougaard
d081077273 feat: integration sync logs 2024-09-15 19:51:38 +04:00
Daniel Hougaard
75034f9350 feat: more expendable audit logs 2024-09-15 19:50:03 +04:00
Daniel Hougaard
eacd7b0c6a feat: made audit logs more searchable with better filters 2024-09-15 19:49:35 +04:00
Daniel Hougaard
5bad77083c feat: more expendable audit logs 2024-09-15 19:49:07 +04:00
Maidul Islam
ea480c222b update default to 20 per page 2024-09-14 23:26:30 -04:00
Scott Wilson
1fb644af4a include secret path in dependency array 2024-09-14 07:01:02 -07:00
Maidul Islam
a6f4a95821 Merge pull request #2427 from Infisical/cancel-button-fix
fixed inactive cancel button
2024-09-14 09:52:01 -04:00
Scott Wilson
8578208f2d fix: hide environments that users does not have read access too 2024-09-14 06:50:45 -07:00
Vladyslav Matsiiako
fc4189ba0f fixed inactive cancel button 2024-09-13 21:31:08 -07:00
Meet
b9ecf42fb6 fix: unlimited users and identities only for enterprise and remove frontend check 2024-09-14 05:54:50 +05:30
Maidul Islam
008e18638f Merge pull request #2425 from Infisical/daniel/fix-invalid-role-creation
fix(project-roles): creation of invalid project roles
2024-09-13 16:42:02 -04:00
Daniel Hougaard
ac3b9c25dd Update permissions.mdx 2024-09-14 00:33:52 +04:00
Daniel Hougaard
f4997dec12 Update project-role-service.ts 2024-09-13 23:59:08 +04:00
Daniel Hougaard
fcf405c630 docs(permissions): creation of project roles with invalid permissions 2024-09-13 23:56:19 +04:00
Daniel Hougaard
efc6876260 fix(api): creation of project roles with invalid permissions 2024-09-13 23:55:56 +04:00
Daniel Hougaard
1025759efb Feat: Integration Audit Logs 2024-09-13 21:00:47 +04:00
Scott Wilson
8bab6d87bb Merge pull request #2424 from scott-ray-wilson/secrets-pagination-fix
Fix: Account for secret import count in secrets offset
2024-09-13 07:37:42 -07:00
Scott Wilson
39a49f12f5 fix: account for secret import count in secrets offset 2024-09-13 07:27:52 -07:00
Meet Shah
cfd841ea08 Merge pull request #2419 from meetcshah19/meet/add-empty-value-log-gcp
chore: add log on empty value being pushed to gcp
2024-09-13 19:53:38 +05:30
Maidul Islam
4d67c03e3e Merge pull request #2423 from scott-ray-wilson/secrets-pagination
Feature: Secrets Overview Page Pagination/Optimizations
2024-09-13 09:56:48 -04:00
Scott Wilson
8826bc5d60 fix: include imports in secret pagination, and rectify tag/value search not working for secrets 2024-09-13 06:25:13 -07:00
Maidul Islam
03fdce67f1 Merge pull request #2417 from akhilmhdh/fix/saml-entra
fix: resolved entra failing
2024-09-13 09:08:07 -04:00
Sheen
72f3f7980e Merge pull request #2414 from Infisical/misc/address-minor-cert-lint-issues
misc: addressed minor cert lint issues
2024-09-13 20:57:40 +08:00
Meet
f1aa2fbd84 chore: better log string 2024-09-13 15:34:12 +05:30
=
217de6250f feat: pagination for main secret page 2024-09-13 14:12:53 +05:30
Scott Wilson
f742bd01d9 refactor to useCallback select instead of queryFn 2024-09-12 22:47:23 -07:00
Scott Wilson
3fe53d5183 remove unused import 2024-09-12 22:08:16 -07:00
Scott Wilson
a5f5f803df feature: secret overview page pagination/optimizations 2024-09-12 21:44:38 -07:00
Sheen Capadngan
c37e3ba635 misc: addressed comments 2024-09-13 12:44:12 +08:00
BlackMagiq
55279e5e41 Merge pull request #2422 from Infisical/pki-docs-improvement
Update README (Expand on PKI / New Features)
2024-09-12 20:16:41 -07:00
Tuan Dang
88fb37e8c6 Made changes as per review 2024-09-12 20:14:25 -07:00
Tuan Dang
6271dcc25d Fix mint.json openapi link back 2024-09-12 20:02:40 -07:00
Tuan Dang
0f7faa6bfe Update README to include newer features, expand on PKI, separate PKI endpoints into separate section in API reference 2024-09-12 19:58:55 -07:00
Tuan Dang
4ace339d5b Update README to include newer features, expand on PKI, separate PKI endpoints into separate section in API reference 2024-09-12 19:57:37 -07:00
=
e8c0d1ece9 fix: resolved entra failing 2024-09-13 07:18:49 +05:30
Maidul Islam
bb1977976c Merge pull request #2421 from Infisical/maidful-edwdwqdhwjq
revert PR #2412
2024-09-12 20:43:38 -04:00
Tuan Dang
bb3da75870 Minor text updates 2024-09-12 17:26:56 -07:00
Maidul Islam
088e888560 Merge pull request #2420 from scott-ray-wilson/identity-pagination-fix
Fix: Apply Project Identity Pagination Prior to Left Join of Roles
2024-09-12 20:23:03 -04:00
Maidul Islam
180241fdf0 revert PR #2412 2024-09-13 00:15:26 +00:00
Scott Wilson
93f27a7ee8 improvement: make limit conditional 2024-09-12 16:19:22 -07:00
Scott Wilson
ed3bc8dd27 fix: apply project identity offset/limit separate from left joins 2024-09-12 16:11:58 -07:00
Maidul Islam
8dc4809ec8 Merge pull request #2416 from akhilmhdh/ui/combobox
UI/combobox
2024-09-12 18:50:43 -04:00
Meet
a55d64e430 chore: add log on empty value being pushed to gcp 2024-09-13 03:52:09 +05:30
Scott Wilson
02d54da74a resolve change requests 2024-09-12 15:22:05 -07:00
=
d660168700 fix: org invite check only when needed 2024-09-13 00:35:48 +05:30
=
1c75fc84f0 feat: added a temporary combobox for identity addition to project 2024-09-13 00:35:48 +05:30
Sheen Capadngan
f63da87c7f Merge remote-tracking branch 'origin/main' into misc/address-minor-cert-lint-issues 2024-09-13 01:46:00 +08:00
Sheen
53b9fe2dec Merge pull request #2401 from Infisical/feat/add-key-usages-for-template-and-cert
feat: add support for configuring certificate key usage and extended key usage
2024-09-13 00:55:19 +08:00
Sheen Capadngan
87dc0eed7e fix: addressed tslint errors 2024-09-12 23:25:26 +08:00
Maidul Islam
f2dd6f94a4 Merge pull request #2409 from scott-ray-wilson/identity-pagination
Feature: Project and Org Identities Table Additions: Pagination, Search and Sort
2024-09-12 11:22:45 -04:00
Sheen Capadngan
ac26ae3893 misc: addressed minor cert lint issues 2024-09-12 23:16:49 +08:00
Scott Wilson
4c65e9910a resolve merge conflict 2024-09-12 08:03:10 -07:00
Daniel Hougaard
5e5ab29ab9 Feat: Integration UI improvements 2024-09-12 13:09:00 +04:00
Daniel Hougaard
5150c102e6 Merge pull request #2380 from Infisical/daniel/invite-multiple-members-to-project
feat: invite multiple members to projects with role assignment
2024-09-12 11:16:41 +04:00
Sheen Capadngan
a79087670e misc: addressed comments and doc changes 2024-09-12 13:27:39 +08:00
Scott Wilson
ce9b66ef14 address feedback suggestions 2024-09-11 12:40:27 -07:00
Sheen Capadngan
bfa533e9d2 misc: api property description 2024-09-11 22:59:19 +08:00
Sheen Capadngan
a8759e7410 feat: added support for custom extended key usages 2024-09-11 22:38:36 +08:00
Scott Wilson
16182a9d1d feature: project and org identity pagination, search and sort 2024-09-11 07:22:08 -07:00
Sheen Capadngan
c1f61f2db4 feat: added custom key usages support for sign endpoint 2024-09-11 20:26:33 +08:00
Sheen Capadngan
4e6b289e1b misc: integrated custom key usages for issue-cert endpoint 2024-09-11 01:57:16 +08:00
Sheen Capadngan
6fab7d9507 Merge remote-tracking branch 'origin/main' into feat/add-key-usages-for-template-and-cert 2024-09-11 00:22:04 +08:00
Sheen Capadngan
1c749c84f2 misc: key usages setup 2024-09-10 21:42:41 +08:00
116 changed files with 4226 additions and 1264 deletions

View File

@@ -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
View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@@ -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({

View File

@@ -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: {

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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 }) => ({

View File

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

View File

@@ -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 */

View File

@@ -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"

View File

@@ -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,

View File

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

View File

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

View File

@@ -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,

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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)

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

@@ -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",

View File

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

View File

@@ -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,

View File

@@ -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 = {

View File

@@ -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(),

View File

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

View File

@@ -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 = {

View File

@@ -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",

View File

@@ -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),

View File

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

View File

@@ -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 ({

View File

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

View File

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

View File

@@ -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 ({

View File

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

View File

@@ -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)

View File

@@ -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`,
{

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 },
{

View File

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

View File

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

View File

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

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

View File

@@ -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",

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
)}

View File

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

View File

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

View File

@@ -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 } = {

View File

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

View File

@@ -1,35 +1,58 @@
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useInfiniteQuery, UseInfiniteQueryOptions, useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { 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
});
};

View File

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

View File

@@ -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 = {

View File

@@ -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 = {

View File

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

View File

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

View File

@@ -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 = {

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,14 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import { createNotification } from "@app/components/notifications";
import { 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`),

View File

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

View File

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

View File

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

View File

@@ -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
};
})
})),
[]
)
}))
});

View File

@@ -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 }),
{}
),
[]
)
}))
});

View File

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

View File

@@ -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 }) =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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