mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 07:28:09 -05:00
Merge branch 'main' of https://github.com/Infisical/infisical into feat/machine-identity-groups
This commit is contained in:
@@ -192,3 +192,28 @@ Feature: Challenge
|
||||
And the value response with jq ".status" should be equal to 400
|
||||
And the value response with jq ".type" should be equal to "urn:ietf:params:acme:error:badCSR"
|
||||
And the value response with jq ".detail" should be equal to "Invalid CSR: Common name + SANs mismatch with order identifiers"
|
||||
|
||||
Scenario: Get certificate without passing challenge when skip DNS ownership verification is enabled
|
||||
Given I create an ACME profile with config as "acme_profile"
|
||||
"""
|
||||
{
|
||||
"skipDnsOwnershipVerification": true
|
||||
}
|
||||
"""
|
||||
When I have an ACME client connecting to "{BASE_URL}/api/v1/cert-manager/acme/profiles/{acme_profile.id}/directory"
|
||||
Then I register a new ACME account with email fangpen@infisical.com and EAB key id "{acme_profile.eab_kid}" with secret "{acme_profile.eab_secret}" as acme_account
|
||||
When I create certificate signing request as csr
|
||||
Then I add names to certificate signing request csr
|
||||
"""
|
||||
{
|
||||
"COMMON_NAME": "localhost"
|
||||
}
|
||||
"""
|
||||
And I create a RSA private key pair as cert_key
|
||||
And I sign the certificate signing request csr with private key cert_key and output it as csr_pem in PEM format
|
||||
And I submit the certificate signing request PEM csr_pem certificate order to the ACME server as order
|
||||
And the value order.body with jq ".status" should be equal to "ready"
|
||||
And I poll and finalize the ACME order order as finalized_order
|
||||
And the value finalized_order.body with jq ".status" should be equal to "valid"
|
||||
And I parse the full-chain certificate from order finalized_order as cert
|
||||
And the value cert with jq ".subject.common_name" should be equal to "localhost"
|
||||
|
||||
@@ -266,6 +266,46 @@ def step_impl(context: Context, ca_id: str, template_id: str, profile_var: str):
|
||||
)
|
||||
|
||||
|
||||
@given(
|
||||
'I create an ACME profile with config as "{profile_var}"'
|
||||
)
|
||||
def step_impl(context: Context, profile_var: str):
|
||||
profile_slug = faker.slug()
|
||||
jwt_token = context.vars["AUTH_TOKEN"]
|
||||
acme_config = replace_vars(json.loads(context.text), context.vars)
|
||||
response = context.http_client.post(
|
||||
"/api/v1/cert-manager/certificate-profiles",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
json={
|
||||
"projectId": context.vars["PROJECT_ID"],
|
||||
"slug": profile_slug,
|
||||
"description": "ACME Profile created by BDD test",
|
||||
"enrollmentType": "acme",
|
||||
"caId": context.vars["CERT_CA_ID"],
|
||||
"certificateTemplateId": context.vars["CERT_TEMPLATE_ID"],
|
||||
"acmeConfig": acme_config,
|
||||
},
|
||||
)
|
||||
response.raise_for_status()
|
||||
resp_json = response.json()
|
||||
profile_id = resp_json["certificateProfile"]["id"]
|
||||
kid = profile_id
|
||||
|
||||
response = context.http_client.get(
|
||||
f"/api/v1/cert-manager/certificate-profiles/{profile_id}/acme/eab-secret/reveal",
|
||||
headers=dict(authorization="Bearer {}".format(jwt_token)),
|
||||
)
|
||||
response.raise_for_status()
|
||||
resp_json = response.json()
|
||||
secret = resp_json["eabSecret"]
|
||||
|
||||
context.vars[profile_var] = AcmeProfile(
|
||||
profile_id,
|
||||
eab_kid=kid,
|
||||
eab_secret=secret,
|
||||
)
|
||||
|
||||
|
||||
@given('I have an ACME cert profile with external ACME CA as "{profile_var}"')
|
||||
def step_impl(context: Context, profile_var: str):
|
||||
profile_id = context.vars.get("PROFILE_ID")
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
import { dropConstraintIfExists } from "./utils/dropConstraintIfExists";
|
||||
|
||||
const FOREIGN_KEY_CONSTRAINT_NAME = "certificate_requests_acme_order_id_fkey";
|
||||
const INDEX_NAME = "certificate_requests_acme_order_id_idx";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.CertificateRequests)) {
|
||||
const hasAcmeOrderId = await knex.schema.hasColumn(TableName.CertificateRequests, "acmeOrderId");
|
||||
|
||||
if (!hasAcmeOrderId) {
|
||||
await knex.schema.alterTable(TableName.CertificateRequests, (t) => {
|
||||
t.uuid("acmeOrderId").nullable();
|
||||
t.foreign("acmeOrderId", FOREIGN_KEY_CONSTRAINT_NAME)
|
||||
.references("id")
|
||||
.inTable(TableName.PkiAcmeOrder)
|
||||
.onDelete("SET NULL");
|
||||
t.index("acmeOrderId", INDEX_NAME);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.CertificateRequests)) {
|
||||
const hasAcmeOrderId = await knex.schema.hasColumn(TableName.CertificateRequests, "acmeOrderId");
|
||||
|
||||
if (hasAcmeOrderId) {
|
||||
await dropConstraintIfExists(TableName.CertificateRequests, FOREIGN_KEY_CONSTRAINT_NAME, knex);
|
||||
await knex.schema.alterTable(TableName.CertificateRequests, (t) => {
|
||||
t.dropIndex("acmeOrderId", INDEX_NAME);
|
||||
t.dropColumn("acmeOrderId");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.PkiAcmeEnrollmentConfig)) {
|
||||
if (!(await knex.schema.hasColumn(TableName.PkiAcmeEnrollmentConfig, "skipDnsOwnershipVerification"))) {
|
||||
await knex.schema.alterTable(TableName.PkiAcmeEnrollmentConfig, (t) => {
|
||||
t.boolean("skipDnsOwnershipVerification").defaultTo(false).notNullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasTable(TableName.PkiAcmeEnrollmentConfig)) {
|
||||
if (await knex.schema.hasColumn(TableName.PkiAcmeEnrollmentConfig, "skipDnsOwnershipVerification")) {
|
||||
await knex.schema.alterTable(TableName.PkiAcmeEnrollmentConfig, (t) => {
|
||||
t.dropColumn("skipDnsOwnershipVerification");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,8 @@ export const CertificateRequestsSchema = z.object({
|
||||
keyAlgorithm: z.string().nullable().optional(),
|
||||
signatureAlgorithm: z.string().nullable().optional(),
|
||||
errorMessage: z.string().nullable().optional(),
|
||||
metadata: z.string().nullable().optional()
|
||||
metadata: z.string().nullable().optional(),
|
||||
acmeOrderId: z.string().uuid().nullable().optional()
|
||||
});
|
||||
|
||||
export type TCertificateRequests = z.infer<typeof CertificateRequestsSchema>;
|
||||
|
||||
@@ -13,7 +13,8 @@ export const PkiAcmeEnrollmentConfigsSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedEabSecret: zodBuffer,
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
skipDnsOwnershipVerification: z.boolean().default(false)
|
||||
});
|
||||
|
||||
export type TPkiAcmeEnrollmentConfigs = z.infer<typeof PkiAcmeEnrollmentConfigsSchema>;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@app/ee/services/external-kms/providers/model";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { deterministicStringify } from "@app/lib/fn/object";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -88,9 +89,11 @@ export const registerExternalKmsEndpoints = <
|
||||
...rest
|
||||
} = externalKms;
|
||||
|
||||
const credentialsToHash = deterministicStringify(configuration.credential);
|
||||
|
||||
const credentialsHash = crypto.nativeCrypto
|
||||
.createHash("sha256")
|
||||
.update(externalKmsData.encryptedProviderInputs)
|
||||
.update(Buffer.from(credentialsToHash))
|
||||
.digest("hex");
|
||||
return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } };
|
||||
}
|
||||
@@ -153,9 +156,12 @@ export const registerExternalKmsEndpoints = <
|
||||
external: { providerInput: externalKmsConfiguration, ...externalKmsData },
|
||||
...rest
|
||||
} = externalKms;
|
||||
|
||||
const credentialsToHash = deterministicStringify(externalKmsConfiguration.credential);
|
||||
|
||||
const credentialsHash = crypto.nativeCrypto
|
||||
.createHash("sha256")
|
||||
.update(externalKmsData.encryptedProviderInputs)
|
||||
.update(Buffer.from(credentialsToHash))
|
||||
.digest("hex");
|
||||
return { ...rest, externalKms: { ...externalKmsData, configuration: externalKmsConfiguration, credentialsHash } };
|
||||
}
|
||||
@@ -222,9 +228,12 @@ export const registerExternalKmsEndpoints = <
|
||||
external: { providerInput: externalKmsConfiguration, ...externalKmsData },
|
||||
...rest
|
||||
} = externalKms;
|
||||
|
||||
const credentialsToHash = deterministicStringify(externalKmsConfiguration.credential);
|
||||
|
||||
const credentialsHash = crypto.nativeCrypto
|
||||
.createHash("sha256")
|
||||
.update(externalKmsData.encryptedProviderInputs)
|
||||
.update(Buffer.from(credentialsToHash))
|
||||
.digest("hex");
|
||||
return { ...rest, externalKms: { ...externalKmsData, configuration: externalKmsConfiguration, credentialsHash } };
|
||||
}
|
||||
@@ -277,9 +286,12 @@ export const registerExternalKmsEndpoints = <
|
||||
external: { providerInput: configuration, ...externalKmsData },
|
||||
...rest
|
||||
} = externalKms;
|
||||
|
||||
const credentialsToHash = deterministicStringify(configuration.credential);
|
||||
|
||||
const credentialsHash = crypto.nativeCrypto
|
||||
.createHash("sha256")
|
||||
.update(externalKmsData.encryptedProviderInputs)
|
||||
.update(Buffer.from(credentialsToHash))
|
||||
.digest("hex");
|
||||
|
||||
return { ...rest, externalKms: { ...externalKmsData, configuration, credentialsHash } };
|
||||
|
||||
@@ -142,6 +142,7 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
|
||||
data: {
|
||||
...req.body,
|
||||
...req.body.type,
|
||||
name: req.body.slug,
|
||||
permissions: req.body.permissions
|
||||
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore-error this is valid ts
|
||||
|
||||
@@ -49,6 +49,7 @@ import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
|
||||
import { WorkflowIntegration } from "@app/services/workflow-integration/workflow-integration-types";
|
||||
|
||||
import { KmipPermission } from "../kmip/kmip-enum";
|
||||
import { AcmeChallengeType, AcmeIdentifierType } from "../pki-acme/pki-acme-schemas";
|
||||
import { ApprovalStatus } from "../secret-approval-request/secret-approval-request-types";
|
||||
|
||||
export type TListProjectAuditLogDTO = {
|
||||
@@ -78,7 +79,9 @@ export type TCreateAuditLogDTO = {
|
||||
| ScimClientActor
|
||||
| PlatformActor
|
||||
| UnknownUserActor
|
||||
| KmipClientActor;
|
||||
| KmipClientActor
|
||||
| AcmeProfileActor
|
||||
| AcmeAccountActor;
|
||||
orgId?: string;
|
||||
projectId?: string;
|
||||
} & BaseAuthData;
|
||||
@@ -574,7 +577,18 @@ export enum EventType {
|
||||
APPROVAL_REQUEST_CANCEL = "approval-request-cancel",
|
||||
APPROVAL_REQUEST_GRANT_LIST = "approval-request-grant-list",
|
||||
APPROVAL_REQUEST_GRANT_GET = "approval-request-grant-get",
|
||||
APPROVAL_REQUEST_GRANT_REVOKE = "approval-request-grant-revoke"
|
||||
APPROVAL_REQUEST_GRANT_REVOKE = "approval-request-grant-revoke",
|
||||
|
||||
// PKI ACME
|
||||
CREATE_ACME_ACCOUNT = "create-acme-account",
|
||||
RETRIEVE_ACME_ACCOUNT = "retrieve-acme-account",
|
||||
CREATE_ACME_ORDER = "create-acme-order",
|
||||
FINALIZE_ACME_ORDER = "finalize-acme-order",
|
||||
DOWNLOAD_ACME_CERTIFICATE = "download-acme-certificate",
|
||||
RESPOND_TO_ACME_CHALLENGE = "respond-to-acme-challenge",
|
||||
PASS_ACME_CHALLENGE = "pass-acme-challenge",
|
||||
ATTEMPT_ACME_CHALLENGE = "attempt-acme-challenge",
|
||||
FAIL_ACME_CHALLENGE = "fail-acme-challenge"
|
||||
}
|
||||
|
||||
export const filterableSecretEvents: EventType[] = [
|
||||
@@ -615,6 +629,15 @@ interface KmipClientActorMetadata {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AcmeProfileActorMetadata {
|
||||
profileId: string;
|
||||
}
|
||||
|
||||
interface AcmeAccountActorMetadata {
|
||||
profileId: string;
|
||||
accountId: string;
|
||||
}
|
||||
|
||||
interface UnknownUserActorMetadata {}
|
||||
|
||||
export interface UserActor {
|
||||
@@ -652,7 +675,25 @@ export interface ScimClientActor {
|
||||
metadata: ScimClientActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor = UserActor | ServiceActor | IdentityActor | ScimClientActor | PlatformActor | KmipClientActor;
|
||||
export interface AcmeProfileActor {
|
||||
type: ActorType.ACME_PROFILE;
|
||||
metadata: AcmeProfileActorMetadata;
|
||||
}
|
||||
|
||||
export interface AcmeAccountActor {
|
||||
type: ActorType.ACME_ACCOUNT;
|
||||
metadata: AcmeAccountActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor =
|
||||
| UserActor
|
||||
| ServiceActor
|
||||
| IdentityActor
|
||||
| ScimClientActor
|
||||
| PlatformActor
|
||||
| KmipClientActor
|
||||
| AcmeProfileActor
|
||||
| AcmeAccountActor;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
@@ -4368,6 +4409,84 @@ interface ApprovalRequestGrantRevokeEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateAcmeAccountEvent {
|
||||
type: EventType.CREATE_ACME_ACCOUNT;
|
||||
metadata: {
|
||||
accountId: string;
|
||||
publicKeyThumbprint: string;
|
||||
emails?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface RetrieveAcmeAccountEvent {
|
||||
type: EventType.RETRIEVE_ACME_ACCOUNT;
|
||||
metadata: {
|
||||
accountId: string;
|
||||
publicKeyThumbprint: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateAcmeOrderEvent {
|
||||
type: EventType.CREATE_ACME_ORDER;
|
||||
metadata: {
|
||||
orderId: string;
|
||||
identifiers: Array<{
|
||||
type: AcmeIdentifierType;
|
||||
value: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface FinalizeAcmeOrderEvent {
|
||||
type: EventType.FINALIZE_ACME_ORDER;
|
||||
metadata: {
|
||||
orderId: string;
|
||||
csr: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DownloadAcmeCertificateEvent {
|
||||
type: EventType.DOWNLOAD_ACME_CERTIFICATE;
|
||||
metadata: {
|
||||
orderId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RespondToAcmeChallengeEvent {
|
||||
type: EventType.RESPOND_TO_ACME_CHALLENGE;
|
||||
metadata: {
|
||||
challengeId: string;
|
||||
type: AcmeChallengeType;
|
||||
};
|
||||
}
|
||||
interface PassedAcmeChallengeEvent {
|
||||
type: EventType.PASS_ACME_CHALLENGE;
|
||||
metadata: {
|
||||
challengeId: string;
|
||||
type: AcmeChallengeType;
|
||||
};
|
||||
}
|
||||
|
||||
interface AttemptAcmeChallengeEvent {
|
||||
type: EventType.ATTEMPT_ACME_CHALLENGE;
|
||||
metadata: {
|
||||
challengeId: string;
|
||||
type: AcmeChallengeType;
|
||||
retryCount: number;
|
||||
errorMessage: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface FailAcmeChallengeEvent {
|
||||
type: EventType.FAIL_ACME_CHALLENGE;
|
||||
metadata: {
|
||||
challengeId: string;
|
||||
type: AcmeChallengeType;
|
||||
retryCount: number;
|
||||
errorMessage: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| CreateSubOrganizationEvent
|
||||
| UpdateSubOrganizationEvent
|
||||
@@ -4768,4 +4887,13 @@ export type Event =
|
||||
| ApprovalRequestCancelEvent
|
||||
| ApprovalRequestGrantListEvent
|
||||
| ApprovalRequestGrantGetEvent
|
||||
| ApprovalRequestGrantRevokeEvent;
|
||||
| ApprovalRequestGrantRevokeEvent
|
||||
| CreateAcmeAccountEvent
|
||||
| RetrieveAcmeAccountEvent
|
||||
| CreateAcmeOrderEvent
|
||||
| FinalizeAcmeOrderEvent
|
||||
| DownloadAcmeCertificateEvent
|
||||
| RespondToAcmeChallengeEvent
|
||||
| PassedAcmeChallengeEvent
|
||||
| AttemptAcmeChallengeEvent
|
||||
| FailAcmeChallengeEvent;
|
||||
|
||||
@@ -380,6 +380,7 @@ export const externalKmsServiceFactory = ({
|
||||
|
||||
const findById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id: kmsId }: TGetExternalKmsByIdDTO) => {
|
||||
const kmsDoc = await kmsDAL.findById(kmsId);
|
||||
if (!kmsDoc) throw new NotFoundError({ message: `Could not find KMS with ID '${kmsId}'` });
|
||||
const { permission } = await permissionService.getOrgPermission({
|
||||
scope: OrganizationActionScope.Any,
|
||||
actor,
|
||||
|
||||
@@ -71,23 +71,29 @@ export const pamFolderDALFactory = (db: TDbClient) => {
|
||||
const findByPath = async (projectId: string, path: string, tx?: Knex) => {
|
||||
try {
|
||||
const dbInstance = tx || db.replicaNode();
|
||||
|
||||
const folders = await dbInstance(TableName.PamFolder)
|
||||
.where(`${TableName.PamFolder}.projectId`, projectId)
|
||||
.select(selectAllTableCols(TableName.PamFolder));
|
||||
|
||||
const pathSegments = path.split("/").filter(Boolean);
|
||||
if (pathSegments.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const foldersByParentId = new Map<string | null, typeof folders>();
|
||||
for (const folder of folders) {
|
||||
const children = foldersByParentId.get(folder.parentId ?? null) ?? [];
|
||||
children.push(folder);
|
||||
foldersByParentId.set(folder.parentId ?? null, children);
|
||||
}
|
||||
|
||||
let parentId: string | null = null;
|
||||
let currentFolder: Awaited<ReturnType<typeof orm.findOne>> | undefined;
|
||||
let currentFolder: (typeof folders)[0] | undefined;
|
||||
|
||||
for await (const segment of pathSegments) {
|
||||
const query = dbInstance(TableName.PamFolder)
|
||||
.where(`${TableName.PamFolder}.projectId`, projectId)
|
||||
.where(`${TableName.PamFolder}.name`, segment);
|
||||
|
||||
if (parentId) {
|
||||
void query.where(`${TableName.PamFolder}.parentId`, parentId);
|
||||
} else {
|
||||
void query.whereNull(`${TableName.PamFolder}.parentId`);
|
||||
}
|
||||
|
||||
currentFolder = await query.first();
|
||||
for (const segment of pathSegments) {
|
||||
const childFolders: typeof folders = foldersByParentId.get(parentId) || [];
|
||||
currentFolder = childFolders.find((folder) => folder.name === segment);
|
||||
|
||||
if (!currentFolder) {
|
||||
return undefined;
|
||||
|
||||
@@ -122,6 +122,11 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
|
||||
const result = await (tx || db)(TableName.PkiAcmeChallenge)
|
||||
.join(TableName.PkiAcmeAuth, `${TableName.PkiAcmeChallenge}.authId`, `${TableName.PkiAcmeAuth}.id`)
|
||||
.join(TableName.PkiAcmeAccount, `${TableName.PkiAcmeAuth}.accountId`, `${TableName.PkiAcmeAccount}.id`)
|
||||
.join(
|
||||
TableName.PkiCertificateProfile,
|
||||
`${TableName.PkiAcmeAccount}.profileId`,
|
||||
`${TableName.PkiCertificateProfile}.id`
|
||||
)
|
||||
.select(
|
||||
selectAllTableCols(TableName.PkiAcmeChallenge),
|
||||
db.ref("id").withSchema(TableName.PkiAcmeAuth).as("authId"),
|
||||
@@ -131,7 +136,9 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
|
||||
db.ref("identifierValue").withSchema(TableName.PkiAcmeAuth).as("authIdentifierValue"),
|
||||
db.ref("expiresAt").withSchema(TableName.PkiAcmeAuth).as("authExpiresAt"),
|
||||
db.ref("id").withSchema(TableName.PkiAcmeAccount).as("accountId"),
|
||||
db.ref("publicKeyThumbprint").withSchema(TableName.PkiAcmeAccount).as("accountPublicKeyThumbprint")
|
||||
db.ref("publicKeyThumbprint").withSchema(TableName.PkiAcmeAccount).as("accountPublicKeyThumbprint"),
|
||||
db.ref("profileId").withSchema(TableName.PkiAcmeAccount).as("profileId"),
|
||||
db.ref("projectId").withSchema(TableName.PkiCertificateProfile).as("projectId")
|
||||
)
|
||||
// For all challenges, acquire update lock on the auth to avoid race conditions
|
||||
.forUpdate(TableName.PkiAcmeAuth)
|
||||
@@ -149,6 +156,8 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
|
||||
authExpiresAt,
|
||||
accountId,
|
||||
accountPublicKeyThumbprint,
|
||||
profileId,
|
||||
projectId,
|
||||
...challenge
|
||||
} = result;
|
||||
return {
|
||||
@@ -161,7 +170,11 @@ export const pkiAcmeChallengeDALFactory = (db: TDbClient) => {
|
||||
expiresAt: authExpiresAt,
|
||||
account: {
|
||||
id: accountId,
|
||||
publicKeyThumbprint: accountPublicKeyThumbprint
|
||||
publicKeyThumbprint: accountPublicKeyThumbprint,
|
||||
project: {
|
||||
id: projectId
|
||||
},
|
||||
profileId
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,9 @@ import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { isPrivateIp } from "@app/lib/ip/ipRange";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
|
||||
import { EventType, TAuditLogServiceFactory } from "../audit-log/audit-log-types";
|
||||
import { TPkiAcmeChallengeDALFactory } from "./pki-acme-challenge-dal";
|
||||
import {
|
||||
AcmeConnectionError,
|
||||
@@ -25,10 +27,12 @@ type TPkiAcmeChallengeServiceFactoryDep = {
|
||||
| "markAsInvalidCascadeById"
|
||||
| "updateById"
|
||||
>;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export const pkiAcmeChallengeServiceFactory = ({
|
||||
acmeChallengeDAL
|
||||
acmeChallengeDAL,
|
||||
auditLogService
|
||||
}: TPkiAcmeChallengeServiceFactoryDep): TPkiAcmeChallengeServiceFactory => {
|
||||
const appCfg = getConfig();
|
||||
const markChallengeAsReady = async (challengeId: string): Promise<TPkiAcmeChallenges> => {
|
||||
@@ -113,7 +117,25 @@ export const pkiAcmeChallengeServiceFactory = ({
|
||||
}
|
||||
logger.info({ challengeId }, "ACME challenge response is correct, marking challenge as valid");
|
||||
await acmeChallengeDAL.markAsValidCascadeById(challengeId);
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: challenge.auth.account.project.id,
|
||||
actor: {
|
||||
type: ActorType.ACME_ACCOUNT,
|
||||
metadata: {
|
||||
profileId: challenge.auth.account.profileId,
|
||||
accountId: challenge.auth.account.id
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.PASS_ACME_CHALLENGE,
|
||||
metadata: {
|
||||
challengeId,
|
||||
type: challenge.type as AcmeChallengeType
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (exp) {
|
||||
let finalAttempt = false;
|
||||
if (retryCount >= 2) {
|
||||
logger.error(
|
||||
exp,
|
||||
@@ -121,35 +143,59 @@ export const pkiAcmeChallengeServiceFactory = ({
|
||||
);
|
||||
// This is the last attempt to validate the challenge response, if it fails, we mark the challenge as invalid
|
||||
await acmeChallengeDAL.markAsInvalidCascadeById(challengeId);
|
||||
finalAttempt = true;
|
||||
}
|
||||
// Properly type and inspect the error
|
||||
if (axios.isAxiosError(exp)) {
|
||||
const axiosError = exp as AxiosError;
|
||||
const errorCode = axiosError.code;
|
||||
const errorMessage = axiosError.message;
|
||||
try {
|
||||
// Properly type and inspect the error
|
||||
if (axios.isAxiosError(exp)) {
|
||||
const axiosError = exp as AxiosError;
|
||||
const errorCode = axiosError.code;
|
||||
const errorMessage = axiosError.message;
|
||||
|
||||
if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
|
||||
throw new AcmeConnectionError({ message: "Connection refused" });
|
||||
if (errorCode === "ECONNREFUSED" || errorMessage.includes("ECONNREFUSED")) {
|
||||
throw new AcmeConnectionError({ message: "Connection refused" });
|
||||
}
|
||||
if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) {
|
||||
throw new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
|
||||
}
|
||||
if (errorCode === "ECONNRESET" || errorMessage.includes("ECONNRESET")) {
|
||||
throw new AcmeConnectionError({ message: "Connection reset by peer" });
|
||||
}
|
||||
if (errorCode === "ECONNABORTED" || errorMessage.includes("timeout")) {
|
||||
logger.error(exp, "Connection timed out while validating ACME challenge response");
|
||||
throw new AcmeConnectionError({ message: "Connection timed out" });
|
||||
}
|
||||
logger.error(exp, "Unknown error validating ACME challenge response");
|
||||
throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
|
||||
}
|
||||
if (errorCode === "ENOTFOUND" || errorMessage.includes("ENOTFOUND")) {
|
||||
throw new AcmeDnsFailureError({ message: "Hostname could not be resolved (DNS failure)" });
|
||||
}
|
||||
if (errorCode === "ECONNRESET" || errorMessage.includes("ECONNRESET")) {
|
||||
throw new AcmeConnectionError({ message: "Connection reset by peer" });
|
||||
}
|
||||
if (errorCode === "ECONNABORTED" || errorMessage.includes("timeout")) {
|
||||
logger.error(exp, "Connection timed out while validating ACME challenge response");
|
||||
throw new AcmeConnectionError({ message: "Connection timed out" });
|
||||
if (exp instanceof Error) {
|
||||
logger.error(exp, "Error validating ACME challenge response");
|
||||
throw exp;
|
||||
}
|
||||
logger.error(exp, "Unknown error validating ACME challenge response");
|
||||
throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
|
||||
} catch (outterExp) {
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: challenge.auth.account.project.id,
|
||||
actor: {
|
||||
type: ActorType.ACME_ACCOUNT,
|
||||
metadata: {
|
||||
profileId: challenge.auth.account.profileId,
|
||||
accountId: challenge.auth.account.id
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: finalAttempt ? EventType.FAIL_ACME_CHALLENGE : EventType.ATTEMPT_ACME_CHALLENGE,
|
||||
metadata: {
|
||||
challengeId,
|
||||
type: challenge.type as AcmeChallengeType,
|
||||
retryCount,
|
||||
errorMessage: exp instanceof Error ? exp.message : "Unknown error"
|
||||
}
|
||||
}
|
||||
});
|
||||
throw outterExp;
|
||||
}
|
||||
if (exp instanceof Error) {
|
||||
logger.error(exp, "Error validating ACME challenge response");
|
||||
throw exp;
|
||||
}
|
||||
logger.error(exp, "Unknown error validating ACME challenge response");
|
||||
throw new AcmeServerInternalError({ message: "Unknown error validating ACME challenge response" });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify, selectAllTableCols, sqlNestRelationships } from "@app/lib/knex";
|
||||
import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types";
|
||||
|
||||
export type TPkiAcmeOrderDALFactory = ReturnType<typeof pkiAcmeOrderDALFactory>;
|
||||
|
||||
@@ -19,6 +20,43 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
const findWithCertificateRequestForSync = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const order = await (tx || db)(TableName.PkiAcmeOrder)
|
||||
.leftJoin(
|
||||
TableName.CertificateRequests,
|
||||
`${TableName.PkiAcmeOrder}.id`,
|
||||
`${TableName.CertificateRequests}.acmeOrderId`
|
||||
)
|
||||
.select(
|
||||
selectAllTableCols(TableName.PkiAcmeOrder),
|
||||
db.ref("id").withSchema(TableName.CertificateRequests).as("certificateRequestId"),
|
||||
db.ref("status").withSchema(TableName.CertificateRequests).as("certificateRequestStatus"),
|
||||
db.ref("certificateId").withSchema(TableName.CertificateRequests).as("certificateId")
|
||||
)
|
||||
.forUpdate(TableName.PkiAcmeOrder)
|
||||
.where(`${TableName.PkiAcmeOrder}.id`, id)
|
||||
.first();
|
||||
if (!order) {
|
||||
return null;
|
||||
}
|
||||
const { certificateRequestId, certificateRequestStatus, certificateId, ...details } = order;
|
||||
return {
|
||||
...details,
|
||||
certificateRequest:
|
||||
certificateRequestId && certificateRequestStatus
|
||||
? {
|
||||
id: certificateRequestId,
|
||||
status: certificateRequestStatus as CertificateRequestStatus,
|
||||
certificateId
|
||||
}
|
||||
: undefined
|
||||
};
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find PKI ACME order by id with certificate request" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByAccountAndOrderIdWithAuthorizations = async (accountId: string, orderId: string, tx?: Knex) => {
|
||||
try {
|
||||
const rows = await (tx || db)(TableName.PkiAcmeOrder)
|
||||
@@ -72,6 +110,7 @@ export const pkiAcmeOrderDALFactory = (db: TDbClient) => {
|
||||
return {
|
||||
...pkiAcmeOrderOrm,
|
||||
findByIdForFinalization,
|
||||
findWithCertificateRequestForSync,
|
||||
findByAccountAndOrderIdWithAuthorizations,
|
||||
listByAccountId
|
||||
};
|
||||
|
||||
@@ -6,8 +6,8 @@ export enum AcmeIdentifierType {
|
||||
|
||||
export enum AcmeOrderStatus {
|
||||
Pending = "pending",
|
||||
Processing = "processing",
|
||||
Ready = "ready",
|
||||
Processing = "processing",
|
||||
Valid = "valid",
|
||||
Invalid = "invalid"
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
importJWK,
|
||||
JWSHeaderParameters
|
||||
} from "jose";
|
||||
import { Knex } from "knex";
|
||||
import { z, ZodError } from "zod";
|
||||
|
||||
import { TPkiAcmeOrders } from "@app/db/schemas";
|
||||
import { TPkiAcmeAccounts } from "@app/db/schemas/pki-acme-accounts";
|
||||
import { TPkiAcmeAuths } from "@app/db/schemas/pki-acme-auths";
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
@@ -17,20 +19,15 @@ import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { isPrivateIp } from "@app/lib/ip/ipRange";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TAppConnectionDALFactory } from "@app/services/app-connection/app-connection-dal";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
|
||||
import {
|
||||
CertExtendedKeyUsage,
|
||||
CertKeyUsage,
|
||||
CertSubjectAlternativeNameType
|
||||
} from "@app/services/certificate/certificate-types";
|
||||
import { orderCertificate } from "@app/services/certificate-authority/acme/acme-certificate-authority-fns";
|
||||
import { CertSubjectAlternativeNameType } from "@app/services/certificate/certificate-types";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { TExternalCertificateAuthorityDALFactory } from "@app/services/certificate-authority/external-certificate-authority-dal";
|
||||
import {
|
||||
TCertificateIssuanceQueueFactory,
|
||||
TIssueCertificateFromProfileJobData
|
||||
} from "@app/services/certificate-authority/certificate-issuance-queue";
|
||||
import {
|
||||
extractAlgorithmsFromCSR,
|
||||
extractCertificateRequestFromCSR
|
||||
@@ -40,6 +37,8 @@ import {
|
||||
EnrollmentType,
|
||||
TCertificateProfileWithConfigs
|
||||
} from "@app/services/certificate-profile/certificate-profile-types";
|
||||
import { TCertificateRequestServiceFactory } from "@app/services/certificate-request/certificate-request-service";
|
||||
import { CertificateRequestStatus } from "@app/services/certificate-request/certificate-request-types";
|
||||
import { TCertificateTemplateV2DALFactory } from "@app/services/certificate-template-v2/certificate-template-v2-dal";
|
||||
import { TCertificateTemplateV2ServiceFactory } from "@app/services/certificate-template-v2/certificate-template-v2-service";
|
||||
import { TCertificateV3ServiceFactory } from "@app/services/certificate-v3/certificate-v3-service";
|
||||
@@ -47,6 +46,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
|
||||
|
||||
import { EventType, TAuditLogServiceFactory } from "../audit-log/audit-log-types";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPkiAcmeAccountDALFactory } from "./pki-acme-account-dal";
|
||||
import { TPkiAcmeAuthDALFactory } from "./pki-acme-auth-dal";
|
||||
@@ -99,13 +99,9 @@ import {
|
||||
|
||||
type TPkiAcmeServiceFactoryDep = {
|
||||
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction" | "findById">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction" | "updateById">;
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa">;
|
||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "update">;
|
||||
certificateProfileDAL: Pick<TCertificateProfileDALFactory, "findByIdWithOwnerOrgId" | "findByIdWithConfigs">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "findOne" | "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "findOne" | "create">;
|
||||
certificateTemplateV2DAL: Pick<TCertificateTemplateV2DALFactory, "findById">;
|
||||
acmeAccountDAL: Pick<
|
||||
TPkiAcmeAccountDALFactory,
|
||||
@@ -113,11 +109,13 @@ type TPkiAcmeServiceFactoryDep = {
|
||||
>;
|
||||
acmeOrderDAL: Pick<
|
||||
TPkiAcmeOrderDALFactory,
|
||||
| "findById"
|
||||
| "create"
|
||||
| "transaction"
|
||||
| "updateById"
|
||||
| "findByAccountAndOrderIdWithAuthorizations"
|
||||
| "findByIdForFinalization"
|
||||
| "findWithCertificateRequestForSync"
|
||||
| "listByAccountId"
|
||||
>;
|
||||
acmeAuthDAL: Pick<TPkiAcmeAuthDALFactory, "create" | "findByAccountIdAndAuthIdWithChallenges">;
|
||||
@@ -134,19 +132,18 @@ type TPkiAcmeServiceFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
certificateV3Service: Pick<TCertificateV3ServiceFactory, "signCertificateFromProfile">;
|
||||
certificateTemplateV2Service: Pick<TCertificateTemplateV2ServiceFactory, "validateCertificateRequest">;
|
||||
certificateRequestService: Pick<TCertificateRequestServiceFactory, "createCertificateRequest">;
|
||||
certificateIssuanceQueue: Pick<TCertificateIssuanceQueueFactory, "queueCertificateIssuance">;
|
||||
acmeChallengeService: Pick<TPkiAcmeChallengeServiceFactory, "markChallengeAsReady">;
|
||||
pkiAcmeQueueService: Pick<TPkiAcmeQueueServiceFactory, "queueChallengeValidation">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
};
|
||||
|
||||
export const pkiAcmeServiceFactory = ({
|
||||
projectDAL,
|
||||
appConnectionDAL,
|
||||
certificateDAL,
|
||||
certificateAuthorityDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateProfileDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateTemplateV2DAL,
|
||||
acmeAccountDAL,
|
||||
acmeOrderDAL,
|
||||
@@ -158,8 +155,11 @@ export const pkiAcmeServiceFactory = ({
|
||||
licenseService,
|
||||
certificateV3Service,
|
||||
certificateTemplateV2Service,
|
||||
certificateRequestService,
|
||||
certificateIssuanceQueue,
|
||||
acmeChallengeService,
|
||||
pkiAcmeQueueService
|
||||
pkiAcmeQueueService,
|
||||
auditLogService
|
||||
}: TPkiAcmeServiceFactoryDep): TPkiAcmeServiceFactory => {
|
||||
const validateAcmeProfile = async (profileId: string): Promise<TCertificateProfileWithConfigs> => {
|
||||
const profile = await certificateProfileDAL.findByIdWithConfigs(profileId);
|
||||
@@ -364,6 +364,52 @@ export const pkiAcmeServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const checkAndSyncAcmeOrderStatus = async ({ orderId }: { orderId: string }): Promise<TPkiAcmeOrders> => {
|
||||
const order = await acmeOrderDAL.findById(orderId);
|
||||
if (!order) {
|
||||
throw new NotFoundError({ message: "ACME order not found" });
|
||||
}
|
||||
if (order.status !== AcmeOrderStatus.Processing) {
|
||||
// We only care about processing orders, as they are the ones that have async certificate requests
|
||||
return order;
|
||||
}
|
||||
return acmeOrderDAL.transaction(async (tx) => {
|
||||
// Lock the order for syncing with async cert request
|
||||
const orderWithCertificateRequest = await acmeOrderDAL.findWithCertificateRequestForSync(orderId, tx);
|
||||
if (!orderWithCertificateRequest) {
|
||||
throw new NotFoundError({ message: "ACME order not found" });
|
||||
}
|
||||
// Check the status again after we have acquired the lock, as things may have changed since we last checked
|
||||
if (
|
||||
orderWithCertificateRequest.status !== AcmeOrderStatus.Processing ||
|
||||
!orderWithCertificateRequest.certificateRequest
|
||||
) {
|
||||
return orderWithCertificateRequest;
|
||||
}
|
||||
let newStatus: AcmeOrderStatus | undefined;
|
||||
let newCertificateId: string | undefined;
|
||||
switch (orderWithCertificateRequest.certificateRequest.status) {
|
||||
case CertificateRequestStatus.PENDING:
|
||||
break;
|
||||
case CertificateRequestStatus.ISSUED:
|
||||
newStatus = AcmeOrderStatus.Valid;
|
||||
newCertificateId = orderWithCertificateRequest.certificateRequest.certificateId ?? undefined;
|
||||
break;
|
||||
case CertificateRequestStatus.FAILED:
|
||||
newStatus = AcmeOrderStatus.Invalid;
|
||||
break;
|
||||
default:
|
||||
throw new AcmeServerInternalError({
|
||||
message: `Invalid certificate request status: ${orderWithCertificateRequest.certificateRequest.status as string}`
|
||||
});
|
||||
}
|
||||
if (newStatus) {
|
||||
return acmeOrderDAL.updateById(orderId, { status: newStatus, certificateId: newCertificateId }, tx);
|
||||
}
|
||||
return orderWithCertificateRequest;
|
||||
});
|
||||
};
|
||||
|
||||
const getAcmeDirectory = async (profileId: string): Promise<TGetAcmeDirectoryResponse> => {
|
||||
const profile = await validateAcmeProfile(profileId);
|
||||
return {
|
||||
@@ -446,6 +492,23 @@ export const pkiAcmeServiceFactory = ({
|
||||
throw new AcmeExternalAccountRequiredError({ message: "External account binding is required" });
|
||||
}
|
||||
if (existingAccount) {
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: profile.projectId,
|
||||
actor: {
|
||||
type: ActorType.ACME_PROFILE,
|
||||
metadata: {
|
||||
profileId: profile.id
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.RETRIEVE_ACME_ACCOUNT,
|
||||
metadata: {
|
||||
accountId: existingAccount.id,
|
||||
publicKeyThumbprint
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
@@ -518,7 +581,25 @@ export const pkiAcmeServiceFactory = ({
|
||||
publicKeyThumbprint,
|
||||
emails: contact ?? []
|
||||
});
|
||||
// TODO: create audit log here
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: profile.projectId,
|
||||
actor: {
|
||||
type: ActorType.ACME_PROFILE,
|
||||
metadata: {
|
||||
profileId: profile.id
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.CREATE_ACME_ACCOUNT,
|
||||
metadata: {
|
||||
accountId: newAccount.id,
|
||||
publicKeyThumbprint: newAccount.publicKeyThumbprint,
|
||||
emails: newAccount.emails
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
status: 201,
|
||||
body: {
|
||||
@@ -567,6 +648,8 @@ export const pkiAcmeServiceFactory = ({
|
||||
accountId: string;
|
||||
payload: TCreateAcmeOrderPayload;
|
||||
}): Promise<TAcmeResponse<TAcmeOrderResource>> => {
|
||||
const profile = await validateAcmeProfile(profileId);
|
||||
const skipDnsOwnershipVerification = profile.acmeConfig?.skipDnsOwnershipVerification ?? false;
|
||||
// TODO: check and see if we have existing orders for this account that meet the criteria
|
||||
// if we do, return the existing order
|
||||
// TODO: check the identifiers and see if are they even allowed for this profile.
|
||||
@@ -592,7 +675,7 @@ export const pkiAcmeServiceFactory = ({
|
||||
const createdOrder = await acmeOrderDAL.create(
|
||||
{
|
||||
accountId: account.id,
|
||||
status: AcmeOrderStatus.Pending,
|
||||
status: skipDnsOwnershipVerification ? AcmeOrderStatus.Ready : AcmeOrderStatus.Pending,
|
||||
notBefore: payload.notBefore ? new Date(payload.notBefore) : undefined,
|
||||
notAfter: payload.notAfter ? new Date(payload.notAfter) : undefined,
|
||||
// TODO: read config from the profile to get the expiration time instead
|
||||
@@ -611,7 +694,7 @@ export const pkiAcmeServiceFactory = ({
|
||||
const auth = await acmeAuthDAL.create(
|
||||
{
|
||||
accountId: account.id,
|
||||
status: AcmeAuthStatus.Pending,
|
||||
status: skipDnsOwnershipVerification ? AcmeAuthStatus.Valid : AcmeAuthStatus.Pending,
|
||||
identifierType: identifier.type,
|
||||
identifierValue: identifier.value,
|
||||
// RFC 8555 suggests a token with at least 128 bits of entropy
|
||||
@@ -623,15 +706,17 @@ export const pkiAcmeServiceFactory = ({
|
||||
},
|
||||
tx
|
||||
);
|
||||
// TODO: support other challenge types here. Currently only HTTP-01 is supported.
|
||||
await acmeChallengeDAL.create(
|
||||
{
|
||||
authId: auth.id,
|
||||
status: AcmeChallengeStatus.Pending,
|
||||
type: AcmeChallengeType.HTTP_01
|
||||
},
|
||||
tx
|
||||
);
|
||||
if (!skipDnsOwnershipVerification) {
|
||||
// TODO: support other challenge types here. Currently only HTTP-01 is supported.
|
||||
await acmeChallengeDAL.create(
|
||||
{
|
||||
authId: auth.id,
|
||||
status: AcmeChallengeStatus.Pending,
|
||||
type: AcmeChallengeType.HTTP_01
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
return auth;
|
||||
})
|
||||
);
|
||||
@@ -643,7 +728,26 @@ export const pkiAcmeServiceFactory = ({
|
||||
})),
|
||||
tx
|
||||
);
|
||||
// TODO: create audit log here
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: profile.projectId,
|
||||
actor: {
|
||||
type: ActorType.ACME_ACCOUNT,
|
||||
metadata: {
|
||||
profileId: account.profileId,
|
||||
accountId: account.id
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.CREATE_ACME_ORDER,
|
||||
metadata: {
|
||||
orderId: createdOrder.id,
|
||||
identifiers: authorizations.map((auth) => ({
|
||||
type: auth.identifierType as AcmeIdentifierType,
|
||||
value: auth.identifierValue
|
||||
}))
|
||||
}
|
||||
}
|
||||
});
|
||||
return { ...createdOrder, authorizations, account };
|
||||
});
|
||||
|
||||
@@ -673,9 +777,12 @@ export const pkiAcmeServiceFactory = ({
|
||||
if (!order) {
|
||||
throw new NotFoundError({ message: "ACME order not found" });
|
||||
}
|
||||
// Sync order first in case if there is a certificate request that needs to be processed
|
||||
await checkAndSyncAcmeOrderStatus({ orderId });
|
||||
const updatedOrder = (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId))!;
|
||||
return {
|
||||
status: 200,
|
||||
body: buildAcmeOrderResource({ profileId, order }),
|
||||
body: buildAcmeOrderResource({ profileId, order: updatedOrder }),
|
||||
headers: {
|
||||
Location: buildUrl(profileId, `/orders/${orderId}`),
|
||||
Link: `<${buildUrl(profileId, "/directory")}>;rel="index"`
|
||||
@@ -683,6 +790,129 @@ export const pkiAcmeServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const processCertificateIssuanceForOrder = async ({
|
||||
caType,
|
||||
accountId,
|
||||
actorOrgId,
|
||||
profileId,
|
||||
orderId,
|
||||
csr,
|
||||
finalizingOrder,
|
||||
certificateRequest,
|
||||
profile,
|
||||
ca,
|
||||
tx
|
||||
}: {
|
||||
caType: CaType;
|
||||
accountId: string;
|
||||
actorOrgId: string;
|
||||
profileId: string;
|
||||
orderId: string;
|
||||
csr: string;
|
||||
finalizingOrder: {
|
||||
notBefore?: Date | null;
|
||||
notAfter?: Date | null;
|
||||
};
|
||||
certificateRequest: ReturnType<typeof extractCertificateRequestFromCSR>;
|
||||
profile: TCertificateProfileWithConfigs;
|
||||
ca: Awaited<ReturnType<typeof certificateAuthorityDAL.findByIdWithAssociatedCa>>;
|
||||
tx?: Knex;
|
||||
}): Promise<{ certificateId?: string; certIssuanceJobData?: TIssueCertificateFromProfileJobData }> => {
|
||||
if (caType === CaType.INTERNAL) {
|
||||
const result = await certificateV3Service.signCertificateFromProfile({
|
||||
actor: ActorType.ACME_ACCOUNT,
|
||||
actorId: accountId,
|
||||
actorAuthMethod: null,
|
||||
actorOrgId,
|
||||
profileId,
|
||||
csr,
|
||||
notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined,
|
||||
notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined,
|
||||
validity: !finalizingOrder.notAfter
|
||||
? {
|
||||
// 47 days, the default TTL comes with Let's Encrypt
|
||||
// TODO: read config from the profile to get the expiration time instead
|
||||
ttl: `${47}d`
|
||||
}
|
||||
: // ttl is not used if notAfter is provided
|
||||
({ ttl: "0d" } as const),
|
||||
enrollmentType: EnrollmentType.ACME
|
||||
});
|
||||
return {
|
||||
certificateId: result.certificateId
|
||||
};
|
||||
}
|
||||
|
||||
const { keyAlgorithm: extractedKeyAlgorithm, signatureAlgorithm: extractedSignatureAlgorithm } =
|
||||
extractAlgorithmsFromCSR(csr);
|
||||
const updatedCertificateRequest = {
|
||||
...certificateRequest,
|
||||
keyAlgorithm: extractedKeyAlgorithm,
|
||||
signatureAlgorithm: extractedSignatureAlgorithm,
|
||||
validity: finalizingOrder.notAfter
|
||||
? (() => {
|
||||
const notBefore = finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : new Date();
|
||||
const notAfter = new Date(finalizingOrder.notAfter);
|
||||
const diffMs = notAfter.getTime() - notBefore.getTime();
|
||||
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
|
||||
return { ttl: `${diffDays}d` };
|
||||
})()
|
||||
: certificateRequest.validity
|
||||
};
|
||||
|
||||
const template = await certificateTemplateV2DAL.findById(profile.certificateTemplateId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Certificate template not found" });
|
||||
}
|
||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||
template.id,
|
||||
updatedCertificateRequest
|
||||
);
|
||||
if (!validationResult.isValid) {
|
||||
throw new AcmeBadCSRError({ message: `Invalid CSR: ${validationResult.errors.join(", ")}` });
|
||||
}
|
||||
|
||||
const certRequest = await certificateRequestService.createCertificateRequest({
|
||||
actor: ActorType.ACME_ACCOUNT,
|
||||
actorId: accountId,
|
||||
actorAuthMethod: null,
|
||||
actorOrgId,
|
||||
projectId: profile.projectId,
|
||||
caId: ca.id,
|
||||
profileId: profile.id,
|
||||
commonName: updatedCertificateRequest.commonName ?? "",
|
||||
keyUsages: updatedCertificateRequest.keyUsages?.map((usage) => usage.toString()) ?? [],
|
||||
extendedKeyUsages: updatedCertificateRequest.extendedKeyUsages?.map((usage) => usage.toString()) ?? [],
|
||||
keyAlgorithm: updatedCertificateRequest.keyAlgorithm || "",
|
||||
signatureAlgorithm: updatedCertificateRequest.signatureAlgorithm || "",
|
||||
altNames: updatedCertificateRequest.subjectAlternativeNames?.map((san) => san.value).join(","),
|
||||
notBefore: updatedCertificateRequest.notBefore,
|
||||
notAfter: updatedCertificateRequest.notAfter,
|
||||
status: CertificateRequestStatus.PENDING,
|
||||
acmeOrderId: orderId,
|
||||
csr,
|
||||
tx
|
||||
});
|
||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||
const csrPem = csrObj.toString("pem");
|
||||
return {
|
||||
certIssuanceJobData: {
|
||||
certificateId: orderId,
|
||||
profileId: profile.id,
|
||||
caId: profile.caId || "",
|
||||
ttl: updatedCertificateRequest.validity?.ttl || "1y",
|
||||
signatureAlgorithm: updatedCertificateRequest.signatureAlgorithm || "",
|
||||
keyAlgorithm: updatedCertificateRequest.keyAlgorithm || "",
|
||||
commonName: updatedCertificateRequest.commonName || "",
|
||||
altNames: updatedCertificateRequest.subjectAlternativeNames?.map((san) => san.value) || [],
|
||||
keyUsages: updatedCertificateRequest.keyUsages?.map((usage) => usage.toString()) ?? [],
|
||||
extendedKeyUsages: updatedCertificateRequest.extendedKeyUsages?.map((usage) => usage.toString()) ?? [],
|
||||
certificateRequestId: certRequest.id,
|
||||
csr: csrPem
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const finalizeAcmeOrder = async ({
|
||||
profileId,
|
||||
accountId,
|
||||
@@ -707,7 +937,11 @@ export const pkiAcmeServiceFactory = ({
|
||||
throw new NotFoundError({ message: "ACME order not found" });
|
||||
}
|
||||
if (order.status === AcmeOrderStatus.Ready) {
|
||||
const { order: updatedOrder, error } = await acmeOrderDAL.transaction(async (tx) => {
|
||||
const {
|
||||
order: updatedOrder,
|
||||
error,
|
||||
certIssuanceJobData
|
||||
} = await acmeOrderDAL.transaction(async (tx) => {
|
||||
const finalizingOrder = (await acmeOrderDAL.findByIdForFinalization(orderId, tx))!;
|
||||
// TODO: ideally, this should be doen with onRequest: verifyAuth([AuthMode.ACME_JWS_SIGNATURE]), instead?
|
||||
const { ownerOrgId: actorOrgId } = (await certificateProfileDAL.findByIdWithOwnerOrgId(profileId, tx))!;
|
||||
@@ -754,94 +988,31 @@ export const pkiAcmeServiceFactory = ({
|
||||
}
|
||||
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||
let errorToReturn: Error | undefined;
|
||||
let certIssuanceJobDataToReturn: TIssueCertificateFromProfileJobData | undefined;
|
||||
try {
|
||||
const { certificateId } = await (async () => {
|
||||
if (caType === CaType.INTERNAL) {
|
||||
const result = await certificateV3Service.signCertificateFromProfile({
|
||||
actor: ActorType.ACME_ACCOUNT,
|
||||
actorId: accountId,
|
||||
actorAuthMethod: null,
|
||||
actorOrgId,
|
||||
profileId,
|
||||
csr,
|
||||
notBefore: finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : undefined,
|
||||
notAfter: finalizingOrder.notAfter ? new Date(finalizingOrder.notAfter) : undefined,
|
||||
validity: !finalizingOrder.notAfter
|
||||
? {
|
||||
// 47 days, the default TTL comes with Let's Encrypt
|
||||
// TODO: read config from the profile to get the expiration time instead
|
||||
ttl: `${47}d`
|
||||
}
|
||||
: // ttl is not used if notAfter is provided
|
||||
({ ttl: "0d" } as const),
|
||||
enrollmentType: EnrollmentType.ACME
|
||||
});
|
||||
return { certificateId: result.certificateId };
|
||||
}
|
||||
const { certificateAuthority } = (await certificateProfileDAL.findByIdWithConfigs(profileId, tx))!;
|
||||
const csrObj = new x509.Pkcs10CertificateRequest(csr);
|
||||
const csrPem = csrObj.toString("pem");
|
||||
|
||||
const { keyAlgorithm: extractedKeyAlgorithm, signatureAlgorithm: extractedSignatureAlgorithm } =
|
||||
extractAlgorithmsFromCSR(csr);
|
||||
|
||||
certificateRequest.keyAlgorithm = extractedKeyAlgorithm;
|
||||
certificateRequest.signatureAlgorithm = extractedSignatureAlgorithm;
|
||||
if (finalizingOrder.notAfter) {
|
||||
const notBefore = finalizingOrder.notBefore ? new Date(finalizingOrder.notBefore) : new Date();
|
||||
const notAfter = new Date(finalizingOrder.notAfter);
|
||||
const diffMs = notAfter.getTime() - notBefore.getTime();
|
||||
const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
|
||||
certificateRequest.validity = { ttl: `${diffDays}d` };
|
||||
}
|
||||
|
||||
const template = await certificateTemplateV2DAL.findById(profile.certificateTemplateId);
|
||||
if (!template) {
|
||||
throw new NotFoundError({ message: "Certificate template not found" });
|
||||
}
|
||||
const validationResult = await certificateTemplateV2Service.validateCertificateRequest(
|
||||
template.id,
|
||||
certificateRequest
|
||||
);
|
||||
if (!validationResult.isValid) {
|
||||
throw new AcmeBadCSRError({ message: `Invalid CSR: ${validationResult.errors.join(", ")}` });
|
||||
}
|
||||
// TODO: this is pretty slow, and we are holding the transaction open for a long time,
|
||||
// we should queue the certificate issuance to a background job instead
|
||||
const cert = await orderCertificate(
|
||||
{
|
||||
caId: certificateAuthority!.id,
|
||||
// It is possible that the CSR does not have a common name, in which case we use an empty string
|
||||
// (more likely than not for a CSR from a modern ACME client like certbot, cert-manager, etc.)
|
||||
commonName: certificateRequest.commonName ?? "",
|
||||
altNames: certificateRequest.subjectAlternativeNames?.map((san) => san.value),
|
||||
csr: Buffer.from(csrPem),
|
||||
// TODO: not 100% sure what are these columns for, but let's put the values for common website SSL certs for now
|
||||
keyUsages: [CertKeyUsage.DIGITAL_SIGNATURE, CertKeyUsage.KEY_ENCIPHERMENT, CertKeyUsage.KEY_AGREEMENT],
|
||||
extendedKeyUsages: [CertExtendedKeyUsage.SERVER_AUTH]
|
||||
},
|
||||
{
|
||||
appConnectionDAL,
|
||||
certificateAuthorityDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
projectDAL
|
||||
}
|
||||
);
|
||||
return { certificateId: cert.id };
|
||||
})();
|
||||
const result = await processCertificateIssuanceForOrder({
|
||||
caType,
|
||||
accountId,
|
||||
actorOrgId,
|
||||
profileId,
|
||||
orderId,
|
||||
csr,
|
||||
finalizingOrder,
|
||||
certificateRequest,
|
||||
profile,
|
||||
ca,
|
||||
tx
|
||||
});
|
||||
await acmeOrderDAL.updateById(
|
||||
orderId,
|
||||
{
|
||||
status: AcmeOrderStatus.Valid,
|
||||
status: result.certificateId ? AcmeOrderStatus.Valid : AcmeOrderStatus.Processing,
|
||||
csr,
|
||||
certificateId
|
||||
certificateId: result.certificateId
|
||||
},
|
||||
tx
|
||||
);
|
||||
certIssuanceJobDataToReturn = result.certIssuanceJobData;
|
||||
} catch (exp) {
|
||||
await acmeOrderDAL.updateById(
|
||||
orderId,
|
||||
@@ -859,18 +1030,43 @@ export const pkiAcmeServiceFactory = ({
|
||||
} else if (exp instanceof AcmeError) {
|
||||
errorToReturn = exp;
|
||||
} else {
|
||||
errorToReturn = new AcmeServerInternalError({ message: "Failed to sign certificate with internal error" });
|
||||
errorToReturn = new AcmeServerInternalError({
|
||||
message: "Failed to sign certificate with internal error"
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
order: (await acmeOrderDAL.findByAccountAndOrderIdWithAuthorizations(accountId, orderId, tx))!,
|
||||
error: errorToReturn
|
||||
error: errorToReturn,
|
||||
certIssuanceJobData: certIssuanceJobDataToReturn
|
||||
};
|
||||
});
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
if (certIssuanceJobData) {
|
||||
// TODO: ideally, this should be done inside the transaction, but the pg-boss queue doesn't support external transactions
|
||||
// as it seems to be. we need to commit the transaction before queuing the job, otherwise the job will fail (not found error).
|
||||
await certificateIssuanceQueue.queueCertificateIssuance(certIssuanceJobData);
|
||||
}
|
||||
order = updatedOrder;
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: profile.projectId,
|
||||
actor: {
|
||||
type: ActorType.ACME_ACCOUNT,
|
||||
metadata: {
|
||||
profileId,
|
||||
accountId
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.FINALIZE_ACME_ORDER,
|
||||
metadata: {
|
||||
orderId: updatedOrder.id,
|
||||
csr: updatedOrder.csr!
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (order.status !== AcmeOrderStatus.Valid) {
|
||||
throw new AcmeOrderNotReadyError({ message: "ACME order is not ready" });
|
||||
}
|
||||
@@ -898,14 +1094,16 @@ export const pkiAcmeServiceFactory = ({
|
||||
if (!order) {
|
||||
throw new NotFoundError({ message: "ACME order not found" });
|
||||
}
|
||||
if (order.status !== AcmeOrderStatus.Valid) {
|
||||
// Sync order first in case if there is a certificate request that needs to be processed
|
||||
const syncedOrder = await checkAndSyncAcmeOrderStatus({ orderId });
|
||||
if (syncedOrder.status !== AcmeOrderStatus.Valid) {
|
||||
throw new AcmeOrderNotReadyError({ message: "ACME order is not valid" });
|
||||
}
|
||||
if (!order.certificateId) {
|
||||
if (!syncedOrder.certificateId) {
|
||||
throw new NotFoundError({ message: "The certificate for this ACME order no longer exists" });
|
||||
}
|
||||
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: order.certificateId });
|
||||
const certBody = await certificateBodyDAL.findOne({ certId: syncedOrder.certificateId });
|
||||
const certificateManagerKeyId = await getProjectKmsCertificateKeyId({
|
||||
projectId: profile.projectId,
|
||||
projectDAL,
|
||||
@@ -926,6 +1124,24 @@ export const pkiAcmeServiceFactory = ({
|
||||
|
||||
const certLeaf = certObj.toString("pem").trim().replace("\n", "\r\n");
|
||||
const certChain = certificateChain.trim().replace("\n", "\r\n");
|
||||
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: profile.projectId,
|
||||
actor: {
|
||||
type: ActorType.ACME_ACCOUNT,
|
||||
metadata: {
|
||||
profileId,
|
||||
accountId
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.DOWNLOAD_ACME_CERTIFICATE,
|
||||
metadata: {
|
||||
orderId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body:
|
||||
@@ -1008,6 +1224,7 @@ export const pkiAcmeServiceFactory = ({
|
||||
authzId: string;
|
||||
challengeId: string;
|
||||
}): Promise<TAcmeResponse<TRespondToAcmeChallengeResponse>> => {
|
||||
const profile = await validateAcmeProfile(profileId);
|
||||
const result = await acmeChallengeDAL.findByAccountAuthAndChallengeId(accountId, authzId, challengeId);
|
||||
if (!result) {
|
||||
throw new NotFoundError({ message: "ACME challenge not found" });
|
||||
@@ -1015,6 +1232,23 @@ export const pkiAcmeServiceFactory = ({
|
||||
await acmeChallengeService.markChallengeAsReady(challengeId);
|
||||
await pkiAcmeQueueService.queueChallengeValidation(challengeId);
|
||||
const challenge = (await acmeChallengeDAL.findByIdForChallengeValidation(challengeId))!;
|
||||
await auditLogService.createAuditLog({
|
||||
projectId: profile.projectId,
|
||||
actor: {
|
||||
type: ActorType.ACME_ACCOUNT,
|
||||
metadata: {
|
||||
profileId,
|
||||
accountId
|
||||
}
|
||||
},
|
||||
event: {
|
||||
type: EventType.RESPOND_TO_ACME_CHALLENGE,
|
||||
metadata: {
|
||||
challengeId,
|
||||
type: challenge.type as AcmeChallengeType
|
||||
}
|
||||
}
|
||||
});
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
|
||||
@@ -103,3 +103,34 @@ export const deepEqualSkipFields = (obj1: unknown, obj2: unknown, skipFields: st
|
||||
|
||||
return deepEqual(filtered1, filtered2);
|
||||
};
|
||||
|
||||
export const deterministicStringify = (value: unknown): string => {
|
||||
if (value === null || value === undefined) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const items = value.map((item) => deterministicStringify(item));
|
||||
return `[${items.join(",")}]`;
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
const sortedKeys = Object.keys(value).sort();
|
||||
const sortedObj: Record<string, unknown> = {};
|
||||
for (const key of sortedKeys) {
|
||||
const val = (value as Record<string, unknown>)[key];
|
||||
if (typeof val === "object" && val !== null) {
|
||||
sortedObj[key] = JSON.parse(deterministicStringify(val));
|
||||
} else {
|
||||
sortedObj[key] = val;
|
||||
}
|
||||
}
|
||||
return JSON.stringify(sortedObj);
|
||||
}
|
||||
|
||||
return JSON.stringify(value);
|
||||
};
|
||||
|
||||
@@ -2308,7 +2308,8 @@ export const registerRoutes = async (
|
||||
});
|
||||
|
||||
const acmeChallengeService = pkiAcmeChallengeServiceFactory({
|
||||
acmeChallengeDAL
|
||||
acmeChallengeDAL,
|
||||
auditLogService
|
||||
});
|
||||
|
||||
const pkiAcmeQueueService = await pkiAcmeQueueServiceFactory({
|
||||
@@ -2318,13 +2319,9 @@ export const registerRoutes = async (
|
||||
|
||||
const pkiAcmeService = pkiAcmeServiceFactory({
|
||||
projectDAL,
|
||||
appConnectionDAL,
|
||||
certificateDAL,
|
||||
certificateAuthorityDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateProfileDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
certificateTemplateV2DAL,
|
||||
acmeAccountDAL,
|
||||
acmeOrderDAL,
|
||||
@@ -2336,8 +2333,11 @@ export const registerRoutes = async (
|
||||
licenseService,
|
||||
certificateV3Service,
|
||||
certificateTemplateV2Service,
|
||||
certificateRequestService,
|
||||
certificateIssuanceQueue,
|
||||
acmeChallengeService,
|
||||
pkiAcmeQueueService
|
||||
pkiAcmeQueueService,
|
||||
auditLogService
|
||||
});
|
||||
|
||||
const pkiSubscriberService = pkiSubscriberServiceFactory({
|
||||
|
||||
@@ -47,7 +47,11 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||
})
|
||||
.optional(),
|
||||
acmeConfig: z.object({}).optional(),
|
||||
acmeConfig: z
|
||||
.object({
|
||||
skipDnsOwnershipVerification: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
externalConfigs: ExternalConfigUnionSchema
|
||||
})
|
||||
.refine(
|
||||
@@ -245,7 +249,8 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
acmeConfig: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
directoryUrl: z.string()
|
||||
directoryUrl: z.string(),
|
||||
skipDnsOwnershipVerification: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
externalConfigs: ExternalConfigUnionSchema
|
||||
@@ -434,6 +439,11 @@ export const registerCertificateProfilesRouter = async (server: FastifyZodProvid
|
||||
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||
})
|
||||
.optional(),
|
||||
acmeConfig: z
|
||||
.object({
|
||||
skipDnsOwnershipVerification: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
externalConfigs: ExternalConfigUnionSchema
|
||||
})
|
||||
.refine(
|
||||
|
||||
@@ -41,6 +41,7 @@ export enum ActorType { // would extend to AWS, Azure, ...
|
||||
IDENTITY = "identity",
|
||||
Machine = "machine",
|
||||
SCIM_CLIENT = "scimClient",
|
||||
ACME_PROFILE = "acmeProfile",
|
||||
ACME_ACCOUNT = "acmeAccount",
|
||||
UNKNOWN_USER = "unknownUser"
|
||||
}
|
||||
|
||||
@@ -168,7 +168,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigAutoRenew"),
|
||||
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiConfigRenewBeforeDays"),
|
||||
db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigId"),
|
||||
db.ref("encryptedEabSecret").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigEncryptedEabSecret")
|
||||
db.ref("encryptedEabSecret").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeConfigEncryptedEabSecret"),
|
||||
db
|
||||
.ref("skipDnsOwnershipVerification")
|
||||
.withSchema(TableName.PkiAcmeEnrollmentConfig)
|
||||
.as("acmeConfigSkipDnsOwnershipVerification")
|
||||
)
|
||||
.where(`${TableName.PkiCertificateProfile}.id`, id)
|
||||
.first();
|
||||
@@ -198,7 +202,8 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
const acmeConfig = result.acmeConfigId
|
||||
? ({
|
||||
id: result.acmeConfigId,
|
||||
encryptedEabSecret: result.acmeConfigEncryptedEabSecret
|
||||
encryptedEabSecret: result.acmeConfigEncryptedEabSecret,
|
||||
skipDnsOwnershipVerification: result.acmeConfigSkipDnsOwnershipVerification ?? false
|
||||
} as TCertificateProfileWithConfigs["acmeConfig"])
|
||||
: undefined;
|
||||
|
||||
@@ -356,7 +361,11 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
db.ref("id").withSchema(TableName.PkiApiEnrollmentConfig).as("apiId"),
|
||||
db.ref("autoRenew").withSchema(TableName.PkiApiEnrollmentConfig).as("apiAutoRenew"),
|
||||
db.ref("renewBeforeDays").withSchema(TableName.PkiApiEnrollmentConfig).as("apiRenewBeforeDays"),
|
||||
db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeId")
|
||||
db.ref("id").withSchema(TableName.PkiAcmeEnrollmentConfig).as("acmeId"),
|
||||
db
|
||||
.ref("skipDnsOwnershipVerification")
|
||||
.withSchema(TableName.PkiAcmeEnrollmentConfig)
|
||||
.as("acmeSkipDnsOwnershipVerification")
|
||||
);
|
||||
|
||||
if (processedRules) {
|
||||
@@ -393,7 +402,8 @@ export const certificateProfileDALFactory = (db: TDbClient) => {
|
||||
|
||||
const acmeConfig = result.acmeId
|
||||
? {
|
||||
id: result.acmeId as string
|
||||
id: result.acmeId as string,
|
||||
skipDnsOwnershipVerification: !!result.acmeSkipDnsOwnershipVerification
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
||||
@@ -30,7 +30,11 @@ export const createCertificateProfileSchema = z
|
||||
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||
})
|
||||
.optional(),
|
||||
acmeConfig: z.object({}).optional()
|
||||
acmeConfig: z
|
||||
.object({
|
||||
skipDnsOwnershipVerification: z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
@@ -155,6 +159,11 @@ export const updateCertificateProfileSchema = z
|
||||
autoRenew: z.boolean().default(false),
|
||||
renewBeforeDays: z.number().min(1).max(30).optional()
|
||||
})
|
||||
.optional(),
|
||||
acmeConfig: z
|
||||
.object({
|
||||
skipDnsOwnershipVerification: z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
})
|
||||
.refine(
|
||||
|
||||
@@ -403,7 +403,13 @@ export const certificateProfileServiceFactory = ({
|
||||
apiConfigId = apiConfig.id;
|
||||
} else if (data.enrollmentType === EnrollmentType.ACME && data.acmeConfig) {
|
||||
const { encryptedEabSecret } = await generateAndEncryptAcmeEabSecret(projectId, kmsService, projectDAL);
|
||||
const acmeConfig = await acmeEnrollmentConfigDAL.create({ encryptedEabSecret }, tx);
|
||||
const acmeConfig = await acmeEnrollmentConfigDAL.create(
|
||||
{
|
||||
skipDnsOwnershipVerification: data.acmeConfig.skipDnsOwnershipVerification ?? false,
|
||||
encryptedEabSecret
|
||||
},
|
||||
tx
|
||||
);
|
||||
acmeConfigId = acmeConfig.id;
|
||||
}
|
||||
|
||||
@@ -505,7 +511,7 @@ export const certificateProfileServiceFactory = ({
|
||||
const updatedData =
|
||||
finalIssuerType === IssuerType.SELF_SIGNED && existingProfile.caId ? { ...data, caId: null } : data;
|
||||
|
||||
const { estConfig, apiConfig, ...profileUpdateData } = updatedData;
|
||||
const { estConfig, apiConfig, acmeConfig, ...profileUpdateData } = updatedData;
|
||||
|
||||
const updatedProfile = await certificateProfileDAL.transaction(async (tx) => {
|
||||
if (estConfig && existingProfile.estConfigId) {
|
||||
@@ -547,6 +553,16 @@ export const certificateProfileServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (acmeConfig && existingProfile.acmeConfigId) {
|
||||
await acmeEnrollmentConfigDAL.updateById(
|
||||
existingProfile.acmeConfigId,
|
||||
{
|
||||
skipDnsOwnershipVerification: acmeConfig.skipDnsOwnershipVerification ?? false
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
|
||||
const profileResult = await certificateProfileDAL.updateById(profileId, profileUpdateData, tx);
|
||||
return profileResult;
|
||||
});
|
||||
|
||||
@@ -46,7 +46,9 @@ export type TCertificateProfileUpdate = Omit<
|
||||
autoRenew?: boolean;
|
||||
renewBeforeDays?: number;
|
||||
};
|
||||
acmeConfig?: unknown;
|
||||
acmeConfig?: {
|
||||
skipDnsOwnershipVerification?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
||||
@@ -83,6 +85,7 @@ export type TCertificateProfileWithConfigs = TCertificateProfile & {
|
||||
id: string;
|
||||
directoryUrl: string;
|
||||
encryptedEabSecret?: Buffer;
|
||||
skipDnsOwnershipVerification?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ export const certificateRequestServiceFactory = ({
|
||||
permissionService
|
||||
}: TCertificateRequestServiceFactoryDep) => {
|
||||
const createCertificateRequest = async ({
|
||||
acmeOrderId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -123,6 +124,7 @@ export const certificateRequestServiceFactory = ({
|
||||
{
|
||||
status,
|
||||
projectId,
|
||||
acmeOrderId,
|
||||
...validatedData
|
||||
},
|
||||
tx
|
||||
|
||||
@@ -21,6 +21,7 @@ export type TCreateCertificateRequestDTO = TProjectPermission & {
|
||||
metadata?: string;
|
||||
status: CertificateRequestStatus;
|
||||
certificateId?: string;
|
||||
acmeOrderId?: string;
|
||||
};
|
||||
|
||||
export type TGetCertificateRequestDTO = TProjectPermission & {
|
||||
|
||||
@@ -1,61 +1,13 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
import { TAcmeEnrollmentConfigInsert, TAcmeEnrollmentConfigUpdate } from "./enrollment-config-types";
|
||||
|
||||
export type TAcmeEnrollmentConfigDALFactory = ReturnType<typeof acmeEnrollmentConfigDALFactory>;
|
||||
|
||||
export const acmeEnrollmentConfigDALFactory = (db: TDbClient) => {
|
||||
const acmeEnrollmentConfigOrm = ormify(db, TableName.PkiAcmeEnrollmentConfig);
|
||||
|
||||
const create = async (data: TAcmeEnrollmentConfigInsert, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db)(TableName.PkiAcmeEnrollmentConfig).insert(data).returning("*");
|
||||
const [acmeConfig] = result;
|
||||
|
||||
if (!acmeConfig) {
|
||||
throw new Error("Failed to create ACME enrollment config");
|
||||
}
|
||||
|
||||
return acmeConfig;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Create ACME enrollment config" });
|
||||
}
|
||||
};
|
||||
|
||||
const updateById = async (id: string, data: TAcmeEnrollmentConfigUpdate, tx?: Knex) => {
|
||||
try {
|
||||
const result = await (tx || db)(TableName.PkiAcmeEnrollmentConfig).where({ id }).update(data).returning("*");
|
||||
const [acmeConfig] = result;
|
||||
|
||||
if (!acmeConfig) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return acmeConfig;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Update ACME enrollment config" });
|
||||
}
|
||||
};
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
try {
|
||||
const acmeConfig = await (tx || db)(TableName.PkiAcmeEnrollmentConfig).where({ id }).first();
|
||||
|
||||
return acmeConfig || null;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find ACME enrollment config by id" });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
...acmeEnrollmentConfigOrm,
|
||||
create,
|
||||
updateById,
|
||||
findById
|
||||
...acmeEnrollmentConfigOrm
|
||||
};
|
||||
};
|
||||
|
||||
@@ -37,4 +37,6 @@ export interface TApiConfigData {
|
||||
renewBeforeDays?: number;
|
||||
}
|
||||
|
||||
export interface TAcmeConfigData {}
|
||||
export interface TAcmeConfigData {
|
||||
skipDnsOwnershipVerification?: boolean;
|
||||
}
|
||||
|
||||
@@ -1986,7 +1986,7 @@ export const projectServiceFactory = ({
|
||||
if (project.type === ProjectType.SecretManager) {
|
||||
projectTypeUrl = "secret-management";
|
||||
} else if (project.type === ProjectType.CertificateManager) {
|
||||
projectTypeUrl = "cert-management";
|
||||
projectTypeUrl = "cert-manager";
|
||||
}
|
||||
|
||||
const callbackPath = `/organizations/${project.orgId}/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`;
|
||||
|
||||
@@ -61,9 +61,7 @@ export const PkiExpirationAlertTemplate = ({
|
||||
</Section>
|
||||
|
||||
<Section className="text-center mt-[32px] mb-[16px]">
|
||||
<BaseButton href={`${siteUrl}/projects/cert-management/${projectId}/policies`}>
|
||||
View Certificate Alerts
|
||||
</BaseButton>
|
||||
<BaseButton href={`${siteUrl}/projects/cert-manager/${projectId}/policies`}>View Certificate Alerts</BaseButton>
|
||||
</Section>
|
||||
</BaseEmailWrapper>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import {
|
||||
AcmeAccountActor,
|
||||
AcmeProfileActor,
|
||||
IdentityActor,
|
||||
KmipClientActor,
|
||||
PlatformActor,
|
||||
@@ -60,6 +62,8 @@ export type TSecretModifiedEvent = {
|
||||
| ScimClientActor
|
||||
| PlatformActor
|
||||
| UnknownUserActor
|
||||
| AcmeAccountActor
|
||||
| AcmeProfileActor
|
||||
| KmipClientActor;
|
||||
};
|
||||
};
|
||||
|
||||
180
docs/docs.json
180
docs/docs.json
@@ -3074,6 +3074,186 @@
|
||||
{
|
||||
"source": "/documentation/platform/pki/est",
|
||||
"destination": "/documentation/platform/pki/enrollment-methods/est"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/endpoints/integrations/create-auth",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/endpoints/integrations/create",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/endpoints/integrations/delete-auth-by-id",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/endpoints/integrations/delete-auth",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/endpoints/integrations/delete",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/endpoints/integrations/find-auth",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/endpoints/integrations/list-auth",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/endpoints/integrations/list-project-integrations",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/endpoints/integrations/update",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/api-reference/overview/examples/integration",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/documentation/platform/integrations",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cicd/circleci",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cicd/codefresh",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cicd/octopus-deploy",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cicd/rundeck",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cicd/travisci",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/aws-parameter-store",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/aws-secret-manager",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/azure-app-configuration",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/azure-devops",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/azure-key-vault",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/checkly",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/cloud-66",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/cloudflare-pages",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/cloudflare-workers",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/databricks",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/digital-ocean-app-platform",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/flyio",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/gcp-secret-manager",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/hashicorp-vault",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/hasura-cloud",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/heroku",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/laravel-forge",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/netlify",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/northflank",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/qovery",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/railway",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/render",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/supabase",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/terraform-cloud",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/vercel",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/windmill",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/overview",
|
||||
"destination": "/integrations/secret-syncs"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/aws-amplify",
|
||||
"destination": "/integrations/cicd/aws-amplify"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/cloud/teamcity",
|
||||
"destination": "/integrations/secret-syncs/teamcity"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -26,6 +26,17 @@ In the following steps, we explore how to issue a X.509 certificate using the AC
|
||||
|
||||

|
||||
|
||||
<Note>
|
||||
|
||||
By default, when the ACME client requests a certificate against the certificate profile for a particular domain, Infisical will verify domain ownership using the [HTTP-01 challenge](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) method prior to issuing a certificate back to the client.
|
||||
|
||||
If you want Infisical to skip domain ownership validation entirely, you can enable the **Skip DNS Ownership Validation** checkbox.
|
||||
|
||||
Note that skipping domain ownership validation for the ACME enrollment method is **not the same** as skipping validation for an [External ACME CA integration](/documentation/platform/pki/ca/acme-ca).
|
||||
|
||||
When using the ACME enrollment, the domain ownership check occurring between the ACME client and Infisical can be skipped. In contrast, External ACME CA integrations always require domain ownership validation, as Infisical must complete a DNS-01 challenge with the upstream ACME-compatible CA.
|
||||
</Note>
|
||||
|
||||
</Step>
|
||||
<Step title="Obtain the ACME configuration">
|
||||
Once you've created the certificate profile, you can obtain its ACME configuration details by clicking the **Reveal ACME EAB** option on the profile.
|
||||
|
||||
@@ -139,7 +139,7 @@ The following steps show how to install cert-manager (using `kubectl`) and obtai
|
||||
```
|
||||
|
||||
<Note>
|
||||
- Currently, the Infisical ACME server only supports the HTTP-01 challenge and requires successful challenge completion before issuing certificates. Support for optional challenges and DNS-01 is planned for a future release.
|
||||
- Currently, the [ACME enrollment method](/documentation/platform/pki/enrollment-methods/acme) only supports the [HTTP-01 challenge](https://letsencrypt.org/docs/challenge-types/#http-01-challenge) method. Support for the [DNS-01 challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) method is planned for a future release. If domain ownership validation is not desired, you can disable it by enabling the **Skip DNS ownership validation** option in your ACME certificate profile configuration.
|
||||
- An `Issuer` is namespace-scoped. Certificates can only be issued using an `Issuer` that exists in the same namespace as the `Certificate` resource.
|
||||
- If you need to issue certificates across multiple namespaces with a single resource, create a `ClusterIssuer` instead. The configuration is identical except `kind: ClusterIssuer` and no `metadata.namespace`.
|
||||
- More details: https://cert-manager.io/docs/configuration/acme/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 313 KiB After Width: | Height: | Size: 154 KiB |
@@ -1,8 +0,0 @@
|
||||
---
|
||||
title: "TeamCity"
|
||||
description: "How to sync secrets from Infisical to TeamCity"
|
||||
---
|
||||
|
||||
<Note>
|
||||
The TeamCity Native Integration will be deprecated in 2026. Please migrate to our new [TeamCity Sync](../secret-syncs/teamcity).
|
||||
</Note>
|
||||
@@ -66,7 +66,7 @@ const PROJECT_TYPE_MENU_ITEMS = [
|
||||
value: ProjectType.SecretManager
|
||||
},
|
||||
{
|
||||
label: "Certificates Management",
|
||||
label: "Certificate Manager",
|
||||
value: ProjectType.CertificateManager
|
||||
},
|
||||
{
|
||||
|
||||
@@ -9,7 +9,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { FormControl, Input, Select, SelectItem, Switch, Tooltip } from "@app/components/v2";
|
||||
import { SECRET_SYNC_INITIAL_SYNC_BEHAVIOR_MAP, SECRET_SYNC_MAP } from "@app/helpers/secretSyncs";
|
||||
import { SecretSync, useSecretSyncOption } from "@app/hooks/api/secretSyncs";
|
||||
import {
|
||||
SecretSync,
|
||||
SecretSyncInitialSyncBehavior,
|
||||
useSecretSyncOption
|
||||
} from "@app/hooks/api/secretSyncs";
|
||||
|
||||
import { TSecretSyncForm } from "../schemas";
|
||||
import { AwsParameterStoreSyncOptionsFields } from "./AwsParameterStoreSyncOptionsFields";
|
||||
@@ -139,13 +143,25 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
{!syncOption?.canImportSecrets && (
|
||||
{!syncOption?.canImportSecrets ? (
|
||||
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
||||
<FontAwesomeIcon className="mr-1" size="xs" icon={faTriangleExclamation} />
|
||||
{destinationName} only supports overwriting destination secrets.{" "}
|
||||
{!currentSyncOption.disableSecretDeletion &&
|
||||
"Secrets not present in Infisical will be removed from the destination."}
|
||||
`Secrets not present in Infisical will be removed from the destination. Consider adding a key schema or disabling secret deletion if you do not want existing secrets to be removed from ${destinationName}.`}
|
||||
</p>
|
||||
) : (
|
||||
currentSyncOption.initialSyncBehavior ===
|
||||
SecretSyncInitialSyncBehavior.OverwriteDestination &&
|
||||
!currentSyncOption.disableSecretDeletion && (
|
||||
<p className="-mt-2.5 mb-2.5 text-xs text-yellow">
|
||||
<FontAwesomeIcon className="mr-1" size="xs" icon={faTriangleExclamation} />
|
||||
Secrets not present in Infisical will be removed from the destination. If you have
|
||||
secrets in {destinationName} that you do not want deleted, consider setting initial
|
||||
sync behavior to import destination secrets. Alternatively, configure a key schema
|
||||
or disable secret deletion below to have Infisical ignore these secrets.
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@@ -183,26 +199,26 @@ export const SecretSyncOptionsFields = ({ hideInitialSync }: Props) => {
|
||||
className="max-w-md"
|
||||
content={
|
||||
<span>
|
||||
We highly recommend using a{" "}
|
||||
We highly recommend configuring a{" "}
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/secret-syncs/overview#key-schemas"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
Key Schema
|
||||
key schema
|
||||
</a>{" "}
|
||||
to ensure that Infisical only manages the specific keys you intend, keeping
|
||||
everything else untouched.
|
||||
to ensure that Infisical only manages secrets in {destinationName} that match
|
||||
the key pattern.
|
||||
<br />
|
||||
<br />
|
||||
Destination secrets that do not match the schema will not be deleted or updated.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div>
|
||||
<span>Infisical strongly advises setting a Key Schema</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} className="text-mineshaft-400" />
|
||||
<div className="text-info">
|
||||
<span>Infisical strongly advises configuring a key schema</span>{" "}
|
||||
<FontAwesomeIcon icon={faCircleInfo} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export const PageHeader = ({ title, description, children, className, scope }: P
|
||||
<div className="mr-4 flex w-full items-center">
|
||||
<h1
|
||||
className={twMerge(
|
||||
"text-2xl font-medium text-white capitalize underline decoration-2 underline-offset-4",
|
||||
"text-2xl font-medium text-white underline decoration-2 underline-offset-4",
|
||||
scope === "org" && "decoration-org/90",
|
||||
scope === "instance" && "decoration-neutral/90",
|
||||
scope === "namespace" && "decoration-sub-org/90",
|
||||
|
||||
@@ -11,8 +11,9 @@ const alertVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-container text-card-foreground",
|
||||
info: "bg-info/10 text-info border-info/20",
|
||||
org: "bg-org/10 text-org border-org/20"
|
||||
info: "bg-info/5 text-info border-info/20",
|
||||
org: "bg-org/5 text-org border-org/20",
|
||||
"sub-org": "bg-sub-org/5 text-sub-org border-sub-org/20"
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -294,36 +294,36 @@ export const ROUTE_PATHS = Object.freeze({
|
||||
},
|
||||
CertManager: {
|
||||
CertAuthDetailsByIDPage: setRoute(
|
||||
"/organizations/$orgId/projects/cert-management/$projectId/ca/$caId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId"
|
||||
"/organizations/$orgId/projects/cert-manager/$projectId/ca/$caId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
|
||||
),
|
||||
SubscribersPage: setRoute(
|
||||
"/organizations/$orgId/projects/cert-management/$projectId/subscribers",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers"
|
||||
"/organizations/$orgId/projects/cert-manager/$projectId/subscribers",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/subscribers"
|
||||
),
|
||||
CertificateAuthoritiesPage: setRoute(
|
||||
"/organizations/$orgId/projects/cert-management/$projectId/certificate-authorities",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
|
||||
"/organizations/$orgId/projects/cert-manager/$projectId/certificate-authorities",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/certificate-authorities"
|
||||
),
|
||||
AlertingPage: setRoute(
|
||||
"/organizations/$orgId/projects/cert-management/$projectId/alerting",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/alerting"
|
||||
"/organizations/$orgId/projects/cert-manager/$projectId/alerting",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/alerting"
|
||||
),
|
||||
PkiCollectionDetailsByIDPage: setRoute(
|
||||
"/organizations/$orgId/projects/cert-management/$projectId/pki-collections/$collectionId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/pki-collections/$collectionId"
|
||||
"/organizations/$orgId/projects/cert-manager/$projectId/pki-collections/$collectionId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
|
||||
),
|
||||
PkiSubscriberDetailsByIDPage: setRoute(
|
||||
"/organizations/$orgId/projects/cert-management/$projectId/subscribers/$subscriberName",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName"
|
||||
"/organizations/$orgId/projects/cert-manager/$projectId/subscribers/$subscriberName",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName"
|
||||
),
|
||||
IntegrationsListPage: setRoute(
|
||||
"/organizations/$orgId/projects/cert-management/$projectId/integrations",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/"
|
||||
"/organizations/$orgId/projects/cert-manager/$projectId/integrations",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/integrations/"
|
||||
),
|
||||
PkiSyncDetailsByIDPage: setRoute(
|
||||
"/organizations/$orgId/projects/cert-management/$projectId/integrations/$syncId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId"
|
||||
"/organizations/$orgId/projects/cert-manager/$projectId/integrations/$syncId",
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/integrations/$syncId"
|
||||
)
|
||||
},
|
||||
Ssh: {
|
||||
|
||||
@@ -61,7 +61,7 @@ export const getProjectBaseURL = (type: ProjectType) => {
|
||||
case ProjectType.SecretManager:
|
||||
return "/organizations/$orgId/projects/secret-management/$projectId";
|
||||
case ProjectType.CertificateManager:
|
||||
return "/organizations/$orgId/projects/cert-management/$projectId";
|
||||
return "/organizations/$orgId/projects/cert-manager/$projectId";
|
||||
default:
|
||||
return `/organizations/$orgId/projects/${type}/$projectId` as const;
|
||||
}
|
||||
@@ -74,7 +74,7 @@ export const getProjectHomePage = (type: ProjectType, environments: ProjectEnv[]
|
||||
case ProjectType.SecretManager:
|
||||
return "/organizations/$orgId/projects/secret-management/$projectId/overview" as const;
|
||||
case ProjectType.CertificateManager:
|
||||
return "/organizations/$orgId/projects/cert-management/$projectId/policies" as const;
|
||||
return "/organizations/$orgId/projects/cert-manager/$projectId/policies" as const;
|
||||
case ProjectType.SecretScanning:
|
||||
return `/organizations/$orgId/projects/${type}/$projectId/data-sources` as const;
|
||||
case ProjectType.PAM:
|
||||
@@ -88,7 +88,7 @@ export const getProjectTitle = (type: ProjectType) => {
|
||||
const titleConvert = {
|
||||
[ProjectType.SecretManager]: "Secrets Management",
|
||||
[ProjectType.KMS]: "Key Management",
|
||||
[ProjectType.CertificateManager]: "Cert Management",
|
||||
[ProjectType.CertificateManager]: "Certificate Manager",
|
||||
[ProjectType.SSH]: "SSH",
|
||||
[ProjectType.SecretScanning]: "Secret Scanning",
|
||||
[ProjectType.PAM]: "PAM"
|
||||
|
||||
@@ -4,6 +4,8 @@ export enum ActorType {
|
||||
USER = "user",
|
||||
SERVICE = "service",
|
||||
IDENTITY = "identity",
|
||||
ACME_PROFILE = "acmeProfile",
|
||||
ACME_ACCOUNT = "acmeAccount",
|
||||
UNKNOWN_USER = "unknownUser"
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,13 @@ interface KmipClientActorMetadata {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface AcmeAccountActorMetadata {
|
||||
profileId: string;
|
||||
accountId: string;
|
||||
}
|
||||
interface AcmeProfileActorMetadata {
|
||||
profileId: string;
|
||||
}
|
||||
interface UserActor {
|
||||
type: ActorType.USER;
|
||||
metadata: UserActorMetadata;
|
||||
@@ -67,13 +74,25 @@ export interface UnknownUserActor {
|
||||
type: ActorType.UNKNOWN_USER;
|
||||
}
|
||||
|
||||
export interface AcmeProfileActor {
|
||||
type: ActorType.ACME_PROFILE;
|
||||
metadata: AcmeProfileActorMetadata;
|
||||
}
|
||||
|
||||
export interface AcmeAccountActor {
|
||||
type: ActorType.ACME_ACCOUNT;
|
||||
metadata: AcmeAccountActorMetadata;
|
||||
}
|
||||
|
||||
export type Actor =
|
||||
| UserActor
|
||||
| ServiceActor
|
||||
| IdentityActor
|
||||
| PlatformActor
|
||||
| UnknownUserActor
|
||||
| KmipClientActor;
|
||||
| KmipClientActor
|
||||
| AcmeProfileActor
|
||||
| AcmeAccountActor;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
|
||||
@@ -62,6 +62,7 @@ export type TCertificateProfileWithDetails = TCertificateProfile & {
|
||||
acmeConfig?: {
|
||||
id: string;
|
||||
directoryUrl: string;
|
||||
skipDnsOwnershipVerification?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export const PkiManagerLayout = () => {
|
||||
<Tabs value="selected">
|
||||
<TabList className="border-b-0">
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/policies"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/policies"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
@@ -52,7 +52,7 @@ export const PkiManagerLayout = () => {
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Certificates</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/certificate-authorities"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/certificate-authorities"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
@@ -65,7 +65,7 @@ export const PkiManagerLayout = () => {
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/alerting"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/alerting"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
@@ -74,7 +74,7 @@ export const PkiManagerLayout = () => {
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Alerting</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/integrations"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/integrations"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
@@ -83,7 +83,7 @@ export const PkiManagerLayout = () => {
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Integrations</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/app-connections"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/app-connections"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
@@ -95,7 +95,7 @@ export const PkiManagerLayout = () => {
|
||||
<>
|
||||
{(subscription.pkiLegacyTemplates || hasExistingSubscribers) && (
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/subscribers"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/subscribers"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
@@ -108,7 +108,7 @@ export const PkiManagerLayout = () => {
|
||||
)}
|
||||
{(subscription.pkiLegacyTemplates || hasExistingTemplates) && (
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/certificate-templates"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/certificate-templates"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
@@ -124,7 +124,7 @@ export const PkiManagerLayout = () => {
|
||||
</>
|
||||
)}
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/access-management"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/access-management"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
@@ -144,7 +144,7 @@ export const PkiManagerLayout = () => {
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/audit-logs"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/audit-logs"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
@@ -153,7 +153,7 @@ export const PkiManagerLayout = () => {
|
||||
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
|
||||
</Link>
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/settings"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/settings"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id
|
||||
|
||||
@@ -82,7 +82,7 @@ export const PkiCollectionModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
});
|
||||
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/pki-collections/$collectionId",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/pki-collections/$collectionId",
|
||||
params: {
|
||||
orgId: currentOrg.id,
|
||||
projectId,
|
||||
|
||||
@@ -66,7 +66,7 @@ export const PkiCollectionTable = ({ handlePopUpOpen }: Props) => {
|
||||
key={`pki-collection-${pkiCollection.id}`}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/pki-collections/$collectionId",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/pki-collections/$collectionId",
|
||||
params: {
|
||||
orgId: currentOrg.id,
|
||||
projectId,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { AlertingPage } from "./AlertingPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/alerting"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/alerting"
|
||||
)({
|
||||
component: AlertingPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
@@ -79,7 +79,7 @@ const Page = () => {
|
||||
|
||||
handlePopUpClose("deleteCa");
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/certificate-authorities",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/certificate-authorities",
|
||||
params: {
|
||||
orgId: currentOrg.id,
|
||||
projectId
|
||||
@@ -100,7 +100,7 @@ const Page = () => {
|
||||
isAllowed ? (
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/certificate-authorities"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/certificate-authorities"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
import { CertAuthDetailsByIDPage } from "./CertAuthDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/ca/$caId"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/ca/$caId"
|
||||
)({
|
||||
component: CertAuthDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Certificate Authorities",
|
||||
link: linkOptions({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/certificate-authorities",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/certificate-authorities",
|
||||
params: {
|
||||
orgId: params.orgId,
|
||||
projectId: params.projectId
|
||||
|
||||
@@ -96,7 +96,7 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
|
||||
onClick={() =>
|
||||
canReadCa &&
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/ca/$caId",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/ca/$caId",
|
||||
params: {
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { CertificateAuthoritiesPage } from "./CertificateAuthoritiesPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/certificate-authorities"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/certificate-authorities"
|
||||
)({
|
||||
component: CertificateAuthoritiesPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
@@ -15,7 +15,7 @@ const IntegrationsListPageQuerySchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/integrations/"
|
||||
)({
|
||||
component: IntegrationsListPage,
|
||||
validateSearch: zodValidator(IntegrationsListPageQuerySchema),
|
||||
|
||||
@@ -64,7 +64,7 @@ export const PkiCollectionPage = () => {
|
||||
});
|
||||
handlePopUpClose("deletePkiCollection");
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/policies",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/policies",
|
||||
params: {
|
||||
orgId: currentOrg.id,
|
||||
projectId: params.projectId
|
||||
@@ -77,7 +77,7 @@ export const PkiCollectionPage = () => {
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/policies"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/policies"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId: params.projectId
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
import { PkiCollectionDetailsByIDPage } from "./PkiCollectionDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/pki-collections/$collectionId"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId"
|
||||
)({
|
||||
component: PkiCollectionDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Certificate Collections",
|
||||
link: linkOptions({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/policies",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/policies",
|
||||
params: {
|
||||
orgId: params.orgId,
|
||||
projectId: params.projectId
|
||||
|
||||
@@ -64,7 +64,7 @@ const Page = () => {
|
||||
|
||||
handlePopUpClose("deletePkiSubscriber");
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/subscribers",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/subscribers",
|
||||
params: {
|
||||
orgId: currentOrg.id,
|
||||
projectId
|
||||
@@ -77,7 +77,7 @@ const Page = () => {
|
||||
{data && (
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<Link
|
||||
to="/organizations/$orgId/projects/cert-management/$projectId/subscribers"
|
||||
to="/organizations/$orgId/projects/cert-manager/$projectId/subscribers"
|
||||
params={{
|
||||
orgId: currentOrg.id,
|
||||
projectId
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
||||
import { PkiSubscriberDetailsByIDPage } from "./PkiSubscriberDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers/$subscriberName"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/subscribers/$subscriberName"
|
||||
)({
|
||||
component: PkiSubscriberDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -13,7 +13,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Subscribers",
|
||||
link: linkOptions({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/subscribers",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/subscribers",
|
||||
params: {
|
||||
orgId: params.orgId,
|
||||
projectId: params.projectId
|
||||
|
||||
@@ -77,7 +77,7 @@ export const PkiSubscribersTable = ({ handlePopUpOpen }: Props) => {
|
||||
key={`pki-subscriber-${subscriber.id}`}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/subscribers/$subscriberName",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/subscribers/$subscriberName",
|
||||
params: {
|
||||
orgId: currentOrg.id,
|
||||
projectId: currentProject.id,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { PkiSubscribersPage } from "./PkiSubscribersPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/subscribers/"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/subscribers/"
|
||||
)({
|
||||
component: PkiSubscribersPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
import { PkiSyncDetailsByIDPage } from "./index";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/integrations/$syncId"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/integrations/$syncId"
|
||||
)({
|
||||
component: PkiSyncDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Integrations",
|
||||
link: linkOptions({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/integrations",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/integrations",
|
||||
params,
|
||||
search: {
|
||||
selectedTab: IntegrationsListPageTabs.PkiSyncs
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { PkiTemplateListPage } from "./PkiTemplateListPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/certificate-templates/"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/certificate-templates/"
|
||||
)({
|
||||
component: PkiTemplateListPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
@@ -50,12 +50,12 @@ export const PoliciesPage = () => {
|
||||
return (
|
||||
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
|
||||
<Helmet>
|
||||
<title>{t("common.head-title", { title: "Certificate Management" })}</title>
|
||||
<title>{t("common.head-title", { title: "Certificate Manager" })}</title>
|
||||
</Helmet>
|
||||
<div className="mx-auto mb-6 w-full max-w-8xl">
|
||||
<PageHeader
|
||||
scope={ProjectType.CertificateManager}
|
||||
title="Certificate Management"
|
||||
title="Certificate Manager"
|
||||
description="Streamline certificate management by creating and maintaining templates, profiles, and certificates in one place"
|
||||
/>
|
||||
|
||||
|
||||
@@ -79,7 +79,11 @@ const createSchema = z
|
||||
renewBeforeDays: z.number().min(1).max(365).optional()
|
||||
})
|
||||
.optional(),
|
||||
acmeConfig: z.object({}).optional(),
|
||||
acmeConfig: z
|
||||
.object({
|
||||
skipDnsOwnershipVerification: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
externalConfigs: z
|
||||
.object({
|
||||
template: z.string().min(1, "Azure ADCS template is required")
|
||||
@@ -219,7 +223,11 @@ const editSchema = z
|
||||
renewBeforeDays: z.number().min(1).max(365).optional()
|
||||
})
|
||||
.optional(),
|
||||
acmeConfig: z.object({}).optional(),
|
||||
acmeConfig: z
|
||||
.object({
|
||||
skipDnsOwnershipVerification: z.boolean().optional()
|
||||
})
|
||||
.optional(),
|
||||
externalConfigs: z
|
||||
.object({
|
||||
template: z.string().optional()
|
||||
@@ -406,7 +414,13 @@ export const CreateProfileModal = ({
|
||||
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
||||
}
|
||||
: undefined,
|
||||
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined,
|
||||
acmeConfig:
|
||||
profile.enrollmentType === EnrollmentType.ACME
|
||||
? {
|
||||
skipDnsOwnershipVerification:
|
||||
profile.acmeConfig?.skipDnsOwnershipVerification || false
|
||||
}
|
||||
: undefined,
|
||||
externalConfigs: profile.externalConfigs
|
||||
? {
|
||||
template:
|
||||
@@ -429,7 +443,9 @@ export const CreateProfileModal = ({
|
||||
autoRenew: false,
|
||||
renewBeforeDays: 30
|
||||
},
|
||||
acmeConfig: {},
|
||||
acmeConfig: {
|
||||
skipDnsOwnershipVerification: false
|
||||
},
|
||||
externalConfigs: undefined
|
||||
}
|
||||
});
|
||||
@@ -476,7 +492,13 @@ export const CreateProfileModal = ({
|
||||
renewBeforeDays: profile.apiConfig?.renewBeforeDays || 30
|
||||
}
|
||||
: undefined,
|
||||
acmeConfig: profile.enrollmentType === EnrollmentType.ACME ? {} : undefined,
|
||||
acmeConfig:
|
||||
profile.enrollmentType === EnrollmentType.ACME
|
||||
? {
|
||||
skipDnsOwnershipVerification:
|
||||
profile.acmeConfig?.skipDnsOwnershipVerification || false
|
||||
}
|
||||
: undefined,
|
||||
externalConfigs: profile.externalConfigs
|
||||
? {
|
||||
template:
|
||||
@@ -667,7 +689,9 @@ export const CreateProfileModal = ({
|
||||
renewBeforeDays: 30
|
||||
});
|
||||
setValue("estConfig", undefined);
|
||||
setValue("acmeConfig", undefined);
|
||||
setValue("acmeConfig", {
|
||||
skipDnsOwnershipVerification: false
|
||||
});
|
||||
}
|
||||
onChange(value);
|
||||
}}
|
||||
@@ -797,7 +821,9 @@ export const CreateProfileModal = ({
|
||||
} else if (watchedEnrollmentType === "acme") {
|
||||
setValue("estConfig", undefined);
|
||||
setValue("apiConfig", undefined);
|
||||
setValue("acmeConfig", {});
|
||||
setValue("acmeConfig", {
|
||||
skipDnsOwnershipVerification: false
|
||||
});
|
||||
}
|
||||
onChange(value);
|
||||
}}
|
||||
@@ -846,7 +872,9 @@ export const CreateProfileModal = ({
|
||||
} else if (value === "acme") {
|
||||
setValue("apiConfig", undefined);
|
||||
setValue("estConfig", undefined);
|
||||
setValue("acmeConfig", {});
|
||||
setValue("acmeConfig", {
|
||||
skipDnsOwnershipVerification: false
|
||||
});
|
||||
}
|
||||
onChange(value);
|
||||
}}
|
||||
@@ -975,10 +1003,24 @@ export const CreateProfileModal = ({
|
||||
<div className="mb-4 space-y-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="acmeConfig"
|
||||
render={({ fieldState: { error } }) => (
|
||||
name="acmeConfig.skipDnsOwnershipVerification"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<div className="flex items-center gap-2">{/* FIXME: ACME configuration */}</div>
|
||||
<div className="flex items-center gap-3 rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<Checkbox
|
||||
id="skipDnsOwnershipVerification"
|
||||
isChecked={value || false}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<span className="text-sm font-medium text-mineshaft-100">
|
||||
Skip DNS Ownership Validation
|
||||
</span>
|
||||
<p className="text-xs text-bunker-300">
|
||||
Skip DNS ownership verification during ACME certificate issuance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { PoliciesPage } from "./PoliciesPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/policies"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/policies"
|
||||
)({
|
||||
component: PoliciesPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { SettingsPage } from "./SettingsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/settings"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/settings"
|
||||
)({
|
||||
component: SettingsPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PkiManagerLayout } from "@app/layouts/PkiManagerLayout";
|
||||
import { ProjectSelect } from "@app/layouts/ProjectLayout/components/ProjectSelect";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout"
|
||||
)({
|
||||
component: PkiManagerLayout,
|
||||
beforeLoad: async ({ params, context }) => {
|
||||
|
||||
@@ -84,6 +84,7 @@ export const OrgIdentityLinkForm = ({ onClose }: Props) => {
|
||||
onChange={onChange}
|
||||
placeholder="Select machine identity..."
|
||||
// onInputChange={setSearchValue}
|
||||
autoFocus
|
||||
options={rootOrgIdentities}
|
||||
getOptionValue={(option) => option.id}
|
||||
getOptionLabel={(option) => option.name}
|
||||
|
||||
@@ -185,7 +185,7 @@ export const OrgIdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Machine 1" />
|
||||
<Input {...field} autoFocus placeholder="Machine 1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -68,6 +68,12 @@ export const LogsTableRow = ({ auditLog, rowNumber, timezone }: Props) => {
|
||||
{auditLog.actor.type === ActorType.IDENTITY && (
|
||||
<Tag label="identity_name" value={auditLog.actor.metadata.name} />
|
||||
)}
|
||||
{auditLog.actor.type === ActorType.ACME_PROFILE && (
|
||||
<Tag label="acme_profile_id" value={auditLog.actor.metadata.profileId} />
|
||||
)}
|
||||
{auditLog.actor.type === ActorType.ACME_ACCOUNT && (
|
||||
<Tag label="acme_account_id" value={auditLog.actor.metadata.accountId} />
|
||||
)}
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -16,7 +16,7 @@ export const IdentityAuthFieldDisplay = ({ label, children, className }: Props)
|
||||
{children ? (
|
||||
<p className="break-words">{children}</p>
|
||||
) : (
|
||||
<p className="text-muted italic">Not set</p>
|
||||
<p className="text-muted">Not set</p>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
|
||||
@@ -33,7 +33,7 @@ export const ProjectsPage = () => {
|
||||
const hasChildRoute = matches.some(
|
||||
(match) =>
|
||||
match.pathname.includes("/secret-management/") ||
|
||||
match.pathname.includes("/cert-management/") ||
|
||||
match.pathname.includes("/cert-manager/") ||
|
||||
match.pathname.includes("/kms/") ||
|
||||
match.pathname.includes("/pam/") ||
|
||||
match.pathname.includes("/ssh/") ||
|
||||
|
||||
@@ -13,7 +13,7 @@ export const OrgProductSelectSection = () => {
|
||||
enabled: true
|
||||
},
|
||||
pkiProductEnabled: {
|
||||
name: "Certificate Management",
|
||||
name: "Certificate Manager",
|
||||
enabled: true
|
||||
},
|
||||
kmsProductEnabled: {
|
||||
|
||||
@@ -49,7 +49,7 @@ const PROJECT_TYPE_MENU_ITEMS = [
|
||||
value: ProjectType.SecretManager
|
||||
},
|
||||
{
|
||||
label: "Certificates Management",
|
||||
label: "Certificate Manager",
|
||||
value: ProjectType.CertificateManager
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Button } from "@app/components/v2";
|
||||
import { faBorderAll, faList } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { IconButton } from "@app/components/v2";
|
||||
import { PamAccountView } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
@@ -9,30 +12,32 @@ type Props = {
|
||||
export const AccountViewToggle = ({ value, onChange }: Props) => {
|
||||
return (
|
||||
<div className="flex gap-0.5 rounded-md border border-mineshaft-600 bg-mineshaft-800 p-1">
|
||||
<Button
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
onChange(PamAccountView.Flat);
|
||||
}}
|
||||
ariaLabel="grid"
|
||||
size="xs"
|
||||
className={`${
|
||||
value === PamAccountView.Flat ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||
>
|
||||
Hide Folders
|
||||
</Button>
|
||||
<Button
|
||||
<FontAwesomeIcon icon={faBorderAll} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
variant="outline_bg"
|
||||
onClick={() => {
|
||||
onChange(PamAccountView.Nested);
|
||||
}}
|
||||
ariaLabel="list"
|
||||
size="xs"
|
||||
className={`${
|
||||
value === PamAccountView.Nested ? "bg-mineshaft-500" : "bg-transparent"
|
||||
} min-w-[2.4rem] rounded border-none hover:bg-mineshaft-600`}
|
||||
>
|
||||
Show Folders
|
||||
</Button>
|
||||
<FontAwesomeIcon icon={faList} />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { LogInIcon, PackageOpenIcon } from "lucide-react";
|
||||
|
||||
import { Badge, UnstableButton } from "@app/components/v3";
|
||||
import { PAM_RESOURCE_TYPE_MAP, TPamAccount } from "@app/hooks/api/pam";
|
||||
|
||||
type Props = {
|
||||
account: TPamAccount;
|
||||
onAccess: (resource: TPamAccount) => void;
|
||||
accountPath?: string;
|
||||
};
|
||||
|
||||
export const PamAccountCard = ({ account, onAccess, accountPath }: Props) => {
|
||||
const { name, description, resource } = account;
|
||||
|
||||
const { image, name: resourceTypeName } = PAM_RESOURCE_TYPE_MAP[account.resource.resourceType];
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={account.id}
|
||||
className="flex flex-col overflow-clip rounded-sm border border-mineshaft-600 bg-mineshaft-800 p-4 text-start transition-transform duration-100"
|
||||
>
|
||||
<div className="flex items-center gap-3.5">
|
||||
<img
|
||||
alt={resourceTypeName}
|
||||
src={`/images/integrations/${image}`}
|
||||
className="size-10 object-contain"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="truncate text-lg font-medium text-mineshaft-100">{name}</p>
|
||||
<UnstableButton onClick={() => onAccess(account)} size="xs" variant="outline">
|
||||
<LogInIcon />
|
||||
Connect
|
||||
</UnstableButton>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className={`${accountPath ? "text-mineshaft-300" : "text-mineshaft-400"} truncate text-xs leading-4`}
|
||||
>
|
||||
{resourceTypeName} - {accountPath || "root"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="neutral" className="mt-3.5">
|
||||
<PackageOpenIcon />
|
||||
{resource.name}
|
||||
</Badge>
|
||||
<p className="mt-2 truncate text-sm text-mineshaft-400">{description || "No description"}</p>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { formatDistance } from "date-fns";
|
||||
import { FolderIcon, PackageOpenIcon } from "lucide-react";
|
||||
import { PackageOpenIcon } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
@@ -39,8 +39,6 @@ type Props = {
|
||||
onUpdate: (resource: TPamAccount) => void;
|
||||
onDelete: (resource: TPamAccount) => void;
|
||||
search: string;
|
||||
isFlatView: boolean;
|
||||
accountPath?: string;
|
||||
isAccessLoading?: boolean;
|
||||
};
|
||||
|
||||
@@ -50,8 +48,6 @@ export const PamAccountRow = ({
|
||||
onAccess,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
isFlatView,
|
||||
accountPath,
|
||||
isAccessLoading
|
||||
}: Props) => {
|
||||
const { id, name } = account;
|
||||
@@ -95,14 +91,6 @@ export const PamAccountRow = ({
|
||||
<HighlightText text={account.resource.name} highlight={search} />
|
||||
</span>
|
||||
</Badge>
|
||||
{isFlatView && accountPath && (
|
||||
<Badge variant="neutral">
|
||||
<FolderIcon />
|
||||
<span>
|
||||
<HighlightText text={accountPath} highlight={search} />
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
{"lastRotatedAt" in account && account.lastRotatedAt && (
|
||||
<Tooltip
|
||||
className="max-w-sm text-center"
|
||||
|
||||
@@ -62,6 +62,7 @@ import { useListPamAccounts, useListPamResources } from "@app/hooks/api/pam/quer
|
||||
import { AccountViewToggle } from "./AccountViewToggle";
|
||||
import { FolderBreadCrumbs } from "./FolderBreadCrumbs";
|
||||
import { PamAccessAccountModal } from "./PamAccessAccountModal";
|
||||
import { PamAccountCard } from "./PamAccountCard";
|
||||
import { PamAccountRow } from "./PamAccountRow";
|
||||
import { PamAddAccountModal } from "./PamAddAccountModal";
|
||||
import { PamAddFolderModal } from "./PamAddFolderModal";
|
||||
@@ -128,7 +129,7 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
||||
setOrderDirection,
|
||||
setOrderBy
|
||||
} = usePagination<PamAccountOrderBy>(PamAccountOrderBy.Name, {
|
||||
initPerPage: getUserTablePreference("pamAccountsTable", PreferenceKey.PerPage, 20),
|
||||
initPerPage: getUserTablePreference("pamAccountsTable", PreferenceKey.PerPage, 18),
|
||||
initSearch
|
||||
});
|
||||
|
||||
@@ -231,10 +232,25 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
||||
|
||||
const resources = resourcesData?.resources || [];
|
||||
|
||||
function accessAccount(account: TPamAccount) {
|
||||
// For AWS IAM, directly open console without modal
|
||||
if (account.resource.resourceType === PamResourceType.AwsIam) {
|
||||
let fullAccountPath = account?.name;
|
||||
const folderPath = account.folderId ? folderPaths[account.folderId] : undefined;
|
||||
if (folderPath) {
|
||||
const path = folderPath.replace(/^\/+|\/+$/g, "");
|
||||
fullAccountPath = `${path}/${account?.name}`;
|
||||
}
|
||||
|
||||
accessAwsIam(account, fullAccountPath);
|
||||
} else {
|
||||
handlePopUpOpen("accessAccount", account);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{accountView === PamAccountView.Nested && <FolderBreadCrumbs path={accountPath} />}
|
||||
<div className="mt-4 flex gap-2">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex gap-2">
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Read} a={ProjectPermissionSub.PamFolders}>
|
||||
{(isAllowed) =>
|
||||
isAllowed && (
|
||||
@@ -392,32 +408,64 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Accounts
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PamAccountOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PamAccountOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PamAccountOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={2} innerKey="pam-accounts" />}
|
||||
{!isLoading && (
|
||||
<>
|
||||
{accountView !== PamAccountView.Flat &&
|
||||
foldersToRender.map((folder) => (
|
||||
{accountView === PamAccountView.Nested && <FolderBreadCrumbs path={accountPath} />}
|
||||
{accountView === PamAccountView.Flat ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
{filteredAccounts.map((account) => (
|
||||
<PamAccountCard
|
||||
key={account.id}
|
||||
account={account}
|
||||
accountPath={account.folderId ? folderPaths[account.folderId] : undefined}
|
||||
onAccess={(e: TPamAccount) => accessAccount(e)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{!isLoading && isContentEmpty && (
|
||||
<EmptyState
|
||||
title={isSearchEmpty ? "No accounts match search" : "No accounts"}
|
||||
icon={isSearchEmpty ? faSearch : faCircleXmark}
|
||||
className="rounded border border-mineshaft-500"
|
||||
/>
|
||||
)}
|
||||
{Boolean(totalCount) && !isLoading && !isContentEmpty && (
|
||||
<Pagination
|
||||
className="col-span-full justify-start! border-transparent bg-transparent pl-2"
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
perPageList={[9, 18, 48, 99]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<div className="flex items-center">
|
||||
Accounts
|
||||
<IconButton
|
||||
variant="plain"
|
||||
className={getClassName(PamAccountOrderBy.Name)}
|
||||
ariaLabel="sort"
|
||||
onClick={() => handleSort(PamAccountOrderBy.Name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={getColSortIcon(PamAccountOrderBy.Name)} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isLoading && <TableSkeleton columns={2} innerKey="pam-accounts" />}
|
||||
{!isLoading && (
|
||||
<>
|
||||
{foldersToRender.map((folder) => (
|
||||
<PamFolderRow
|
||||
key={folder.id}
|
||||
folder={folder}
|
||||
@@ -427,53 +475,39 @@ export const PamAccountsTable = ({ projectId }: Props) => {
|
||||
onDelete={(e) => handlePopUpOpen("deleteFolder", e)}
|
||||
/>
|
||||
))}
|
||||
{filteredAccounts.map((account) => (
|
||||
<PamAccountRow
|
||||
key={account.id}
|
||||
account={account}
|
||||
search={search}
|
||||
isFlatView={accountView === PamAccountView.Flat}
|
||||
accountPath={account.folderId ? folderPaths[account.folderId] : undefined}
|
||||
isAccessLoading={loadingAccountId === account.id}
|
||||
onAccess={(e: TPamAccount) => {
|
||||
// For AWS IAM, directly open console without modal
|
||||
if (e.resource.resourceType === PamResourceType.AwsIam) {
|
||||
let fullAccountPath = e?.name;
|
||||
const folderPath = e.folderId ? folderPaths[e.folderId] : undefined;
|
||||
if (folderPath) {
|
||||
const path = folderPath.replace(/^\/+|\/+$/g, "");
|
||||
fullAccountPath = `${path}/${e?.name}`;
|
||||
}
|
||||
|
||||
accessAwsIam(e, fullAccountPath);
|
||||
} else {
|
||||
handlePopUpOpen("accessAccount", e);
|
||||
}
|
||||
}}
|
||||
onUpdate={(e) => handlePopUpOpen("updateAccount", e)}
|
||||
onDelete={(e) => handlePopUpOpen("deleteAccount", e)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
{Boolean(totalCount) && !isLoading && (
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && isContentEmpty && (
|
||||
<EmptyState
|
||||
title={isSearchEmpty ? "No accounts match search" : "No accounts"}
|
||||
icon={isSearchEmpty ? faSearch : faCircleXmark}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
{filteredAccounts.map((account) => (
|
||||
<PamAccountRow
|
||||
key={account.id}
|
||||
account={account}
|
||||
search={search}
|
||||
isAccessLoading={loadingAccountId === account.id}
|
||||
onAccess={(e: TPamAccount) => accessAccount(e)}
|
||||
onUpdate={(e) => handlePopUpOpen("updateAccount", e)}
|
||||
onDelete={(e) => handlePopUpOpen("deleteAccount", e)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isLoading && isContentEmpty && (
|
||||
<EmptyState
|
||||
title={isSearchEmpty ? "No accounts match search" : "No accounts"}
|
||||
icon={isSearchEmpty ? faSearch : faCircleXmark}
|
||||
/>
|
||||
)}
|
||||
{Boolean(totalCount) && !isLoading && !isContentEmpty && (
|
||||
<Pagination
|
||||
count={totalCount}
|
||||
page={page}
|
||||
perPage={perPage}
|
||||
onChangePage={(newPage) => setPage(newPage)}
|
||||
onChangePerPage={handlePerPageChange}
|
||||
perPageList={[9, 18, 48, 99]}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
)}
|
||||
<PamDeleteFolderModal
|
||||
isOpen={popUp.deleteFolder.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("deleteFolder", isOpen)}
|
||||
|
||||
@@ -48,6 +48,7 @@ export const PamAddAccountModal = ({
|
||||
onComplete={(account) => {
|
||||
if (onComplete) onComplete(account);
|
||||
onOpenChange(false);
|
||||
setSelectedResource(null);
|
||||
}}
|
||||
onBack={() => setSelectedResource(null)}
|
||||
resourceId={selectedResource.id}
|
||||
|
||||
@@ -170,7 +170,7 @@ export const ProjectIdentityModal = ({ onClose, identity }: ContentProps) => {
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} placeholder="Machine 1" />
|
||||
<Input {...field} autoFocus placeholder="Machine 1" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -113,6 +113,7 @@ export const ProjectLinkIdentityModal = ({ handlePopUpToggle }: Props) => {
|
||||
onChange={onChange}
|
||||
isLoading={isMembershipsLoading}
|
||||
placeholder="Select machine identity..."
|
||||
autoFocus
|
||||
// onInputChange={setSearchValue}
|
||||
options={filteredIdentityMembershipOrgs.map((membership) => ({
|
||||
name: membership.name,
|
||||
|
||||
@@ -151,6 +151,11 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
type: "success"
|
||||
});
|
||||
handlePopUpToggle("addMember", false);
|
||||
if (requesterEmail) {
|
||||
navigate({
|
||||
search: (prev) => ({ ...prev, requesterEmail: "" })
|
||||
});
|
||||
}
|
||||
reset();
|
||||
};
|
||||
|
||||
@@ -203,7 +208,7 @@ export const AddMemberModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||
<Modal
|
||||
isOpen={popUp?.addMember?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen)
|
||||
if (!isOpen && requesterEmail)
|
||||
navigate({
|
||||
search: (prev) => ({ ...prev, requesterEmail: "" })
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ const AccessControlPageQuerySchema = z.object({
|
||||
});
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/access-management"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/access-management"
|
||||
)({
|
||||
component: AccessControlPage,
|
||||
validateSearch: zodValidator(AccessControlPageQuerySchema),
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { AppConnectionsPage } from "./AppConnectionsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/app-connections"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/app-connections"
|
||||
)({
|
||||
component: AppConnectionsPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
|
||||
import { AuditLogsPage } from "./AuditLogsPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/audit-logs"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/audit-logs"
|
||||
)({
|
||||
component: AuditLogsPage,
|
||||
beforeLoad: ({ context }) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
import { GroupDetailsByIDPage } from "./GroupDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/groups/$groupId"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/groups/$groupId"
|
||||
)({
|
||||
component: GroupDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/access-management",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/access-management",
|
||||
params: {
|
||||
orgId: params.orgId,
|
||||
projectId: params.projectId
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { subject } from "@casl/ability";
|
||||
import { DropdownMenu } from "@radix-ui/react-dropdown-menu";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { ChevronLeftIcon, EllipsisIcon, InfoIcon } from "lucide-react";
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgIcon,
|
||||
SubOrgIcon,
|
||||
UnstableAlert,
|
||||
UnstableAlertDescription,
|
||||
UnstableAlertTitle,
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
UnstableCardDescription,
|
||||
UnstableCardHeader,
|
||||
UnstableCardTitle,
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuItem,
|
||||
UnstableDropdownMenuTrigger,
|
||||
@@ -63,7 +64,7 @@ const Page = () => {
|
||||
select: (el) => el.identityId as string
|
||||
});
|
||||
const { currentProject, projectId } = useProject();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentOrg, isSubOrganization } = useOrganization();
|
||||
|
||||
const { data: identityMembershipDetails, isPending: isMembershipDetailsLoading } =
|
||||
useGetProjectIdentityMembershipV2(projectId, identityId);
|
||||
@@ -164,6 +165,12 @@ const Page = () => {
|
||||
return <UnstablePageLoader />;
|
||||
}
|
||||
|
||||
const isOrgIdentity = !isProjectIdentity;
|
||||
const isSubOrgIdentity =
|
||||
isOrgIdentity &&
|
||||
isSubOrganization &&
|
||||
currentOrg.rootOrgId !== identityMembershipDetails?.identity.orgId;
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-8xl flex-col">
|
||||
{identityMembershipDetails ? (
|
||||
@@ -187,7 +194,7 @@ const Page = () => {
|
||||
description={`Configure and manage${isProjectIdentity ? " machine identity and " : " "}project access control`}
|
||||
title={identityMembershipDetails.identity.name}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableButton variant="outline">
|
||||
Options
|
||||
@@ -197,7 +204,7 @@ const Page = () => {
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(identityMembershipDetails.id);
|
||||
navigator.clipboard.writeText(identityMembershipDetails.identity.id);
|
||||
createNotification({
|
||||
text: "Machine identity ID copied to clipboard",
|
||||
type: "info"
|
||||
@@ -211,7 +218,6 @@ const Page = () => {
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identityMembershipDetails?.identity.id
|
||||
})}
|
||||
passThrough={false}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
@@ -221,7 +227,7 @@ const Page = () => {
|
||||
Assume Privileges
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
content="Assume the privileges of the machine identity, allowing you to replicate their access behavior."
|
||||
content="Assume the privileges of this machine identity, allowing you to replicate their access behavior."
|
||||
>
|
||||
<div>
|
||||
<InfoIcon className="text-muted" />
|
||||
@@ -251,12 +257,13 @@ const Page = () => {
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</UnstableDropdownMenu>
|
||||
</PageHeader>
|
||||
<div className="flex flex-col gap-5 lg:flex-row">
|
||||
<ProjectIdentityDetailsSection
|
||||
identity={identity || { ...identityMembershipDetails?.identity, projectId: "" }}
|
||||
isOrgIdentity={!isProjectIdentity}
|
||||
isOrgIdentity={isOrgIdentity}
|
||||
isSubOrgIdentity={isSubOrgIdentity}
|
||||
membership={identityMembershipDetails!}
|
||||
/>
|
||||
|
||||
@@ -275,15 +282,15 @@ const Page = () => {
|
||||
</UnstableCardDescription>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
<UnstableAlert variant="org">
|
||||
<OrgIcon />
|
||||
<UnstableAlert variant={isSubOrgIdentity ? "sub-org" : "org"}>
|
||||
{isSubOrgIdentity ? <SubOrgIcon /> : <OrgIcon />}
|
||||
<UnstableAlertTitle>
|
||||
Machine identity managed by organization
|
||||
Machine identity managed by {isSubOrgIdentity ? "sub-" : ""}organization
|
||||
</UnstableAlertTitle>
|
||||
<UnstableAlertDescription>
|
||||
<p>
|
||||
This machine identity's authentication methods are controlled by your
|
||||
organization. To make changes,{" "}
|
||||
This machine identity's authentication methods are managed by your
|
||||
{isSubOrgIdentity ? "sub-" : ""}organization. <br /> To make changes,{" "}
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionIdentityActions.Read}
|
||||
an={OrgPermissionSubjects.Identity}
|
||||
@@ -295,10 +302,10 @@ const Page = () => {
|
||||
className="inline-block cursor-pointer text-foreground underline underline-offset-2"
|
||||
params={{
|
||||
identityId,
|
||||
orgId: currentOrg.id
|
||||
orgId: identityMembershipDetails.identity.orgId
|
||||
}}
|
||||
>
|
||||
go to organization access control
|
||||
go to {isSubOrgIdentity ? "sub-" : ""}organization access control
|
||||
</Link>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isDirty, isSubmitting }
|
||||
} = form;
|
||||
|
||||
@@ -310,7 +311,7 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
|
||||
variant="link"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={onGoBack}
|
||||
onClick={() => reset()}
|
||||
>
|
||||
Discard Changes
|
||||
</Button>
|
||||
|
||||
@@ -81,9 +81,7 @@ export const IdentityProjectAdditionalPrivilegeSection = ({ identityMembershipDe
|
||||
return (
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader
|
||||
// className="border-b"
|
||||
>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Project Additional Privileges</UnstableCardTitle>
|
||||
<UnstableCardDescription>
|
||||
Assign one-off policies to this machine identity
|
||||
|
||||
@@ -96,9 +96,7 @@ export const IdentityRoleDetailsSection = ({
|
||||
return (
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader
|
||||
// className="border-b"
|
||||
>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Project Roles</UnstableCardTitle>
|
||||
<UnstableCardDescription>
|
||||
Manage roles assigned to this machine identity
|
||||
@@ -208,8 +206,6 @@ export const IdentityRoleDetailsSection = ({
|
||||
a={subject(ProjectPermissionSub.Identity, {
|
||||
identityId: identityMembershipDetails.identity.id
|
||||
})}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
@@ -223,7 +219,6 @@ export const IdentityRoleDetailsSection = ({
|
||||
isDisabled={!isAllowed}
|
||||
variant="danger"
|
||||
>
|
||||
{/* <TrashIcon /> */}
|
||||
Remove Role
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DetailValue,
|
||||
OrgIcon,
|
||||
ProjectIcon,
|
||||
SubOrgIcon,
|
||||
UnstableButtonGroup,
|
||||
UnstableCard,
|
||||
UnstableCardAction,
|
||||
@@ -30,10 +31,16 @@ import { ProjectIdentityModal } from "@app/pages/project/AccessControlPage/compo
|
||||
type Props = {
|
||||
identity: TProjectIdentity;
|
||||
isOrgIdentity?: boolean;
|
||||
isSubOrgIdentity?: boolean;
|
||||
membership: IdentityProjectMembershipV1;
|
||||
};
|
||||
|
||||
export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, membership }: Props) => {
|
||||
export const ProjectIdentityDetailsSection = ({
|
||||
identity,
|
||||
isOrgIdentity,
|
||||
isSubOrgIdentity,
|
||||
membership
|
||||
}: Props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention,@typescript-eslint/no-unused-vars
|
||||
const [_, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
@@ -43,10 +50,8 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
|
||||
return (
|
||||
<>
|
||||
<UnstableCard className="w-full max-w-[22rem]">
|
||||
<UnstableCardHeader
|
||||
// className="border-b"
|
||||
>
|
||||
<UnstableCard className="w-full lg:max-w-[24rem]">
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Details</UnstableCardTitle>
|
||||
<UnstableCardDescription>Machine identity details</UnstableCardDescription>
|
||||
{!isOrgIdentity && (
|
||||
@@ -92,7 +97,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
>
|
||||
{/* TODO(scott): color this should be a button variant */}
|
||||
{/* TODO(scott): color this should be a button variant and create re-usable copy button */}
|
||||
{isCopyingId ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
|
||||
</UnstableIconButton>
|
||||
</Tooltip>
|
||||
@@ -102,9 +107,9 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
<DetailLabel>Managed by</DetailLabel>
|
||||
<DetailValue>
|
||||
{isOrgIdentity ? (
|
||||
<Badge variant="org">
|
||||
<OrgIcon />
|
||||
Organization
|
||||
<Badge variant={isSubOrgIdentity ? "sub-org" : "org"}>
|
||||
{isSubOrgIdentity ? <SubOrgIcon /> : <OrgIcon />}
|
||||
{isSubOrgIdentity ? "Sub-" : ""}Organization
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="project">
|
||||
@@ -129,7 +134,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
</UnstableButtonGroup>
|
||||
))
|
||||
) : (
|
||||
<span className="text-muted italic">No metadata</span>
|
||||
<span className="text-muted">No metadata</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
@@ -145,7 +150,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
{membership.lastLoginAuthMethod ? (
|
||||
identityAuthToNameMap[membership.lastLoginAuthMethod]
|
||||
) : (
|
||||
<span className="text-muted italic">N/A</span>
|
||||
<span className="text-muted">N/A</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
@@ -155,7 +160,7 @@ export const ProjectIdentityDetailsSection = ({ identity, isOrgIdentity, members
|
||||
{membership.lastLoginTime ? (
|
||||
format(membership.lastLoginTime, "PPpp")
|
||||
) : (
|
||||
<span className="text-muted italic">N/A</span>
|
||||
<span className="text-muted">N/A</span>
|
||||
)}
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/identities/$identityId"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/identities/$identityId"
|
||||
)({
|
||||
component: IdentityDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/access-management",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/access-management",
|
||||
params: {
|
||||
orgId: params.orgId,
|
||||
projectId: params.projectId
|
||||
|
||||
@@ -3,25 +3,34 @@ import { useTranslation } from "react-i18next";
|
||||
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Link, useNavigate, useParams } from "@tanstack/react-router";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { EllipsisIcon, InfoIcon } from "lucide-react";
|
||||
|
||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
ConfirmActionModal,
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
PageHeader,
|
||||
Spinner
|
||||
Spinner,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
Badge,
|
||||
UnstableButton,
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuItem,
|
||||
UnstableDropdownMenuTrigger
|
||||
} from "@app/components/v3";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
useOrganization,
|
||||
useProject
|
||||
useProject,
|
||||
useUser
|
||||
} from "@app/context";
|
||||
import { getProjectBaseURL, getProjectHomePage } from "@app/helpers/project";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
@@ -35,6 +44,7 @@ import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
|
||||
import { MemberProjectAdditionalPrivilegeSection } from "./components/MemberProjectAdditionalPrivilegeSection";
|
||||
import { MemberRoleDetailsSection } from "./components/MemberRoleDetailsSection";
|
||||
import { ProjectMemberDetailsSection } from "./components/ProjectMemberDetailsSection";
|
||||
|
||||
export const Page = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -44,12 +54,14 @@ export const Page = () => {
|
||||
});
|
||||
const { currentOrg } = useOrganization();
|
||||
const { currentProject, projectId } = useProject();
|
||||
const {
|
||||
user: { id: currentUserId }
|
||||
} = useUser();
|
||||
|
||||
const { data: membershipDetails, isPending: isMembershipDetailsLoading } =
|
||||
useGetWorkspaceUserDetails(projectId, membershipId);
|
||||
|
||||
const { mutateAsync: removeUserFromWorkspace, isPending: isRemovingUserFromWorkspace } =
|
||||
useDeleteUserFromWorkspace();
|
||||
const { mutateAsync: removeUserFromWorkspace } = useDeleteUserFromWorkspace();
|
||||
const assumePrivileges = useAssumeProjectPrivileges();
|
||||
|
||||
const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
@@ -112,8 +124,10 @@ export const Page = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const isOwnProjectMembershipDetails = currentUserId === membershipDetails?.user?.id;
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex max-w-8xl flex-col justify-between bg-bunker-800 text-white">
|
||||
<div className="mx-auto flex max-w-8xl flex-col">
|
||||
{membershipDetails ? (
|
||||
<>
|
||||
<Link
|
||||
@@ -125,7 +139,7 @@ export const Page = () => {
|
||||
search={{
|
||||
selectedTab: ProjectAccessControlTabs.Member
|
||||
}}
|
||||
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
|
||||
className="mb-4 flex w-fit items-center gap-x-1 text-sm text-mineshaft-400 transition duration-100 hover:text-mineshaft-400/80"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
Project Users
|
||||
@@ -135,62 +149,100 @@ export const Page = () => {
|
||||
title={
|
||||
membershipDetails.user.firstName || membershipDetails.user.lastName
|
||||
? `${membershipDetails.user.firstName} ${membershipDetails.user.lastName}`
|
||||
: "-"
|
||||
: membershipDetails.user.email ||
|
||||
membershipDetails.user.username ||
|
||||
membershipDetails.inviteEmail ||
|
||||
"Unnamed User"
|
||||
}
|
||||
description={`User joined on ${membershipDetails?.createdAt && formatRelative(new Date(membershipDetails?.createdAt || ""), new Date())}`}
|
||||
description="Configure and manage project access control"
|
||||
>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionMemberActions.AssumePrivileges}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Assume privileges of the user"
|
||||
passThrough={false}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
isLoading={assumePrivileges.isPending}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("assumePrivileges", { userId: membershipDetails?.user?.id })
|
||||
}
|
||||
>
|
||||
Assume Privileges
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionMemberActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Remove from project"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="danger"
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
isDisabled={!isAllowed}
|
||||
isLoading={isRemovingUserFromWorkspace}
|
||||
onClick={() => handlePopUpOpen("removeMember")}
|
||||
>
|
||||
Remove User
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
{isOwnProjectMembershipDetails ? (
|
||||
<Tooltip
|
||||
side="right"
|
||||
content="You cannot modify your own membership. Ask a project admin to make changes to your membership."
|
||||
>
|
||||
<Badge variant="info" className="ml-2">
|
||||
<InfoIcon /> Your project membership
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableButton variant="outline">
|
||||
Options
|
||||
<EllipsisIcon />
|
||||
</UnstableButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(membershipDetails.user.id);
|
||||
createNotification({
|
||||
text: "User ID copied to clipboard",
|
||||
type: "info"
|
||||
});
|
||||
}}
|
||||
>
|
||||
Copy User ID
|
||||
</UnstableDropdownMenuItem>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionMemberActions.AssumePrivileges}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() =>
|
||||
handlePopUpOpen("assumePrivileges", {
|
||||
userId: membershipDetails.user.id
|
||||
})
|
||||
}
|
||||
>
|
||||
Assume Privileges
|
||||
<Tooltip
|
||||
side="bottom"
|
||||
content="Assume the privileges of this user, allowing you to replicate their access behavior."
|
||||
>
|
||||
<div>
|
||||
<InfoIcon className="text-muted" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionMemberActions.Delete}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
variant="danger"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={() => handlePopUpOpen("removeMember")}
|
||||
>
|
||||
Remove User From Project
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
)}
|
||||
</PageHeader>
|
||||
<MemberRoleDetailsSection
|
||||
membershipDetails={membershipDetails}
|
||||
isMembershipDetailsLoading={isMembershipDetailsLoading}
|
||||
onOpenUpgradeModal={() =>
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
text: "Assigning custom roles to members can be unlocked if you upgrade to Infisical Pro plan."
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MemberProjectAdditionalPrivilegeSection membershipDetails={membershipDetails} />
|
||||
<div className="flex flex-col gap-5 lg:flex-row">
|
||||
<ProjectMemberDetailsSection membership={membershipDetails} />
|
||||
<div className="flex flex-1 flex-col gap-y-5">
|
||||
<MemberRoleDetailsSection
|
||||
membershipDetails={membershipDetails}
|
||||
isMembershipDetailsLoading={isMembershipDetailsLoading}
|
||||
onOpenUpgradeModal={() =>
|
||||
handlePopUpOpen("upgradePlan", {
|
||||
text: "Assigning custom roles to members can be unlocked if you upgrade to Infisical Pro plan."
|
||||
})
|
||||
}
|
||||
/>
|
||||
<MemberProjectAdditionalPrivilegeSection membershipDetails={membershipDetails} />
|
||||
</div>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.removeMember.isOpen}
|
||||
deleteKey="remove"
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
import { faEllipsisV, faFolder, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format, formatDistance } from "date-fns";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ClockAlertIcon, ClockIcon, EllipsisIcon, PlusIcon } from "lucide-react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { DeleteActionModal, Lottie, Modal, ModalContent, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
Tag,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
Badge,
|
||||
UnstableButton,
|
||||
UnstableCard,
|
||||
UnstableCardAction,
|
||||
UnstableCardContent,
|
||||
UnstableCardDescription,
|
||||
UnstableCardHeader,
|
||||
UnstableCardTitle,
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuItem,
|
||||
UnstableDropdownMenuTrigger,
|
||||
UnstableEmpty,
|
||||
UnstableEmptyContent,
|
||||
UnstableEmptyDescription,
|
||||
UnstableEmptyHeader,
|
||||
UnstableEmptyTitle,
|
||||
UnstableIconButton,
|
||||
UnstableTable,
|
||||
UnstableTableBody,
|
||||
UnstableTableCell,
|
||||
UnstableTableHead,
|
||||
UnstableTableHeader,
|
||||
UnstableTableRow
|
||||
} from "@app/components/v3";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionMemberActions,
|
||||
@@ -68,188 +77,216 @@ export const MemberProjectAdditionalPrivilegeSection = ({ membershipDetails }: P
|
||||
handlePopUpClose("deletePrivilege");
|
||||
};
|
||||
|
||||
const hasAdditionalPrivileges = Boolean(userProjectPrivileges?.length);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<AnimatePresence>
|
||||
{popUp?.modifyPrivilege.isOpen ? (
|
||||
<motion.div
|
||||
key="privilege-modify"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 30 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: 30 }}
|
||||
className="absolute min-h-40 w-full"
|
||||
>
|
||||
<MembershipProjectAdditionalPrivilegeModifySection
|
||||
onGoBack={() => handlePopUpClose("modifyPrivilege")}
|
||||
projectMembershipId={membershipDetails?.id}
|
||||
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||
isDisabled={
|
||||
isOwnProjectMembershipDetails ||
|
||||
permission.cannot(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member)
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="privilege-list"
|
||||
transition={{ duration: 0.3 }}
|
||||
initial={{ opacity: 0, translateX: 0 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0, translateX: -30 }}
|
||||
className="absolute w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">
|
||||
Project Additional Privileges
|
||||
</h3>
|
||||
{userId !== membershipDetails?.user?.id &&
|
||||
membershipDetails?.status !== "invited" && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Add Privilege"
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Project Additional Privileges</UnstableCardTitle>
|
||||
<UnstableCardDescription>Assign one-off policies to this user</UnstableCardDescription>
|
||||
{!isOwnProjectMembershipDetails && hasAdditionalPrivileges && (
|
||||
<UnstableCardAction>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
variant="outline"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyPrivilege");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyPrivilege");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<PlusIcon />
|
||||
Add Additional Privileges
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableCardAction>
|
||||
)}
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{isPending ? (
|
||||
// scott: todo proper loader
|
||||
<div className="flex h-40 w-full items-center justify-center">
|
||||
<Lottie icon="infisical_loading_white" isAutoPlay className="w-16" />
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Name</Th>
|
||||
<Th>Duration</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPending && <TableSkeleton columns={3} innerKey="user-project-memberships" />}
|
||||
{!isPending &&
|
||||
userProjectPrivileges?.map((privilegeDetails) => {
|
||||
const isTemporary = privilegeDetails?.isTemporary;
|
||||
const isExpired =
|
||||
privilegeDetails.isTemporary &&
|
||||
new Date() > new Date(privilegeDetails.temporaryAccessEndTime || "");
|
||||
) : userProjectPrivileges?.length ? (
|
||||
<UnstableTable>
|
||||
<UnstableTableHeader>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableHead className="w-1/2">Name</UnstableTableHead>
|
||||
<UnstableTableHead className="w-1/2">Duration</UnstableTableHead>
|
||||
{!isOwnProjectMembershipDetails && <UnstableTableHead className="w-5" />}
|
||||
</UnstableTableRow>
|
||||
</UnstableTableHeader>
|
||||
<UnstableTableBody>
|
||||
{!isPending &&
|
||||
userProjectPrivileges?.map((privilegeDetails) => {
|
||||
const isTemporary = privilegeDetails?.isTemporary;
|
||||
const isExpired =
|
||||
privilegeDetails.isTemporary &&
|
||||
new Date() > new Date(privilegeDetails.temporaryAccessEndTime || "");
|
||||
|
||||
let text = "Permanent";
|
||||
let toolTipText = "Non-Expiring Access";
|
||||
if (privilegeDetails.isTemporary) {
|
||||
if (isExpired) {
|
||||
text = "Access Expired";
|
||||
toolTipText = "Timed Access Expired";
|
||||
} else {
|
||||
text = formatDistance(
|
||||
new Date(privilegeDetails.temporaryAccessEndTime || ""),
|
||||
new Date()
|
||||
);
|
||||
toolTipText = `Until ${format(
|
||||
new Date(privilegeDetails.temporaryAccessEndTime || ""),
|
||||
"yyyy-MM-dd hh:mm:ss aaa"
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
let text = "Permanent";
|
||||
let toolTipText = "Non-Expiring Access";
|
||||
if (privilegeDetails.isTemporary) {
|
||||
if (isExpired) {
|
||||
text = "Access Expired";
|
||||
toolTipText = "Timed Access Expired";
|
||||
} else {
|
||||
text = formatDistance(
|
||||
new Date(privilegeDetails.temporaryAccessEndTime || ""),
|
||||
new Date()
|
||||
);
|
||||
toolTipText = `Until ${format(
|
||||
new Date(privilegeDetails.temporaryAccessEndTime || ""),
|
||||
"yyyy-MM-dd hh:mm:ss aaa"
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tr
|
||||
key={`user-project-privilege-${privilegeDetails?.id}`}
|
||||
className="group w-full cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(evt) => {
|
||||
if (evt.key === "Enter") {
|
||||
handlePopUpOpen("modifyPrivilege", privilegeDetails);
|
||||
}
|
||||
}}
|
||||
onClick={() => handlePopUpOpen("modifyPrivilege", privilegeDetails)}
|
||||
>
|
||||
<Td>{privilegeDetails.slug}</Td>
|
||||
<Td>
|
||||
<Tooltip asChild={false} content={toolTipText}>
|
||||
<Tag
|
||||
className={twMerge(
|
||||
"capitalize",
|
||||
isTemporary && "text-primary",
|
||||
isExpired && "text-red-600"
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="flex space-x-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
return (
|
||||
<UnstableTableRow key={`user-project-privilege-${privilegeDetails?.id}`}>
|
||||
<UnstableTableCell className="max-w-0 truncate">
|
||||
{privilegeDetails.slug}
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
{isTemporary ? (
|
||||
<Tooltip content={toolTipText}>
|
||||
<Badge
|
||||
className="capitalize"
|
||||
variant={isExpired ? "danger" : "warning"}
|
||||
>
|
||||
{isExpired ? <ClockAlertIcon /> : <ClockIcon />}
|
||||
{text}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</UnstableTableCell>
|
||||
{!isOwnProjectMembershipDetails && (
|
||||
<UnstableTableCell>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton size="xs" variant="ghost">
|
||||
<EllipsisIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete-icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("modifyPrivilege", privilegeDetails);
|
||||
}}
|
||||
>
|
||||
Edit Additional Privilege
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
isDisabled={!isAllowed}
|
||||
variant="danger"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handlePopUpOpen("deletePrivilege", {
|
||||
id: privilegeDetails?.id,
|
||||
slug: privilegeDetails?.slug
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
Remove Additional Privilege
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<IconButton
|
||||
ariaLabel="more-icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsisV} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isPending && !userProjectPrivileges?.length && (
|
||||
<EmptyState title="This user has no additional privileges" icon={faFolder} />
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePrivilege.isOpen}
|
||||
deleteKey="remove"
|
||||
title={`Do you want to remove role ${
|
||||
(popUp?.deletePrivilege?.data as { slug: string; id: string })?.slug
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePrivilege", isOpen)}
|
||||
onDeleteApproved={() => handlePrivilegeDelete()}
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableTableCell>
|
||||
)}
|
||||
</UnstableTableRow>
|
||||
);
|
||||
})}
|
||||
</UnstableTableBody>
|
||||
</UnstableTable>
|
||||
) : (
|
||||
<UnstableEmpty className="border">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>This user has no additional privileges</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
Add an additional privilege to grant one-off access policies
|
||||
</UnstableEmptyDescription>
|
||||
</UnstableEmptyHeader>
|
||||
{!isOwnProjectMembershipDetails && (
|
||||
<UnstableEmptyContent>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
variant="project"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyPrivilege");
|
||||
}}
|
||||
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
||||
>
|
||||
<PlusIcon />
|
||||
Add Additional Privileges
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableEmptyContent>
|
||||
)}
|
||||
</UnstableEmpty>
|
||||
)}
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
<Modal
|
||||
isOpen={popUp.modifyPrivilege.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("modifyPrivilege", isOpen)}
|
||||
>
|
||||
<ModalContent
|
||||
className="max-w-6xl"
|
||||
title="Additional Privileges"
|
||||
subTitle="Additional privileges take precedence over roles when permissions conflict"
|
||||
>
|
||||
<MembershipProjectAdditionalPrivilegeModifySection
|
||||
onGoBack={() => handlePopUpClose("modifyPrivilege")}
|
||||
projectMembershipId={membershipDetails?.id}
|
||||
privilegeId={(popUp?.modifyPrivilege?.data as { id: string })?.id}
|
||||
isDisabled={
|
||||
isOwnProjectMembershipDetails ||
|
||||
permission.cannot(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member)
|
||||
}
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePrivilege.isOpen}
|
||||
deleteKey="remove"
|
||||
title={`Do you want to remove role ${
|
||||
(popUp?.deletePrivilege?.data as { slug: string; id: string })?.slug
|
||||
}?`}
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePrivilege", isOpen)}
|
||||
onDeleteApproved={() => handlePrivilegeDelete()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import { faCaretDown, faChevronLeft, faClock, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCaretDown, faClock, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format, formatDistance } from "date-fns";
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
Tag,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { UnstableSeparator } from "@app/components/v3";
|
||||
import {
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSub,
|
||||
@@ -111,6 +112,7 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isDirty, isSubmitting }
|
||||
} = form;
|
||||
|
||||
@@ -176,59 +178,9 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<form className="flex flex-col gap-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormProvider {...form}>
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-2">
|
||||
<Button
|
||||
leftIcon={<FontAwesomeIcon icon={faChevronLeft} />}
|
||||
className="text-lg font-medium text-mineshaft-100"
|
||||
variant="link"
|
||||
onClick={onGoBack}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isDirty && (
|
||||
<Button
|
||||
className="mr-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={onGoBack}
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
type="submit"
|
||||
className={twMerge(
|
||||
"mr-4 h-10 border border-primary",
|
||||
isDirty && "bg-primary text-black"
|
||||
)}
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<AddPoliciesButton
|
||||
isDisabled={isDisabled}
|
||||
projectType={currentProject.type}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 border-b border-gray-800 p-4 pt-2 first:rounded-t-md last:rounded-b-md">
|
||||
<div className="text-lg">Overview</div>
|
||||
<p className="mb-4 text-sm text-mineshaft-300">
|
||||
Additional privileges take precedence over roles when permissions conflict
|
||||
</p>
|
||||
<div>
|
||||
<div className="flex items-end space-x-6">
|
||||
<div className="w-full max-w-md">
|
||||
<Controller
|
||||
@@ -344,22 +296,61 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="mb-2 text-lg">Policies</div>
|
||||
{(isCreate || !isPending) && <PermissionEmptyState />}
|
||||
<div>
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={subject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[subject].title}
|
||||
key={`project-permission-${subject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(subject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
))}
|
||||
<UnstableSeparator />
|
||||
<div>
|
||||
<div className="mb-3 flex w-full items-center justify-between">
|
||||
<div className="text-lg">Policies</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
{isDirty && (
|
||||
<Button
|
||||
className="mr-4 text-mineshaft-300"
|
||||
variant="link"
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
onClick={() => reset()}
|
||||
>
|
||||
Discard Changes
|
||||
</Button>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<AddPoliciesButton
|
||||
isDisabled={isDisabled}
|
||||
projectType={currentProject.type}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{(isCreate || !isPending) && <PermissionEmptyState />}
|
||||
<div className="scrollbar-thin max-h-[50vh] overflow-y-auto">
|
||||
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map(
|
||||
(permissionSubject) => (
|
||||
<GeneralPermissionPolicies
|
||||
subject={permissionSubject}
|
||||
actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions}
|
||||
title={PROJECT_PERMISSION_OBJECT[permissionSubject].title}
|
||||
key={`project-permission-${permissionSubject}`}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{renderConditionalComponents(permissionSubject, isDisabled)}
|
||||
</GeneralPermissionPolicies>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<UnstableSeparator />
|
||||
<div className="flex w-full items-center justify-end gap-x-2">
|
||||
<Button colorSchema="secondary" variant="plain" onClick={onGoBack}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||
isLoading={isSubmitting}
|
||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</FormProvider>
|
||||
</form>
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import { faFolder, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { format, formatDistance } from "date-fns";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ClockAlertIcon, ClockIcon, EllipsisIcon, PencilIcon } from "lucide-react";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { DeleteActionModal, Lottie, Modal, ModalContent, Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
DeleteActionModal,
|
||||
EmptyState,
|
||||
IconButton,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
Tag,
|
||||
TBody,
|
||||
Td,
|
||||
Th,
|
||||
THead,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
Badge,
|
||||
UnstableButton,
|
||||
UnstableCard,
|
||||
UnstableCardAction,
|
||||
UnstableCardContent,
|
||||
UnstableCardDescription,
|
||||
UnstableCardHeader,
|
||||
UnstableCardTitle,
|
||||
UnstableDropdownMenu,
|
||||
UnstableDropdownMenuContent,
|
||||
UnstableDropdownMenuItem,
|
||||
UnstableDropdownMenuTrigger,
|
||||
UnstableEmpty,
|
||||
UnstableEmptyContent,
|
||||
UnstableEmptyDescription,
|
||||
UnstableEmptyHeader,
|
||||
UnstableEmptyTitle,
|
||||
UnstableIconButton,
|
||||
UnstableTable,
|
||||
UnstableTableBody,
|
||||
UnstableTableCell,
|
||||
UnstableTableHead,
|
||||
UnstableTableHeader,
|
||||
UnstableTableRow
|
||||
} from "@app/components/v3/generic";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProject, useUser } from "@app/context";
|
||||
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
@@ -88,131 +96,176 @@ export const MemberRoleDetailsSection = ({
|
||||
handlePopUpClose("deleteRole");
|
||||
};
|
||||
|
||||
const hasRoles = Boolean(membershipDetails?.roles.length);
|
||||
|
||||
return (
|
||||
<div className="mb-4 w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
|
||||
<h3 className="text-lg font-medium text-mineshaft-100">Project Roles</h3>
|
||||
{!isOwnProjectMembershipDetails && membershipDetails?.status !== "invited" && (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Edit Role(s)"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyRole");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
<>
|
||||
<UnstableCard>
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Project Roles</UnstableCardTitle>
|
||||
<UnstableCardDescription>Manage roles assigned to this user</UnstableCardDescription>
|
||||
{!isOwnProjectMembershipDetails && hasRoles && (
|
||||
<UnstableCardAction>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencil} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
</div>
|
||||
<div className="py-4">
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Role</Th>
|
||||
<Th>Duration</Th>
|
||||
<Th className="w-5" />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isMembershipDetailsLoading && (
|
||||
<TableSkeleton columns={3} innerKey="user-project-memberships" />
|
||||
)}
|
||||
{!isMembershipDetailsLoading &&
|
||||
membershipDetails?.roles?.map((roleDetails) => {
|
||||
const isTemporary = roleDetails?.isTemporary;
|
||||
const isExpired =
|
||||
roleDetails.isTemporary &&
|
||||
new Date() > new Date(roleDetails.temporaryAccessEndTime || "");
|
||||
|
||||
let text = "Permanent";
|
||||
let toolTipText = "Non-Expiring Access";
|
||||
if (roleDetails.isTemporary) {
|
||||
if (isExpired) {
|
||||
text = "Access Expired";
|
||||
toolTipText = "Timed Access Expired";
|
||||
} else {
|
||||
text = formatDistance(
|
||||
new Date(roleDetails.temporaryAccessEndTime || ""),
|
||||
new Date()
|
||||
);
|
||||
toolTipText = `Until ${format(
|
||||
new Date(roleDetails.temporaryAccessEndTime || ""),
|
||||
"yyyy-MM-dd hh:mm:ss aaa"
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tr className="group h-10" key={`user-project-membership-${roleDetails?.id}`}>
|
||||
<Td className="capitalize">
|
||||
{roleDetails.role === "custom"
|
||||
? roleDetails.customRoleName
|
||||
: formatProjectRoleName(roleDetails.role)}
|
||||
</Td>
|
||||
<Td>
|
||||
<Tooltip asChild={false} content={toolTipText}>
|
||||
<Tag
|
||||
className={twMerge(
|
||||
"capitalize",
|
||||
isTemporary && "text-primary",
|
||||
isExpired && "text-red-600"
|
||||
)}
|
||||
>
|
||||
{text}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</Td>
|
||||
<Td>
|
||||
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
renderTooltip
|
||||
allowedLabel="Remove Role"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="copy icon"
|
||||
variant="plain"
|
||||
className="group relative"
|
||||
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", {
|
||||
id: roleDetails?.id,
|
||||
slug: roleDetails?.customRoleName || roleDetails?.role
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
{!isMembershipDetailsLoading && !membershipDetails?.roles?.length && (
|
||||
<EmptyState title="This user has no roles" icon={faFolder} />
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
size="xs"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyRole");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<PencilIcon />
|
||||
Edit Roles
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableCardAction>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
{
|
||||
/* eslint-disable-next-line no-nested-ternary */
|
||||
isMembershipDetailsLoading ? (
|
||||
// scott: todo proper loader
|
||||
<div className="flex h-40 w-full items-center justify-center">
|
||||
<Lottie icon="infisical_loading_white" isAutoPlay className="w-16" />
|
||||
</div>
|
||||
) : hasRoles ? (
|
||||
<UnstableTable>
|
||||
<UnstableTableHeader>
|
||||
<UnstableTableRow>
|
||||
<UnstableTableHead className="w-1/2">Role</UnstableTableHead>
|
||||
<UnstableTableHead className="w-1/2">Duration</UnstableTableHead>
|
||||
{!isOwnProjectMembershipDetails && <UnstableTableHead className="w-5" />}
|
||||
</UnstableTableRow>
|
||||
</UnstableTableHeader>
|
||||
<UnstableTableBody>
|
||||
{membershipDetails?.roles?.map((roleDetails) => {
|
||||
const isTemporary = roleDetails?.isTemporary;
|
||||
const isExpired =
|
||||
roleDetails.isTemporary &&
|
||||
new Date() > new Date(roleDetails.temporaryAccessEndTime || "");
|
||||
|
||||
let text = "Permanent";
|
||||
let toolTipText = "Non-Expiring Access";
|
||||
if (roleDetails.isTemporary) {
|
||||
if (isExpired) {
|
||||
text = "Access Expired";
|
||||
toolTipText = "Timed Access Expired";
|
||||
} else {
|
||||
text = formatDistance(
|
||||
new Date(roleDetails.temporaryAccessEndTime || ""),
|
||||
new Date()
|
||||
);
|
||||
toolTipText = `Until ${format(
|
||||
new Date(roleDetails.temporaryAccessEndTime || ""),
|
||||
"yyyy-MM-dd hh:mm:ss aaa"
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<UnstableTableRow
|
||||
className="group h-10"
|
||||
key={`user-project-identity-${roleDetails?.id}`}
|
||||
>
|
||||
<UnstableTableCell className="max-w-0 truncate">
|
||||
{roleDetails.role === "custom"
|
||||
? roleDetails.customRoleName
|
||||
: formatProjectRoleName(roleDetails.role)}
|
||||
</UnstableTableCell>
|
||||
<UnstableTableCell>
|
||||
{isTemporary ? (
|
||||
<Tooltip content={toolTipText}>
|
||||
<Badge
|
||||
className="capitalize"
|
||||
variant={isExpired ? "danger" : "warning"}
|
||||
>
|
||||
{isExpired ? <ClockAlertIcon /> : <ClockIcon />}
|
||||
{text}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</UnstableTableCell>
|
||||
{!isOwnProjectMembershipDetails && (
|
||||
<UnstableTableCell>
|
||||
<UnstableDropdownMenu>
|
||||
<UnstableDropdownMenuTrigger asChild>
|
||||
<UnstableIconButton size="xs" variant="ghost">
|
||||
<EllipsisIcon />
|
||||
</UnstableIconButton>
|
||||
</UnstableDropdownMenuTrigger>
|
||||
<UnstableDropdownMenuContent align="end">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableDropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handlePopUpOpen("deleteRole", {
|
||||
id: roleDetails?.id,
|
||||
slug: roleDetails?.customRoleName || roleDetails?.role
|
||||
});
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
variant="danger"
|
||||
>
|
||||
Remove Role
|
||||
</UnstableDropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableDropdownMenuContent>
|
||||
</UnstableDropdownMenu>
|
||||
</UnstableTableCell>
|
||||
)}
|
||||
</UnstableTableRow>
|
||||
);
|
||||
})}
|
||||
</UnstableTableBody>
|
||||
</UnstableTable>
|
||||
) : (
|
||||
<UnstableEmpty className="border">
|
||||
<UnstableEmptyHeader>
|
||||
<UnstableEmptyTitle>This user doesn t have any roles</UnstableEmptyTitle>
|
||||
<UnstableEmptyDescription>
|
||||
Give this user one or more roles
|
||||
</UnstableEmptyDescription>
|
||||
</UnstableEmptyHeader>
|
||||
<UnstableEmptyContent>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<UnstableButton
|
||||
variant="project"
|
||||
size="xs"
|
||||
onClick={() => {
|
||||
handlePopUpOpen("modifyRole");
|
||||
}}
|
||||
isDisabled={!isAllowed || isOwnProjectMembershipDetails}
|
||||
>
|
||||
<PencilIcon />
|
||||
Edit Roles
|
||||
</UnstableButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</UnstableEmptyContent>
|
||||
</UnstableEmpty>
|
||||
)
|
||||
}
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteRole.isOpen}
|
||||
deleteKey="remove"
|
||||
@@ -234,6 +287,6 @@ export const MemberRoleDetailsSection = ({
|
||||
/>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { format } from "date-fns";
|
||||
import { CheckIcon, ClipboardListIcon } from "lucide-react";
|
||||
|
||||
import { Tooltip } from "@app/components/v2";
|
||||
import {
|
||||
Detail,
|
||||
DetailGroup,
|
||||
DetailLabel,
|
||||
DetailValue,
|
||||
UnstableCard,
|
||||
UnstableCardContent,
|
||||
UnstableCardDescription,
|
||||
UnstableCardHeader,
|
||||
UnstableCardTitle,
|
||||
UnstableIconButton
|
||||
} from "@app/components/v3";
|
||||
import { useTimedReset } from "@app/hooks";
|
||||
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||
|
||||
type Props = {
|
||||
membership: TWorkspaceUser;
|
||||
};
|
||||
|
||||
export const ProjectMemberDetailsSection = ({ membership }: Props) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/naming-convention
|
||||
const [_copyId, isCopyingId, setCopyTextId] = useTimedReset<string>({
|
||||
initialState: "Copy ID to clipboard"
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/naming-convention
|
||||
const [_copyEmail, isCopyingEmail, setCopyEmail] = useTimedReset<string>({
|
||||
initialState: "Copy email to clipboard"
|
||||
});
|
||||
|
||||
const {
|
||||
user: { email, username, firstName, lastName, id: userId }
|
||||
} = membership;
|
||||
|
||||
const name = firstName || lastName ? `${firstName} ${lastName}`.trim() : null;
|
||||
|
||||
return (
|
||||
<UnstableCard className="w-full lg:max-w-[24rem]">
|
||||
<UnstableCardHeader>
|
||||
<UnstableCardTitle>Details</UnstableCardTitle>
|
||||
<UnstableCardDescription>User membership details</UnstableCardDescription>
|
||||
</UnstableCardHeader>
|
||||
<UnstableCardContent>
|
||||
<DetailGroup>
|
||||
<Detail>
|
||||
<DetailLabel>Name</DetailLabel>
|
||||
<DetailValue>{name || <span className="text-muted">Not set</span>}</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>ID</DetailLabel>
|
||||
<DetailValue className="flex items-center gap-x-1">
|
||||
{membership.user.id}
|
||||
<Tooltip content="Copy user ID to clipboard">
|
||||
<UnstableIconButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(userId);
|
||||
setCopyTextId("Copied");
|
||||
}}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
>
|
||||
{/* TODO(scott): color this should be a button variant and create re-usable copy button */}
|
||||
{isCopyingId ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
|
||||
</UnstableIconButton>
|
||||
</Tooltip>
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
<Detail>
|
||||
<DetailLabel>Email</DetailLabel>
|
||||
<DetailValue className="flex items-center gap-x-1">
|
||||
{email}
|
||||
<Tooltip content="Copy user email to clipboard">
|
||||
<UnstableIconButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(email);
|
||||
setCopyEmail("Copied");
|
||||
}}
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
>
|
||||
{/* TODO(scott): color this should be a button variant and create re-usable copy button */}
|
||||
{isCopyingEmail ? <CheckIcon /> : <ClipboardListIcon className="text-label" />}
|
||||
</UnstableIconButton>
|
||||
</Tooltip>
|
||||
</DetailValue>
|
||||
</Detail>
|
||||
{username !== email && (
|
||||
<Detail>
|
||||
<DetailLabel>Username</DetailLabel>
|
||||
<DetailValue>{username || <span className="text-muted">Not set</span>}</DetailValue>
|
||||
</Detail>
|
||||
)}
|
||||
<Detail>
|
||||
<DetailLabel>Joined project</DetailLabel>
|
||||
<DetailValue>{format(membership.createdAt, "PPpp")}</DetailValue>
|
||||
</Detail>
|
||||
</DetailGroup>
|
||||
</UnstableCardContent>
|
||||
</UnstableCard>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/members/$membershipId"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/members/$membershipId"
|
||||
)({
|
||||
component: MemberDetailsByIDPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/access-management",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/access-management",
|
||||
params: {
|
||||
orgId: params.orgId,
|
||||
projectId: params.projectId
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ProjectAccessControlTabs } from "@app/types/project";
|
||||
import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage";
|
||||
|
||||
export const Route = createFileRoute(
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-management/$projectId/_cert-manager-layout/roles/$roleSlug"
|
||||
"/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/cert-manager/$projectId/_cert-manager-layout/roles/$roleSlug"
|
||||
)({
|
||||
component: RoleDetailsBySlugPage,
|
||||
beforeLoad: ({ context, params }) => {
|
||||
@@ -15,7 +15,7 @@ export const Route = createFileRoute(
|
||||
{
|
||||
label: "Access Control",
|
||||
link: linkOptions({
|
||||
to: "/organizations/$orgId/projects/cert-management/$projectId/access-management",
|
||||
to: "/organizations/$orgId/projects/cert-manager/$projectId/access-management",
|
||||
params: {
|
||||
orgId: params.orgId,
|
||||
projectId: params.projectId
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { Helmet } from "react-helmet";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faWarning } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
@@ -14,7 +12,6 @@ import {
|
||||
useProject
|
||||
} from "@app/context";
|
||||
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||
import { useGetWorkspaceIntegrations } from "@app/hooks/api";
|
||||
import { ProjectType } from "@app/hooks/api/projects/types";
|
||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||
|
||||
@@ -35,9 +32,6 @@ export const IntegrationsListPage = () => {
|
||||
from: ROUTE_PATHS.SecretManager.IntegrationsListPage.id
|
||||
});
|
||||
|
||||
const { data: integrations } = useGetWorkspaceIntegrations(currentProject.id);
|
||||
const hasNativeIntegrations = Boolean(integrations?.length);
|
||||
|
||||
const updateSelectedTab = (tab: string) => {
|
||||
navigate({
|
||||
to: ROUTE_PATHS.SecretManager.IntegrationsListPage.path,
|
||||
@@ -77,11 +71,9 @@ export const IntegrationsListPage = () => {
|
||||
<Tab variant="project" value={IntegrationsListPageTabs.InfrastructureIntegrations}>
|
||||
Infrastructure Integrations
|
||||
</Tab>
|
||||
{hasNativeIntegrations && (
|
||||
<Tab variant="project" value={IntegrationsListPageTabs.NativeIntegrations}>
|
||||
Native Integrations
|
||||
</Tab>
|
||||
)}
|
||||
<Tab variant="project" value={IntegrationsListPageTabs.NativeIntegrations}>
|
||||
Native Integrations
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={IntegrationsListPageTabs.SecretSyncs}>
|
||||
<ProjectPermissionCan
|
||||
@@ -98,47 +90,15 @@ export const IntegrationsListPage = () => {
|
||||
<TabPanel value={IntegrationsListPageTabs.InfrastructureIntegrations}>
|
||||
<InfrastructureIntegrationTab />
|
||||
</TabPanel>
|
||||
{hasNativeIntegrations && (
|
||||
<TabPanel value={IntegrationsListPageTabs.NativeIntegrations}>
|
||||
<div className="mb-4 flex items-start rounded-md border border-yellow-600/75 bg-yellow-900/20 px-3 py-2">
|
||||
<div className="flex text-sm text-yellow-100">
|
||||
<FontAwesomeIcon icon={faWarning} className="mt-1 mr-2 text-yellow-600" />
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
We're moving Native Integrations to{" "}
|
||||
<a
|
||||
href="https://infisical.com/docs/integrations/secret-syncs/overview"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-2 hover:text-mineshaft-100"
|
||||
>
|
||||
Secret Syncs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p className="mt-0.5 text-yellow-100/80">
|
||||
If the integration you need isn't available in the Secret Syncs menu,
|
||||
please get in touch with us at{" "}
|
||||
<a
|
||||
href="mailto:team@infisical.com"
|
||||
className="underline underline-offset-2 hover:text-mineshaft-100"
|
||||
>
|
||||
team@infisical.com
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Integrations}
|
||||
>
|
||||
<NativeIntegrationsTab />
|
||||
</ProjectPermissionCan>
|
||||
</TabPanel>
|
||||
)}
|
||||
<TabPanel value={IntegrationsListPageTabs.NativeIntegrations}>
|
||||
<ProjectPermissionCan
|
||||
renderGuardBanner
|
||||
I={ProjectPermissionActions.Read}
|
||||
a={ProjectPermissionSub.Integrations}
|
||||
>
|
||||
<NativeIntegrationsTab />
|
||||
</ProjectPermissionCan>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user