mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 20:26:05 +00:00
Work on order
This commit is contained in:
@@ -36,7 +36,7 @@ def step_impl(context: Context, profile_var: str):
|
|||||||
# TODO: Fixed value for now, just to make test much easier,
|
# TODO: Fixed value for now, just to make test much easier,
|
||||||
# we should call infisical API to create such profile instead
|
# we should call infisical API to create such profile instead
|
||||||
# in the future
|
# in the future
|
||||||
profile_id = "0e96a01b-017e-4660-8b3d-ff26018fe0ce"
|
profile_id = "dd6e09c8-d5b8-4bfd-b436-4ab5c93d5d7e"
|
||||||
context.vars[profile_var] = AcmeProfile(profile_id)
|
context.vars[profile_var] = AcmeProfile(profile_id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { AcmeBadPublicKeyError } from "@app/ee/services/pki-acme/pki-acme-errors";
|
|
||||||
import {
|
import {
|
||||||
CreateAcmeAccountBodySchema,
|
|
||||||
CreateAcmeAccountResponseSchema,
|
CreateAcmeAccountResponseSchema,
|
||||||
|
CreateAcmeOrderBodySchema,
|
||||||
CreateAcmeOrderResponseSchema,
|
CreateAcmeOrderResponseSchema,
|
||||||
CreateAcmeOrderSchema,
|
|
||||||
DeactivateAcmeAccountResponseSchema,
|
DeactivateAcmeAccountResponseSchema,
|
||||||
DeactivateAcmeAccountSchema,
|
DeactivateAcmeAccountSchema,
|
||||||
DownloadAcmeCertificateSchema,
|
DownloadAcmeCertificateSchema,
|
||||||
@@ -25,7 +23,6 @@ import {
|
|||||||
RespondToAcmeChallengeResponseSchema,
|
RespondToAcmeChallengeResponseSchema,
|
||||||
RespondToAcmeChallengeSchema
|
RespondToAcmeChallengeSchema
|
||||||
} from "@app/ee/services/pki-acme/pki-acme-schemas";
|
} from "@app/ee/services/pki-acme/pki-acme-schemas";
|
||||||
import { TRawJwsPayload } from "@app/ee/services/pki-acme/pki-acme-types";
|
|
||||||
import { ApiDocsTags } from "@app/lib/api-docs";
|
import { ApiDocsTags } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
|
||||||
@@ -104,29 +101,23 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME New Account - register a new account or find existing one",
|
description: "ACME New Account - register a new account or find existing one",
|
||||||
...RawJwsPayloadSchema.shape,
|
params: z.object({
|
||||||
|
profileId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: RawJwsPayloadSchema,
|
||||||
response: {
|
response: {
|
||||||
201: CreateAcmeAccountResponseSchema
|
201: CreateAcmeAccountResponseSchema
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handler: async (req, res) => {
|
handler: async (req, res) => {
|
||||||
const { payload, protectedHeader } = await server.services.pkiAcme.validateJwsPayload(
|
const { payload, protectedHeader } = await server.services.pkiAcme.validateNewAccountJwsPayload(req.body);
|
||||||
req.body as TRawJwsPayload,
|
|
||||||
async (protectedHeader) => {
|
|
||||||
if (!protectedHeader.jwk) {
|
|
||||||
throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" });
|
|
||||||
}
|
|
||||||
return protectedHeader.jwk as unknown as JsonWebKey;
|
|
||||||
},
|
|
||||||
CreateAcmeAccountBodySchema
|
|
||||||
);
|
|
||||||
const { alg, jwk } = protectedHeader;
|
const { alg, jwk } = protectedHeader;
|
||||||
const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount(
|
const { status, body, headers } = await server.services.pkiAcme.createAcmeAccount({
|
||||||
req.params.profileId,
|
profileId: req.params.profileId,
|
||||||
alg,
|
alg,
|
||||||
jwk!,
|
jwk: jwk!,
|
||||||
payload
|
payload
|
||||||
);
|
});
|
||||||
// TODO: DRY
|
// TODO: DRY
|
||||||
res.code(status);
|
res.code(status);
|
||||||
for (const [key, value] of Object.entries(headers)) {
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
@@ -153,7 +144,10 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.PkiAcme],
|
tags: [ApiDocsTags.PkiAcme],
|
||||||
description: "ACME New Order - apply for a new certificate",
|
description: "ACME New Order - apply for a new certificate",
|
||||||
...CreateAcmeOrderSchema.shape,
|
params: z.object({
|
||||||
|
profileId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
body: RawJwsPayloadSchema,
|
||||||
response: {
|
response: {
|
||||||
201: CreateAcmeOrderResponseSchema
|
201: CreateAcmeOrderResponseSchema
|
||||||
}
|
}
|
||||||
@@ -161,6 +155,11 @@ export const registerPkiAcmeRouter = async (server: FastifyZodProvider) => {
|
|||||||
// TODO: replace with verify ACME signature here instead
|
// TODO: replace with verify ACME signature here instead
|
||||||
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req, res) => {
|
handler: async (req, res) => {
|
||||||
|
const { payload, protectedHeader, accountId } = await server.services.pkiAcme.validateExistingAccountJwsPayload(
|
||||||
|
req.params.profileId,
|
||||||
|
req.body,
|
||||||
|
CreateAcmeOrderBodySchema
|
||||||
|
);
|
||||||
const order = await server.services.pkiAcme.createAcmeOrder(req.params.profileId, req.body);
|
const order = await server.services.pkiAcme.createAcmeOrder(req.params.profileId, req.body);
|
||||||
res.code(201);
|
res.code(201);
|
||||||
return order;
|
return order;
|
||||||
|
|||||||
@@ -26,24 +26,9 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateById = async (id: string, data: TPkiAcmeAccountsUpdate, tx?: Knex) => {
|
const findById = async (profileId: string, id: string, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).update(data).returning("*");
|
const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, id }).first();
|
||||||
const [account] = result;
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return account;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Update PKI ACME account" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const findById = async (id: string, tx?: Knex) => {
|
|
||||||
try {
|
|
||||||
const account = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).first();
|
|
||||||
|
|
||||||
return account || null;
|
return account || null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -51,16 +36,6 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findByProfileId = async (profileId: string, tx?: Knex) => {
|
|
||||||
try {
|
|
||||||
const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId }).first();
|
|
||||||
|
|
||||||
return account || null;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Find PKI ACME account by profile id" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const findByPublicKey = async (profileId: string, alg: string, publicKey: unknown, tx?: Knex) => {
|
const findByPublicKey = async (profileId: string, alg: string, publicKey: unknown, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, alg, publicKey }).first();
|
const account = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId, alg, publicKey }).first();
|
||||||
@@ -71,35 +46,12 @@ export const pkiAcmeAccountDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const findManyByProfileId = async (profileId: string, tx?: Knex) => {
|
|
||||||
try {
|
|
||||||
const accounts = await (tx || db)(TableName.PkiAcmeAccount).where({ profileId });
|
|
||||||
|
|
||||||
return accounts;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Find many PKI ACME accounts by profile id" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteById = async (id: string, tx?: Knex) => {
|
|
||||||
try {
|
|
||||||
const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).delete().returning("*");
|
|
||||||
const [account] = result;
|
|
||||||
|
|
||||||
return account || null;
|
|
||||||
} catch (error) {
|
|
||||||
throw new DatabaseError({ error, name: "Delete PKI ACME account by id" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...pkiAcmeAccountOrm,
|
...pkiAcmeAccountOrm,
|
||||||
create,
|
create,
|
||||||
updateById,
|
updateById,
|
||||||
findById,
|
findById,
|
||||||
findByProfileId,
|
findByProfileId,
|
||||||
findByPublicKey,
|
findByPublicKey
|
||||||
findManyByProfileId,
|
|
||||||
deleteById
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { TPkiAcmeOrderDALFactory } from "./pki-acme-order-dal";
|
|||||||
import { CreateAcmeAccountBodySchema, ProtectedHeaderSchema } from "./pki-acme-schemas";
|
import { CreateAcmeAccountBodySchema, ProtectedHeaderSchema } from "./pki-acme-schemas";
|
||||||
import {
|
import {
|
||||||
TAcmeResponse,
|
TAcmeResponse,
|
||||||
|
TAuthenciatedJwsPayload,
|
||||||
TCreateAcmeAccountPayload,
|
TCreateAcmeAccountPayload,
|
||||||
TCreateAcmeAccountResponse,
|
TCreateAcmeAccountResponse,
|
||||||
TCreateAcmeOrderPayload,
|
TCreateAcmeOrderPayload,
|
||||||
@@ -43,7 +44,7 @@ import {
|
|||||||
|
|
||||||
type TPkiAcmeServiceFactoryDep = {
|
type TPkiAcmeServiceFactoryDep = {
|
||||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
|
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
|
||||||
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findByPublicKey" | "create">;
|
acmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findById" | "findByPublicKey" | "create">;
|
||||||
acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create">;
|
acmeOrderDAL: Pick<TPkiAcmeOrderDALFactory, "create">;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,6 +70,14 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
return `${baseUrl}${path}`;
|
return `${baseUrl}${path}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const extractAccountIdFromKid = (kid: string, profileId: string): string => {
|
||||||
|
const kidPrefix = buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/`);
|
||||||
|
if (!kid.startsWith(kidPrefix)) {
|
||||||
|
throw new AcmeMalformedError({ detail: "KID must start with the profile account URL" });
|
||||||
|
}
|
||||||
|
return kid.slice(kidPrefix.length);
|
||||||
|
};
|
||||||
|
|
||||||
const validateJwsPayload = async <T>(
|
const validateJwsPayload = async <T>(
|
||||||
rawJwsPayload: TRawJwsPayload,
|
rawJwsPayload: TRawJwsPayload,
|
||||||
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>,
|
getJWK: (protectedHeader: JWSHeaderParameters) => Promise<JsonWebKey>,
|
||||||
@@ -120,7 +129,7 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
rawJwsPayload,
|
rawJwsPayload,
|
||||||
async (protectedHeader) => {
|
async (protectedHeader) => {
|
||||||
if (!protectedHeader.jwk) {
|
if (!protectedHeader.jwk) {
|
||||||
throw new AcmeBadPublicKeyError({ detail: "JWK is required in the protected header" });
|
throw new AcmeMalformedError({ detail: "JWK is required in the protected header" });
|
||||||
}
|
}
|
||||||
return protectedHeader.jwk as unknown as JsonWebKey;
|
return protectedHeader.jwk as unknown as JsonWebKey;
|
||||||
},
|
},
|
||||||
@@ -128,6 +137,36 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateExistingAccountJwsPayload = async <T>(
|
||||||
|
profileId: string,
|
||||||
|
rawJwsPayload: TRawJwsPayload,
|
||||||
|
schema: z.ZodSchema<T>
|
||||||
|
): Promise<TAuthenciatedJwsPayload<T>> => {
|
||||||
|
const profile = await validateAcmeProfile(profileId);
|
||||||
|
const result = await validateJwsPayload(
|
||||||
|
rawJwsPayload,
|
||||||
|
async (protectedHeader) => {
|
||||||
|
if (!protectedHeader.kid) {
|
||||||
|
throw new AcmeMalformedError({ detail: "KID is required in the protected header" });
|
||||||
|
}
|
||||||
|
const accountId = extractAccountIdFromKid(protectedHeader.kid, profileId);
|
||||||
|
const account = await acmeAccountDAL.findById(profile.id, accountId);
|
||||||
|
if (!account) {
|
||||||
|
throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" });
|
||||||
|
}
|
||||||
|
if (account.alg !== protectedHeader.alg) {
|
||||||
|
throw new AcmeMalformedError({ detail: "ACME account algorithm mismatch" });
|
||||||
|
}
|
||||||
|
return account.publicKey as JsonWebKey;
|
||||||
|
},
|
||||||
|
schema
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
accountId: extractAccountIdFromKid(result.protectedHeader.kid!, profileId)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
|
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
|
||||||
await validateAcmeProfile(profileId);
|
await validateAcmeProfile(profileId);
|
||||||
return {
|
return {
|
||||||
@@ -195,13 +234,25 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const createAcmeOrder = async (
|
const createAcmeOrder = async ({
|
||||||
profileId: string,
|
profileId,
|
||||||
account: TPkiAcmeAccounts,
|
accountId,
|
||||||
payload: TCreateAcmeOrderPayload
|
payload
|
||||||
): Promise<TAcmeResponse<TCreateAcmeOrderResponse>> => {
|
}: {
|
||||||
const profile = await validateAcmeProfile(profileId);
|
profileId: string;
|
||||||
|
accountId: string;
|
||||||
|
payload: TCreateAcmeOrderPayload;
|
||||||
|
}): Promise<TAcmeResponse<TCreateAcmeOrderResponse>> => {
|
||||||
|
const account = await acmeAccountDAL.findById(profileId, accountId)!;
|
||||||
|
|
||||||
|
// TODO: check and see if we have existing orders for this account that meet the criteria
|
||||||
|
// if we do, return the existing order
|
||||||
|
|
||||||
|
orders = await acmeOrderDAL.create({
|
||||||
|
profileId,
|
||||||
|
accountId,
|
||||||
|
status: "pending"
|
||||||
|
});
|
||||||
// FIXME: Implement ACME new order creation
|
// FIXME: Implement ACME new order creation
|
||||||
const orderId = "FIXME-order-id";
|
const orderId = "FIXME-order-id";
|
||||||
return {
|
return {
|
||||||
@@ -315,6 +366,7 @@ export const pkiAcmeServiceFactory = ({
|
|||||||
return {
|
return {
|
||||||
validateJwsPayload,
|
validateJwsPayload,
|
||||||
validateNewAccountJwsPayload,
|
validateNewAccountJwsPayload,
|
||||||
|
validateExistingAccountJwsPayload,
|
||||||
getAcmeDirectory,
|
getAcmeDirectory,
|
||||||
getAcmeNewNonce,
|
getAcmeNewNonce,
|
||||||
createAcmeAccount,
|
createAcmeAccount,
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ export type TJwsPayload<T> = {
|
|||||||
protectedHeader: TProtectedHeader;
|
protectedHeader: TProtectedHeader;
|
||||||
payload: T;
|
payload: T;
|
||||||
};
|
};
|
||||||
|
export type TAuthenciatedJwsPayload<T> = TJwsPayload<T> & {
|
||||||
|
accountId: string;
|
||||||
|
};
|
||||||
export type TAcmeResponse<TPayload> = {
|
export type TAcmeResponse<TPayload> = {
|
||||||
status: number;
|
status: number;
|
||||||
headers: Record<string, string>;
|
headers: Record<string, string>;
|
||||||
@@ -55,6 +58,11 @@ export type TPkiAcmeServiceFactory = {
|
|||||||
schema: z.ZodSchema<T>
|
schema: z.ZodSchema<T>
|
||||||
) => Promise<TJwsPayload<T>>;
|
) => Promise<TJwsPayload<T>>;
|
||||||
validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>;
|
validateNewAccountJwsPayload: (rawJwsPayload: TRawJwsPayload) => Promise<TJwsPayload<TCreateAcmeAccountPayload>>;
|
||||||
|
validateExistingAccountJwsPayload: <T>(
|
||||||
|
profileId: string,
|
||||||
|
rawJwsPayload: TRawJwsPayload,
|
||||||
|
schema: z.ZodSchema<T>
|
||||||
|
) => Promise<TAuthenciatedJwsPayload<T>>;
|
||||||
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
|
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
|
||||||
getAcmeNewNonce: (profileId: string) => Promise<string>;
|
getAcmeNewNonce: (profileId: string) => Promise<string>;
|
||||||
createAcmeAccount: ({
|
createAcmeAccount: ({
|
||||||
@@ -68,10 +76,15 @@ export type TPkiAcmeServiceFactory = {
|
|||||||
jwk: JsonWebKey;
|
jwk: JsonWebKey;
|
||||||
payload: TCreateAcmeAccountPayload;
|
payload: TCreateAcmeAccountPayload;
|
||||||
}) => Promise<TAcmeResponse<TCreateAcmeAccountResponse>>;
|
}) => Promise<TAcmeResponse<TCreateAcmeAccountResponse>>;
|
||||||
createAcmeOrder: (
|
createAcmeOrder: ({
|
||||||
profileId: string,
|
profileId,
|
||||||
body: TCreateAcmeOrderPayload
|
accountId,
|
||||||
) => Promise<TAcmeResponse<TCreateAcmeOrderResponse>>;
|
payload
|
||||||
|
}: {
|
||||||
|
profileId: string;
|
||||||
|
accountId: string;
|
||||||
|
payload: TCreateAcmeOrderPayload;
|
||||||
|
}) => Promise<TAcmeResponse<TCreateAcmeOrderResponse>>;
|
||||||
deactivateAcmeAccount: (
|
deactivateAcmeAccount: (
|
||||||
profileId: string,
|
profileId: string,
|
||||||
accountId: string,
|
accountId: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user