mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-05 07:30:33 +00:00
Compare commits
45 Commits
daniel/int
...
infisical/
Author | SHA1 | Date | |
---|---|---|---|
|
8bab6d87bb | ||
|
39a49f12f5 | ||
|
cfd841ea08 | ||
|
4d67c03e3e | ||
|
8826bc5d60 | ||
|
03fdce67f1 | ||
|
72f3f7980e | ||
|
f1aa2fbd84 | ||
|
217de6250f | ||
|
f742bd01d9 | ||
|
3fe53d5183 | ||
|
a5f5f803df | ||
|
c37e3ba635 | ||
|
55279e5e41 | ||
|
88fb37e8c6 | ||
|
6271dcc25d | ||
|
0f7faa6bfe | ||
|
4ace339d5b | ||
|
e8c0d1ece9 | ||
|
bb1977976c | ||
|
bb3da75870 | ||
|
088e888560 | ||
|
180241fdf0 | ||
|
93f27a7ee8 | ||
|
ed3bc8dd27 | ||
|
8dc4809ec8 | ||
|
a55d64e430 | ||
|
02d54da74a | ||
|
d660168700 | ||
|
1c75fc84f0 | ||
|
f63da87c7f | ||
|
53b9fe2dec | ||
|
87dc0eed7e | ||
|
f2dd6f94a4 | ||
|
ac26ae3893 | ||
|
4c65e9910a | ||
|
a79087670e | ||
|
ce9b66ef14 | ||
|
bfa533e9d2 | ||
|
a8759e7410 | ||
|
16182a9d1d | ||
|
c1f61f2db4 | ||
|
4e6b289e1b | ||
|
6fab7d9507 | ||
|
1c749c84f2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -63,6 +63,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Editor specific
|
# Editor specific
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
.idea/*
|
||||||
|
|
||||||
frontend-build
|
frontend-build
|
||||||
|
|
||||||
|
@@ -0,0 +1,85 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
// Certificate template
|
||||||
|
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
|
||||||
|
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.CertificateTemplate, (tb) => {
|
||||||
|
if (!hasKeyUsagesCol) {
|
||||||
|
tb.specificType("keyUsages", "text[]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasExtendedKeyUsagesCol) {
|
||||||
|
tb.specificType("extendedKeyUsages", "text[]");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasKeyUsagesCol) {
|
||||||
|
await knex(TableName.CertificateTemplate).update({
|
||||||
|
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasExtendedKeyUsagesCol) {
|
||||||
|
await knex(TableName.CertificateTemplate).update({
|
||||||
|
extendedKeyUsages: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate
|
||||||
|
const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages");
|
||||||
|
const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages");
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (tb) => {
|
||||||
|
if (!doesCertTableHaveKeyUsages) {
|
||||||
|
tb.specificType("keyUsages", "text[]");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doesCertTableHaveExtendedKeyUsages) {
|
||||||
|
tb.specificType("extendedKeyUsages", "text[]");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!doesCertTableHaveKeyUsages) {
|
||||||
|
await knex(TableName.Certificate).update({
|
||||||
|
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doesCertTableHaveExtendedKeyUsages) {
|
||||||
|
await knex(TableName.Certificate).update({
|
||||||
|
extendedKeyUsages: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
// Certificate Template
|
||||||
|
const hasKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "keyUsages");
|
||||||
|
const hasExtendedKeyUsagesCol = await knex.schema.hasColumn(TableName.CertificateTemplate, "extendedKeyUsages");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.CertificateTemplate, (t) => {
|
||||||
|
if (hasKeyUsagesCol) {
|
||||||
|
t.dropColumn("keyUsages");
|
||||||
|
}
|
||||||
|
if (hasExtendedKeyUsagesCol) {
|
||||||
|
t.dropColumn("extendedKeyUsages");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Certificate
|
||||||
|
const doesCertTableHaveKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "keyUsages");
|
||||||
|
const doesCertTableHaveExtendedKeyUsages = await knex.schema.hasColumn(TableName.Certificate, "extendedKeyUsages");
|
||||||
|
await knex.schema.alterTable(TableName.Certificate, (t) => {
|
||||||
|
if (doesCertTableHaveKeyUsages) {
|
||||||
|
t.dropColumn("keyUsages");
|
||||||
|
}
|
||||||
|
if (doesCertTableHaveExtendedKeyUsages) {
|
||||||
|
t.dropColumn("extendedKeyUsages");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -16,7 +16,9 @@ export const CertificateTemplatesSchema = z.object({
|
|||||||
subjectAlternativeName: z.string(),
|
subjectAlternativeName: z.string(),
|
||||||
ttl: z.string(),
|
ttl: z.string(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
keyUsages: z.string().array().nullable().optional(),
|
||||||
|
extendedKeyUsages: z.string().array().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>;
|
export type TCertificateTemplates = z.infer<typeof CertificateTemplatesSchema>;
|
||||||
|
@@ -22,7 +22,9 @@ export const CertificatesSchema = z.object({
|
|||||||
revocationReason: z.number().nullable().optional(),
|
revocationReason: z.number().nullable().optional(),
|
||||||
altNames: z.string().default("").nullable().optional(),
|
altNames: z.string().default("").nullable().optional(),
|
||||||
caCertId: z.string().uuid(),
|
caCertId: z.string().uuid(),
|
||||||
certificateTemplateId: z.string().uuid().nullable().optional()
|
certificateTemplateId: z.string().uuid().nullable().optional(),
|
||||||
|
keyUsages: z.string().array().nullable().optional(),
|
||||||
|
extendedKeyUsages: z.string().array().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
export type TCertificates = z.infer<typeof CertificatesSchema>;
|
||||||
|
@@ -11,6 +11,30 @@ export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
|
|||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Get CRL in DER format (deprecated)",
|
||||||
|
params: z.object({
|
||||||
|
crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.instanceof(Buffer)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
|
||||||
|
|
||||||
|
res.header("Content-Type", "application/pkix-crl");
|
||||||
|
|
||||||
|
return Buffer.from(crl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:crlId/der",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
schema: {
|
schema: {
|
||||||
description: "Get CRL in DER format",
|
description: "Get CRL in DER format",
|
||||||
params: z.object({
|
params: z.object({
|
||||||
|
@@ -100,9 +100,20 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
async (req, profile, cb) => {
|
async (req, profile, cb) => {
|
||||||
try {
|
try {
|
||||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||||
const email = 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(
|
logger.info(
|
||||||
{
|
{
|
||||||
err: new Error("Invalid saml request. Missing email or first name"),
|
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}`
|
`email: ${email} firstName: ${profile.firstName as string}`
|
||||||
);
|
);
|
||||||
throw new BadRequestError({ message: "Invalid request. Missing email or first name" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
|
||||||
externalId: profile.nameID,
|
externalId: profile.nameID,
|
||||||
email,
|
email,
|
||||||
firstName: profile.firstName as string,
|
firstName,
|
||||||
lastName: profile.lastName as string,
|
lastName: lastName as string,
|
||||||
relayState: (req.body as { RelayState?: string }).RelayState,
|
relayState: (req.body as { RelayState?: string }).RelayState,
|
||||||
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
authProvider: (req as unknown as FastifyRequest).ssoConfig?.authProvider as string,
|
||||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId as string
|
||||||
|
@@ -363,7 +363,12 @@ export const ORGANIZATIONS = {
|
|||||||
membershipId: "The ID of the membership to delete."
|
membershipId: "The ID of the membership to delete."
|
||||||
},
|
},
|
||||||
LIST_IDENTITY_MEMBERSHIPS: {
|
LIST_IDENTITY_MEMBERSHIPS: {
|
||||||
orgId: "The ID of the organization to get identity memberships from."
|
orgId: "The ID of the organization to get identity memberships from.",
|
||||||
|
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
|
||||||
|
limit: "The number of identity memberships to return.",
|
||||||
|
orderBy: "The column to order identity memberships by.",
|
||||||
|
orderDirection: "The direction identity memberships will be sorted in.",
|
||||||
|
search: "The text string that identity membership names will be filtered by."
|
||||||
},
|
},
|
||||||
GET_PROJECTS: {
|
GET_PROJECTS: {
|
||||||
organizationId: "The ID of the organization to get projects from."
|
organizationId: "The ID of the organization to get projects from."
|
||||||
@@ -472,7 +477,12 @@ export const PROJECT_USERS = {
|
|||||||
|
|
||||||
export const PROJECT_IDENTITIES = {
|
export const PROJECT_IDENTITIES = {
|
||||||
LIST_IDENTITY_MEMBERSHIPS: {
|
LIST_IDENTITY_MEMBERSHIPS: {
|
||||||
projectId: "The ID of the project to get identity memberships from."
|
projectId: "The ID of the project to get identity memberships from.",
|
||||||
|
offset: "The offset to start from. If you enter 10, it will start from the 10th identity membership.",
|
||||||
|
limit: "The number of identity memberships to return.",
|
||||||
|
orderBy: "The column to order identity memberships by.",
|
||||||
|
orderDirection: "The direction identity memberships will be sorted in.",
|
||||||
|
search: "The text string that identity membership names will be filtered by."
|
||||||
},
|
},
|
||||||
GET_IDENTITY_MEMBERSHIP_BY_ID: {
|
GET_IDENTITY_MEMBERSHIP_BY_ID: {
|
||||||
identityId: "The ID of the identity to get the membership for.",
|
identityId: "The ID of the identity to get the membership for.",
|
||||||
@@ -1073,6 +1083,10 @@ export const CERTIFICATE_AUTHORITIES = {
|
|||||||
certificateChain: "The certificate chain of the CA",
|
certificateChain: "The certificate chain of the CA",
|
||||||
serialNumber: "The serial number of the CA certificate"
|
serialNumber: "The serial number of the CA certificate"
|
||||||
},
|
},
|
||||||
|
GET_CERT_BY_ID: {
|
||||||
|
caId: "The ID of the CA to get the CA certificate from",
|
||||||
|
caCertId: "The ID of the CA certificate to get"
|
||||||
|
},
|
||||||
GET_CA_CERTS: {
|
GET_CA_CERTS: {
|
||||||
caId: "The ID of the CA to get the CA certificates for",
|
caId: "The ID of the CA to get the CA certificates for",
|
||||||
certificate: "The certificate body of the CA certificate",
|
certificate: "The certificate body of the CA certificate",
|
||||||
@@ -1112,11 +1126,15 @@ export const CERTIFICATE_AUTHORITIES = {
|
|||||||
issuingCaCertificate: "The certificate of the issuing CA",
|
issuingCaCertificate: "The certificate of the issuing CA",
|
||||||
certificateChain: "The certificate chain of the issued certificate",
|
certificateChain: "The certificate chain of the issued certificate",
|
||||||
privateKey: "The private key of the issued certificate",
|
privateKey: "The private key of the issued certificate",
|
||||||
serialNumber: "The serial number of the issued certificate"
|
serialNumber: "The serial number of the issued certificate",
|
||||||
|
keyUsages: "The key usage extension of the certificate",
|
||||||
|
extendedKeyUsages: "The extended key usage extension of the certificate"
|
||||||
},
|
},
|
||||||
SIGN_CERT: {
|
SIGN_CERT: {
|
||||||
caId: "The ID of the CA to issue the certificate from",
|
caId: "The ID of the CA to issue the certificate from",
|
||||||
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
|
pkiCollectionId: "The ID of the PKI collection to add the certificate to",
|
||||||
|
keyUsages: "The key usage extension of the certificate",
|
||||||
|
extendedKeyUsages: "The extended key usage extension of the certificate",
|
||||||
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
|
csr: "The pem-encoded CSR to sign with the CA to be used for certificate issuance",
|
||||||
friendlyName: "A friendly name for the certificate",
|
friendlyName: "A friendly name for the certificate",
|
||||||
commonName: "The common name (CN) for the certificate",
|
commonName: "The common name (CN) for the certificate",
|
||||||
@@ -1166,7 +1184,10 @@ export const CERTIFICATE_TEMPLATES = {
|
|||||||
name: "The name of the template",
|
name: "The name of the template",
|
||||||
commonName: "The regular expression string to use for validating common names",
|
commonName: "The regular expression string to use for validating common names",
|
||||||
subjectAlternativeName: "The regular expression string to use for validating subject alternative names",
|
subjectAlternativeName: "The regular expression string to use for validating subject alternative names",
|
||||||
ttl: "The max TTL for the template"
|
ttl: "The max TTL for the template",
|
||||||
|
keyUsages: "The key usage constraint or default value for when template is used during certificate issuance",
|
||||||
|
extendedKeyUsages:
|
||||||
|
"The extended key usage constraint or default value for when template is used during certificate issuance"
|
||||||
},
|
},
|
||||||
GET: {
|
GET: {
|
||||||
certificateTemplateId: "The ID of the certificate template to get"
|
certificateTemplateId: "The ID of the certificate template to get"
|
||||||
@@ -1178,7 +1199,11 @@ export const CERTIFICATE_TEMPLATES = {
|
|||||||
name: "The updated name of the template",
|
name: "The updated name of the template",
|
||||||
commonName: "The updated regular expression string for validating common names",
|
commonName: "The updated regular expression string for validating common names",
|
||||||
subjectAlternativeName: "The updated regular expression string for validating subject alternative names",
|
subjectAlternativeName: "The updated regular expression string for validating subject alternative names",
|
||||||
ttl: "The updated max TTL for the template"
|
ttl: "The updated max TTL for the template",
|
||||||
|
keyUsages:
|
||||||
|
"The updated key usage constraint or default value for when template is used during certificate issuance",
|
||||||
|
extendedKeyUsages:
|
||||||
|
"The updated extended key usage constraint or default value for when template is used during certificate issuance"
|
||||||
},
|
},
|
||||||
DELETE: {
|
DELETE: {
|
||||||
certificateTemplateId: "The ID of the certificate template to delete"
|
certificateTemplateId: "The ID of the certificate template to delete"
|
||||||
|
@@ -52,3 +52,8 @@ export enum SecretSharingAccessType {
|
|||||||
Anyone = "anyone",
|
Anyone = "anyone",
|
||||||
Organization = "organization"
|
Organization = "organization"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrderByDirection {
|
||||||
|
ASC = "asc",
|
||||||
|
DESC = "desc"
|
||||||
|
}
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
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 { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { 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 { CaRenewalType, CaStatus, CaType } from "@app/services/certificate-authority/certificate-authority-types";
|
||||||
import {
|
import {
|
||||||
validateAltNamesField,
|
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({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:caId",
|
url: "/:caId",
|
||||||
@@ -573,7 +601,9 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
||||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
|
||||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
|
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter),
|
||||||
|
keyUsages: z.nativeEnum(CertKeyUsage).array().optional(),
|
||||||
|
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -653,7 +683,9 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
|||||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
||||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
|
||||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
|
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter),
|
||||||
|
keyUsages: z.nativeEnum(CertKeyUsage).array().optional(),
|
||||||
|
extendedKeyUsages: z.nativeEnum(CertExtendedKeyUsage).array().optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
@@ -7,7 +7,7 @@ import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
|||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { CrlReason } from "@app/services/certificate/certificate-types";
|
import { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types";
|
||||||
import {
|
import {
|
||||||
validateAltNamesField,
|
validateAltNamesField,
|
||||||
validateCaDateField
|
validateCaDateField
|
||||||
@@ -86,7 +86,17 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.ttl),
|
||||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notBefore),
|
||||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter)
|
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.notAfter),
|
||||||
|
keyUsages: z
|
||||||
|
.nativeEnum(CertKeyUsage)
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.keyUsages),
|
||||||
|
extendedKeyUsages: z
|
||||||
|
.nativeEnum(CertExtendedKeyUsage)
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.describe(CERTIFICATE_AUTHORITIES.ISSUE_CERT.extendedKeyUsages)
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@@ -177,7 +187,17 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
|||||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.ttl),
|
||||||
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
|
notBefore: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notBefore),
|
||||||
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter)
|
notAfter: validateCaDateField.optional().describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.notAfter),
|
||||||
|
keyUsages: z
|
||||||
|
.nativeEnum(CertKeyUsage)
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.keyUsages),
|
||||||
|
extendedKeyUsages: z
|
||||||
|
.nativeEnum(CertExtendedKeyUsage)
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.describe(CERTIFICATE_AUTHORITIES.SIGN_CERT.extendedKeyUsages)
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
@@ -7,6 +7,7 @@ import { CERTIFICATE_TEMPLATES } from "@app/lib/api-docs";
|
|||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||||
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
import { sanitizedCertificateTemplate } from "@app/services/certificate-template/certificate-template-schema";
|
||||||
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
import { validateTemplateRegexField } from "@app/services/certificate-template/certificate-template-validators";
|
||||||
|
|
||||||
@@ -74,7 +75,19 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
ttl: z
|
ttl: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl)
|
.describe(CERTIFICATE_TEMPLATES.CREATE.ttl),
|
||||||
|
keyUsages: z
|
||||||
|
.nativeEnum(CertKeyUsage)
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.default([CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT])
|
||||||
|
.describe(CERTIFICATE_TEMPLATES.CREATE.keyUsages),
|
||||||
|
extendedKeyUsages: z
|
||||||
|
.nativeEnum(CertExtendedKeyUsage)
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.default([])
|
||||||
|
.describe(CERTIFICATE_TEMPLATES.CREATE.extendedKeyUsages)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: sanitizedCertificateTemplate
|
200: sanitizedCertificateTemplate
|
||||||
@@ -130,7 +143,13 @@ export const registerCertificateTemplateRouter = async (server: FastifyZodProvid
|
|||||||
.string()
|
.string()
|
||||||
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
.refine((val) => ms(val) > 0, "TTL must be a positive number")
|
||||||
.optional()
|
.optional()
|
||||||
.describe(CERTIFICATE_TEMPLATES.UPDATE.ttl)
|
.describe(CERTIFICATE_TEMPLATES.UPDATE.ttl),
|
||||||
|
keyUsages: z.nativeEnum(CertKeyUsage).array().optional().describe(CERTIFICATE_TEMPLATES.UPDATE.keyUsages),
|
||||||
|
extendedKeyUsages: z
|
||||||
|
.nativeEnum(CertExtendedKeyUsage)
|
||||||
|
.array()
|
||||||
|
.optional()
|
||||||
|
.describe(CERTIFICATE_TEMPLATES.UPDATE.extendedKeyUsages)
|
||||||
}),
|
}),
|
||||||
params: z.object({
|
params: z.object({
|
||||||
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
|
certificateTemplateId: z.string().describe(CERTIFICATE_TEMPLATES.UPDATE.certificateTemplateId)
|
||||||
|
@@ -246,12 +246,13 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: true
|
description: true
|
||||||
}).optional(),
|
}).optional(),
|
||||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||||
}).array()
|
}).array(),
|
||||||
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const identities = await server.services.identity.listOrgIdentities({
|
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
@@ -259,7 +260,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
orgId: req.query.orgId
|
orgId: req.query.orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { identities };
|
return { identities: identityMemberships, totalCount };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -2,9 +2,11 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { OrgIdentityOrderBy } from "@app/services/identity/identity-types";
|
||||||
|
|
||||||
export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -24,6 +26,27 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
orgId: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orgId)
|
orgId: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orgId)
|
||||||
}),
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
offset: z.coerce.number().min(0).default(0).describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.offset).optional(),
|
||||||
|
limit: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(1)
|
||||||
|
.max(20000) // TODO: temp limit until combobox added to add identity to project modal, reduce once added
|
||||||
|
.default(100)
|
||||||
|
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.limit)
|
||||||
|
.optional(),
|
||||||
|
orderBy: z
|
||||||
|
.nativeEnum(OrgIdentityOrderBy)
|
||||||
|
.default(OrgIdentityOrderBy.Name)
|
||||||
|
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy)
|
||||||
|
.optional(),
|
||||||
|
orderDirection: z
|
||||||
|
.nativeEnum(OrderByDirection)
|
||||||
|
.default(OrderByDirection.ASC)
|
||||||
|
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection)
|
||||||
|
.optional(),
|
||||||
|
search: z.string().trim().describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.search).optional()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityMemberships: IdentityOrgMembershipsSchema.merge(
|
identityMemberships: IdentityOrgMembershipsSchema.merge(
|
||||||
@@ -37,20 +60,26 @@ export const registerIdentityOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
}).optional(),
|
}).optional(),
|
||||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true })
|
||||||
})
|
})
|
||||||
).array()
|
).array(),
|
||||||
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const identityMemberships = await server.services.identity.listOrgIdentities({
|
const { identityMemberships, totalCount } = await server.services.identity.listOrgIdentities({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
orgId: req.params.orgId
|
orgId: req.params.orgId,
|
||||||
|
limit: req.query.limit,
|
||||||
|
offset: req.query.offset,
|
||||||
|
orderBy: req.query.orderBy,
|
||||||
|
orderDirection: req.query.orderDirection,
|
||||||
|
search: req.query.search
|
||||||
});
|
});
|
||||||
|
|
||||||
return { identityMemberships };
|
return { identityMemberships, totalCount };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -7,11 +7,13 @@ import {
|
|||||||
ProjectMembershipRole,
|
ProjectMembershipRole,
|
||||||
ProjectUserMembershipRolesSchema
|
ProjectUserMembershipRolesSchema
|
||||||
} from "@app/db/schemas";
|
} 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 { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { ProjectIdentityOrderBy } from "@app/services/identity-project/identity-project-types";
|
||||||
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
|
import { ProjectUserMembershipTemporaryMode } from "@app/services/project-membership/project-membership-types";
|
||||||
|
|
||||||
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
import { SanitizedProjectSchema } from "../sanitizedSchemas";
|
||||||
@@ -214,6 +216,32 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
|||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.projectId)
|
projectId: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.projectId)
|
||||||
}),
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
offset: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0)
|
||||||
|
.default(0)
|
||||||
|
.describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.offset)
|
||||||
|
.optional(),
|
||||||
|
limit: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(1)
|
||||||
|
.max(20000) // TODO: temp limit until combobox added to add identity to project modal, reduce once added
|
||||||
|
.default(100)
|
||||||
|
.describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.limit)
|
||||||
|
.optional(),
|
||||||
|
orderBy: z
|
||||||
|
.nativeEnum(ProjectIdentityOrderBy)
|
||||||
|
.default(ProjectIdentityOrderBy.Name)
|
||||||
|
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderBy)
|
||||||
|
.optional(),
|
||||||
|
orderDirection: z
|
||||||
|
.nativeEnum(OrderByDirection)
|
||||||
|
.default(OrderByDirection.ASC)
|
||||||
|
.describe(ORGANIZATIONS.LIST_IDENTITY_MEMBERSHIPS.orderDirection)
|
||||||
|
.optional(),
|
||||||
|
search: z.string().trim().describe(PROJECT_IDENTITIES.LIST_IDENTITY_MEMBERSHIPS.search).optional()
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
identityMemberships: z
|
identityMemberships: z
|
||||||
@@ -239,19 +267,25 @@ export const registerIdentityProjectRouter = async (server: FastifyZodProvider)
|
|||||||
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
|
identity: IdentitiesSchema.pick({ name: true, id: true, authMethod: true }),
|
||||||
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
project: SanitizedProjectSchema.pick({ name: true, id: true })
|
||||||
})
|
})
|
||||||
.array()
|
.array(),
|
||||||
|
totalCount: z.number()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const identityMemberships = await server.services.identityProject.listProjectIdentities({
|
const { identityMemberships, totalCount } = await server.services.identityProject.listProjectIdentities({
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
projectId: req.params.projectId
|
projectId: req.params.projectId,
|
||||||
|
limit: req.query.limit,
|
||||||
|
offset: req.query.offset,
|
||||||
|
orderBy: req.query.orderBy,
|
||||||
|
orderDirection: req.query.orderDirection,
|
||||||
|
search: req.query.search
|
||||||
});
|
});
|
||||||
return { identityMemberships };
|
return { identityMemberships, totalCount };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -15,7 +15,7 @@ import {
|
|||||||
|
|
||||||
/* eslint-disable no-bitwise */
|
/* eslint-disable no-bitwise */
|
||||||
export const createSerialNumber = () => {
|
export const createSerialNumber = () => {
|
||||||
const randomBytes = crypto.randomBytes(32);
|
const randomBytes = crypto.randomBytes(20);
|
||||||
randomBytes[0] &= 0x7f; // ensure the first bit is 0
|
randomBytes[0] &= 0x7f; // ensure the first bit is 0
|
||||||
return randomBytes.toString("hex");
|
return randomBytes.toString("hex");
|
||||||
};
|
};
|
||||||
|
@@ -19,7 +19,13 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
|
|||||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||||
|
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
import { 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 { TCertificateTemplateDALFactory } from "../certificate-template/certificate-template-dal";
|
||||||
import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns";
|
import { validateCertificateDetailsAgainstTemplate } from "../certificate-template/certificate-template-fns";
|
||||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||||
@@ -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
|
* Issue certificate to be imported back in for intermediate CA
|
||||||
*/
|
*/
|
||||||
@@ -776,6 +815,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
notAfter,
|
notAfter,
|
||||||
maxPathLength
|
maxPathLength
|
||||||
}: TSignIntermediateDTO) => {
|
}: TSignIntermediateDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const ca = await certificateAuthorityDAL.findById(caId);
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
if (!ca) throw new BadRequestError({ message: "CA not found" });
|
||||||
|
|
||||||
@@ -850,7 +890,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
throw new BadRequestError({ message: "notAfter date is after CA certificate's notAfter date" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { caPrivateKey } = await getCaCredentials({
|
const { caPrivateKey, caSecret } = await getCaCredentials({
|
||||||
caId: ca.id,
|
caId: ca.id,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
@@ -859,6 +899,11 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const serialNumber = createSerialNumber();
|
const serialNumber = createSerialNumber();
|
||||||
|
|
||||||
|
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||||
|
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||||
|
|
||||||
|
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||||
const intermediateCert = await x509.X509CertificateGenerator.create({
|
const intermediateCert = await x509.X509CertificateGenerator.create({
|
||||||
serialNumber,
|
serialNumber,
|
||||||
subject: csrObj.subject,
|
subject: csrObj.subject,
|
||||||
@@ -878,7 +923,11 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
),
|
),
|
||||||
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
|
new x509.BasicConstraintsExtension(true, maxPathLength === -1 ? undefined : maxPathLength, true),
|
||||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||||
|
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||||
|
new x509.AuthorityInfoAccessExtension({
|
||||||
|
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||||
|
})
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1052,7 +1101,9 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages
|
||||||
}: TIssueCertFromCaDTO) => {
|
}: TIssueCertFromCaDTO) => {
|
||||||
let ca: TCertificateAuthorities | undefined;
|
let ca: TCertificateAuthorities | undefined;
|
||||||
let certificateTemplate: TCertificateTemplates | undefined;
|
let certificateTemplate: TCertificateTemplates | undefined;
|
||||||
@@ -1168,16 +1219,70 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}`;
|
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||||
|
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||||
|
|
||||||
const extensions: x509.Extension[] = [
|
const extensions: x509.Extension[] = [
|
||||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
|
||||||
new x509.BasicConstraintsExtension(false),
|
new x509.BasicConstraintsExtension(false),
|
||||||
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||||
|
new x509.AuthorityInfoAccessExtension({
|
||||||
|
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||||
|
}),
|
||||||
|
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// handle key usages
|
||||||
|
let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? [];
|
||||||
|
if (keyUsages === undefined && !certificateTemplate) {
|
||||||
|
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyUsages === undefined && certificateTemplate) {
|
||||||
|
selectedKeyUsages = (certificateTemplate.keyUsages ?? []) as CertKeyUsage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyUsages?.length && certificateTemplate) {
|
||||||
|
const validKeyUsages = certificateTemplate.keyUsages || [];
|
||||||
|
if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid key usage value based on template policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
selectedKeyUsages = keyUsages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
||||||
|
if (keyUsagesBitValue) {
|
||||||
|
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle extended key usages
|
||||||
|
let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = extendedKeyUsages ?? [];
|
||||||
|
if (extendedKeyUsages === undefined && certificateTemplate) {
|
||||||
|
selectedExtendedKeyUsages = (certificateTemplate.extendedKeyUsages ?? []) as CertExtendedKeyUsage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extendedKeyUsages?.length && certificateTemplate) {
|
||||||
|
const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || [];
|
||||||
|
if (extendedKeyUsages.some((eku) => !validExtendedKeyUsages.includes(eku))) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid extended key usage value based on template policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
selectedExtendedKeyUsages = extendedKeyUsages;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedExtendedKeyUsages.length) {
|
||||||
|
extensions.push(
|
||||||
|
new x509.ExtendedKeyUsageExtension(
|
||||||
|
selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let altNamesArray: {
|
let altNamesArray: {
|
||||||
type: "email" | "dns";
|
type: "email" | "dns";
|
||||||
value: string;
|
value: string;
|
||||||
@@ -1259,7 +1364,9 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
altNames,
|
altNames,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
notBefore: notBeforeDate,
|
notBefore: notBeforeDate,
|
||||||
notAfter: notAfterDate
|
notAfter: notAfterDate,
|
||||||
|
keyUsages: selectedKeyUsages,
|
||||||
|
extendedKeyUsages: selectedExtendedKeyUsages
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -1308,6 +1415,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
* Note: CSR is generated externally and submitted to Infisical.
|
* Note: CSR is generated externally and submitted to Infisical.
|
||||||
*/
|
*/
|
||||||
const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
|
const signCertFromCa = async (dto: TSignCertFromCaDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
let ca: TCertificateAuthorities | undefined;
|
let ca: TCertificateAuthorities | undefined;
|
||||||
let certificateTemplate: TCertificateTemplates | undefined;
|
let certificateTemplate: TCertificateTemplates | undefined;
|
||||||
|
|
||||||
@@ -1321,7 +1429,9 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
altNames,
|
altNames,
|
||||||
ttl,
|
ttl,
|
||||||
notBefore,
|
notBefore,
|
||||||
notAfter
|
notAfter,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages
|
||||||
} = dto;
|
} = dto;
|
||||||
|
|
||||||
let collectionId = pkiCollectionId;
|
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"
|
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,
|
caId: ca.id,
|
||||||
certificateAuthorityDAL,
|
certificateAuthorityDAL,
|
||||||
certificateAuthoritySecretDAL,
|
certificateAuthoritySecretDAL,
|
||||||
@@ -1440,13 +1550,115 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
kmsService
|
kmsService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
|
||||||
|
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}/der`;
|
||||||
|
|
||||||
|
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}/der`;
|
||||||
const extensions: x509.Extension[] = [
|
const extensions: x509.Extension[] = [
|
||||||
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
|
|
||||||
new x509.BasicConstraintsExtension(false),
|
new x509.BasicConstraintsExtension(false),
|
||||||
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
|
||||||
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
|
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey),
|
||||||
|
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
|
||||||
|
new x509.AuthorityInfoAccessExtension({
|
||||||
|
caIssuers: new x509.GeneralName("url", caIssuerUrl)
|
||||||
|
}),
|
||||||
|
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// handle key usages
|
||||||
|
const csrKeyUsageExtension = csrObj.getExtension("2.5.29.15") as x509.KeyUsagesExtension;
|
||||||
|
let csrKeyUsages: CertKeyUsage[] = [];
|
||||||
|
if (csrKeyUsageExtension) {
|
||||||
|
csrKeyUsages = Object.values(CertKeyUsage).filter(
|
||||||
|
(keyUsage) => (x509.KeyUsageFlags[keyUsage] & csrKeyUsageExtension.usages) !== 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedKeyUsages: CertKeyUsage[] = keyUsages ?? [];
|
||||||
|
if (keyUsages === undefined && !certificateTemplate) {
|
||||||
|
if (csrKeyUsageExtension) {
|
||||||
|
selectedKeyUsages = csrKeyUsages;
|
||||||
|
} else {
|
||||||
|
selectedKeyUsages = [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyUsages === undefined && certificateTemplate) {
|
||||||
|
if (csrKeyUsageExtension) {
|
||||||
|
const validKeyUsages = certificateTemplate.keyUsages || [];
|
||||||
|
if (csrKeyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid key usage value based on template policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
selectedKeyUsages = csrKeyUsages;
|
||||||
|
} else {
|
||||||
|
selectedKeyUsages = (certificateTemplate.keyUsages ?? []) as CertKeyUsage[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyUsages?.length && certificateTemplate) {
|
||||||
|
const validKeyUsages = certificateTemplate.keyUsages || [];
|
||||||
|
if (keyUsages.some((keyUsage) => !validKeyUsages.includes(keyUsage))) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid key usage value based on template policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
selectedKeyUsages = keyUsages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyUsagesBitValue = selectedKeyUsages.reduce((accum, keyUsage) => accum | x509.KeyUsageFlags[keyUsage], 0);
|
||||||
|
if (keyUsagesBitValue) {
|
||||||
|
extensions.push(new x509.KeyUsagesExtension(keyUsagesBitValue, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle extended key usages
|
||||||
|
const csrExtendedKeyUsageExtension = csrObj.getExtension("2.5.29.37") as x509.ExtendedKeyUsageExtension;
|
||||||
|
let csrExtendedKeyUsages: CertExtendedKeyUsage[] = [];
|
||||||
|
if (csrExtendedKeyUsageExtension) {
|
||||||
|
csrExtendedKeyUsages = csrExtendedKeyUsageExtension.usages.map(
|
||||||
|
(ekuOid) => CertExtendedKeyUsageOIDToName[ekuOid as string]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedExtendedKeyUsages: CertExtendedKeyUsage[] = extendedKeyUsages ?? [];
|
||||||
|
if (extendedKeyUsages === undefined && !certificateTemplate && csrExtendedKeyUsageExtension) {
|
||||||
|
selectedExtendedKeyUsages = csrExtendedKeyUsages;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extendedKeyUsages === undefined && certificateTemplate) {
|
||||||
|
if (csrExtendedKeyUsageExtension) {
|
||||||
|
const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || [];
|
||||||
|
if (csrExtendedKeyUsages.some((eku) => !validExtendedKeyUsages.includes(eku))) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid extended key usage value based on template policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
selectedExtendedKeyUsages = csrExtendedKeyUsages;
|
||||||
|
} else {
|
||||||
|
selectedExtendedKeyUsages = (certificateTemplate.extendedKeyUsages ?? []) as CertExtendedKeyUsage[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extendedKeyUsages?.length && certificateTemplate) {
|
||||||
|
const validExtendedKeyUsages = certificateTemplate.extendedKeyUsages || [];
|
||||||
|
if (extendedKeyUsages.some((keyUsage) => !validExtendedKeyUsages.includes(keyUsage))) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid extended key usage value based on template policy"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
selectedExtendedKeyUsages = extendedKeyUsages;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedExtendedKeyUsages.length) {
|
||||||
|
extensions.push(
|
||||||
|
new x509.ExtendedKeyUsageExtension(
|
||||||
|
selectedExtendedKeyUsages.map((eku) => x509.ExtendedKeyUsage[eku]),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let altNamesFromCsr: string = "";
|
let altNamesFromCsr: string = "";
|
||||||
let altNamesArray: {
|
let altNamesArray: {
|
||||||
type: "email" | "dns";
|
type: "email" | "dns";
|
||||||
@@ -1542,7 +1754,9 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
altNames: altNamesFromCsr || altNames,
|
altNames: altNamesFromCsr || altNames,
|
||||||
serialNumber,
|
serialNumber,
|
||||||
notBefore: notBeforeDate,
|
notBefore: notBeforeDate,
|
||||||
notAfter: notAfterDate
|
notAfter: notAfterDate,
|
||||||
|
keyUsages: selectedKeyUsages,
|
||||||
|
extendedKeyUsages: selectedExtendedKeyUsages
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -1628,6 +1842,7 @@ export const certificateAuthorityServiceFactory = ({
|
|||||||
renewCaCert,
|
renewCaCert,
|
||||||
getCaCerts,
|
getCaCerts,
|
||||||
getCaCert,
|
getCaCert,
|
||||||
|
getCaCertById,
|
||||||
signIntermediate,
|
signIntermediate,
|
||||||
importCertToCa,
|
importCertToCa,
|
||||||
issueCertFromCa,
|
issueCertFromCa,
|
||||||
|
@@ -4,7 +4,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
|||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
|
|
||||||
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
import { TCertificateAuthorityCrlDALFactory } from "../../ee/services/certificate-authority-crl/certificate-authority-crl-dal";
|
||||||
import { CertKeyAlgorithm } from "../certificate/certificate-types";
|
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificate/certificate-types";
|
||||||
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cert-dal";
|
||||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||||
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
import { TCertificateAuthoritySecretDALFactory } from "./certificate-authority-secret-dal";
|
||||||
@@ -97,6 +97,8 @@ export type TIssueCertFromCaDTO = {
|
|||||||
ttl: string;
|
ttl: string;
|
||||||
notBefore?: string;
|
notBefore?: string;
|
||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
|
keyUsages?: CertKeyUsage[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TSignCertFromCaDTO =
|
export type TSignCertFromCaDTO =
|
||||||
@@ -112,6 +114,8 @@ export type TSignCertFromCaDTO =
|
|||||||
ttl?: string;
|
ttl?: string;
|
||||||
notBefore?: string;
|
notBefore?: string;
|
||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
|
keyUsages?: CertKeyUsage[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
}
|
}
|
||||||
| ({
|
| ({
|
||||||
isInternal: false;
|
isInternal: false;
|
||||||
@@ -125,6 +129,8 @@ export type TSignCertFromCaDTO =
|
|||||||
ttl: string;
|
ttl: string;
|
||||||
notBefore?: string;
|
notBefore?: string;
|
||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
|
keyUsages?: CertKeyUsage[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
} & Omit<TProjectPermission, "projectId">);
|
} & Omit<TProjectPermission, "projectId">);
|
||||||
|
|
||||||
export type TGetCaCertificateTemplatesDTO = {
|
export type TGetCaCertificateTemplatesDTO = {
|
||||||
|
@@ -9,7 +9,9 @@ export const sanitizedCertificateTemplate = CertificateTemplatesSchema.pick({
|
|||||||
commonName: true,
|
commonName: true,
|
||||||
subjectAlternativeName: true,
|
subjectAlternativeName: true,
|
||||||
pkiCollectionId: true,
|
pkiCollectionId: true,
|
||||||
ttl: true
|
ttl: true,
|
||||||
|
keyUsages: true,
|
||||||
|
extendedKeyUsages: true
|
||||||
}).merge(
|
}).merge(
|
||||||
z.object({
|
z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
|
@@ -57,7 +57,9 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages
|
||||||
}: TCreateCertTemplateDTO) => {
|
}: TCreateCertTemplateDTO) => {
|
||||||
const ca = await certificateAuthorityDAL.findById(caId);
|
const ca = await certificateAuthorityDAL.findById(caId);
|
||||||
if (!ca) {
|
if (!ca) {
|
||||||
@@ -86,7 +88,9 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
name,
|
name,
|
||||||
commonName,
|
commonName,
|
||||||
subjectAlternativeName,
|
subjectAlternativeName,
|
||||||
ttl
|
ttl,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -113,7 +117,9 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages
|
||||||
}: TUpdateCertTemplateDTO) => {
|
}: TUpdateCertTemplateDTO) => {
|
||||||
const certTemplate = await certificateTemplateDAL.getById(id);
|
const certTemplate = await certificateTemplateDAL.getById(id);
|
||||||
if (!certTemplate) {
|
if (!certTemplate) {
|
||||||
@@ -153,7 +159,9 @@ export const certificateTemplateServiceFactory = ({
|
|||||||
commonName,
|
commonName,
|
||||||
subjectAlternativeName,
|
subjectAlternativeName,
|
||||||
name,
|
name,
|
||||||
ttl
|
ttl,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||||
|
|
||||||
export type TCreateCertTemplateDTO = {
|
export type TCreateCertTemplateDTO = {
|
||||||
caId: string;
|
caId: string;
|
||||||
@@ -7,6 +8,8 @@ export type TCreateCertTemplateDTO = {
|
|||||||
commonName: string;
|
commonName: string;
|
||||||
subjectAlternativeName: string;
|
subjectAlternativeName: string;
|
||||||
ttl: string;
|
ttl: string;
|
||||||
|
keyUsages: CertKeyUsage[];
|
||||||
|
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateCertTemplateDTO = {
|
export type TUpdateCertTemplateDTO = {
|
||||||
@@ -17,6 +20,8 @@ export type TUpdateCertTemplateDTO = {
|
|||||||
commonName?: string;
|
commonName?: string;
|
||||||
subjectAlternativeName?: string;
|
subjectAlternativeName?: string;
|
||||||
ttl?: string;
|
ttl?: string;
|
||||||
|
keyUsages?: CertKeyUsage[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TGetCertTemplateDTO = {
|
export type TGetCertTemplateDTO = {
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import * as x509 from "@peculiar/x509";
|
||||||
|
|
||||||
import { TProjectPermission } from "@app/lib/types";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export enum CertStatus {
|
export enum CertStatus {
|
||||||
@@ -12,6 +14,36 @@ export enum CertKeyAlgorithm {
|
|||||||
ECDSA_P384 = "EC_secp384r1"
|
ECDSA_P384 = "EC_secp384r1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CertKeyUsage {
|
||||||
|
DIGITAL_SIGNATURE = "digitalSignature",
|
||||||
|
KEY_ENCIPHERMENT = "keyEncipherment",
|
||||||
|
NON_REPUDIATION = "nonRepudiation",
|
||||||
|
DATA_ENCIPHERMENT = "dataEncipherment",
|
||||||
|
KEY_AGREEMENT = "keyAgreement",
|
||||||
|
KEY_CERT_SIGN = "keyCertSign",
|
||||||
|
CRL_SIGN = "cRLSign",
|
||||||
|
ENCIPHER_ONLY = "encipherOnly",
|
||||||
|
DECIPHER_ONLY = "decipherOnly"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertExtendedKeyUsage {
|
||||||
|
CLIENT_AUTH = "clientAuth",
|
||||||
|
SERVER_AUTH = "serverAuth",
|
||||||
|
CODE_SIGNING = "codeSigning",
|
||||||
|
EMAIL_PROTECTION = "emailProtection",
|
||||||
|
TIMESTAMPING = "timeStamping",
|
||||||
|
OCSP_SIGNING = "ocspSigning"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CertExtendedKeyUsageOIDToName: Record<string, CertExtendedKeyUsage> = {
|
||||||
|
[x509.ExtendedKeyUsage.clientAuth]: CertExtendedKeyUsage.CLIENT_AUTH,
|
||||||
|
[x509.ExtendedKeyUsage.serverAuth]: CertExtendedKeyUsage.SERVER_AUTH,
|
||||||
|
[x509.ExtendedKeyUsage.codeSigning]: CertExtendedKeyUsage.CODE_SIGNING,
|
||||||
|
[x509.ExtendedKeyUsage.emailProtection]: CertExtendedKeyUsage.EMAIL_PROTECTION,
|
||||||
|
[x509.ExtendedKeyUsage.ocspSigning]: CertExtendedKeyUsage.OCSP_SIGNING,
|
||||||
|
[x509.ExtendedKeyUsage.timeStamping]: CertExtendedKeyUsage.TIMESTAMPING
|
||||||
|
};
|
||||||
|
|
||||||
export enum CrlReason {
|
export enum CrlReason {
|
||||||
UNSPECIFIED = "UNSPECIFIED",
|
UNSPECIFIED = "UNSPECIFIED",
|
||||||
KEY_COMPROMISE = "KEY_COMPROMISE",
|
KEY_COMPROMISE = "KEY_COMPROMISE",
|
||||||
|
@@ -1,9 +1,11 @@
|
|||||||
import { Knex } from "knex";
|
import { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
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 { 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>;
|
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 {
|
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)
|
.where(`${TableName.IdentityProjectMembership}.projectId`, projectId)
|
||||||
.join(TableName.Project, `${TableName.IdentityProjectMembership}.projectId`, `${TableName.Project}.id`)
|
.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) => {
|
.where((qb) => {
|
||||||
if (filter.identityId) {
|
if (filter.identityId) {
|
||||||
void qb.where("identityId", 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)
|
db.ref("name").as("projectName").withSchema(TableName.Project)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: scott - joins seem to reorder identities so need to order again, for the sake of urgency will optimize at a later point
|
||||||
|
if (filter.orderBy) {
|
||||||
|
switch (filter.orderBy) {
|
||||||
|
case "name":
|
||||||
|
void query.orderBy(`${TableName.Identity}.${filter.orderBy}`, filter.orderDirection);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await query;
|
||||||
|
|
||||||
const members = sqlNestRelationships({
|
const members = sqlNestRelationships({
|
||||||
data: docs,
|
data: docs,
|
||||||
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({
|
parentMapper: ({ identityId, identityName, identityAuthMethod, id, createdAt, updatedAt, projectName }) => ({
|
||||||
@@ -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 {
|
return {
|
||||||
...identityProjectOrm,
|
...identityProjectOrm,
|
||||||
findByIdentityId,
|
findByIdentityId,
|
||||||
findByProjectId
|
findByProjectId,
|
||||||
|
getCountByProjectId
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -268,7 +268,12 @@ export const identityProjectServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
}: TListProjectIdentityDTO) => {
|
}: TListProjectIdentityDTO) => {
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
@@ -279,8 +284,17 @@ export const identityProjectServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
||||||
|
|
||||||
const identityMemberships = await identityProjectDAL.findByProjectId(projectId);
|
const identityMemberships = await identityProjectDAL.findByProjectId(projectId, {
|
||||||
return identityMemberships;
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalCount = await identityProjectDAL.getCountByProjectId(projectId, { search });
|
||||||
|
|
||||||
|
return { identityMemberships, totalCount };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getProjectIdentityByIdentityId = async ({
|
const getProjectIdentityByIdentityId = async ({
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { TProjectPermission } from "@app/lib/types";
|
import { OrderByDirection, TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||||
|
|
||||||
@@ -40,8 +40,18 @@ export type TDeleteProjectIdentityDTO = {
|
|||||||
identityId: string;
|
identityId: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TListProjectIdentityDTO = TProjectPermission;
|
export type TListProjectIdentityDTO = {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
orderBy?: ProjectIdentityOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
} & TProjectPermission;
|
||||||
|
|
||||||
export type TGetProjectIdentityByIdentityIdDTO = {
|
export type TGetProjectIdentityByIdentityIdDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
} & TProjectPermission;
|
} & TProjectPermission;
|
||||||
|
|
||||||
|
export enum ProjectIdentityOrderBy {
|
||||||
|
Name = "name"
|
||||||
|
}
|
||||||
|
@@ -4,6 +4,8 @@ import { TDbClient } from "@app/db";
|
|||||||
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
|
import { TableName, TIdentityOrgMemberships } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||||
|
import { OrderByDirection } from "@app/lib/types";
|
||||||
|
import { TListOrgIdentitiesByOrgIdDTO } from "@app/services/identity/identity-types";
|
||||||
|
|
||||||
export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>;
|
export type TIdentityOrgDALFactory = ReturnType<typeof identityOrgDALFactory>;
|
||||||
|
|
||||||
@@ -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 {
|
try {
|
||||||
const docs = await (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||||
.where(filter)
|
.where(filter)
|
||||||
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
||||||
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
.leftJoin(TableName.OrgRoles, `${TableName.IdentityOrgMembership}.roleId`, `${TableName.OrgRoles}.id`)
|
||||||
@@ -44,6 +57,30 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
.select(db.ref("id").as("identityId").withSchema(TableName.Identity))
|
.select(db.ref("id").as("identityId").withSchema(TableName.Identity))
|
||||||
.select(db.ref("name").as("identityName").withSchema(TableName.Identity))
|
.select(db.ref("name").as("identityName").withSchema(TableName.Identity))
|
||||||
.select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity));
|
.select(db.ref("authMethod").as("identityAuthMethod").withSchema(TableName.Identity));
|
||||||
|
|
||||||
|
if (limit) {
|
||||||
|
void query.offset(offset).limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderBy) {
|
||||||
|
switch (orderBy) {
|
||||||
|
case "name":
|
||||||
|
void query.orderBy(`${TableName.Identity}.${orderBy}`, orderDirection);
|
||||||
|
break;
|
||||||
|
case "role":
|
||||||
|
void query.orderBy(`${TableName.IdentityOrgMembership}.${orderBy}`, orderDirection);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search?.length) {
|
||||||
|
void query.whereILike(`${TableName.Identity}.name`, `%${search}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const docs = await query;
|
||||||
|
|
||||||
return docs.map(
|
return docs.map(
|
||||||
({
|
({
|
||||||
crId,
|
crId,
|
||||||
@@ -79,5 +116,27 @@ export const identityOrgDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { ...identityOrgOrm, find, findOne };
|
const countAllOrgIdentities = async (
|
||||||
|
{ search, ...filter }: Partial<TIdentityOrgMemberships> & Pick<TListOrgIdentitiesByOrgIdDTO, "search">,
|
||||||
|
tx?: Knex
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const query = (tx || db.replicaNode())(TableName.IdentityOrgMembership)
|
||||||
|
.where(filter)
|
||||||
|
.join(TableName.Identity, `${TableName.IdentityOrgMembership}.identityId`, `${TableName.Identity}.id`)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if (search?.length) {
|
||||||
|
void query.whereILike(`${TableName.Identity}.name`, `%${search}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const identities = await query;
|
||||||
|
|
||||||
|
return Number(identities[0].count);
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "countAllOrgIdentities" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { ...identityOrgOrm, find, findOne, countAllOrgIdentities };
|
||||||
};
|
};
|
||||||
|
@@ -6,7 +6,6 @@ import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/pe
|
|||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
|
||||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
TCreateIdentityDTO,
|
TCreateIdentityDTO,
|
||||||
TDeleteIdentityDTO,
|
TDeleteIdentityDTO,
|
||||||
TGetIdentityByIdDTO,
|
TGetIdentityByIdDTO,
|
||||||
|
TListOrgIdentitiesByOrgIdDTO,
|
||||||
TListProjectIdentitiesByIdentityIdDTO,
|
TListProjectIdentitiesByIdentityIdDTO,
|
||||||
TUpdateIdentityDTO
|
TUpdateIdentityDTO
|
||||||
} from "./identity-types";
|
} from "./identity-types";
|
||||||
@@ -195,14 +195,36 @@ export const identityServiceFactory = ({
|
|||||||
return { ...deletedIdentity, orgId: identityOrgMembership.orgId };
|
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);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
const identityMemberships = await identityOrgMembershipDAL.find({
|
const identityMemberships = await identityOrgMembershipDAL.find({
|
||||||
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId
|
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
});
|
});
|
||||||
return identityMemberships;
|
|
||||||
|
const totalCount = await identityOrgMembershipDAL.countAllOrgIdentities({
|
||||||
|
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId,
|
||||||
|
search
|
||||||
|
});
|
||||||
|
|
||||||
|
return { identityMemberships, totalCount };
|
||||||
};
|
};
|
||||||
|
|
||||||
const listProjectIdentitiesByIdentityId = async ({
|
const listProjectIdentitiesByIdentityId = async ({
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { IPType } from "@app/lib/ip";
|
import { IPType } from "@app/lib/ip";
|
||||||
import { TOrgPermission } from "@app/lib/types";
|
import { OrderByDirection, TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export type TCreateIdentityDTO = {
|
export type TCreateIdentityDTO = {
|
||||||
role: string;
|
role: string;
|
||||||
@@ -29,3 +29,16 @@ export interface TIdentityTrustedIp {
|
|||||||
export type TListProjectIdentitiesByIdentityIdDTO = {
|
export type TListProjectIdentitiesByIdentityIdDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
|
export type TListOrgIdentitiesByOrgIdDTO = {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
orderBy?: OrgIdentityOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export enum OrgIdentityOrderBy {
|
||||||
|
Name = "name",
|
||||||
|
Role = "role"
|
||||||
|
}
|
||||||
|
@@ -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(
|
await request.post(
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
|
||||||
{
|
{
|
||||||
@@ -237,6 +243,12 @@ const syncSecretsGCPSecretManager = async ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else if (secrets[key].value !== res[key]) {
|
} else if (secrets[key].value !== res[key]) {
|
||||||
|
if (!secrets[key].value) {
|
||||||
|
logger.warn(
|
||||||
|
`syncSecretsGcpsecretManager: update secret value in gcp where [key=${key}] and integration appId [appId=${integration.appId}]`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await request.post(
|
await request.post(
|
||||||
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
|
`${IntegrationUrls.GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
|
||||||
{
|
{
|
||||||
|
@@ -458,7 +458,6 @@ export const orgServiceFactory = ({
|
|||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
|
|
||||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
|
||||||
|
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
|
|
||||||
@@ -583,6 +582,8 @@ export const orgServiceFactory = ({
|
|||||||
|
|
||||||
// if there exist no org membership we set is as given by the request
|
// if there exist no org membership we set is as given by the request
|
||||||
if (!inviteeMembership) {
|
if (!inviteeMembership) {
|
||||||
|
// as its used by project invite also
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
|
||||||
let roleId;
|
let roleId;
|
||||||
const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug;
|
const orgRole = isCustomOrgRole ? OrgMembershipRole.Custom : organizationRoleSlug;
|
||||||
if (isCustomOrgRole) {
|
if (isCustomOrgRole) {
|
||||||
|
@@ -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.
|
- Common Name (CN): A regular expression used to validate the common name in certificate requests.
|
||||||
- Alternative Names (SANs): A regular expression used to validate subject alternative names in certificate requests.
|
- Alternative Names (SANs): A regular expression used to validate subject alternative names in certificate requests.
|
||||||
- TTL: The maximum Time-to-Live (TTL) for certificates issued using this template.
|
- TTL: The maximum Time-to-Live (TTL) for certificates issued using this template.
|
||||||
|
- Key Usage: The key usage constraint or default value for certificates issued using this template.
|
||||||
|
- Extended Key Usage: The extended key usage constraint or default value for certificates issued using this template.
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Creating a certificate">
|
<Step title="Creating a certificate">
|
||||||
To create a certificate, head to your Project > Internal PKI > Certificates and press **Issue** under the Certificates section.
|
To create a certificate, head to your Project > Internal PKI > Certificates and press **Issue** under the Certificates section.
|
||||||
@@ -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`.
|
- Common Name (CN): The (common) name for the certificate like `service.acme.com`.
|
||||||
- Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`.
|
- Alternative Names (SANs): A comma-delimited list of Subject Alternative Names (SANs) for the certificate; these can be host names or email addresses like `app1.acme.com, app2.acme.com`.
|
||||||
- TTL: The lifetime of the certificate in seconds.
|
- TTL: The lifetime of the certificate in seconds.
|
||||||
|
- Key Usage: The key usage extension of the certificate.
|
||||||
|
- Extended Key Usage: The extended key usage extension of the certificate.
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None**
|
Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None**
|
||||||
and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same.
|
and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same.
|
||||||
|
|
||||||
That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates.
|
That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Copying the certificate details">
|
<Step title="Copying the certificate details">
|
||||||
Once you have created the certificate from step 1, you'll be presented with the certificate details including the **Certificate Body**, **Certificate Chain**, and **Private Key**.
|
Once you have created the certificate from step 1, you'll be presented with the certificate details including the **Certificate Body**, **Certificate Chain**, and **Private Key**.
|
||||||
@@ -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.
|
With certificate templates, you can specify, for example, that issued certificates must have a common name (CN) adhering to a specific format like .*.acme.com or perhaps that the max TTL cannot be more than 1 year.
|
||||||
|
|
||||||
To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint, specifying the issuing CA.
|
To create a certificate template, make an API request to the [Create Certificate Template](/api-reference/endpoints/certificate-templates/create) API endpoint, specifying the issuing CA.
|
||||||
|
|
||||||
### Sample request
|
### Sample request
|
||||||
|
|
||||||
```bash Request
|
```bash Request
|
||||||
@@ -132,6 +137,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
ttl: "...",
|
ttl: "...",
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Creating a certificate">
|
<Step title="Creating a certificate">
|
||||||
To create a certificate under the certificate template, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
|
To create a certificate under the certificate template, make an API request to the [Issue Certificate](/api-reference/endpoints/certificates/issue-cert) API endpoint,
|
||||||
@@ -164,7 +170,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
<Note>
|
<Note>
|
||||||
Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None**
|
Note that Infisical PKI supports issuing certificates without certificate templates as well. If this is desired, then you can set the **Certificate Template** field to **None**
|
||||||
and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same.
|
and specify the **Issuing CA** and optional **Certificate Collection** fields; the rest of the fields for the issued certificate remain the same.
|
||||||
|
|
||||||
That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates.
|
That said, we recommend using certificate templates to enforce policies and attach expiration monitoring on issued certificates.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
@@ -197,6 +203,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
|
|||||||
serialNumber: "..."
|
serialNumber: "..."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 379 KiB After Width: | Height: | Size: 399 KiB |
Binary file not shown.
Before Width: | Height: | Size: 517 KiB After Width: | Height: | Size: 518 KiB |
@@ -696,7 +696,12 @@
|
|||||||
{
|
{
|
||||||
"group": "Audit Logs",
|
"group": "Audit Logs",
|
||||||
"pages": ["api-reference/endpoints/audit-logs/export-audit-log"]
|
"pages": ["api-reference/endpoints/audit-logs/export-audit-log"]
|
||||||
},
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Infisical PKI",
|
||||||
|
"pages": [
|
||||||
{
|
{
|
||||||
"group": "Certificate Authorities",
|
"group": "Certificate Authorities",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
83
frontend/src/components/v2/ComboBox/ComboBox.tsx
Normal file
83
frontend/src/components/v2/ComboBox/ComboBox.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faCaretDown, faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { Combobox, Transition } from "@headlessui/react";
|
||||||
|
import { ByComparator } from "@headlessui/react/dist/types";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
type ComboBoxProps<T extends object> = {
|
||||||
|
value?: T;
|
||||||
|
className?: string;
|
||||||
|
items: {
|
||||||
|
value: T;
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
}[];
|
||||||
|
by: ByComparator<T>;
|
||||||
|
defaultValue?: T;
|
||||||
|
displayValue: (value: T) => string;
|
||||||
|
onSelectChange: (value: T) => void;
|
||||||
|
onFilter: (value: { value: T }, filterQuery: string) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(akhilmhdh): This is a very temporary one due to limitation of present situation
|
||||||
|
// don't mind the api for now will be switched to aria later
|
||||||
|
export const ComboBox = <T extends object>({
|
||||||
|
onSelectChange,
|
||||||
|
onFilter,
|
||||||
|
displayValue,
|
||||||
|
by,
|
||||||
|
items,
|
||||||
|
...props
|
||||||
|
}: ComboBoxProps<T>) => {
|
||||||
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
|
const filteredResult =
|
||||||
|
query === "" ? items.slice(0, 20) : items.filter((el) => onFilter(el, query)).slice(0, 20);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Combobox by={by} {...props} onChange={onSelectChange}>
|
||||||
|
<div className="relative">
|
||||||
|
<Combobox.Input
|
||||||
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
|
displayValue={displayValue}
|
||||||
|
className=" inline-flex w-full items-center justify-between rounded-md bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none focus:bg-mineshaft-700/80 data-[placeholder]:text-mineshaft-200"
|
||||||
|
/>
|
||||||
|
<Combobox.Button className="absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<FontAwesomeIcon icon={faCaretDown} size="sm" aria-hidden="true" />
|
||||||
|
</Combobox.Button>
|
||||||
|
<Transition
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
afterLeave={() => setQuery("")}
|
||||||
|
>
|
||||||
|
<Combobox.Options className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md bg-mineshaft-900 py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
|
||||||
|
{filteredResult.map(({ value, key, label }) => (
|
||||||
|
<Combobox.Option
|
||||||
|
key={key}
|
||||||
|
value={value}
|
||||||
|
className={({ active }) =>
|
||||||
|
`relative cursor-pointer select-none py-2 pl-10 pr-4 transition-all hover:bg-mineshaft-500 ${
|
||||||
|
active ? "text-primary" : "text-white"
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ selected }) => (
|
||||||
|
<>
|
||||||
|
{label}
|
||||||
|
{selected ? (
|
||||||
|
<div className={twMerge("absolute top-2 left-3 text-primary")}>
|
||||||
|
<FontAwesomeIcon icon={faCheck} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Combobox.Option>
|
||||||
|
))}
|
||||||
|
</Combobox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/components/v2/ComboBox/index.tsx
Normal file
1
frontend/src/components/v2/ComboBox/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ComboBox } from "./ComboBox";
|
@@ -11,6 +11,7 @@ type Props = {
|
|||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
isReadOnly?: boolean;
|
isReadOnly?: boolean;
|
||||||
autoCapitalization?: boolean;
|
autoCapitalization?: boolean;
|
||||||
|
containerClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const inputVariants = cva(
|
const inputVariants = cva(
|
||||||
@@ -71,6 +72,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
(
|
(
|
||||||
{
|
{
|
||||||
className,
|
className,
|
||||||
|
containerClassName,
|
||||||
isRounded = true,
|
isRounded = true,
|
||||||
isFullWidth = true,
|
isFullWidth = true,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
@@ -94,7 +96,15 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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>}
|
{leftIcon && <span className="absolute left-0 ml-3 text-sm">{leftIcon}</span>}
|
||||||
<input
|
<input
|
||||||
{...props}
|
{...props}
|
||||||
|
@@ -11,13 +11,24 @@ export type ModalContentProps = DialogPrimitive.DialogContentProps & {
|
|||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
subTitle?: ReactNode;
|
subTitle?: ReactNode;
|
||||||
footerContent?: ReactNode;
|
footerContent?: ReactNode;
|
||||||
|
bodyClassName?: string;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
overlayClassName?: string;
|
overlayClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||||
(
|
(
|
||||||
{ children, title, subTitle, className, overlayClassName, footerContent, onClose, ...props },
|
{
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
subTitle,
|
||||||
|
className,
|
||||||
|
overlayClassName,
|
||||||
|
footerContent,
|
||||||
|
bodyClassName,
|
||||||
|
onClose,
|
||||||
|
...props
|
||||||
|
},
|
||||||
forwardedRef
|
forwardedRef
|
||||||
) => (
|
) => (
|
||||||
<DialogPrimitive.Portal>
|
<DialogPrimitive.Portal>
|
||||||
@@ -35,7 +46,10 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
|||||||
style={{ maxHeight: "90%" }}
|
style={{ maxHeight: "90%" }}
|
||||||
>
|
>
|
||||||
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
|
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
|
||||||
<CardBody className="overflow-y-auto overflow-x-hidden" style={{ maxHeight: "90%" }}>
|
<CardBody
|
||||||
|
className={twMerge("overflow-y-auto overflow-x-hidden", bodyClassName)}
|
||||||
|
style={{ maxHeight: "90%" }}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
{footerContent && <CardFooter>{footerContent}</CardFooter>}
|
{footerContent && <CardFooter>{footerContent}</CardFooter>}
|
||||||
|
@@ -40,6 +40,8 @@ export const Pagination = ({
|
|||||||
const upperLimit = Math.ceil(count / perPage);
|
const upperLimit = Math.ceil(count / perPage);
|
||||||
const nextPageNumber = Math.min(upperLimit, page + 1);
|
const nextPageNumber = Math.min(upperLimit, page + 1);
|
||||||
const canGoNext = page + 1 <= upperLimit;
|
const canGoNext = page + 1 <= upperLimit;
|
||||||
|
const canGoFirst = page > 1;
|
||||||
|
const canGoLast = page < upperLimit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -50,7 +52,7 @@ export const Pagination = ({
|
|||||||
>
|
>
|
||||||
<div className="mr-6 flex items-center space-x-2">
|
<div className="mr-6 flex items-center space-x-2">
|
||||||
<div className="text-xs">
|
<div className="text-xs">
|
||||||
{(page - 1) * perPage} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
|
{(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -73,6 +75,16 @@ export const Pagination = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="pagination-first"
|
||||||
|
className="relative"
|
||||||
|
onClick={() => onChangePage(1)}
|
||||||
|
isDisabled={!canGoFirst}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className="absolute left-2.5 top-1 text-xs" icon={faChevronLeft} />
|
||||||
|
<FontAwesomeIcon className="text-xs" icon={faChevronLeft} />
|
||||||
|
</IconButton>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
ariaLabel="pagination-prev"
|
ariaLabel="pagination-prev"
|
||||||
@@ -89,6 +101,16 @@ export const Pagination = ({
|
|||||||
>
|
>
|
||||||
<FontAwesomeIcon className="text-xs" icon={faChevronRight} />
|
<FontAwesomeIcon className="text-xs" icon={faChevronRight} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="pagination-last"
|
||||||
|
className="relative"
|
||||||
|
onClick={() => onChangePage(upperLimit)}
|
||||||
|
isDisabled={!canGoLast}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon className="absolute left-2.5 top-1 text-xs" icon={faChevronRight} />
|
||||||
|
<FontAwesomeIcon className="text-xs" icon={faChevronRight} />
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { CertKeyAlgorithm } from "../certificates/enums";
|
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificates/enums";
|
||||||
import { CaRenewalType, CaStatus, CaType } from "./enums";
|
import { CaRenewalType, CaStatus, CaType } from "./enums";
|
||||||
|
|
||||||
export type TCertificateAuthority = {
|
export type TCertificateAuthority = {
|
||||||
@@ -91,6 +91,8 @@ export type TCreateCertificateDTO = {
|
|||||||
ttl: string; // string compatible with ms
|
ttl: string; // string compatible with ms
|
||||||
notBefore?: string;
|
notBefore?: string;
|
||||||
notAfter?: string;
|
notAfter?: string;
|
||||||
|
keyUsages: CertKeyUsage[];
|
||||||
|
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateCertificateResponse = {
|
export type TCreateCertificateResponse = {
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { CertExtendedKeyUsage, CertKeyUsage } from "../certificates/enums";
|
||||||
|
|
||||||
export type TCertificateTemplate = {
|
export type TCertificateTemplate = {
|
||||||
id: string;
|
id: string;
|
||||||
caId: string;
|
caId: string;
|
||||||
@@ -8,6 +10,8 @@ export type TCertificateTemplate = {
|
|||||||
commonName: string;
|
commonName: string;
|
||||||
subjectAlternativeName: string;
|
subjectAlternativeName: string;
|
||||||
ttl: string;
|
ttl: string;
|
||||||
|
keyUsages: CertKeyUsage[];
|
||||||
|
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateCertificateTemplateDTO = {
|
export type TCreateCertificateTemplateDTO = {
|
||||||
@@ -18,6 +22,8 @@ export type TCreateCertificateTemplateDTO = {
|
|||||||
subjectAlternativeName: string;
|
subjectAlternativeName: string;
|
||||||
ttl: string;
|
ttl: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
keyUsages: CertKeyUsage[];
|
||||||
|
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateCertificateTemplateDTO = {
|
export type TUpdateCertificateTemplateDTO = {
|
||||||
@@ -29,6 +35,8 @@ export type TUpdateCertificateTemplateDTO = {
|
|||||||
subjectAlternativeName?: string;
|
subjectAlternativeName?: string;
|
||||||
ttl?: string;
|
ttl?: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
keyUsages?: CertKeyUsage[];
|
||||||
|
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteCertificateTemplateDTO = {
|
export type TDeleteCertificateTemplateDTO = {
|
||||||
|
@@ -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 } = {
|
export const certStatusToNameMap: { [K in CertStatus]: string } = {
|
||||||
[CertStatus.ACTIVE]: "Active",
|
[CertStatus.ACTIVE]: "Active",
|
||||||
@@ -69,3 +75,24 @@ export const crlReasons = [
|
|||||||
},
|
},
|
||||||
{ label: crlReasonToNameMap[CrlReason.A_A_COMPROMISE], value: CrlReason.A_A_COMPROMISE }
|
{ label: crlReasonToNameMap[CrlReason.A_A_COMPROMISE], value: CrlReason.A_A_COMPROMISE }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const KEY_USAGES_OPTIONS = [
|
||||||
|
{ value: CertKeyUsage.DIGITAL_SIGNATURE, label: "Digital Signature" },
|
||||||
|
{ value: CertKeyUsage.KEY_ENCIPHERMENT, label: "Key Encipherment" },
|
||||||
|
{ value: CertKeyUsage.NON_REPUDIATION, label: "Non Repudiation" },
|
||||||
|
{ value: CertKeyUsage.DATA_ENCIPHERMENT, label: "Data Encipherment" },
|
||||||
|
{ value: CertKeyUsage.KEY_AGREEMENT, label: "Key Agreement" },
|
||||||
|
{ value: CertKeyUsage.KEY_CERT_SIGN, label: "Certificate Sign" },
|
||||||
|
{ value: CertKeyUsage.CRL_SIGN, label: "CRL Sign" },
|
||||||
|
{ value: CertKeyUsage.ENCIPHER_ONLY, label: "Encipher Only" },
|
||||||
|
{ value: CertKeyUsage.DECIPHER_ONLY, label: "Decipher Only" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const EXTENDED_KEY_USAGES_OPTIONS = [
|
||||||
|
{ value: CertExtendedKeyUsage.CLIENT_AUTH, label: "Client Auth" },
|
||||||
|
{ value: CertExtendedKeyUsage.SERVER_AUTH, label: "Server Auth" },
|
||||||
|
{ value: CertExtendedKeyUsage.EMAIL_PROTECTION, label: "Email Protection" },
|
||||||
|
{ value: CertExtendedKeyUsage.OCSP_SIGNING, label: "OCSP Signing" },
|
||||||
|
{ value: CertExtendedKeyUsage.CODE_SIGNING, label: "Code Signing" },
|
||||||
|
{ value: CertExtendedKeyUsage.TIMESTAMPING, label: "Timestamping" }
|
||||||
|
] as const;
|
||||||
|
@@ -22,3 +22,24 @@ export enum CrlReason {
|
|||||||
PRIVILEGE_WITHDRAWN = "PRIVILEGE_WITHDRAWN",
|
PRIVILEGE_WITHDRAWN = "PRIVILEGE_WITHDRAWN",
|
||||||
A_A_COMPROMISE = "A_A_COMPROMISE"
|
A_A_COMPROMISE = "A_A_COMPROMISE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CertKeyUsage {
|
||||||
|
DIGITAL_SIGNATURE = "digitalSignature",
|
||||||
|
KEY_ENCIPHERMENT = "keyEncipherment",
|
||||||
|
NON_REPUDIATION = "nonRepudiation",
|
||||||
|
DATA_ENCIPHERMENT = "dataEncipherment",
|
||||||
|
KEY_AGREEMENT = "keyAgreement",
|
||||||
|
KEY_CERT_SIGN = "keyCertSign",
|
||||||
|
CRL_SIGN = "cRLSign",
|
||||||
|
ENCIPHER_ONLY = "encipherOnly",
|
||||||
|
DECIPHER_ONLY = "decipherOnly"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CertExtendedKeyUsage {
|
||||||
|
CLIENT_AUTH = "clientAuth",
|
||||||
|
SERVER_AUTH = "serverAuth",
|
||||||
|
CODE_SIGNING = "codeSigning",
|
||||||
|
EMAIL_PROTECTION = "emailProtection",
|
||||||
|
TIMESTAMPING = "timeStamping",
|
||||||
|
OCSP_SIGNING = "ocspSigning"
|
||||||
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { CertStatus } from "./enums";
|
import { CertExtendedKeyUsage, CertKeyUsage, CertStatus } from "./enums";
|
||||||
|
|
||||||
export type TCertificate = {
|
export type TCertificate = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -11,6 +11,8 @@ export type TCertificate = {
|
|||||||
serialNumber: string;
|
serialNumber: string;
|
||||||
notBefore: string;
|
notBefore: string;
|
||||||
notAfter: string;
|
notAfter: string;
|
||||||
|
keyUsages: CertKeyUsage[];
|
||||||
|
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteCertDTO = {
|
export type TDeleteCertDTO = {
|
||||||
|
4
frontend/src/hooks/api/generic/types.ts
Normal file
4
frontend/src/hooks/api/generic/types.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum OrderByDirection {
|
||||||
|
ASC = "asc",
|
||||||
|
DESC = "desc"
|
||||||
|
}
|
@@ -469,3 +469,8 @@ export type RevokeTokenDTO = {
|
|||||||
export type RevokeTokenRes = {
|
export type RevokeTokenRes = {
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TProjectIdentitiesList = {
|
||||||
|
identityMemberships: IdentityMembership[];
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
|
@@ -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 { apiRequest } from "@app/config/request";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
|
||||||
import { TGroupOrgMembership } from "../groups/types";
|
import { TGroupOrgMembership } from "../groups/types";
|
||||||
import { IdentityMembershipOrg } from "../identities/types";
|
|
||||||
import {
|
import {
|
||||||
BillingDetails,
|
BillingDetails,
|
||||||
Invoice,
|
Invoice,
|
||||||
License,
|
License,
|
||||||
Organization,
|
Organization,
|
||||||
|
OrgIdentityOrderBy,
|
||||||
OrgPlanTable,
|
OrgPlanTable,
|
||||||
PlanBillingInfo,
|
PlanBillingInfo,
|
||||||
PmtMethod,
|
PmtMethod,
|
||||||
ProductsTable,
|
ProductsTable,
|
||||||
TaxID,
|
TaxID,
|
||||||
|
TListOrgIdentitiesDTO,
|
||||||
|
TOrgIdentitiesList,
|
||||||
UpdateOrgDTO
|
UpdateOrgDTO
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -30,6 +33,12 @@ export const organizationKeys = {
|
|||||||
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const,
|
getOrgLicenses: (orgId: string) => [{ orgId }, "organization-licenses"] as const,
|
||||||
getOrgIdentityMemberships: (orgId: string) =>
|
getOrgIdentityMemberships: (orgId: string) =>
|
||||||
[{ orgId }, "organization-identity-memberships"] as const,
|
[{ orgId }, "organization-identity-memberships"] as const,
|
||||||
|
// allows invalidation using above key without knowing params
|
||||||
|
getOrgIdentityMembershipsWithParams: ({
|
||||||
|
organizationId: orgId,
|
||||||
|
...params
|
||||||
|
}: TListOrgIdentitiesDTO) =>
|
||||||
|
[...organizationKeys.getOrgIdentityMemberships(orgId), params] as const,
|
||||||
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const
|
getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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({
|
return useQuery({
|
||||||
queryKey: organizationKeys.getOrgIdentityMemberships(organizationId),
|
queryKey: organizationKeys.getOrgIdentityMembershipsWithParams({
|
||||||
|
organizationId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
|
}),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const {
|
const { data } = await apiRequest.get<TOrgIdentitiesList>(
|
||||||
data: { identityMemberships }
|
`/api/v2/organizations/${organizationId}/identity-memberships`,
|
||||||
} = await apiRequest.get<{ identityMemberships: IdentityMembershipOrg[] }>(
|
{ params }
|
||||||
`/api/v2/organizations/${organizationId}/identity-memberships`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return identityMemberships;
|
return data;
|
||||||
},
|
},
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
import { IdentityMembershipOrg } from "@app/hooks/api/identities/types";
|
||||||
|
|
||||||
export type Organization = {
|
export type Organization = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -102,3 +105,22 @@ export type ProductsTable = {
|
|||||||
head: ProductsTableHead[];
|
head: ProductsTableHead[];
|
||||||
rows: ProductsTableRow[];
|
rows: ProductsTableRow[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TListOrgIdentitiesDTO = {
|
||||||
|
organizationId: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: OrgIdentityOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TOrgIdentitiesList = {
|
||||||
|
identityMemberships: IdentityMembershipOrg[];
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum OrgIdentityOrderBy {
|
||||||
|
Name = "name",
|
||||||
|
Role = "role"
|
||||||
|
}
|
||||||
|
@@ -163,27 +163,29 @@ export const useGetImportedSecretsAllEnvs = ({
|
|||||||
queryFn: () => fetchImportedSecrets(projectId, env, path).catch(() => []),
|
queryFn: () => fetchImportedSecrets(projectId, env, path).catch(() => []),
|
||||||
enabled: Boolean(projectId) && Boolean(env),
|
enabled: Boolean(projectId) && Boolean(env),
|
||||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
select: (data: TImportedSecrets[]) => {
|
select: useCallback(
|
||||||
return data.map((el) => ({
|
(data: Awaited<ReturnType<typeof fetchImportedSecrets>>) =>
|
||||||
environment: el.environment,
|
data.map((el) => ({
|
||||||
secretPath: el.secretPath,
|
environment: el.environment,
|
||||||
environmentInfo: el.environmentInfo,
|
secretPath: el.secretPath,
|
||||||
folderId: el.folderId,
|
environmentInfo: el.environmentInfo,
|
||||||
secrets: el.secrets.map((encSecret) => {
|
folderId: el.folderId,
|
||||||
return {
|
secrets: el.secrets.map((encSecret) => {
|
||||||
id: encSecret.id,
|
return {
|
||||||
env: encSecret.environment,
|
id: encSecret.id,
|
||||||
key: encSecret.secretKey,
|
env: encSecret.environment,
|
||||||
value: encSecret.secretValue,
|
key: encSecret.secretKey,
|
||||||
tags: encSecret.tags,
|
value: encSecret.secretValue,
|
||||||
comment: encSecret.secretComment,
|
tags: encSecret.tags,
|
||||||
createdAt: encSecret.createdAt,
|
comment: encSecret.secretComment,
|
||||||
updatedAt: encSecret.updatedAt,
|
createdAt: encSecret.createdAt,
|
||||||
version: encSecret.version
|
updatedAt: encSecret.updatedAt,
|
||||||
};
|
version: encSecret.version
|
||||||
})
|
};
|
||||||
}));
|
})
|
||||||
}
|
})),
|
||||||
|
[]
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -108,7 +108,7 @@ export const useGetProjectSecrets = ({
|
|||||||
// wait for all values to be available
|
// wait for all values to be available
|
||||||
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
|
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
|
||||||
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
|
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
|
||||||
queryFn: async () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
const serverResponse = error.response?.data as { message: string };
|
const serverResponse = error.response?.data as { message: string };
|
||||||
@@ -119,7 +119,10 @@ export const useGetProjectSecrets = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
select: ({ secrets }) => mergePersonalSecrets(secrets)
|
select: useCallback(
|
||||||
|
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => mergePersonalSecrets(data.secrets),
|
||||||
|
[]
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useGetProjectSecretsAllEnv = ({
|
export const useGetProjectSecretsAllEnv = ({
|
||||||
@@ -131,7 +134,11 @@ export const useGetProjectSecretsAllEnv = ({
|
|||||||
|
|
||||||
const secrets = useQueries({
|
const secrets = useQueries({
|
||||||
queries: envs.map((environment) => ({
|
queries: envs.map((environment) => ({
|
||||||
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
|
queryKey: secretKeys.getProjectSecret({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
|
}),
|
||||||
enabled: Boolean(workspaceId && environment),
|
enabled: Boolean(workspaceId && environment),
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
if (axios.isAxiosError(error) && !isErrorHandled) {
|
if (axios.isAxiosError(error) && !isErrorHandled) {
|
||||||
@@ -147,12 +154,17 @@ export const useGetProjectSecretsAllEnv = ({
|
|||||||
setIsErrorHandled.on();
|
setIsErrorHandled.on();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
queryFn: async () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }),
|
||||||
select: (el: SecretV3RawResponse) =>
|
staleTime: 60 * 1000,
|
||||||
mergePersonalSecrets(el.secrets).reduce<Record<string, SecretV3RawSanitized>>(
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
(prev, curr) => ({ ...prev, [curr.key]: curr }),
|
select: useCallback(
|
||||||
{}
|
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) =>
|
||||||
)
|
mergePersonalSecrets(data.secrets).reduce<Record<string, SecretV3RawSanitized>>(
|
||||||
|
(prev, curr) => ({ ...prev, [curr.key]: curr }),
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
|
||||||
import { CaStatus } from "../ca/enums";
|
import { CaStatus } from "../ca/enums";
|
||||||
import { TCertificateAuthority } from "../ca/types";
|
import { TCertificateAuthority } from "../ca/types";
|
||||||
@@ -8,7 +9,7 @@ import { TCertificate } from "../certificates/types";
|
|||||||
import { TCertificateTemplate } from "../certificateTemplates/types";
|
import { TCertificateTemplate } from "../certificateTemplates/types";
|
||||||
import { TGroupMembership } from "../groups/types";
|
import { TGroupMembership } from "../groups/types";
|
||||||
import { identitiesKeys } from "../identities/queries";
|
import { identitiesKeys } from "../identities/queries";
|
||||||
import { IdentityMembership } from "../identities/types";
|
import { TProjectIdentitiesList } from "../identities/types";
|
||||||
import { IntegrationAuth } from "../integrationAuth/types";
|
import { IntegrationAuth } from "../integrationAuth/types";
|
||||||
import { TIntegration } from "../integrations/types";
|
import { TIntegration } from "../integrations/types";
|
||||||
import { TPkiAlert } from "../pkiAlerts/types";
|
import { TPkiAlert } from "../pkiAlerts/types";
|
||||||
@@ -24,8 +25,10 @@ import {
|
|||||||
DeleteEnvironmentDTO,
|
DeleteEnvironmentDTO,
|
||||||
DeleteWorkspaceDTO,
|
DeleteWorkspaceDTO,
|
||||||
NameWorkspaceSecretsDTO,
|
NameWorkspaceSecretsDTO,
|
||||||
|
ProjectIdentityOrderBy,
|
||||||
RenameWorkspaceDTO,
|
RenameWorkspaceDTO,
|
||||||
TGetUpgradeProjectStatusDTO,
|
TGetUpgradeProjectStatusDTO,
|
||||||
|
TListProjectIdentitiesDTO,
|
||||||
ToggleAutoCapitalizationDTO,
|
ToggleAutoCapitalizationDTO,
|
||||||
TUpdateWorkspaceIdentityRoleDTO,
|
TUpdateWorkspaceIdentityRoleDTO,
|
||||||
TUpdateWorkspaceUserRoleDTO,
|
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({
|
return useQuery({
|
||||||
queryKey: workspaceKeys.getWorkspaceIdentityMemberships(workspaceId),
|
queryKey: workspaceKeys.getWorkspaceIdentityMembershipsWithParams({
|
||||||
|
workspaceId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
|
}),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const {
|
const params = new URLSearchParams({
|
||||||
data: { identityMemberships }
|
offset: String(offset),
|
||||||
} = await apiRequest.get<{ identityMemberships: IdentityMembership[] }>(
|
limit: String(limit),
|
||||||
`/api/v2/workspace/${workspaceId}/identity-memberships`
|
orderBy: String(orderBy),
|
||||||
|
orderDirection: String(orderDirection),
|
||||||
|
search: String(search)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await apiRequest.get<TProjectIdentitiesList>(
|
||||||
|
`/api/v2/workspace/${workspaceId}/identity-memberships`,
|
||||||
|
{ params }
|
||||||
);
|
);
|
||||||
return identityMemberships;
|
return data;
|
||||||
},
|
},
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
...options
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { TListProjectIdentitiesDTO } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import type { CaStatus } from "../ca";
|
import type { CaStatus } from "../ca";
|
||||||
|
|
||||||
export const workspaceKeys = {
|
export const workspaceKeys = {
|
||||||
@@ -15,6 +17,12 @@ export const workspaceKeys = {
|
|||||||
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
|
getWorkspaceUsers: (workspaceId: string) => [{ workspaceId }, "workspace-users"] as const,
|
||||||
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
getWorkspaceIdentityMemberships: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
[{ workspaceId }, "workspace-identity-memberships"] as const,
|
||||||
|
// allows invalidation using above key without knowing params
|
||||||
|
getWorkspaceIdentityMembershipsWithParams: ({
|
||||||
|
workspaceId,
|
||||||
|
...params
|
||||||
|
}: TListProjectIdentitiesDTO) =>
|
||||||
|
[...workspaceKeys.getWorkspaceIdentityMemberships(workspaceId), params] as const,
|
||||||
getWorkspaceGroupMemberships: (workspaceId: string) =>
|
getWorkspaceGroupMemberships: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-groups"] as const,
|
[{ workspaceId }, "workspace-groups"] as const,
|
||||||
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
|
getWorkspaceCas: ({ projectSlug }: { projectSlug: string }) =>
|
||||||
|
@@ -1,3 +1,5 @@
|
|||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
|
||||||
import { TProjectRole } from "../roles/types";
|
import { TProjectRole } from "../roles/types";
|
||||||
|
|
||||||
export enum ProjectVersion {
|
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"
|
||||||
|
}
|
||||||
|
@@ -43,11 +43,6 @@ export const useNavigateToSelectOrganization = () => {
|
|||||||
await navigateUserToOrg(router, config.defaultAuthOrgId);
|
await navigateUserToOrg(router, config.defaultAuthOrgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let localOrgId = localStorage.getItem("orgData.id")
|
|
||||||
if(!cliCallbackPort && localOrgId != null){
|
|
||||||
await navigateUserToOrg(router, localOrgId);
|
|
||||||
}
|
|
||||||
|
|
||||||
queryClient.invalidateQueries(userKeys.getUser);
|
queryClient.invalidateQueries(userKeys.getUser);
|
||||||
let redirectTo = "/login/select-organization";
|
let redirectTo = "/login/select-organization";
|
||||||
|
|
||||||
|
@@ -1,5 +1,12 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
@@ -11,8 +18,12 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
|
Spinner,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -23,7 +34,10 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
|
import { useDebounce } from "@app/hooks";
|
||||||
import { useGetIdentityMembershipOrgs, useGetOrgRoles, useUpdateIdentity } from "@app/hooks/api";
|
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";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -36,22 +50,60 @@ type Props = {
|
|||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const INIT_PER_PAGE = 10;
|
||||||
|
|
||||||
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { currentOrg } = useOrganization();
|
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 { 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 }) => {
|
const handleChangeRole = async ({ identityId, role }: { identityId: string; role: string }) => {
|
||||||
try {
|
try {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId,
|
identityId,
|
||||||
role,
|
role,
|
||||||
organizationId: orgId
|
organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -71,124 +123,180 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<div>
|
||||||
<Table>
|
<Input
|
||||||
<THead>
|
containerClassName="mb-4"
|
||||||
<Tr>
|
value={search}
|
||||||
<Th>Name</Th>
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
<Th>Role</Th>
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
<Th className="w-5" />
|
placeholder="Search identities by name..."
|
||||||
</Tr>
|
/>
|
||||||
</THead>
|
<TableContainer>
|
||||||
<TBody>
|
<Table>
|
||||||
{isLoading && <TableSkeleton columns={4} innerKey="org-identities" />}
|
<THead>
|
||||||
{!isLoading &&
|
<Tr className="h-14">
|
||||||
data?.map(({ identity: { id, name }, role, customRole }) => {
|
<Th className="w-1/2">
|
||||||
return (
|
<div className="flex items-center">
|
||||||
<Tr
|
Name
|
||||||
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
<IconButton
|
||||||
key={`identity-${id}`}
|
variant="plain"
|
||||||
onClick={() => router.push(`/org/${orgId}/identities/${id}`)}
|
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Name ? "" : "opacity-30"}`}
|
||||||
>
|
ariaLabel="sort"
|
||||||
<Td>{name}</Td>
|
onClick={() => handleSort(OrgIdentityOrderBy.Name)}
|
||||||
<Td>
|
>
|
||||||
<OrgPermissionCan
|
<FontAwesomeIcon
|
||||||
I={OrgPermissionActions.Edit}
|
icon={
|
||||||
a={OrgPermissionSubjects.Identity}
|
orderDirection === OrderByDirection.DESC &&
|
||||||
>
|
orderBy === OrgIdentityOrderBy.Name
|
||||||
{(isAllowed) => {
|
? faArrowUp
|
||||||
return (
|
: faArrowDown
|
||||||
<Select
|
}
|
||||||
value={role === "custom" ? (customRole?.slug as string) : role}
|
/>
|
||||||
isDisabled={!isAllowed}
|
</IconButton>
|
||||||
className="w-40 bg-mineshaft-600"
|
</div>
|
||||||
dropdownContainerClassName="border border-mineshaft-600 bg-mineshaft-800"
|
</Th>
|
||||||
onValueChange={(selectedRole) =>
|
<Th>
|
||||||
handleChangeRole({
|
<div className="flex items-center">
|
||||||
identityId: id,
|
Role
|
||||||
role: selectedRole
|
<IconButton
|
||||||
})
|
variant="plain"
|
||||||
}
|
className={`ml-2 ${orderBy === OrgIdentityOrderBy.Role ? "" : "opacity-30"}`}
|
||||||
>
|
ariaLabel="sort"
|
||||||
{(roles || []).map(({ slug, name: roleName }) => (
|
onClick={() => handleSort(OrgIdentityOrderBy.Role)}
|
||||||
<SelectItem value={slug} key={`owner-option-${slug}`}>
|
>
|
||||||
{roleName}
|
<FontAwesomeIcon
|
||||||
</SelectItem>
|
icon={
|
||||||
))}
|
orderDirection === OrderByDirection.DESC &&
|
||||||
</Select>
|
orderBy === OrgIdentityOrderBy.Role
|
||||||
);
|
? faArrowUp
|
||||||
}}
|
: faArrowDown
|
||||||
</OrgPermissionCan>
|
}
|
||||||
</Td>
|
/>
|
||||||
<Td>
|
</IconButton>
|
||||||
<DropdownMenu>
|
</div>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
</Th>
|
||||||
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||||
<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>
|
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
</THead>
|
||||||
</TBody>
|
<TBody>
|
||||||
</Table>
|
{isLoading && <TableSkeleton columns={3} innerKey="org-identities" />}
|
||||||
</TableContainer>
|
{!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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -7,7 +7,12 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
@@ -28,6 +33,11 @@ import {
|
|||||||
useListWorkspacePkiCollections
|
useListWorkspacePkiCollections
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||||
|
import {
|
||||||
|
EXTENDED_KEY_USAGES_OPTIONS,
|
||||||
|
KEY_USAGES_OPTIONS
|
||||||
|
} from "@app/hooks/api/certificates/constants";
|
||||||
|
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { CertificateContent } from "./CertificateContent";
|
import { CertificateContent } from "./CertificateContent";
|
||||||
@@ -39,7 +49,26 @@ const schema = z.object({
|
|||||||
friendlyName: z.string(),
|
friendlyName: z.string(),
|
||||||
commonName: z.string().trim().min(1),
|
commonName: z.string().trim().min(1),
|
||||||
altNames: z.string(),
|
altNames: z.string(),
|
||||||
ttl: z.string().trim()
|
ttl: z.string().trim(),
|
||||||
|
keyUsages: z.object({
|
||||||
|
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.CRL_SIGN]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional()
|
||||||
|
}),
|
||||||
|
extendedKeyUsages: z.object({
|
||||||
|
[CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
@@ -88,7 +117,14 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
setValue,
|
setValue,
|
||||||
watch
|
watch
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema)
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
keyUsages: {
|
||||||
|
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
|
||||||
|
[CertKeyUsage.KEY_ENCIPHERMENT]: true
|
||||||
|
},
|
||||||
|
extendedKeyUsages: {}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedCertTemplateId = watch("certificateTemplateId");
|
const selectedCertTemplateId = watch("certificateTemplateId");
|
||||||
@@ -107,7 +143,11 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
commonName: cert.commonName,
|
commonName: cert.commonName,
|
||||||
altNames: cert.altNames,
|
altNames: cert.altNames,
|
||||||
certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE,
|
certificateTemplateId: cert.certificateTemplateId ?? CERT_TEMPLATE_NONE_VALUE,
|
||||||
ttl: ""
|
ttl: "",
|
||||||
|
keyUsages: Object.fromEntries((cert.keyUsages || []).map((name) => [name, true])),
|
||||||
|
extendedKeyUsages: Object.fromEntries(
|
||||||
|
(cert.extendedKeyUsages || []).map((name) => [name, true])
|
||||||
|
)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
@@ -116,7 +156,12 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
commonName: "",
|
commonName: "",
|
||||||
altNames: "",
|
altNames: "",
|
||||||
ttl: "",
|
ttl: "",
|
||||||
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE
|
certificateTemplateId: CERT_TEMPLATE_NONE_VALUE,
|
||||||
|
keyUsages: {
|
||||||
|
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
|
||||||
|
[CertKeyUsage.KEY_ENCIPHERMENT]: true
|
||||||
|
},
|
||||||
|
extendedKeyUsages: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [cert]);
|
}, [cert]);
|
||||||
@@ -124,6 +169,14 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!cert && selectedCertTemplate) {
|
if (!cert && selectedCertTemplate) {
|
||||||
setValue("ttl", selectedCertTemplate.ttl);
|
setValue("ttl", selectedCertTemplate.ttl);
|
||||||
|
setValue(
|
||||||
|
"keyUsages",
|
||||||
|
Object.fromEntries(selectedCertTemplate.keyUsages.map((name) => [name, true]))
|
||||||
|
);
|
||||||
|
setValue(
|
||||||
|
"extendedKeyUsages",
|
||||||
|
Object.fromEntries(selectedCertTemplate.extendedKeyUsages.map((name) => [name, true]))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}, [selectedCertTemplate, cert]);
|
}, [selectedCertTemplate, cert]);
|
||||||
|
|
||||||
@@ -133,7 +186,9 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
collectionId,
|
collectionId,
|
||||||
commonName,
|
commonName,
|
||||||
altNames,
|
altNames,
|
||||||
ttl
|
ttl,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentWorkspace?.slug) return;
|
if (!currentWorkspace?.slug) return;
|
||||||
@@ -146,7 +201,13 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
friendlyName,
|
friendlyName,
|
||||||
commonName,
|
commonName,
|
||||||
altNames,
|
altNames,
|
||||||
ttl
|
ttl,
|
||||||
|
keyUsages: Object.entries(keyUsages)
|
||||||
|
.filter(([, value]) => value)
|
||||||
|
.map(([key]) => key as CertKeyUsage),
|
||||||
|
extendedKeyUsages: Object.entries(extendedKeyUsages)
|
||||||
|
.filter(([, value]) => value)
|
||||||
|
.map(([key]) => key as CertExtendedKeyUsage)
|
||||||
});
|
});
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
@@ -363,8 +424,87 @@ export const CertificateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
|
||||||
|
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||||
|
<div className="order-1 ml-3">Key Usage</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="keyUsages"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Key Usage"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<div className="mt-2 mb-7 grid grid-cols-2 gap-2">
|
||||||
|
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={optionValue}
|
||||||
|
key={optionValue}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
isDisabled={Boolean(cert)}
|
||||||
|
isChecked={value[optionValue]}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[optionValue]: state
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="extendedKeyUsages"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Extended Key Usage"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<div className="mt-2 mb-7 grid grid-cols-2 gap-2">
|
||||||
|
{EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={optionValue}
|
||||||
|
key={optionValue}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
isDisabled={Boolean(cert)}
|
||||||
|
isChecked={value[optionValue]}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[optionValue]: state
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
{!cert && (
|
{!cert && (
|
||||||
<div className="flex items-center">
|
<div className="mt-4 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@@ -7,7 +7,12 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
Button,
|
Button,
|
||||||
|
Checkbox,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
Input,
|
Input,
|
||||||
@@ -25,8 +30,14 @@ import {
|
|||||||
useGetCertTemplate,
|
useGetCertTemplate,
|
||||||
useListWorkspaceCas,
|
useListWorkspaceCas,
|
||||||
useListWorkspacePkiCollections,
|
useListWorkspacePkiCollections,
|
||||||
useUpdateCertTemplate} from "@app/hooks/api";
|
useUpdateCertTemplate
|
||||||
|
} from "@app/hooks/api";
|
||||||
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||||
|
import {
|
||||||
|
EXTENDED_KEY_USAGES_OPTIONS,
|
||||||
|
KEY_USAGES_OPTIONS
|
||||||
|
} from "@app/hooks/api/certificates/constants";
|
||||||
|
import { CertExtendedKeyUsage, CertKeyUsage } from "@app/hooks/api/certificates/enums";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const validateTemplateRegexField = z
|
const validateTemplateRegexField = z
|
||||||
@@ -45,7 +56,26 @@ const schema = z.object({
|
|||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
commonName: validateTemplateRegexField,
|
commonName: validateTemplateRegexField,
|
||||||
subjectAlternativeName: validateTemplateRegexField,
|
subjectAlternativeName: validateTemplateRegexField,
|
||||||
ttl: z.string().trim().min(1)
|
ttl: z.string().trim().min(1),
|
||||||
|
keyUsages: z.object({
|
||||||
|
[CertKeyUsage.DIGITAL_SIGNATURE]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.KEY_ENCIPHERMENT]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.NON_REPUDIATION]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.DATA_ENCIPHERMENT]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.KEY_AGREEMENT]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.KEY_CERT_SIGN]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.CRL_SIGN]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.ENCIPHER_ONLY]: z.boolean().optional(),
|
||||||
|
[CertKeyUsage.DECIPHER_ONLY]: z.boolean().optional()
|
||||||
|
}),
|
||||||
|
extendedKeyUsages: z.object({
|
||||||
|
[CertExtendedKeyUsage.CLIENT_AUTH]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.CODE_SIGNING]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.EMAIL_PROTECTION]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.OCSP_SIGNING]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.SERVER_AUTH]: z.boolean().optional(),
|
||||||
|
[CertExtendedKeyUsage.TIMESTAMPING]: z.boolean().optional()
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
@@ -61,9 +91,9 @@ type Props = {
|
|||||||
|
|
||||||
export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Props) => {
|
export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Props) => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const { data: ca } = useGetCaById(caId);
|
const { data: ca } = useGetCaById(caId);
|
||||||
|
|
||||||
const { data: certTemplate } = useGetCertTemplate(
|
const { data: certTemplate } = useGetCertTemplate(
|
||||||
(popUp?.certificateTemplate?.data as { id: string })?.id || ""
|
(popUp?.certificateTemplate?.data as { id: string })?.id || ""
|
||||||
);
|
);
|
||||||
@@ -86,7 +116,13 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
|||||||
reset,
|
reset,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: zodResolver(schema)
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
keyUsages: {
|
||||||
|
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
|
||||||
|
[CertKeyUsage.KEY_ENCIPHERMENT]: true
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -97,14 +133,23 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
|||||||
commonName: certTemplate.commonName,
|
commonName: certTemplate.commonName,
|
||||||
subjectAlternativeName: certTemplate.subjectAlternativeName,
|
subjectAlternativeName: certTemplate.subjectAlternativeName,
|
||||||
collectionId: certTemplate.pkiCollectionId ?? undefined,
|
collectionId: certTemplate.pkiCollectionId ?? undefined,
|
||||||
ttl: certTemplate.ttl
|
ttl: certTemplate.ttl,
|
||||||
|
keyUsages: Object.fromEntries(certTemplate.keyUsages.map((name) => [name, true]) ?? []),
|
||||||
|
extendedKeyUsages: Object.fromEntries(
|
||||||
|
certTemplate.extendedKeyUsages.map((name) => [name, true]) ?? []
|
||||||
|
)
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
caId,
|
caId,
|
||||||
name: "",
|
name: "",
|
||||||
commonName: "",
|
commonName: "",
|
||||||
ttl: ""
|
ttl: "",
|
||||||
|
keyUsages: {
|
||||||
|
[CertKeyUsage.DIGITAL_SIGNATURE]: true,
|
||||||
|
[CertKeyUsage.KEY_ENCIPHERMENT]: true
|
||||||
|
},
|
||||||
|
extendedKeyUsages: {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [certTemplate, ca]);
|
}, [certTemplate, ca]);
|
||||||
@@ -114,7 +159,9 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
|||||||
name,
|
name,
|
||||||
commonName,
|
commonName,
|
||||||
subjectAlternativeName,
|
subjectAlternativeName,
|
||||||
ttl
|
ttl,
|
||||||
|
keyUsages,
|
||||||
|
extendedKeyUsages
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
if (!currentWorkspace?.id) {
|
if (!currentWorkspace?.id) {
|
||||||
return;
|
return;
|
||||||
@@ -130,7 +177,13 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
|||||||
name,
|
name,
|
||||||
commonName,
|
commonName,
|
||||||
subjectAlternativeName,
|
subjectAlternativeName,
|
||||||
ttl
|
ttl,
|
||||||
|
keyUsages: Object.entries(keyUsages)
|
||||||
|
.filter(([, value]) => value)
|
||||||
|
.map(([key]) => key as CertKeyUsage),
|
||||||
|
extendedKeyUsages: Object.entries(extendedKeyUsages)
|
||||||
|
.filter(([, value]) => value)
|
||||||
|
.map(([key]) => key as CertExtendedKeyUsage)
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -145,7 +198,13 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
|||||||
name,
|
name,
|
||||||
commonName,
|
commonName,
|
||||||
subjectAlternativeName,
|
subjectAlternativeName,
|
||||||
ttl
|
ttl,
|
||||||
|
keyUsages: Object.entries(keyUsages)
|
||||||
|
.filter(([, value]) => value)
|
||||||
|
.map(([key]) => key as CertKeyUsage),
|
||||||
|
extendedKeyUsages: Object.entries(extendedKeyUsages)
|
||||||
|
.filter(([, value]) => value)
|
||||||
|
.map(([key]) => key as CertExtendedKeyUsage)
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -332,7 +391,84 @@ export const CertificateTemplateModal = ({ popUp, handlePopUpToggle, caId }: Pro
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center">
|
<Accordion type="single" collapsible className="w-full">
|
||||||
|
<AccordionItem value="key-usages" className="data-[state=open]:border-none">
|
||||||
|
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||||
|
<div className="order-1 ml-3">Key Usage</div>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="keyUsages"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Key Usage"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<div className="mt-2 mb-7 grid grid-cols-2 gap-2">
|
||||||
|
{KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={optionValue}
|
||||||
|
key={optionValue}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
isChecked={value[optionValue]}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[optionValue]: state
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="extendedKeyUsages"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
label="Extended Key Usage"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<div className="mt-2 mb-7 grid grid-cols-2 gap-2">
|
||||||
|
{EXTENDED_KEY_USAGES_OPTIONS.map(({ label, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={optionValue}
|
||||||
|
key={optionValue}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
isChecked={value[optionValue]}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[optionValue]: state
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
<div className="mt-4 flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@@ -1,11 +1,15 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
faArrowUpRightFromSquare,
|
faArrowDown,
|
||||||
faClock,
|
faArrowUp,
|
||||||
faEdit,
|
faArrowUpRightFromSquare,
|
||||||
faPlus,
|
faClock,
|
||||||
faServer,
|
faEdit,
|
||||||
faXmark
|
faMagnifyingGlass,
|
||||||
|
faPlus,
|
||||||
|
faServer,
|
||||||
|
faXmark
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
@@ -15,337 +19,418 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DeleteActionModal,
|
DeleteActionModal,
|
||||||
EmptyState,
|
EmptyState,
|
||||||
HoverCard,
|
HoverCard,
|
||||||
HoverCardContent,
|
HoverCardContent,
|
||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
IconButton,
|
IconButton,
|
||||||
Modal,
|
Input,
|
||||||
ModalContent,
|
Modal,
|
||||||
Table,
|
ModalContent,
|
||||||
TableContainer,
|
Pagination,
|
||||||
TableSkeleton,
|
Spinner,
|
||||||
Tag,
|
Table,
|
||||||
TBody,
|
TableContainer,
|
||||||
Td,
|
TableSkeleton,
|
||||||
Th,
|
Tag,
|
||||||
THead,
|
TBody,
|
||||||
Tooltip,
|
Td,
|
||||||
Tr
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
import { withProjectPermission } from "@app/hoc";
|
||||||
|
import { useDebounce } from "@app/hooks";
|
||||||
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
|
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
||||||
|
import { ProjectIdentityOrderBy } from "@app/hooks/api/workspace/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityModal } from "./components/IdentityModal";
|
import { IdentityModal } from "./components/IdentityModal";
|
||||||
import { IdentityRoleForm } from "./components/IdentityRoleForm";
|
import { IdentityRoleForm } from "./components/IdentityRoleForm";
|
||||||
|
|
||||||
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
||||||
|
const INIT_PER_PAGE = 10;
|
||||||
const formatRoleName = (role: string, customRoleName?: string) => {
|
const formatRoleName = (role: string, customRoleName?: string) => {
|
||||||
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
||||||
if (role === ProjectMembershipRole.Member) return "Developer";
|
if (role === ProjectMembershipRole.Member) return "Developer";
|
||||||
if (role === ProjectMembershipRole.NoAccess) return "No access";
|
if (role === ProjectMembershipRole.NoAccess) return "No access";
|
||||||
return role;
|
return role;
|
||||||
};
|
};
|
||||||
export const IdentityTab = withProjectPermission(
|
export const IdentityTab = withProjectPermission(
|
||||||
() => {
|
() => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const workspaceId = currentWorkspace?.id ?? "";
|
const [page, setPage] = useState(1);
|
||||||
|
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||||
|
const [orderDirection, setOrderDirection] = useState(OrderByDirection.ASC);
|
||||||
|
const [orderBy, setOrderBy] = useState(ProjectIdentityOrderBy.Name);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const debouncedSearch = useDebounce(search);
|
||||||
|
|
||||||
const { data, isLoading } = useGetWorkspaceIdentityMemberships(currentWorkspace?.id || "");
|
const workspaceId = currentWorkspace?.id ?? "";
|
||||||
const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();
|
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const offset = (page - 1) * perPage;
|
||||||
"identity",
|
const { data, isLoading, isFetching } = useGetWorkspaceIdentityMemberships(
|
||||||
"deleteIdentity",
|
{
|
||||||
"upgradePlan",
|
workspaceId: currentWorkspace?.id || "",
|
||||||
"updateRole"
|
offset,
|
||||||
] as const);
|
limit: perPage,
|
||||||
|
orderDirection,
|
||||||
|
orderBy,
|
||||||
|
search: debouncedSearch
|
||||||
|
},
|
||||||
|
{ keepPreviousData: true }
|
||||||
|
);
|
||||||
|
const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace();
|
||||||
|
|
||||||
const onRemoveIdentitySubmit = async (identityId: string) => {
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
try {
|
"identity",
|
||||||
await deleteMutateAsync({
|
"deleteIdentity",
|
||||||
identityId,
|
"upgradePlan",
|
||||||
workspaceId
|
"updateRole"
|
||||||
});
|
] as const);
|
||||||
|
|
||||||
createNotification({
|
const onRemoveIdentitySubmit = async (identityId: string) => {
|
||||||
text: "Successfully removed identity from project",
|
try {
|
||||||
type: "success"
|
await deleteMutateAsync({
|
||||||
});
|
identityId,
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
handlePopUpClose("deleteIdentity");
|
createNotification({
|
||||||
} catch (err) {
|
text: "Successfully removed identity from project",
|
||||||
console.error(err);
|
type: "success"
|
||||||
const error = err as any;
|
});
|
||||||
const text = error?.response?.data?.message ?? "Failed to remove identity from project";
|
|
||||||
|
|
||||||
createNotification({
|
handlePopUpClose("deleteIdentity");
|
||||||
text,
|
} catch (err) {
|
||||||
type: "error"
|
console.error(err);
|
||||||
});
|
const error = err as any;
|
||||||
}
|
const text = error?.response?.data?.message ?? "Failed to remove identity from project";
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
createNotification({
|
||||||
<motion.div
|
text,
|
||||||
key="identity-role-panel"
|
type: "error"
|
||||||
transition={{ duration: 0.15 }}
|
});
|
||||||
initial={{ opacity: 0, translateX: 30 }}
|
}
|
||||||
animate={{ opacity: 1, translateX: 0 }}
|
};
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
|
||||||
|
useEffect(() => {
|
||||||
|
// reset page if no longer valid
|
||||||
|
if (data && data.totalCount < offset) setPage(1);
|
||||||
|
}, [data?.totalCount]);
|
||||||
|
|
||||||
|
const handleSort = (column: ProjectIdentityOrderBy) => {
|
||||||
|
if (column === orderBy) {
|
||||||
|
setOrderDirection((prev) =>
|
||||||
|
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOrderBy(column);
|
||||||
|
setOrderDirection(OrderByDirection.ASC);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key="identity-role-panel"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
||||||
|
<div className="flex w-full justify-end pr-4">
|
||||||
|
<Link href="https://infisical.com/docs/documentation/platform/identities/overview">
|
||||||
|
<span className="w-max cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||||
|
Documentation{" "}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.06rem] ml-1 text-xs"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={ProjectPermissionSub.Identity}
|
||||||
>
|
>
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
{(isAllowed) => (
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<Button
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
|
colorSchema="primary"
|
||||||
<div className="flex w-full justify-end pr-4">
|
type="submit"
|
||||||
<Link href="https://infisical.com/docs/documentation/platform/identities/overview">
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
<span className="w-max cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
onClick={() => handlePopUpOpen("identity")}
|
||||||
Documentation{" "}
|
isDisabled={!isAllowed}
|
||||||
<FontAwesomeIcon
|
>
|
||||||
icon={faArrowUpRightFromSquare}
|
Add identity
|
||||||
className="mb-[0.06rem] ml-1 text-xs"
|
</Button>
|
||||||
/>
|
)}
|
||||||
</span>
|
</ProjectPermissionCan>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
<Input
|
||||||
<ProjectPermissionCan
|
containerClassName="mb-4"
|
||||||
I={ProjectPermissionActions.Create}
|
value={search}
|
||||||
a={ProjectPermissionSub.Identity}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
>
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
{(isAllowed) => (
|
placeholder="Search identities by name..."
|
||||||
<Button
|
/>
|
||||||
colorSchema="primary"
|
<TableContainer>
|
||||||
type="submit"
|
<Table>
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
<THead>
|
||||||
onClick={() => handlePopUpOpen("identity")}
|
<Tr className="h-14">
|
||||||
isDisabled={!isAllowed}
|
<Th className="w-1/3">
|
||||||
>
|
<div className="flex items-center">
|
||||||
Add identity
|
Name
|
||||||
</Button>
|
<IconButton
|
||||||
)}
|
variant="plain"
|
||||||
</ProjectPermissionCan>
|
className={`ml-2 ${
|
||||||
|
orderBy === ProjectIdentityOrderBy.Name ? "" : "opacity-30"
|
||||||
|
}`}
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() => handleSort(ProjectIdentityOrderBy.Name)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={
|
||||||
|
orderDirection === OrderByDirection.DESC &&
|
||||||
|
orderBy === ProjectIdentityOrderBy.Name
|
||||||
|
? faArrowUp
|
||||||
|
: faArrowDown
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<TableContainer>
|
</Th>
|
||||||
<Table>
|
<Th className="w-1/3">Role</Th>
|
||||||
<THead>
|
<Th>Added on</Th>
|
||||||
<Tr>
|
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||||
<Th>Name</Th>
|
</Tr>
|
||||||
<Th>Role</Th>
|
</THead>
|
||||||
<Th>Added on</Th>
|
<TBody>
|
||||||
<Th className="w-5" />
|
{isLoading && <TableSkeleton columns={4} innerKey="project-identities" />}
|
||||||
</Tr>
|
{!isLoading &&
|
||||||
</THead>
|
data &&
|
||||||
<TBody>
|
data.identityMemberships.length > 0 &&
|
||||||
{isLoading && <TableSkeleton columns={7} innerKey="project-identities" />}
|
data.identityMemberships.map((identityMember, index) => {
|
||||||
{!isLoading &&
|
const {
|
||||||
data &&
|
identity: { id, name },
|
||||||
data.length > 0 &&
|
roles,
|
||||||
data.map((identityMember, index) => {
|
createdAt
|
||||||
const {
|
} = identityMember;
|
||||||
identity: { id, name },
|
return (
|
||||||
roles,
|
<Tr className="h-10" key={`st-v3-${id}`}>
|
||||||
createdAt
|
<Td>{name}</Td>
|
||||||
} = identityMember;
|
|
||||||
return (
|
|
||||||
<Tr className="h-10" key={`st-v3-${id}`}>
|
|
||||||
<Td>{name}</Td>
|
|
||||||
|
|
||||||
<Td>
|
<Td>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{roles
|
{roles
|
||||||
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
|
.slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
|
||||||
.map(
|
.map(
|
||||||
({
|
({
|
||||||
role,
|
role,
|
||||||
customRoleName,
|
customRoleName,
|
||||||
id: roleId,
|
id: roleId,
|
||||||
isTemporary,
|
isTemporary,
|
||||||
temporaryAccessEndTime
|
temporaryAccessEndTime
|
||||||
}) => {
|
}) => {
|
||||||
const isExpired =
|
const isExpired =
|
||||||
new Date() > new Date(temporaryAccessEndTime || ("" as string));
|
new Date() > new Date(temporaryAccessEndTime || ("" as string));
|
||||||
return (
|
return (
|
||||||
<Tag key={roleId}>
|
<Tag key={roleId}>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="capitalize">
|
<div className="capitalize">
|
||||||
{formatRoleName(role, customRoleName)}
|
{formatRoleName(role, customRoleName)}
|
||||||
</div>
|
</div>
|
||||||
{isTemporary && (
|
{isTemporary && (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
isExpired
|
isExpired
|
||||||
? "Timed role expired"
|
? "Timed role expired"
|
||||||
: "Timed role access"
|
: "Timed role access"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faClock}
|
icon={faClock}
|
||||||
className={twMerge(isExpired && "text-red-600")}
|
className={twMerge(isExpired && "text-red-600")}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
{roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && (
|
{roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && (
|
||||||
<HoverCard>
|
<HoverCard>
|
||||||
<HoverCardTrigger>
|
<HoverCardTrigger>
|
||||||
<Tag>+{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}</Tag>
|
<Tag>+{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE}</Tag>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="border border-gray-700 bg-mineshaft-800 p-4">
|
<HoverCardContent className="border border-gray-700 bg-mineshaft-800 p-4">
|
||||||
{roles
|
{roles
|
||||||
.slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
|
.slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE)
|
||||||
.map(
|
.map(
|
||||||
({
|
({
|
||||||
role,
|
role,
|
||||||
customRoleName,
|
customRoleName,
|
||||||
id: roleId,
|
id: roleId,
|
||||||
isTemporary,
|
isTemporary,
|
||||||
temporaryAccessEndTime
|
temporaryAccessEndTime
|
||||||
}) => {
|
}) => {
|
||||||
const isExpired =
|
const isExpired =
|
||||||
new Date() >
|
new Date() >
|
||||||
new Date(temporaryAccessEndTime || ("" as string));
|
new Date(temporaryAccessEndTime || ("" as string));
|
||||||
return (
|
return (
|
||||||
<Tag key={roleId} className="capitalize">
|
<Tag key={roleId} className="capitalize">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div>{formatRoleName(role, customRoleName)}</div>
|
<div>{formatRoleName(role, customRoleName)}</div>
|
||||||
{isTemporary && (
|
{isTemporary && (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={
|
content={
|
||||||
isExpired
|
isExpired
|
||||||
? "Access expired"
|
? "Access expired"
|
||||||
: "Temporary access"
|
: "Temporary access"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faClock}
|
icon={faClock}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
new Date() >
|
new Date() >
|
||||||
new Date(
|
new Date(
|
||||||
temporaryAccessEndTime as string
|
temporaryAccessEndTime as string
|
||||||
) && "text-red-600"
|
) && "text-red-600"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Tag>
|
</Tag>
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</HoverCardContent>
|
|
||||||
</HoverCard>
|
|
||||||
)}
|
|
||||||
<Tooltip content="Edit permission">
|
|
||||||
<IconButton
|
|
||||||
size="sm"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update-role"
|
|
||||||
onClick={() =>
|
|
||||||
handlePopUpOpen("updateRole", { ...identityMember, index })
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faEdit} />
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</Td>
|
|
||||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
|
||||||
<Td className="flex justify-end">
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Delete}
|
|
||||||
a={ProjectPermissionSub.Identity}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("deleteIdentity", {
|
|
||||||
identityId: id,
|
|
||||||
name
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="ml-4"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
</Td>
|
|
||||||
</Tr>
|
|
||||||
);
|
);
|
||||||
})}
|
}
|
||||||
{!isLoading && data && data?.length === 0 && (
|
)}
|
||||||
<Tr>
|
</HoverCardContent>
|
||||||
<Td colSpan={7}>
|
</HoverCard>
|
||||||
<EmptyState
|
)}
|
||||||
title="No identities have been added to this project"
|
<Tooltip content="Edit permission">
|
||||||
icon={faServer}
|
<IconButton
|
||||||
/>
|
size="sm"
|
||||||
</Td>
|
variant="plain"
|
||||||
</Tr>
|
ariaLabel="update-role"
|
||||||
)}
|
onClick={() =>
|
||||||
</TBody>
|
handlePopUpOpen("updateRole", { ...identityMember, index })
|
||||||
</Table>
|
}
|
||||||
</TableContainer>
|
>
|
||||||
<Modal
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
isOpen={popUp.updateRole.isOpen}
|
</IconButton>
|
||||||
onOpenChange={(state) => handlePopUpToggle("updateRole", state)}
|
</Tooltip>
|
||||||
>
|
</div>
|
||||||
<ModalContent
|
</Td>
|
||||||
className="max-w-3xl"
|
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||||
title={`Manage Access for ${(popUp.updateRole.data as IdentityMembership)?.identity?.name
|
<Td className="flex justify-end">
|
||||||
}`}
|
<ProjectPermissionCan
|
||||||
subTitle={`
|
I={ProjectPermissionActions.Delete}
|
||||||
|
a={ProjectPermissionSub.Identity}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("deleteIdentity", {
|
||||||
|
identityId: id,
|
||||||
|
name
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="ml-4"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</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 added to this project"
|
||||||
|
}
|
||||||
|
icon={faServer}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp.updateRole.isOpen}
|
||||||
|
onOpenChange={(state) => handlePopUpToggle("updateRole", state)}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
className="max-w-3xl"
|
||||||
|
title={`Manage Access for ${
|
||||||
|
(popUp.updateRole.data as IdentityMembership)?.identity?.name
|
||||||
|
}`}
|
||||||
|
subTitle={`
|
||||||
Configure role-based access control by assigning machine identities a mix of roles and specific privileges. An identity will gain access to all actions within the roles assigned to it, not just the actions those roles share in common. You must choose at least one permanent role.
|
Configure role-based access control by assigning machine identities a mix of roles and specific privileges. An identity will gain access to all actions within the roles assigned to it, not just the actions those roles share in common. You must choose at least one permanent role.
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<IdentityRoleForm
|
<IdentityRoleForm
|
||||||
onOpenUpgradeModal={(description) =>
|
onOpenUpgradeModal={(description) =>
|
||||||
handlePopUpOpen("upgradePlan", { description })
|
handlePopUpOpen("upgradePlan", { description })
|
||||||
}
|
}
|
||||||
identityProjectMember={
|
identityProjectMember={
|
||||||
data?.[
|
data?.identityMemberships[
|
||||||
(popUp.updateRole?.data as IdentityMembership & { index: number })?.index
|
(popUp.updateRole?.data as IdentityMembership & { index: number })?.index
|
||||||
] as IdentityMembership
|
] as IdentityMembership
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteIdentity.isOpen}
|
isOpen={popUp.deleteIdentity.isOpen}
|
||||||
title={`Are you sure want to remove ${(popUp?.deleteIdentity?.data as { name: string })?.name || ""
|
title={`Are you sure want to remove ${
|
||||||
} from the project?`}
|
(popUp?.deleteIdentity?.data as { name: string })?.name || ""
|
||||||
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
} from the project?`}
|
||||||
deleteKey="confirm"
|
onChange={(isOpen) => handlePopUpToggle("deleteIdentity", isOpen)}
|
||||||
onDeleteApproved={() =>
|
deleteKey="confirm"
|
||||||
onRemoveIdentitySubmit(
|
onDeleteApproved={() =>
|
||||||
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
|
onRemoveIdentitySubmit(
|
||||||
)
|
(popUp?.deleteIdentity?.data as { identityId: string })?.identityId
|
||||||
}
|
)
|
||||||
/>
|
}
|
||||||
</div>
|
/>
|
||||||
</motion.div>
|
</div>
|
||||||
);
|
</motion.div>
|
||||||
},
|
);
|
||||||
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity }
|
},
|
||||||
|
{ action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity }
|
||||||
);
|
);
|
||||||
|
@@ -1,11 +1,12 @@
|
|||||||
import { useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
import { Button, FormControl, Modal, ModalClose, ModalContent } from "@app/components/v2";
|
||||||
|
import { ComboBox } from "@app/components/v2/ComboBox";
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityToWorkspace,
|
useAddIdentityToWorkspace,
|
||||||
@@ -17,8 +18,14 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
|
|
||||||
const schema = yup
|
const schema = yup
|
||||||
.object({
|
.object({
|
||||||
identityId: yup.string().required("Identity id is required"),
|
identity: yup.object({
|
||||||
role: yup.string()
|
id: yup.string().required("Identity id is required"),
|
||||||
|
name: yup.string().required("Identity name is required")
|
||||||
|
}),
|
||||||
|
role: yup.object({
|
||||||
|
slug: yup.string().required("role slug is required"),
|
||||||
|
name: yup.string().required("role name is required")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
@@ -33,14 +40,26 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const orgId = currentOrg?.id || "";
|
const organizationId = currentOrg?.id || "";
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
const projectSlug = currentWorkspace?.slug || "";
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
|
|
||||||
const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId);
|
const { data: identityMembershipOrgsData } = useGetIdentityMembershipOrgs({
|
||||||
const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId);
|
organizationId,
|
||||||
|
limit: 20000 // TODO: this is temp to preserve functionality for bitcoindepot, will replace with combobox in separate PR
|
||||||
|
});
|
||||||
|
const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships;
|
||||||
|
const { data: identityMembershipsData } = useGetWorkspaceIdentityMemberships({
|
||||||
|
workspaceId,
|
||||||
|
limit: 20000 // TODO: this is temp to preserve functionality for bitcoindepot, will optimize in PR referenced above
|
||||||
|
});
|
||||||
|
const identityMemberships = identityMembershipsData?.identityMemberships;
|
||||||
|
|
||||||
const { data: roles } = useGetProjectRoles(projectSlug);
|
const {
|
||||||
|
data: roles,
|
||||||
|
isLoading: isRolesLoading,
|
||||||
|
isFetched: isRolesFetched
|
||||||
|
} = useGetProjectRoles(projectSlug);
|
||||||
|
|
||||||
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
|
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
|
||||||
|
|
||||||
@@ -58,17 +77,24 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
|
setValue,
|
||||||
formState: { isSubmitting }
|
formState: { isSubmitting }
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: yupResolver(schema)
|
resolver: yupResolver(schema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = async ({ identityId, role }: FormData) => {
|
useEffect(() => {
|
||||||
|
if (!isRolesFetched || !roles) return;
|
||||||
|
|
||||||
|
setValue("role", { name: roles[0]?.name, slug: roles[0]?.slug });
|
||||||
|
}, [isRolesFetched, roles]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ identity, role }: FormData) => {
|
||||||
try {
|
try {
|
||||||
await addIdentityToWorkspaceMutateAsync({
|
await addIdentityToWorkspaceMutateAsync({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
identityId,
|
identityId: identity.id,
|
||||||
role: role || undefined
|
role: role.slug || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -76,7 +102,17 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
type: "success"
|
type: "success"
|
||||||
});
|
});
|
||||||
|
|
||||||
reset();
|
const nextAvailableMembership = filteredIdentityMembershipOrgs.filter(
|
||||||
|
(membership) => membership.identity.id !== identity.id
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
// prevents combobox from displaying previously added identity
|
||||||
|
reset({
|
||||||
|
identity: {
|
||||||
|
name: nextAvailableMembership?.identity.name,
|
||||||
|
id: nextAvailableMembership?.identity.id
|
||||||
|
}
|
||||||
|
});
|
||||||
handlePopUpToggle("identity", false);
|
handlePopUpToggle("identity", false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -98,34 +134,41 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
reset();
|
reset();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title="Add Identity to Project">
|
<ModalContent title="Add Identity to Project" bodyClassName="overflow-visible">
|
||||||
{filteredIdentityMembershipOrgs.length ? (
|
{filteredIdentityMembershipOrgs.length ? (
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="identityId"
|
name="identity"
|
||||||
defaultValue={filteredIdentityMembershipOrgs?.[0]?.id}
|
defaultValue={{
|
||||||
|
id: filteredIdentityMembershipOrgs?.[0]?.identity?.id,
|
||||||
|
name: filteredIdentityMembershipOrgs?.[0]?.identity?.name
|
||||||
|
}}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
<FormControl label="Identity" errorText={error?.message} isError={Boolean(error)}>
|
||||||
<Select
|
<ComboBox
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
by="id"
|
||||||
{filteredIdentityMembershipOrgs.map(({ identity }) => (
|
value={{ id: field.value.id, name: field.value.name }}
|
||||||
<SelectItem value={identity.id} key={`org-identity-${identity.id}`}>
|
defaultValue={{ id: field.value.id, name: field.value.name }}
|
||||||
{identity.name}
|
onSelectChange={(value) => onChange({ id: value.id, name: value.name })}
|
||||||
</SelectItem>
|
displayValue={(el) => el.name}
|
||||||
))}
|
onFilter={({ value }, filterQuery) =>
|
||||||
</Select>
|
value.name.toLowerCase().includes(filterQuery.toLowerCase())
|
||||||
|
}
|
||||||
|
items={filteredIdentityMembershipOrgs.map(({ identity }) => ({
|
||||||
|
key: identity.id,
|
||||||
|
value: { id: identity.id, name: identity.name },
|
||||||
|
label: identity.name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="role"
|
name="role"
|
||||||
defaultValue=""
|
defaultValue={{ name: "", slug: "" }}
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Role"
|
label="Role"
|
||||||
@@ -133,18 +176,22 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
isError={Boolean(error)}
|
isError={Boolean(error)}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
>
|
>
|
||||||
<Select
|
<ComboBox
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
by="slug"
|
||||||
{(roles || []).map(({ name, slug }) => (
|
value={{ slug: field.value.slug, name: field.value.name }}
|
||||||
<SelectItem value={slug} key={`st-role-${slug}`}>
|
defaultValue={{ slug: field.value.slug, name: field.value.name }}
|
||||||
{name}
|
onSelectChange={(value) => onChange({ slug: value.slug, name: value.name })}
|
||||||
</SelectItem>
|
displayValue={(el) => el.name}
|
||||||
))}
|
onFilter={({ value }, filterQuery) =>
|
||||||
</Select>
|
value.name.toLowerCase().includes(filterQuery.toLowerCase())
|
||||||
|
}
|
||||||
|
items={(roles || []).map(({ slug, name }) => ({
|
||||||
|
key: slug,
|
||||||
|
value: { slug, name },
|
||||||
|
label: name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -158,9 +205,11 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
>
|
>
|
||||||
{popUp?.identity?.data ? "Update" : "Create"}
|
{popUp?.identity?.data ? "Update" : "Create"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button colorSchema="secondary" variant="plain">
|
<ModalClose asChild>
|
||||||
Cancel
|
<Button colorSchema="secondary" variant="plain">
|
||||||
</Button>
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
@@ -169,7 +218,9 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
All identities in your organization have already been added to this project.
|
All identities in your organization have already been added to this project.
|
||||||
</div>
|
</div>
|
||||||
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
|
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
|
||||||
<Button variant="outline_bg">Create a new identity</Button>
|
<Button isDisabled={isRolesLoading} isLoading={isRolesLoading} variant="outline_bg">
|
||||||
|
Create a new identity
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
@@ -8,14 +8,14 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import NavHeader from "@app/components/navigation/NavHeader";
|
import NavHeader from "@app/components/navigation/NavHeader";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { PermissionDeniedBanner } from "@app/components/permissions";
|
import { PermissionDeniedBanner } from "@app/components/permissions";
|
||||||
import { ContentLoader } from "@app/components/v2";
|
import { ContentLoader, Pagination } from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { useDebounce, usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useGetDynamicSecrets,
|
useGetDynamicSecrets,
|
||||||
useGetImportedSecretsSingleEnv,
|
useGetImportedSecretsSingleEnv,
|
||||||
@@ -39,7 +39,7 @@ import { SecretImportListView } from "./components/SecretImportListView";
|
|||||||
import { SecretListView } from "./components/SecretListView";
|
import { SecretListView } from "./components/SecretListView";
|
||||||
import { SnapshotView } from "./components/SnapshotView";
|
import { SnapshotView } from "./components/SnapshotView";
|
||||||
import { StoreProvider } from "./SecretMainPage.store";
|
import { StoreProvider } from "./SecretMainPage.store";
|
||||||
import { Filter, GroupBy, SortDir } from "./SecretMainPage.types";
|
import { Filter, SortDir } from "./SecretMainPage.types";
|
||||||
|
|
||||||
const LOADER_TEXT = [
|
const LOADER_TEXT = [
|
||||||
"Retrieving your encrypted secrets...",
|
"Retrieving your encrypted secrets...",
|
||||||
@@ -47,6 +47,7 @@ const LOADER_TEXT = [
|
|||||||
"Getting secret import links..."
|
"Getting secret import links..."
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const INIT_PER_PAGE = 10;
|
||||||
export const SecretMainPage = () => {
|
export const SecretMainPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
|
const { currentWorkspace, isLoading: isWorkspaceLoading } = useWorkspace();
|
||||||
@@ -59,6 +60,10 @@ export const SecretMainPage = () => {
|
|||||||
tags: {},
|
tags: {},
|
||||||
searchFilter: (router.query.searchFilter as string) || ""
|
searchFilter: (router.query.searchFilter as string) || ""
|
||||||
});
|
});
|
||||||
|
const debouncedSearchFilter = useDebounce(filter.searchFilter);
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||||
|
const paginationOffset = (page - 1) * perPage;
|
||||||
|
|
||||||
const [snapshotId, setSnapshotId] = useState<string | null>(null);
|
const [snapshotId, setSnapshotId] = useState<string | null>(null);
|
||||||
const isRollbackMode = Boolean(snapshotId);
|
const isRollbackMode = Boolean(snapshotId);
|
||||||
@@ -185,11 +190,6 @@ export const SecretMainPage = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGroupByChange = useCallback(
|
|
||||||
(groupBy?: GroupBy) => setFilter((state) => ({ ...state, groupBy })),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTagToggle = useCallback(
|
const handleTagToggle = useCallback(
|
||||||
(tagId: string) =>
|
(tagId: string) =>
|
||||||
setFilter((state) => {
|
setFilter((state) => {
|
||||||
@@ -223,6 +223,113 @@ export const SecretMainPage = () => {
|
|||||||
const loadingOnAccess =
|
const loadingOnAccess =
|
||||||
canReadSecret &&
|
canReadSecret &&
|
||||||
(isSecretsLoading || isSecretImportsLoading || isFoldersLoading || isDynamicSecretLoading);
|
(isSecretsLoading || isSecretImportsLoading || isFoldersLoading || isDynamicSecretLoading);
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
const filteredSecrets =
|
||||||
|
secrets
|
||||||
|
?.filter(({ key, tags: secretTags, value }) => {
|
||||||
|
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
|
||||||
|
return (
|
||||||
|
(!isTagFilterActive || secretTags?.some(({ id }) => filter.tags?.[id])) &&
|
||||||
|
(key.toUpperCase().includes(debouncedSearchFilter.toUpperCase()) ||
|
||||||
|
value?.toLowerCase().includes(debouncedSearchFilter.toLowerCase()))
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.sort((a, b) =>
|
||||||
|
sortDir === SortDir.ASC ? a.key.localeCompare(b.key) : b.key.localeCompare(a.key)
|
||||||
|
) ?? [];
|
||||||
|
const filteredFolders =
|
||||||
|
folders
|
||||||
|
?.filter(({ name }) => name.toLowerCase().includes(debouncedSearchFilter.toLowerCase()))
|
||||||
|
.sort((a, b) =>
|
||||||
|
sortDir === "asc" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
||||||
|
) ?? [];
|
||||||
|
const filteredDynamicSecrets =
|
||||||
|
dynamicSecrets
|
||||||
|
?.filter(({ name }) => name.toLowerCase().includes(debouncedSearchFilter.toLowerCase()))
|
||||||
|
.sort((a, b) =>
|
||||||
|
sortDir === "asc" ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name)
|
||||||
|
) ?? [];
|
||||||
|
const filteredSecretImports =
|
||||||
|
secretImports
|
||||||
|
?.filter(({ importPath }) =>
|
||||||
|
importPath.toLowerCase().includes(debouncedSearchFilter.toLowerCase())
|
||||||
|
)
|
||||||
|
.sort((a, b) =>
|
||||||
|
sortDir === "asc"
|
||||||
|
? a.importPath.localeCompare(b.importPath)
|
||||||
|
: b.importPath.localeCompare(a.importPath)
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
const totalRows =
|
||||||
|
filteredSecretImports.length +
|
||||||
|
filteredFolders.length +
|
||||||
|
filteredDynamicSecrets.length +
|
||||||
|
filteredSecrets.length;
|
||||||
|
|
||||||
|
const paginatedImports = filteredSecretImports.slice(
|
||||||
|
paginationOffset,
|
||||||
|
paginationOffset + perPage
|
||||||
|
);
|
||||||
|
|
||||||
|
let remainingRows = perPage - paginatedImports.length;
|
||||||
|
const foldersStartIndex = Math.max(0, paginationOffset - filteredSecretImports.length);
|
||||||
|
const paginatedFolders =
|
||||||
|
remainingRows > 0
|
||||||
|
? filteredFolders.slice(foldersStartIndex, foldersStartIndex + remainingRows)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
remainingRows -= paginatedFolders.length;
|
||||||
|
const dynamicSecretStartIndex = Math.max(
|
||||||
|
0,
|
||||||
|
paginationOffset - filteredSecretImports.length - filteredFolders.length
|
||||||
|
);
|
||||||
|
const paginatiedDynamicSecrets =
|
||||||
|
remainingRows > 0
|
||||||
|
? filteredDynamicSecrets.slice(
|
||||||
|
dynamicSecretStartIndex,
|
||||||
|
dynamicSecretStartIndex + remainingRows
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
remainingRows -= paginatiedDynamicSecrets.length;
|
||||||
|
const secretStartIndex = Math.max(
|
||||||
|
0,
|
||||||
|
paginationOffset -
|
||||||
|
filteredSecretImports.length -
|
||||||
|
filteredFolders.length -
|
||||||
|
filteredDynamicSecrets.length
|
||||||
|
);
|
||||||
|
|
||||||
|
const paginatiedSecrets =
|
||||||
|
remainingRows > 0
|
||||||
|
? filteredSecrets.slice(secretStartIndex, secretStartIndex + remainingRows)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
imports: paginatedImports,
|
||||||
|
folders: paginatedFolders,
|
||||||
|
secrets: paginatiedSecrets,
|
||||||
|
dynamicSecrets: paginatiedDynamicSecrets,
|
||||||
|
totalRows
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
sortDir,
|
||||||
|
debouncedSearchFilter,
|
||||||
|
folders,
|
||||||
|
secrets,
|
||||||
|
dynamicSecrets,
|
||||||
|
paginationOffset,
|
||||||
|
perPage,
|
||||||
|
filter.tags,
|
||||||
|
importedSecrets
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// reset page if no longer valid
|
||||||
|
if (rows.totalRows < paginationOffset) setPage(1);
|
||||||
|
}, [rows.totalRows]);
|
||||||
|
|
||||||
// loading screen when you don't have permission but as folder's is viewable need to wait for that
|
// loading screen when you don't have permission but as folder's is viewable need to wait for that
|
||||||
const loadingOnDenied = !canReadSecret && isFoldersLoading;
|
const loadingOnDenied = !canReadSecret && isFoldersLoading;
|
||||||
if (loadingOnAccess || loadingOnDenied) {
|
if (loadingOnAccess || loadingOnDenied) {
|
||||||
@@ -258,7 +365,6 @@ export const SecretMainPage = () => {
|
|||||||
filter={filter}
|
filter={filter}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
onVisiblilityToggle={handleToggleVisibility}
|
onVisiblilityToggle={handleToggleVisibility}
|
||||||
onGroupByChange={handleGroupByChange}
|
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={handleSearchChange}
|
||||||
onToggleTagFilter={handleTagToggle}
|
onToggleTagFilter={handleTagToggle}
|
||||||
snapshotCount={snapshotCount || 0}
|
snapshotCount={snapshotCount || 0}
|
||||||
@@ -291,7 +397,7 @@ export const SecretMainPage = () => {
|
|||||||
{canReadSecret && (
|
{canReadSecret && (
|
||||||
<SecretImportListView
|
<SecretImportListView
|
||||||
searchTerm={filter.searchFilter}
|
searchTerm={filter.searchFilter}
|
||||||
secretImports={secretImports}
|
secretImports={rows.imports}
|
||||||
isFetching={isSecretImportsLoading || isSecretImportsFetching}
|
isFetching={isSecretImportsLoading || isSecretImportsFetching}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
@@ -301,7 +407,7 @@ export const SecretMainPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<FolderListView
|
<FolderListView
|
||||||
folders={folders}
|
folders={rows.folders}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
@@ -314,15 +420,13 @@ export const SecretMainPage = () => {
|
|||||||
environment={environment}
|
environment={environment}
|
||||||
projectSlug={projectSlug}
|
projectSlug={projectSlug}
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
dynamicSecrets={dynamicSecrets || []}
|
dynamicSecrets={rows.dynamicSecrets || []}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canReadSecret && (
|
{canReadSecret && (
|
||||||
<SecretListView
|
<SecretListView
|
||||||
secrets={secrets}
|
secrets={rows.secrets}
|
||||||
tags={tags}
|
tags={tags}
|
||||||
filter={filter}
|
|
||||||
sortDir={sortDir}
|
|
||||||
isVisible={isVisible}
|
isVisible={isVisible}
|
||||||
environment={environment}
|
environment={environment}
|
||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
@@ -331,6 +435,16 @@ export const SecretMainPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
|
{!canReadSecret && folders?.length === 0 && <PermissionDeniedBanner />}
|
||||||
|
{!loadingOnAccess && rows.totalRows > INIT_PER_PAGE && (
|
||||||
|
<Pagination
|
||||||
|
className="border-t border-solid border-t-mineshaft-600"
|
||||||
|
count={rows.totalRows}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CreateSecretForm
|
<CreateSecretForm
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
export type Filter = {
|
export type Filter = {
|
||||||
tags: Record<string, boolean>;
|
tags: Record<string, boolean>;
|
||||||
searchFilter: string;
|
searchFilter: string;
|
||||||
groupBy?: GroupBy | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum SortDir {
|
export enum SortDir {
|
||||||
@@ -9,6 +8,8 @@ export enum SortDir {
|
|||||||
DESC = "desc"
|
DESC = "desc"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum GroupBy {
|
export enum RowType {
|
||||||
PREFIX = "prefix"
|
Folder = "folder",
|
||||||
|
DynamicSecret = "dynamic",
|
||||||
|
Secret = "Secret"
|
||||||
}
|
}
|
||||||
|
@@ -62,7 +62,7 @@ import {
|
|||||||
useSelectedSecretActions,
|
useSelectedSecretActions,
|
||||||
useSelectedSecrets
|
useSelectedSecrets
|
||||||
} from "../../SecretMainPage.store";
|
} from "../../SecretMainPage.store";
|
||||||
import { Filter, GroupBy } from "../../SecretMainPage.types";
|
import { Filter } from "../../SecretMainPage.types";
|
||||||
import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
|
import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
|
||||||
import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
||||||
import { FolderForm } from "./FolderForm";
|
import { FolderForm } from "./FolderForm";
|
||||||
@@ -81,7 +81,6 @@ type Props = {
|
|||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
snapshotCount: number;
|
snapshotCount: number;
|
||||||
isSnapshotCountLoading?: boolean;
|
isSnapshotCountLoading?: boolean;
|
||||||
onGroupByChange: (opt?: GroupBy) => void;
|
|
||||||
onSearchChange: (term: string) => void;
|
onSearchChange: (term: string) => void;
|
||||||
onToggleTagFilter: (tagId: string) => void;
|
onToggleTagFilter: (tagId: string) => void;
|
||||||
onVisiblilityToggle: () => void;
|
onVisiblilityToggle: () => void;
|
||||||
@@ -101,7 +100,6 @@ export const ActionBar = ({
|
|||||||
isSnapshotCountLoading,
|
isSnapshotCountLoading,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onToggleTagFilter,
|
onToggleTagFilter,
|
||||||
onGroupByChange,
|
|
||||||
onVisiblilityToggle,
|
onVisiblilityToggle,
|
||||||
onClickRollbackMode
|
onClickRollbackMode
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -307,16 +305,6 @@ export const ActionBar = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="p-0">
|
<DropdownMenuContent align="end" className="p-0">
|
||||||
<DropdownMenuGroup>Group By</DropdownMenuGroup>
|
|
||||||
<DropdownMenuItem
|
|
||||||
iconPos="right"
|
|
||||||
icon={
|
|
||||||
filter?.groupBy === GroupBy.PREFIX && <FontAwesomeIcon icon={faCheckCircle} />
|
|
||||||
}
|
|
||||||
onClick={() => onGroupByChange(!filter.groupBy ? GroupBy.PREFIX : undefined)}
|
|
||||||
>
|
|
||||||
Prefix
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuGroup>Filter By</DropdownMenuGroup>
|
<DropdownMenuGroup>Filter By</DropdownMenuGroup>
|
||||||
<DropdownSubMenu>
|
<DropdownSubMenu>
|
||||||
<DropdownSubMenuTrigger
|
<DropdownSubMenuTrigger
|
||||||
|
@@ -123,7 +123,7 @@ export const SecretImportListView = ({
|
|||||||
if (!isFetching) {
|
if (!isFetching) {
|
||||||
setItems(secretImports);
|
setItems(secretImports);
|
||||||
}
|
}
|
||||||
}, [isFetching]);
|
}, [isFetching, secretImports]);
|
||||||
|
|
||||||
const { mutateAsync: deleteSecretImport } = useDeleteSecretImport();
|
const { mutateAsync: deleteSecretImport } = useDeleteSecretImport();
|
||||||
const { mutate: updateSecretImport } = useUpdateSecretImport();
|
const { mutate: updateSecretImport } = useUpdateSecretImport();
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { CreateTagModal } from "@app/components/tags/CreateTagModal";
|
import { CreateTagModal } from "@app/components/tags/CreateTagModal";
|
||||||
@@ -16,7 +15,7 @@ import { WsTag } from "@app/hooks/api/types";
|
|||||||
import { AddShareSecretModal } from "@app/views/ShareSecretPage/components/AddShareSecretModal";
|
import { AddShareSecretModal } from "@app/views/ShareSecretPage/components/AddShareSecretModal";
|
||||||
|
|
||||||
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
|
||||||
import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types";
|
import { Filter } from "../../SecretMainPage.types";
|
||||||
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
|
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
|
||||||
import { SecretItem } from "./SecretItem";
|
import { SecretItem } from "./SecretItem";
|
||||||
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
|
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
|
||||||
@@ -26,53 +25,11 @@ type Props = {
|
|||||||
environment: string;
|
environment: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
secretPath?: string;
|
secretPath?: string;
|
||||||
filter: Filter;
|
|
||||||
sortDir?: SortDir;
|
|
||||||
tags?: WsTag[];
|
tags?: WsTag[];
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
isProtectedBranch?: boolean;
|
isProtectedBranch?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const reorderSecretGroupByUnderscore = (secrets: SecretV3RawSanitized[], sortDir: SortDir) => {
|
|
||||||
const groupedSecrets: Record<string, SecretV3RawSanitized[]> = {};
|
|
||||||
secrets.forEach((secret) => {
|
|
||||||
const lastSeperatorIndex = secret.key.lastIndexOf("_");
|
|
||||||
const namespace =
|
|
||||||
lastSeperatorIndex !== -1 ? secret.key.substring(0, lastSeperatorIndex) : "misc";
|
|
||||||
if (!groupedSecrets?.[namespace]) groupedSecrets[namespace] = [];
|
|
||||||
groupedSecrets[namespace].push(secret);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Object.keys(groupedSecrets)
|
|
||||||
.sort((a, b) =>
|
|
||||||
sortDir === SortDir.ASC
|
|
||||||
? a.toLowerCase().localeCompare(b.toLowerCase())
|
|
||||||
: b.toLowerCase().localeCompare(a.toLowerCase())
|
|
||||||
)
|
|
||||||
.map((namespace) => ({ namespace, secrets: groupedSecrets[namespace] }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const reorderSecret = (
|
|
||||||
secrets: SecretV3RawSanitized[],
|
|
||||||
sortDir: SortDir,
|
|
||||||
filter?: GroupBy | null
|
|
||||||
) => {
|
|
||||||
if (filter === GroupBy.PREFIX) {
|
|
||||||
return reorderSecretGroupByUnderscore(secrets, sortDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
namespace: "",
|
|
||||||
secrets: secrets?.sort((a, b) =>
|
|
||||||
sortDir === SortDir.ASC
|
|
||||||
? a.key.toLowerCase().localeCompare(b.key.toLowerCase())
|
|
||||||
: b.key.toLowerCase().localeCompare(a.key.toLowerCase())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const filterSecrets = (secrets: SecretV3RawSanitized[], filter: Filter) =>
|
export const filterSecrets = (secrets: SecretV3RawSanitized[], filter: Filter) =>
|
||||||
secrets.filter(({ key, value, tags }) => {
|
secrets.filter(({ key, value, tags }) => {
|
||||||
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
|
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
|
||||||
@@ -88,8 +45,6 @@ export const SecretListView = ({
|
|||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretPath = "/",
|
secretPath = "/",
|
||||||
filter,
|
|
||||||
sortDir = SortDir.ASC,
|
|
||||||
tags: wsTags = [],
|
tags: wsTags = [],
|
||||||
isVisible,
|
isVisible,
|
||||||
isProtectedBranch = false
|
isProtectedBranch = false
|
||||||
@@ -331,52 +286,30 @@ export const SecretListView = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{reorderSecret(secrets, sortDir, filter.groupBy).map(
|
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
|
||||||
({ namespace, secrets: groupedSecrets }) => {
|
<FontAwesomeIcon icon={icon} symbol={symbol} key={`font-awesome-svg-spritie-${symbol}`} />
|
||||||
const filteredSecrets = filterSecrets(groupedSecrets, filter);
|
))}
|
||||||
return (
|
{secrets.map((secret) => (
|
||||||
<div className="flex flex-col" key={`${namespace}-${groupedSecrets.length}`}>
|
<SecretItem
|
||||||
<div
|
environment={environment}
|
||||||
className={twMerge(
|
secretPath={secretPath}
|
||||||
"text-md h-0 bg-bunker-600 capitalize transition-all",
|
tags={wsTags}
|
||||||
Boolean(namespace) && Boolean(filteredSecrets.length) && "h-11 py-3 pl-4 "
|
isSelected={selectedSecrets?.[secret.id]}
|
||||||
)}
|
onToggleSecretSelect={toggleSelectedSecret}
|
||||||
key={namespace}
|
isVisible={isVisible}
|
||||||
>
|
secret={secret}
|
||||||
{namespace}
|
key={secret.id}
|
||||||
</div>
|
onSaveSecret={handleSaveSecret}
|
||||||
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
|
onDeleteSecret={onDeleteSecret}
|
||||||
<FontAwesomeIcon
|
onDetailViewSecret={onDetailViewSecret}
|
||||||
icon={icon}
|
onCreateTag={onCreateTag}
|
||||||
symbol={symbol}
|
handleSecretShare={() =>
|
||||||
key={`font-awesome-svg-spritie-${symbol}`}
|
handlePopUpOpen("createSharedSecret", {
|
||||||
/>
|
value: secret.valueOverride ?? secret.value
|
||||||
))}
|
})
|
||||||
{filteredSecrets.map((secret) => (
|
}
|
||||||
<SecretItem
|
/>
|
||||||
environment={environment}
|
))}
|
||||||
secretPath={secretPath}
|
|
||||||
tags={wsTags}
|
|
||||||
isSelected={selectedSecrets?.[secret.id]}
|
|
||||||
onToggleSecretSelect={toggleSelectedSecret}
|
|
||||||
isVisible={isVisible}
|
|
||||||
secret={secret}
|
|
||||||
key={secret.id}
|
|
||||||
onSaveSecret={handleSaveSecret}
|
|
||||||
onDeleteSecret={onDeleteSecret}
|
|
||||||
onDetailViewSecret={onDetailViewSecret}
|
|
||||||
onCreateTag={onCreateTag}
|
|
||||||
handleSecretShare={() =>
|
|
||||||
handlePopUpOpen("createSharedSecret", {
|
|
||||||
value: secret.valueOverride ?? secret.value
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteSecret.isOpen}
|
isOpen={popUp.deleteSecret.isOpen}
|
||||||
deleteKey={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}
|
deleteKey={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
|
Pagination,
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -49,7 +50,7 @@ import {
|
|||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { useDebounce, usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateFolder,
|
useCreateFolder,
|
||||||
useCreateSecretV3,
|
useCreateSecretV3,
|
||||||
@@ -78,6 +79,14 @@ export enum EntryType {
|
|||||||
SECRET = "secret"
|
SECRET = "secret"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RowType {
|
||||||
|
Folder = "folder",
|
||||||
|
DynamicSecret = "dynamic",
|
||||||
|
Secret = "Secret"
|
||||||
|
}
|
||||||
|
|
||||||
|
const INIT_PER_PAGE = 10;
|
||||||
|
|
||||||
export const SecretOverviewPage = () => {
|
export const SecretOverviewPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -101,6 +110,7 @@ export const SecretOverviewPage = () => {
|
|||||||
const workspaceId = currentWorkspace?.id as string;
|
const workspaceId = currentWorkspace?.id as string;
|
||||||
const projectSlug = currentWorkspace?.slug as string;
|
const projectSlug = currentWorkspace?.slug as string;
|
||||||
const [searchFilter, setSearchFilter] = useState("");
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
|
const debouncedSearchFilter = useDebounce(searchFilter);
|
||||||
const secretPath = (router.query?.secretPath as string) || "/";
|
const secretPath = (router.query?.secretPath as string) || "/";
|
||||||
|
|
||||||
const [selectedEntries, setSelectedEntries] = useState<{
|
const [selectedEntries, setSelectedEntries] = useState<{
|
||||||
@@ -111,6 +121,9 @@ export const SecretOverviewPage = () => {
|
|||||||
[EntryType.SECRET]: {}
|
[EntryType.SECRET]: {}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [perPage, setPerPage] = useState(INIT_PER_PAGE);
|
||||||
|
|
||||||
const toggleSelectedEntry = useCallback(
|
const toggleSelectedEntry = useCallback(
|
||||||
(type: EntryType, key: string) => {
|
(type: EntryType, key: string) => {
|
||||||
const isChecked = Boolean(selectedEntries[type]?.[key]);
|
const isChecked = Boolean(selectedEntries[type]?.[key]);
|
||||||
@@ -439,7 +452,40 @@ export const SecretOverviewPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isWorkspaceLoading) {
|
const rows = useMemo(() => {
|
||||||
|
const filteredSecretNames =
|
||||||
|
secKeys
|
||||||
|
?.filter((name) => name.toUpperCase().includes(debouncedSearchFilter.toUpperCase()))
|
||||||
|
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a))) ?? [];
|
||||||
|
const filteredFolderNames =
|
||||||
|
folderNames
|
||||||
|
?.filter((name) => name.toLowerCase().includes(debouncedSearchFilter.toLowerCase()))
|
||||||
|
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a))) ?? [];
|
||||||
|
const filteredDynamicSecrets =
|
||||||
|
dynamicSecretNames
|
||||||
|
?.filter((name) => name.toLowerCase().includes(debouncedSearchFilter.toLowerCase()))
|
||||||
|
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a))) ?? [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...filteredFolderNames.map((name) => ({ name, type: RowType.Folder })),
|
||||||
|
...filteredDynamicSecrets.map((name) => ({ name, type: RowType.DynamicSecret })),
|
||||||
|
...filteredSecretNames.map((name) => ({ name, type: RowType.Secret }))
|
||||||
|
];
|
||||||
|
}, [sortDir, debouncedSearchFilter, secKeys, folderNames, dynamicSecretNames]);
|
||||||
|
|
||||||
|
const paginationOffset = (page - 1) * perPage;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// reset page if no longer valid
|
||||||
|
if (rows.length < paginationOffset) setPage(1);
|
||||||
|
}, [rows.length]);
|
||||||
|
|
||||||
|
const isTableLoading =
|
||||||
|
folders?.some(({ isLoading }) => isLoading) ||
|
||||||
|
secrets?.some(({ isLoading }) => isLoading) ||
|
||||||
|
dynamicSecrets?.some(({ isLoading }) => isLoading);
|
||||||
|
|
||||||
|
if (isWorkspaceLoading || isTableLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<img
|
<img
|
||||||
@@ -454,32 +500,16 @@ export const SecretOverviewPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isTableLoading = !(
|
|
||||||
folders?.some(({ isLoading }) => !isLoading) && secrets?.some(({ isLoading }) => !isLoading)
|
|
||||||
);
|
|
||||||
|
|
||||||
const canViewOverviewPage = Boolean(userAvailableEnvs.length);
|
const canViewOverviewPage = Boolean(userAvailableEnvs.length);
|
||||||
// This is needed to also show imports from other paths – right now those are missing.
|
// This is needed to also show imports from other paths – right now those are missing.
|
||||||
// const combinedKeys = [...secKeys, ...secretImports.map((impSecrets) => impSecrets?.data?.map((impSec) => impSec.secrets?.map((impSecKey) => impSecKey.key))).flat().flat()];
|
// const combinedKeys = [...secKeys, ...secretImports.map((impSecrets) => impSecrets?.data?.map((impSec) => impSec.secrets?.map((impSecKey) => impSecKey.key))).flat().flat()];
|
||||||
const filteredSecretNames = secKeys
|
|
||||||
?.filter((name) => name.toUpperCase().includes(searchFilter.toUpperCase()))
|
|
||||||
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
|
||||||
const filteredFolderNames = folderNames
|
|
||||||
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
|
|
||||||
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
|
||||||
const filteredDynamicSecrets = dynamicSecretNames
|
|
||||||
?.filter((name) => name.toLowerCase().includes(searchFilter.toLowerCase()))
|
|
||||||
.sort((a, b) => (sortDir === "asc" ? a.localeCompare(b) : b.localeCompare(a)));
|
|
||||||
|
|
||||||
const isTableEmpty =
|
const isTableEmpty =
|
||||||
!(
|
!(
|
||||||
folders?.every(({ isLoading }) => isLoading) &&
|
folders?.every(({ isLoading }) => isLoading) &&
|
||||||
secrets?.every(({ isLoading }) => isLoading) &&
|
secrets?.every(({ isLoading }) => isLoading) &&
|
||||||
dynamicSecrets?.every(({ isLoading }) => isLoading)
|
dynamicSecrets?.every(({ isLoading }) => isLoading)
|
||||||
) &&
|
) && rows.length === 0;
|
||||||
filteredSecretNames?.length === 0 &&
|
|
||||||
filteredFolderNames?.length === 0 &&
|
|
||||||
filteredDynamicSecrets?.length === 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -656,7 +686,7 @@ export const SecretOverviewPage = () => {
|
|||||||
resetSelectedEntries={resetSelectedEntries}
|
resetSelectedEntries={resetSelectedEntries}
|
||||||
/>
|
/>
|
||||||
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
|
<div className="thin-scrollbar mt-4" ref={parentTableRef}>
|
||||||
<TableContainer className="max-h-[calc(100vh-250px)] overflow-y-auto">
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
<Tr className="sticky top-0 z-20 border-0">
|
<Tr className="sticky top-0 z-20 border-0">
|
||||||
@@ -753,7 +783,7 @@ export const SecretOverviewPage = () => {
|
|||||||
<Td colSpan={visibleEnvs.length + 1}>
|
<Td colSpan={visibleEnvs.length + 1}>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title={
|
title={
|
||||||
searchFilter
|
debouncedSearchFilter
|
||||||
? "No secret found for your search, add one now"
|
? "No secret found for your search, add one now"
|
||||||
: "Let's add some secrets"
|
: "Let's add some secrets"
|
||||||
}
|
}
|
||||||
@@ -774,48 +804,59 @@ export const SecretOverviewPage = () => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
{!isTableLoading &&
|
{!isTableLoading &&
|
||||||
filteredFolderNames.map((folderName, index) => (
|
rows.slice(paginationOffset, paginationOffset + perPage).map((row, index) => {
|
||||||
<SecretOverviewFolderRow
|
switch (row.type) {
|
||||||
folderName={folderName}
|
case RowType.Secret:
|
||||||
isFolderPresentInEnv={isFolderPresentInEnv}
|
if (visibleEnvs?.length === 0) return null;
|
||||||
isSelected={selectedEntries.folder[folderName]}
|
return (
|
||||||
onToggleFolderSelect={() => toggleSelectedEntry(EntryType.FOLDER, folderName)}
|
<SecretOverviewTableRow
|
||||||
environments={visibleEnvs}
|
isSelected={selectedEntries.secret[row.name]}
|
||||||
key={`overview-${folderName}-${index + 1}`}
|
onToggleSecretSelect={() =>
|
||||||
onClick={handleFolderClick}
|
toggleSelectedEntry(EntryType.SECRET, row.name)
|
||||||
onToggleFolderEdit={(name: string) =>
|
}
|
||||||
handlePopUpOpen("updateFolder", { name })
|
secretPath={secretPath}
|
||||||
}
|
getImportedSecretByKey={getImportedSecretByKey}
|
||||||
/>
|
isImportedSecretPresentInEnv={isImportedSecretPresentInEnv}
|
||||||
))}
|
onSecretCreate={handleSecretCreate}
|
||||||
{!isTableLoading &&
|
onSecretDelete={handleSecretDelete}
|
||||||
filteredDynamicSecrets.map((dynamicSecretName, index) => (
|
onSecretUpdate={handleSecretUpdate}
|
||||||
<SecretOverviewDynamicSecretRow
|
key={`overview-${row.name}-${index + 1}`}
|
||||||
dynamicSecretName={dynamicSecretName}
|
environments={visibleEnvs}
|
||||||
isDynamicSecretInEnv={isDynamicSecretPresentInEnv}
|
secretKey={row.name}
|
||||||
environments={visibleEnvs}
|
getSecretByKey={getSecretByKey}
|
||||||
key={`overview-${dynamicSecretName}-${index + 1}`}
|
expandableColWidth={expandableTableWidth}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
{!isTableLoading &&
|
case RowType.DynamicSecret:
|
||||||
visibleEnvs?.length > 0 &&
|
return (
|
||||||
filteredSecretNames.map((key, index) => (
|
<SecretOverviewDynamicSecretRow
|
||||||
<SecretOverviewTableRow
|
dynamicSecretName={row.name}
|
||||||
isSelected={selectedEntries.secret[key]}
|
isDynamicSecretInEnv={isDynamicSecretPresentInEnv}
|
||||||
onToggleSecretSelect={() => toggleSelectedEntry(EntryType.SECRET, key)}
|
environments={visibleEnvs}
|
||||||
secretPath={secretPath}
|
key={`overview-${row.name}-${index + 1}`}
|
||||||
getImportedSecretByKey={getImportedSecretByKey}
|
/>
|
||||||
isImportedSecretPresentInEnv={isImportedSecretPresentInEnv}
|
);
|
||||||
onSecretCreate={handleSecretCreate}
|
case RowType.Folder:
|
||||||
onSecretDelete={handleSecretDelete}
|
return (
|
||||||
onSecretUpdate={handleSecretUpdate}
|
<SecretOverviewFolderRow
|
||||||
key={`overview-${key}-${index + 1}`}
|
folderName={row.name}
|
||||||
environments={visibleEnvs}
|
isFolderPresentInEnv={isFolderPresentInEnv}
|
||||||
secretKey={key}
|
isSelected={selectedEntries.folder[row.name]}
|
||||||
getSecretByKey={getSecretByKey}
|
onToggleFolderSelect={() =>
|
||||||
expandableColWidth={expandableTableWidth}
|
toggleSelectedEntry(EntryType.FOLDER, row.name)
|
||||||
/>
|
}
|
||||||
))}
|
environments={visibleEnvs}
|
||||||
|
key={`overview-${row.name}-${index + 1}`}
|
||||||
|
onClick={handleFolderClick}
|
||||||
|
onToggleFolderEdit={(name: string) =>
|
||||||
|
handlePopUpOpen("updateFolder", { name })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
</TBody>
|
</TBody>
|
||||||
<TFoot>
|
<TFoot>
|
||||||
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
|
<Tr className="sticky bottom-0 z-10 border-0 bg-mineshaft-800">
|
||||||
@@ -842,6 +883,16 @@ export const SecretOverviewPage = () => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</TFoot>
|
</TFoot>
|
||||||
</Table>
|
</Table>
|
||||||
|
{!isTableLoading && rows.length > INIT_PER_PAGE && (
|
||||||
|
<Pagination
|
||||||
|
className="border-t border-solid border-t-mineshaft-600"
|
||||||
|
count={rows.length}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Reference in New Issue
Block a user