Merge remote-tracking branch 'origin/main' into misc/address-minor-cert-lint-issues

This commit is contained in:
Sheen Capadngan
2024-09-13 01:46:00 +08:00
47 changed files with 1845 additions and 544 deletions
+1
View File
@@ -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>;
+3 -1
View File
@@ -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>;
+26 -5
View File
@@ -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.",
@@ -1116,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",
@@ -1170,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"
@@ -1182,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"
+5
View File
@@ -52,3 +52,8 @@ export enum SecretSharingAccessType {
Anyone = "anyone", Anyone = "anyone",
Organization = "organization" Organization = "organization"
} }
export enum OrderByDirection {
ASC = "asc",
DESC = "desc"
}
@@ -8,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,
@@ -601,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) => {
@@ -681,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 };
} }
}); });
@@ -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";
@@ -1095,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;
@@ -1215,7 +1223,6 @@ export const certificateAuthorityServiceFactory = ({
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}`; const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}`;
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),
@@ -1226,6 +1233,56 @@ export const certificateAuthorityServiceFactory = ({
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy 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;
@@ -1307,7 +1364,9 @@ export const certificateAuthorityServiceFactory = ({
altNames, altNames,
serialNumber, serialNumber,
notBefore: notBeforeDate, notBefore: notBeforeDate,
notAfter: notAfterDate notAfter: notAfterDate,
keyUsages: selectedKeyUsages,
extendedKeyUsages: selectedExtendedKeyUsages
}, },
tx tx
); );
@@ -1370,7 +1429,9 @@ export const certificateAuthorityServiceFactory = ({
altNames, altNames,
ttl, ttl,
notBefore, notBefore,
notAfter notAfter,
keyUsages,
extendedKeyUsages
} = dto; } = dto;
let collectionId = pkiCollectionId; let collectionId = pkiCollectionId;
@@ -1494,7 +1555,6 @@ export const certificateAuthorityServiceFactory = ({
const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}`; const caIssuerUrl = `${appCfg.SITE_URL}/api/v1/pki/ca/${ca.id}/certificates/${caCert.id}`;
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),
@@ -1505,6 +1565,100 @@ export const certificateAuthorityServiceFactory = ({
new x509.CertificatePolicyExtension(["2.5.29.32.0"]) // anyPolicy 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";
@@ -1600,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
); );
@@ -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",
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas"; import { TableName } 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, sqlNestRelationships } from "@app/lib/knex";
import { TListProjectIdentityDTO } from "@app/services/identity-project/identity-project-types";
export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>; export type TIdentityProjectDALFactory = ReturnType<typeof identityProjectDALFactory>;
@@ -107,9 +108,16 @@ 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) 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(TableName.Identity, `${TableName.IdentityProjectMembership}.identityId`, `${TableName.Identity}.id`)
@@ -117,6 +125,10 @@ export const identityProjectDALFactory = (db: TDbClient) => {
if (filter.identityId) { if (filter.identityId) {
void qb.where("identityId", filter.identityId); void qb.where("identityId", filter.identityId);
} }
if (filter.search) {
void qb.whereILike(`${TableName.Identity}.name`, `%${filter.search}%`);
}
}) })
.join( .join(
TableName.IdentityProjectMembershipRole, TableName.IdentityProjectMembershipRole,
@@ -154,6 +166,22 @@ export const identityProjectDALFactory = (db: TDbClient) => {
db.ref("name").as("projectName").withSchema(TableName.Project) db.ref("name").as("projectName").withSchema(TableName.Project)
); );
if (filter.limit) {
void query.offset(filter.offset ?? 0).limit(filter.limit);
}
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 +236,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"
}
@@ -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,6 +78,8 @@ 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**
@@ -83,6 +87,7 @@ In the following steps, we explore how to issue a X.509 certificate under a CA.
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**.
@@ -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,
@@ -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

+11 -1
View File
@@ -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}
@@ -50,7 +50,7 @@ export const Pagination = ({
> >
<div className="mr-6 flex items-center space-x-2"> <div className="mr-6 flex items-center space-x-2">
<div className="text-xs"> <div className="text-xs">
{(page - 1) * perPage} - {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>
+3 -1
View File
@@ -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"
}
+3 -1
View File
@@ -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
View 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;
};
+51 -10
View File
@@ -1,19 +1,22 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { 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"
}
+46 -10
View File
@@ -1,6 +1,7 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { 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 }) =>
+15
View File
@@ -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"
}
+1 -1
View File
@@ -43,7 +43,7 @@ export const useNavigateToSelectOrganization = () => {
await navigateUserToOrg(router, config.defaultAuthOrgId); await navigateUserToOrg(router, config.defaultAuthOrgId);
} }
let localOrgId = localStorage.getItem("orgData.id") const localOrgId = localStorage.getItem("orgData.id")
if(!cliCallbackPort && localOrgId != null){ if(!cliCallbackPort && localOrgId != null){
await navigateUserToOrg(router, localOrgId); await navigateUserToOrg(router, localOrgId);
} }
@@ -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>;
@@ -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 }
); );
@@ -5,7 +5,15 @@ 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,
Select,
SelectItem
} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context"; import { useOrganization, useWorkspace } from "@app/context";
import { import {
useAddIdentityToWorkspace, useAddIdentityToWorkspace,
@@ -33,12 +41,20 @@ 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 } = useGetProjectRoles(projectSlug);
@@ -158,9 +174,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>
) : ( ) : (