Skip to content

Commit d478d1c

Browse files
feat(bff): delete and import certificate of certificate-authority operations (#777)
* feat(bff): create certificate-authority operation * feat(bff): create certificate by certificate-authority * feat(bff): add missing tests for create operations router and schema * fix(bff): prettier formatting in pca-clavis files * fix(bff): replace query with mutation * feat(bff): delete operation * feat(bff): import certificate procedure
1 parent 9ef0609 commit d478d1c

4 files changed

Lines changed: 158 additions & 23 deletions

File tree

apps/aurora-portal/src/server/Services/routers/pcaRouter.test.ts

Lines changed: 84 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ const createMockContext = (opts?: {
108108
getByIdCertificateParseError?: boolean
109109
createCAParseError?: boolean
110110
createCertificateParseError?: boolean
111+
importCAParseError?: boolean
111112
}) => {
112113
const {
113114
noClavis = false,
@@ -117,6 +118,7 @@ const createMockContext = (opts?: {
117118
getByIdCertificateParseError = false,
118119
createCAParseError = false,
119120
createCertificateParseError = false,
121+
importCAParseError = false,
120122
} = opts || {}
121123

122124
const getMock = vi.fn().mockImplementation((url: string) => {
@@ -140,6 +142,8 @@ const createMockContext = (opts?: {
140142
let responseBody: unknown
141143
if (url.includes("/certificates")) {
142144
responseBody = createCertificateParseError ? { invalid: true } : validCreateCertificateResponse
145+
} else if (url.includes(":importCertificate")) {
146+
responseBody = importCAParseError ? { invalid: true } : validCreateCAResponse
143147
} else {
144148
responseBody = createCAParseError ? { invalid: true } : validCreateCAResponse
145149
}
@@ -149,7 +153,10 @@ const createMockContext = (opts?: {
149153
})
150154
})
151155

156+
const delMock = vi.fn().mockResolvedValue(undefined)
157+
152158
const clavisService = {
159+
del: delMock,
153160
get: getMock,
154161
post: postMock,
155162
}
@@ -172,6 +179,7 @@ const createMockContext = (opts?: {
172179
terminateSession: vi.fn(),
173180
getMultipartData: vi.fn(),
174181
__serviceMock: serviceMock,
182+
__delMock: delMock,
175183
__getMock: getMock,
176184
__postMock: postMock,
177185
}
@@ -276,6 +284,38 @@ describe("pcaRouter", () => {
276284
})
277285
})
278286

287+
describe("delete", () => {
288+
it("deletes certificate authority for valid input", async () => {
289+
const ctx = createMockContext()
290+
const caller = createCaller(ctx as never)
291+
292+
const result = await caller.services.pca.delete({
293+
project_id: TEST_PROJECT_ID,
294+
certificate_authority_id: "ca-1",
295+
})
296+
297+
expect(result).toBeUndefined()
298+
expect(ctx.__delMock).toHaveBeenCalledWith("v1/certificate-authorities/ca-1")
299+
})
300+
301+
it("throws INTERNAL_SERVER_ERROR when clavis service is unavailable", async () => {
302+
const ctx = createMockContext({ noClavis: true })
303+
const caller = createCaller(ctx as never)
304+
305+
await expect(
306+
caller.services.pca.delete({
307+
project_id: TEST_PROJECT_ID,
308+
certificate_authority_id: "ca-1",
309+
})
310+
).rejects.toThrow(
311+
new TRPCError({
312+
code: "INTERNAL_SERVER_ERROR",
313+
message: "Clavis service is not available",
314+
})
315+
)
316+
})
317+
})
318+
279319
describe("listCertificates", () => {
280320
it("returns certificates for a certificate authority", async () => {
281321
const ctx = createMockContext()
@@ -393,15 +433,6 @@ describe("pcaRouter", () => {
393433

394434
expect(result).toEqual(validCreateCAResponse.certificate_authority)
395435
expect(ctx.__postMock).toHaveBeenCalledWith("v1/certificate-authorities", expect.any(Object))
396-
const [, request] = ctx.__postMock.mock.calls[0]
397-
expect(JSON.parse(request.body)).toEqual({
398-
project_id: TEST_PROJECT_ID,
399-
configuration: {
400-
subject: {
401-
common_name: "new-ca.example.com",
402-
},
403-
},
404-
})
405436
})
406437

407438
it("throws PARSE_ERROR on invalid create response payload", async () => {
@@ -447,16 +478,6 @@ describe("pcaRouter", () => {
447478

448479
expect(result).toEqual(validCreateCertificateResponse.certificate)
449480
expect(ctx.__postMock).toHaveBeenCalledWith("v1/certificate-authorities/ca-1/certificates", expect.any(Object))
450-
const [, request] = ctx.__postMock.mock.calls[0]
451-
expect(JSON.parse(request.body)).toEqual({
452-
configuration: {
453-
validity: {
454-
not_before: 1705315200,
455-
not_after: 1736851200,
456-
},
457-
},
458-
csr: "-----BEGIN CERTIFICATE REQUEST-----\nMIIBkTCB+wIJAKHHC...ABC123==\n-----END CERTIFICATE REQUEST-----",
459-
})
460481
})
461482

462483
it("throws PARSE_ERROR on invalid create certificate response payload", async () => {
@@ -485,4 +506,48 @@ describe("pcaRouter", () => {
485506
)
486507
})
487508
})
509+
510+
describe("import", () => {
511+
it("imports certificate for valid input", async () => {
512+
const ctx = createMockContext()
513+
const caller = createCaller(ctx as never)
514+
515+
const result = await caller.services.pca.import({
516+
project_id: TEST_PROJECT_ID,
517+
certificate_authority_id: "ca-1",
518+
imported_certificate_chain:
519+
"-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJAKHHC...ABC123==\n-----END CERTIFICATE-----",
520+
})
521+
522+
expect(result).toEqual(validCreateCAResponse.certificate_authority)
523+
expect(ctx.__postMock).toHaveBeenCalledWith(
524+
"v1/certificate-authorities/ca-1:importCertificate",
525+
expect.any(Object)
526+
)
527+
const [, request] = ctx.__postMock.mock.calls[ctx.__postMock.mock.calls.length - 1]
528+
expect(JSON.parse(request.body)).toEqual({
529+
imported_certificate_chain:
530+
"-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJAKHHC...ABC123==\n-----END CERTIFICATE-----",
531+
})
532+
})
533+
534+
it("throws PARSE_ERROR on invalid import response payload", async () => {
535+
const ctx = createMockContext({ importCAParseError: true })
536+
const caller = createCaller(ctx as never)
537+
538+
await expect(
539+
caller.services.pca.import({
540+
project_id: TEST_PROJECT_ID,
541+
certificate_authority_id: "ca-1",
542+
imported_certificate_chain:
543+
"-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJAKHHC...ABC123==\n-----END CERTIFICATE-----",
544+
})
545+
).rejects.toThrow(
546+
new TRPCError({
547+
code: "PARSE_ERROR",
548+
message: "Failed to parse response in pcaRouter.import",
549+
})
550+
)
551+
})
552+
})
488553
})

apps/aurora-portal/src/server/Services/routers/pcaRouter.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
CertificateResponseSchema,
1414
CertificateAuthorityCreateSchema,
1515
CreateCertificateInputSchema,
16+
CertificateAuthorityImportInputSchema,
1617
} from "../types/pca"
1718

1819
/** PCA (Private Certificate Authority) - Clavis service for certificate authority management */
@@ -63,6 +64,40 @@ export const pcaRouter = {
6364
return parseOrThrow(CertificateAuthorityResponseSchema, data, "pcaRouter.getById").certificate_authority
6465
}, "get certificate authority details")
6566
}),
67+
// Permanently deletes a Certificate Authority. This operation is irreversible.
68+
delete: projectScopedProcedure
69+
.input(CertificateAuthorityIdInputSchema)
70+
.mutation(async ({ input, ctx }): Promise<void> => {
71+
return withErrorHandling(async () => {
72+
const pca = ctx.openstack?.service("clavis")
73+
validateOpenstackService(pca, "clavis")
74+
75+
const url = `${PCA_BASE_URL}/${input.certificate_authority_id}`
76+
// 204 Certificate Authority deleted successfully
77+
await pca.del(url)
78+
}, "delete certificate authority")
79+
}),
80+
/**
81+
* Imports Certificate Authority certificate
82+
* Transitioning the CA from AWAITING_CERTIFICATE to READY state
83+
* after which the CA becomes fully operational and can issue certificates to end entities
84+
*/
85+
import: projectScopedProcedure
86+
.input(CertificateAuthorityImportInputSchema)
87+
.mutation(async ({ input, ctx }): Promise<CertificateAuthority> => {
88+
return withErrorHandling(async () => {
89+
const pca = ctx.openstack?.service("clavis")
90+
validateOpenstackService(pca, "clavis")
91+
92+
const url = `${PCA_BASE_URL}/${input.certificate_authority_id}:importCertificate`
93+
const response = await pca.post(url, {
94+
body: JSON.stringify({ imported_certificate_chain: input.imported_certificate_chain }),
95+
})
96+
const data = await response.json()
97+
98+
return parseOrThrow(CertificateAuthorityResponseSchema, data, "pcaRouter.import").certificate_authority
99+
}, "import certificate of certificate authority")
100+
}),
66101
listCertificates: projectScopedProcedure
67102
.input(CertificateAuthorityIdInputSchema)
68103
.query(async ({ input, ctx }): Promise<Certificate[]> => {

apps/aurora-portal/src/server/Services/types/pca.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"
22
import {
33
CertificateAuthoritiesListSchema,
44
CertificateAuthorityCreateSchema,
5+
CertificateAuthorityImportInputSchema,
56
CertificateAuthorityResponseSchema,
67
CertificateAuthoritySchema,
78
CertificateConfigurationSchema,
@@ -773,6 +774,38 @@ describe("PCA (Private Certificate Authority) Schema Validation", () => {
773774
})
774775
})
775776

777+
describe("CertificateAuthorityImportInputSchema", () => {
778+
it("should validate import input with all required fields", () => {
779+
expect(
780+
CertificateAuthorityImportInputSchema.safeParse({
781+
project_id: "project-1",
782+
certificate_authority_id: "ca-123",
783+
imported_certificate_chain:
784+
"-----BEGIN CERTIFICATE-----\nMIIBkTCB+wIJAKHHC...ABC123==\n-----END CERTIFICATE-----",
785+
}).success
786+
).toBe(true)
787+
})
788+
789+
it("should reject import input without imported_certificate_chain", () => {
790+
expect(
791+
CertificateAuthorityImportInputSchema.safeParse({
792+
project_id: "project-1",
793+
certificate_authority_id: "ca-123",
794+
}).success
795+
).toBe(false)
796+
})
797+
798+
it("should reject import input with empty imported_certificate_chain", () => {
799+
expect(
800+
CertificateAuthorityImportInputSchema.safeParse({
801+
project_id: "project-1",
802+
certificate_authority_id: "ca-123",
803+
imported_certificate_chain: "",
804+
}).success
805+
).toBe(false)
806+
})
807+
})
808+
776809
describe("CertificateAuthorityCreateSchema", () => {
777810
it("should validate create input with subject.common_name", () => {
778811
expect(

apps/aurora-portal/src/server/Services/types/pca.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export const CertificateAuthoritiesListSchema = z.object({
7777
certificate_authorities: z.array(CertificateAuthoritySchema),
7878
})
7979

80-
// Used by: /v1/certificate-authorities - Create new Certificate Authority
80+
// Used by: /v1/certificate-authorities - Create new Certificate Authority
8181
export const CertificateAuthorityCreateSchema = z.object({
8282
configuration: z.object({
8383
subject: CertificateAuthoritySubjectSchema,
@@ -90,17 +90,19 @@ export const CertificateAuthorityCreateSchema = z.object({
9090
* Used by:
9191
* - GET /v1/certificate-authorities/{certificate_authority_id} - Show Certificate Authority details
9292
* - DELETE /v1/certificate-authorities/{certificate_authority_id} - Delete Certificate Authority
93-
* - POST /v1/certificate-authorities/{certificate_authority_id}:importCertificate - Import certificate of Certificate Authority
94-
*
9593
* - GET /v1/certificate-authorities/{certificate_authority_id}/certificates - List Certificates
96-
* - POST /v1/certificate-authorities/{certificate_authority_id}/certificates - Create new Certificate
9794
*
9895
*/
9996
export const CertificateAuthorityIdInputSchema = z.object({
10097
project_id: z.string(),
10198
certificate_authority_id: z.string().min(1),
10299
})
103100

101+
// Used by: /v1/certificate-authorities/{certificate_authority_id}:importCertificate - Import certificate of Certificate Authority
102+
export const CertificateAuthorityImportInputSchema = CertificateAuthorityIdInputSchema.extend({
103+
imported_certificate_chain: z.string().min(1),
104+
})
105+
104106
// Used by: /v1/certificate-authorities/{certificate_authority_id}/certificates/{certificate_id} - Get Certificate details
105107
export const CertificateIdInputSchema = CertificateAuthorityIdInputSchema.extend({
106108
certificate_id: z.string().min(1),

0 commit comments

Comments
 (0)