mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Implement account creation
This commit is contained in:
105
backend/src/ee/services/pki-acme/pki-acme-account-dal.ts
Normal file
105
backend/src/ee/services/pki-acme/pki-acme-account-dal.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { TPkiAcmeAccountsInsert, TPkiAcmeAccountsUpdate } from "@app/db/schemas/pki-acme-accounts";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TPkiAcmeAccountDALFactory = ReturnType<typeof pkiAcmeAccountDALFactory>;
|
||||
|
||||
export const pkiAcmeAccountDALFactory = (db: TDbClient) => {
|
||||
const pkiAcmeAccountOrm = ormify(db, TableName.PkiAcmeAccount);
|
||||
|
||||
const create = async (data: TPkiAcmeAccountsInsert, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db)(TableName.PkiAcmeAccount).insert(data).returning("*");
|
||||
const [account] = result;
|
||||
|
||||
if (!account) {
|
||||
throw new Error("Failed to create PKI ACME account");
|
||||
}
|
||||
|
||||
return account;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Create PKI ACME account" });
|
||||
}
|
||||
};
|
||||
|
||||
const updateById = async (id: string, data: TPkiAcmeAccountsUpdate, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db)(TableName.PkiAcmeAccount).where({ id }).update(data).returning("*");
|
||||
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;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find PKI ACME account by id" });
|
||||
}
|
||||
};
|
||||
|
||||
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 (publicKey: unknown, tx?: Knex) => {
|
||||
try {
|
||||
const account = await (tx || db)(TableName.PkiAcmeAccount).where({ publicKey }).first();
|
||||
|
||||
return account || null;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find PKI ACME account by public key" });
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
...pkiAcmeAccountOrm,
|
||||
create,
|
||||
updateById,
|
||||
findById,
|
||||
findByProfileId,
|
||||
findByPublicKey,
|
||||
findManyByProfileId,
|
||||
deleteById
|
||||
};
|
||||
};
|
||||
@@ -22,7 +22,7 @@ export const RawJwsPayloadSchema = z.object({
|
||||
|
||||
export const JwsPayloadSchema = z.object({
|
||||
protectedHeader: ProtectedHeaderSchema,
|
||||
payload: z.any()
|
||||
payload: z.unknown()
|
||||
});
|
||||
|
||||
// Directory endpoint
|
||||
@@ -71,8 +71,7 @@ export const CreateAcmeAccountSchema = z.object({
|
||||
export const CreateAcmeAccountResponseSchema = z.object({
|
||||
status: z.string(),
|
||||
contact: z.array(z.string()).optional(),
|
||||
orders: z.string().optional(),
|
||||
accountUrl: z.string()
|
||||
orders: z.string().optional()
|
||||
});
|
||||
|
||||
// New Order payload schema
|
||||
|
||||
@@ -3,7 +3,7 @@ import { NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal";
|
||||
|
||||
import { AcmeBadPublicKeyError, AcmeMalformedError } from "./pki-acme-errors";
|
||||
import { AcmeAccountDoesNotExistError, AcmeBadPublicKeyError, AcmeMalformedError } from "./pki-acme-errors";
|
||||
|
||||
import {
|
||||
EnrollmentType,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose";
|
||||
import { ProtectedHeaderSchema } from "./pki-acme-schemas";
|
||||
import {
|
||||
TAcmeResponse,
|
||||
TCreateAcmeAccountPayload,
|
||||
TCreateAcmeAccountResponse,
|
||||
TCreateAcmeOrderPayload,
|
||||
@@ -24,17 +25,23 @@ import {
|
||||
TGetAcmeDirectoryResponse,
|
||||
TGetAcmeOrderResponse,
|
||||
TJwsPayload,
|
||||
TJwsPayloadWithJwk,
|
||||
TListAcmeOrdersResponse,
|
||||
TPkiAcmeServiceFactory,
|
||||
TRawJwsPayload,
|
||||
TRespondToAcmeChallengeResponse
|
||||
} from "./pki-acme-types";
|
||||
import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal";
|
||||
|
||||
type TPkiAcmeServiceFactoryDep = {
|
||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
|
||||
pkiAcmeAccountDAL: Pick<TPkiAcmeAccountDALFactory, "findByPublicKey" | "create">;
|
||||
};
|
||||
|
||||
export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
|
||||
export const pkiAcmeServiceFactory = ({
|
||||
certificateProfileDAL,
|
||||
pkiAcmeAccountDAL
|
||||
}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
|
||||
const validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => {
|
||||
const profile = await certificateProfileDAL.findById(profileId);
|
||||
if (!profile) {
|
||||
@@ -52,7 +59,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
||||
return `${baseUrl}${path}`;
|
||||
};
|
||||
|
||||
const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise<TJwsPayload> => {
|
||||
const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise<TJwsPayloadWithJwk> => {
|
||||
const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify(
|
||||
rawJwsPayload,
|
||||
async (protectedHeader: JWSHeaderParameters | undefined) => {
|
||||
@@ -72,10 +79,11 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
||||
if (!success) {
|
||||
throw new AcmeMalformedError({ detail: "Invalid protected header" });
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload;
|
||||
// TODO: also consume the nonce here
|
||||
return { payload, protectedHeader };
|
||||
return { payload, protectedHeader, jwk: protectedHeader.jwk as JsonWebKey };
|
||||
};
|
||||
|
||||
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
|
||||
@@ -96,18 +104,47 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
||||
|
||||
const createAcmeAccount = async (
|
||||
profileId: string,
|
||||
jwk: JWK,
|
||||
payload: TCreateAcmeAccountPayload
|
||||
): Promise<TCreateAcmeAccountResponse> => {
|
||||
): Promise<TAcmeResponse<TCreateAcmeAccountResponse>> => {
|
||||
const profile = await validateAcmeProfile(profileId);
|
||||
// FIXME: Implement ACME new account registration
|
||||
// Use EAB authentication to find corresponding Infisical machine identity
|
||||
// Check permissions and return account information
|
||||
const accountId = "FIXME-account-id";
|
||||
// TODO: the jwk as json obj may not be the best idea for indexing.
|
||||
// Maybe we should find a way to serialize the jwk deterministically.
|
||||
let account = await pkiAcmeAccountDAL.findByPublicKey(jwk);
|
||||
if (payload.onlyReturnExisting && !account) {
|
||||
throw new AcmeAccountDoesNotExistError({ message: "ACME account not found" });
|
||||
}
|
||||
if (account) {
|
||||
// With the same public key, we found an existing account, just return it
|
||||
return {
|
||||
status: 200,
|
||||
payload: {
|
||||
status: "valid",
|
||||
contact: account.emails,
|
||||
orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}/orders`)
|
||||
},
|
||||
headers: {
|
||||
Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
account = await pkiAcmeAccountDAL.create({
|
||||
profileId,
|
||||
publicKey: jwk,
|
||||
emails: payload.contact ?? []
|
||||
});
|
||||
// TODO: check EAB authentication here
|
||||
return {
|
||||
status: "valid",
|
||||
accountUrl: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`),
|
||||
contact: [],
|
||||
orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}/orders`)
|
||||
status: 201,
|
||||
payload: {
|
||||
status: "valid",
|
||||
contact: account.emails,
|
||||
orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}/orders`)
|
||||
},
|
||||
headers: {
|
||||
Location: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${account.id}`)
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -116,6 +153,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
||||
payload: TCreateAcmeOrderPayload
|
||||
): Promise<TCreateAcmeOrderResponse> => {
|
||||
const profile = await validateAcmeProfile(profileId);
|
||||
|
||||
// FIXME: Implement ACME new order creation
|
||||
const orderId = "FIXME-order-id";
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { JWK } from "jose";
|
||||
import {
|
||||
CreateAcmeAccountBodySchema,
|
||||
CreateAcmeAccountResponseSchema,
|
||||
@@ -39,11 +40,24 @@ export type TCreateAcmeOrderPayload = z.infer<typeof CreateAcmeOrderBodySchema>;
|
||||
export type TDeactivateAcmeAccountPayload = z.infer<typeof DeactivateAcmeAccountBodySchema>;
|
||||
export type TFinalizeAcmeOrderPayload = z.infer<typeof FinalizeAcmeOrderBodySchema>;
|
||||
|
||||
export type TJwsPayloadWithJwk = TJwsPayload & {
|
||||
jwk: JsonWebKey;
|
||||
};
|
||||
export type TAcmeResponse<TPayload> = {
|
||||
status: number;
|
||||
headers: Record<string, string>;
|
||||
payload: TPayload;
|
||||
};
|
||||
|
||||
export type TPkiAcmeServiceFactory = {
|
||||
validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise<TJwsPayload>;
|
||||
validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise<TJwsPayloadWithJwk>;
|
||||
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
|
||||
getAcmeNewNonce: (profileId: string) => Promise<string>;
|
||||
createAcmeAccount: (profileId: string, body: TCreateAcmeAccountPayload) => Promise<TCreateAcmeAccountResponse>;
|
||||
createAcmeAccount: (
|
||||
profileId: string,
|
||||
jwk: JsonWebKey,
|
||||
body: TCreateAcmeAccountPayload
|
||||
) => Promise<TAcmeResponse<TCreateAcmeAccountResponse>>;
|
||||
createAcmeOrder: (profileId: string, body: TCreateAcmeOrderPayload) => Promise<TCreateAcmeOrderResponse>;
|
||||
deactivateAcmeAccount: (
|
||||
profileId: string,
|
||||
|
||||
Reference in New Issue
Block a user