mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 06:26:27 +00:00
Implement account creation
This commit is contained in:
@@ -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({
|
export const JwsPayloadSchema = z.object({
|
||||||
protectedHeader: ProtectedHeaderSchema,
|
protectedHeader: ProtectedHeaderSchema,
|
||||||
payload: z.any()
|
payload: z.unknown()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Directory endpoint
|
// Directory endpoint
|
||||||
@@ -71,8 +71,7 @@ export const CreateAcmeAccountSchema = z.object({
|
|||||||
export const CreateAcmeAccountResponseSchema = z.object({
|
export const CreateAcmeAccountResponseSchema = z.object({
|
||||||
status: z.string(),
|
status: z.string(),
|
||||||
contact: z.array(z.string()).optional(),
|
contact: z.array(z.string()).optional(),
|
||||||
orders: z.string().optional(),
|
orders: z.string().optional()
|
||||||
accountUrl: z.string()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// New Order payload schema
|
// 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 { 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 {
|
import {
|
||||||
EnrollmentType,
|
EnrollmentType,
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose";
|
import { flattenedVerify, importJWK, JWK, JWSHeaderParameters } from "jose";
|
||||||
import { ProtectedHeaderSchema } from "./pki-acme-schemas";
|
import { ProtectedHeaderSchema } from "./pki-acme-schemas";
|
||||||
import {
|
import {
|
||||||
|
TAcmeResponse,
|
||||||
TCreateAcmeAccountPayload,
|
TCreateAcmeAccountPayload,
|
||||||
TCreateAcmeAccountResponse,
|
TCreateAcmeAccountResponse,
|
||||||
TCreateAcmeOrderPayload,
|
TCreateAcmeOrderPayload,
|
||||||
@@ -24,17 +25,23 @@ import {
|
|||||||
TGetAcmeDirectoryResponse,
|
TGetAcmeDirectoryResponse,
|
||||||
TGetAcmeOrderResponse,
|
TGetAcmeOrderResponse,
|
||||||
TJwsPayload,
|
TJwsPayload,
|
||||||
|
TJwsPayloadWithJwk,
|
||||||
TListAcmeOrdersResponse,
|
TListAcmeOrdersResponse,
|
||||||
TPkiAcmeServiceFactory,
|
TPkiAcmeServiceFactory,
|
||||||
TRawJwsPayload,
|
TRawJwsPayload,
|
||||||
TRespondToAcmeChallengeResponse
|
TRespondToAcmeChallengeResponse
|
||||||
} from "./pki-acme-types";
|
} from "./pki-acme-types";
|
||||||
|
import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal";
|
||||||
|
|
||||||
type TPkiAcmeServiceFactoryDep = {
|
type TPkiAcmeServiceFactoryDep = {
|
||||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findById">;
|
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 validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => {
|
||||||
const profile = await certificateProfileDAL.findById(profileId);
|
const profile = await certificateProfileDAL.findById(profileId);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
@@ -52,7 +59,7 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
|||||||
return `${baseUrl}${path}`;
|
return `${baseUrl}${path}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise<TJwsPayload> => {
|
const validateCreateAcmeAccountJwsPayload = async (rawJwsPayload: TRawJwsPayload): Promise<TJwsPayloadWithJwk> => {
|
||||||
const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify(
|
const { payload: rawPayload, protectedHeader: rawProtectedHeader } = await flattenedVerify(
|
||||||
rawJwsPayload,
|
rawJwsPayload,
|
||||||
async (protectedHeader: JWSHeaderParameters | undefined) => {
|
async (protectedHeader: JWSHeaderParameters | undefined) => {
|
||||||
@@ -72,10 +79,11 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
|||||||
if (!success) {
|
if (!success) {
|
||||||
throw new AcmeMalformedError({ detail: "Invalid protected header" });
|
throw new AcmeMalformedError({ detail: "Invalid protected header" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload;
|
const payload = JSON.parse(decoder.decode(rawPayload)) as TCreateAcmeAccountPayload;
|
||||||
// TODO: also consume the nonce here
|
// TODO: also consume the nonce here
|
||||||
return { payload, protectedHeader };
|
return { payload, protectedHeader, jwk: protectedHeader.jwk as JsonWebKey };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
|
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
|
||||||
@@ -96,18 +104,47 @@ export const pkiAcmeServiceFactory = ({ certificateProfileDAL }: TPkiAcmeService
|
|||||||
|
|
||||||
const createAcmeAccount = async (
|
const createAcmeAccount = async (
|
||||||
profileId: string,
|
profileId: string,
|
||||||
|
jwk: JWK,
|
||||||
payload: TCreateAcmeAccountPayload
|
payload: TCreateAcmeAccountPayload
|
||||||
): Promise<TCreateAcmeAccountResponse> => {
|
): Promise<TAcmeResponse<TCreateAcmeAccountResponse>> => {
|
||||||
const profile = await validateAcmeProfile(profileId);
|
const profile = await validateAcmeProfile(profileId);
|
||||||
// FIXME: Implement ACME new account registration
|
// TODO: the jwk as json obj may not be the best idea for indexing.
|
||||||
// Use EAB authentication to find corresponding Infisical machine identity
|
// Maybe we should find a way to serialize the jwk deterministically.
|
||||||
// Check permissions and return account information
|
let account = await pkiAcmeAccountDAL.findByPublicKey(jwk);
|
||||||
const accountId = "FIXME-account-id";
|
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 {
|
return {
|
||||||
status: "valid",
|
status: 201,
|
||||||
accountUrl: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}`),
|
payload: {
|
||||||
contact: [],
|
status: "valid",
|
||||||
orders: buildUrl(`/api/v1/pki/acme/profiles/${profileId}/accounts/${accountId}/orders`)
|
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
|
payload: TCreateAcmeOrderPayload
|
||||||
): Promise<TCreateAcmeOrderResponse> => {
|
): Promise<TCreateAcmeOrderResponse> => {
|
||||||
const profile = await validateAcmeProfile(profileId);
|
const profile = await validateAcmeProfile(profileId);
|
||||||
|
|
||||||
// FIXME: Implement ACME new order creation
|
// FIXME: Implement ACME new order creation
|
||||||
const orderId = "FIXME-order-id";
|
const orderId = "FIXME-order-id";
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { JWK } from "jose";
|
||||||
import {
|
import {
|
||||||
CreateAcmeAccountBodySchema,
|
CreateAcmeAccountBodySchema,
|
||||||
CreateAcmeAccountResponseSchema,
|
CreateAcmeAccountResponseSchema,
|
||||||
@@ -39,11 +40,24 @@ export type TCreateAcmeOrderPayload = z.infer<typeof CreateAcmeOrderBodySchema>;
|
|||||||
export type TDeactivateAcmeAccountPayload = z.infer<typeof DeactivateAcmeAccountBodySchema>;
|
export type TDeactivateAcmeAccountPayload = z.infer<typeof DeactivateAcmeAccountBodySchema>;
|
||||||
export type TFinalizeAcmeOrderPayload = z.infer<typeof FinalizeAcmeOrderBodySchema>;
|
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 = {
|
export type TPkiAcmeServiceFactory = {
|
||||||
validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise<TJwsPayload>;
|
validateCreateAcmeAccountJwsPayload(body: TRawJwsPayload): Promise<TJwsPayloadWithJwk>;
|
||||||
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
|
getAcmeDirectory: (profileId: string) => Promise<TGetAcmeDirectoryResponse>;
|
||||||
getAcmeNewNonce: (profileId: string) => Promise<string>;
|
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>;
|
createAcmeOrder: (profileId: string, body: TCreateAcmeOrderPayload) => Promise<TCreateAcmeOrderResponse>;
|
||||||
deactivateAcmeAccount: (
|
deactivateAcmeAccount: (
|
||||||
profileId: string,
|
profileId: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user