Compare commits

...

15 Commits

Author SHA1 Message Date
67c1cb9bf1 fix: this pr addresses null null name issue with invited users 2024-08-23 15:40:06 +08:00
f0938330a7 Merge pull request #2326 from Infisical/daniel/disallow-user-creation-on-member-group-fix
Fix: Disallow org members to invite new members
2024-08-22 17:33:46 -04:00
e1bb0ac3ad Update org-permission.ts 2024-08-23 01:21:57 +04:00
f54d930de2 Fix: Disallow org members to invite new members 2024-08-23 01:13:45 +04:00
4a1dfda41f Merge pull request #2324 from Infisical/maidul-udfysfgj32
Remove service token depreciation notice
2024-08-22 14:29:55 -04:00
c238b7b6ae remove service token notice 2024-08-22 13:57:40 -04:00
83d314ba32 Merge pull request #2314 from Infisical/install-external-ca
Install Intermediate CA with External Parent CA
2024-08-22 09:41:52 -07:00
b94a0ffa6c Merge pull request #2322 from akhilmhdh/fix/build-mismatch-lines
feat: added backend build sourcemap for line matching
2024-08-22 09:34:12 -04:00
=
b60e404243 feat: added backend build sourcemap for line matching 2024-08-22 15:18:33 +05:30
10120e1825 Merge pull request #2317 from akhilmhdh/feat/debounce-last-used
feat: added identity and service token postgres update debounced
2024-08-22 00:50:54 -04:00
31e66c18e7 Merge pull request #2320 from Infisical/maidul-deuyfgwyu
Set default to host sts endpoint for aws auth
2024-08-21 22:38:49 -04:00
fb06f5a3bc default to host sts for aws auth 2024-08-21 22:29:30 -04:00
=
e821a11271 feat: added identity and service token postgres update debounced 2024-08-21 22:21:31 +05:30
af4428acec Add external parent ca support to docs 2024-08-20 22:43:29 -07:00
61370cc6b2 Finish allow installing intermediate CA with external parent CA 2024-08-20 21:44:41 -07:00
34 changed files with 1480 additions and 4136 deletions

4530
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,9 +34,9 @@
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine", "dev": "tsx watch --clear-screen=false ./src/main.ts | pino-pretty --colorize --colorizeObjects --singleLine",
"dev:docker": "nodemon", "dev:docker": "nodemon",
"build": "tsup", "build": "tsup --sourcemap",
"build:frontend": "npm run build --prefix ../frontend", "build:frontend": "npm run build --prefix ../frontend",
"start": "node dist/main.mjs", "start": "node --enable-source-maps dist/main.mjs",
"type:check": "tsc --noEmit", "type:check": "tsc --noEmit",
"lint:fix": "eslint --fix --ext js,ts ./src", "lint:fix": "eslint --fix --ext js,ts ./src",
"lint": "eslint 'src/**/*.ts'", "lint": "eslint 'src/**/*.ts'",

View File

@ -126,7 +126,6 @@ const buildMemberPermission = () => {
can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Read, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace); can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member); can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups); can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role); can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings); can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);

View File

@ -19,11 +19,15 @@ export const KeyStorePrefixes = {
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) => SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const, `sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) => SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
`sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const `sync-integration-last-run-${projectId}-${environmentSlug}-${secretPath}` as const,
IdentityAccessTokenStatusUpdate: (identityAccessTokenId: string) =>
`identity-access-token-status:${identityAccessTokenId}`,
ServiceTokenStatusUpdate: (serviceTokenId: string) => `service-token-status:${serviceTokenId}`
}; };
export const KeyStoreTtls = { export const KeyStoreTtls = {
SetSyncSecretIntegrationLastRunTimestampInSeconds: 10 SetSyncSecretIntegrationLastRunTimestampInSeconds: 10,
AccessTokenStatusUpdateInSeconds: 120
}; };
type TWaitTillReady = { type TWaitTillReady = {

View File

@ -1,3 +1,8 @@
export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000; export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
export const secondsToMillis = (seconds: number) => seconds * 1000; export const secondsToMillis = (seconds: number) => seconds * 1000;
export const applyJitter = (delayMs: number, jitterMs: number) => {
const jitter = Math.floor(Math.random() * (2 * jitterMs)) - jitterMs;
return delayMs + jitter;
};

View File

@ -27,7 +27,8 @@ export enum QueueName {
CaCrlRotation = "ca-crl-rotation", CaCrlRotation = "ca-crl-rotation",
SecretReplication = "secret-replication", SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration" ProjectV3Migration = "project-v3-migration",
AccessTokenStatusUpdate = "access-token-status-update"
} }
export enum QueueJobs { export enum QueueJobs {
@ -48,7 +49,9 @@ export enum QueueJobs {
CaCrlRotation = "ca-crl-rotation-job", CaCrlRotation = "ca-crl-rotation-job",
SecretReplication = "secret-replication", SecretReplication = "secret-replication",
SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication SecretSync = "secret-sync", // parent queue to push integration sync, webhook, and secret replication
ProjectV3Migration = "project-v3-migration" ProjectV3Migration = "project-v3-migration",
IdentityAccessTokenStatusUpdate = "identity-access-token-status-update",
ServiceTokenStatusUpdate = "service-token-status-update"
} }
export type TQueueJobTypes = { export type TQueueJobTypes = {
@ -148,6 +151,15 @@ export type TQueueJobTypes = {
name: QueueJobs.ProjectV3Migration; name: QueueJobs.ProjectV3Migration;
payload: { projectId: string }; payload: { projectId: string };
}; };
[QueueName.AccessTokenStatusUpdate]:
| {
name: QueueJobs.IdentityAccessTokenStatusUpdate;
payload: { identityAccessTokenId: string; numberOfUses: number };
}
| {
name: QueueJobs.ServiceTokenStatusUpdate;
payload: { serviceTokenId: string };
};
}; };
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>; export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@ -73,6 +73,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { TQueueServiceFactory } from "@app/queue"; import { TQueueServiceFactory } from "@app/queue";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { accessTokenQueueServiceFactory } from "@app/services/access-token-queue/access-token-queue";
import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal";
import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service";
import { authDALFactory } from "@app/services/auth/auth-dal"; import { authDALFactory } from "@app/services/auth/auth-dal";
@ -953,12 +954,20 @@ export const registerRoutes = async (
kmsService kmsService
}); });
const accessTokenQueue = accessTokenQueueServiceFactory({
keyStore,
identityAccessTokenDAL,
queueService,
serviceTokenDAL
});
const serviceTokenService = serviceTokenServiceFactory({ const serviceTokenService = serviceTokenServiceFactory({
projectEnvDAL, projectEnvDAL,
serviceTokenDAL, serviceTokenDAL,
userDAL, userDAL,
permissionService, permissionService,
projectDAL projectDAL,
accessTokenQueue
}); });
const identityService = identityServiceFactory({ const identityService = identityServiceFactory({
@ -968,10 +977,13 @@ export const registerRoutes = async (
identityProjectDAL, identityProjectDAL,
licenseService licenseService
}); });
const identityAccessTokenService = identityAccessTokenServiceFactory({ const identityAccessTokenService = identityAccessTokenServiceFactory({
identityAccessTokenDAL, identityAccessTokenDAL,
identityOrgMembershipDAL identityOrgMembershipDAL,
accessTokenQueue
}); });
const identityProjectService = identityProjectServiceFactory({ const identityProjectService = identityProjectServiceFactory({
permissionService, permissionService,
projectDAL, projectDAL,

View File

@ -0,0 +1,125 @@
import { z } from "zod";
import { KeyStorePrefixes, KeyStoreTtls, TKeyStoreFactory } from "@app/keystore/keystore";
import { applyJitter, secondsToMillis } from "@app/lib/dates";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
import { TServiceTokenDALFactory } from "../service-token/service-token-dal";
type TAccessTokenQueueServiceFactoryDep = {
queueService: TQueueServiceFactory;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry">;
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "updateById">;
serviceTokenDAL: Pick<TServiceTokenDALFactory, "updateById">;
};
export type TAccessTokenQueueServiceFactory = ReturnType<typeof accessTokenQueueServiceFactory>;
export const AccessTokenStatusSchema = z.object({
lastUpdatedAt: z.string().datetime(),
numberOfUses: z.number()
});
export const accessTokenQueueServiceFactory = ({
queueService,
keyStore,
identityAccessTokenDAL,
serviceTokenDAL
}: TAccessTokenQueueServiceFactoryDep) => {
const getIdentityTokenDetailsInCache = async (identityAccessTokenId: string) => {
const tokenDetailsInCache = await keyStore.getItem(
KeyStorePrefixes.IdentityAccessTokenStatusUpdate(identityAccessTokenId)
);
if (tokenDetailsInCache) {
return AccessTokenStatusSchema.parseAsync(JSON.parse(tokenDetailsInCache));
}
};
const updateServiceTokenStatus = async (serviceTokenId: string) => {
await keyStore.setItemWithExpiry(
KeyStorePrefixes.ServiceTokenStatusUpdate(serviceTokenId),
KeyStoreTtls.AccessTokenStatusUpdateInSeconds,
JSON.stringify({ lastUpdatedAt: new Date() })
);
await queueService.queue(
QueueName.AccessTokenStatusUpdate,
QueueJobs.ServiceTokenStatusUpdate,
{
serviceTokenId
},
{
delay: applyJitter(secondsToMillis(KeyStoreTtls.AccessTokenStatusUpdateInSeconds / 2), secondsToMillis(10)),
// https://docs.bullmq.io/guide/jobs/job-ids
jobId: KeyStorePrefixes.ServiceTokenStatusUpdate(serviceTokenId).replaceAll(":", "_"),
removeOnFail: true,
removeOnComplete: true
}
);
};
const updateIdentityAccessTokenStatus = async (identityAccessTokenId: string, numberOfUses: number) => {
await keyStore.setItemWithExpiry(
KeyStorePrefixes.IdentityAccessTokenStatusUpdate(identityAccessTokenId),
KeyStoreTtls.AccessTokenStatusUpdateInSeconds,
JSON.stringify({ lastUpdatedAt: new Date(), numberOfUses })
);
await queueService.queue(
QueueName.AccessTokenStatusUpdate,
QueueJobs.IdentityAccessTokenStatusUpdate,
{
identityAccessTokenId,
numberOfUses
},
{
delay: applyJitter(secondsToMillis(KeyStoreTtls.AccessTokenStatusUpdateInSeconds / 2), secondsToMillis(10)),
jobId: KeyStorePrefixes.IdentityAccessTokenStatusUpdate(identityAccessTokenId).replaceAll(":", "_"),
removeOnFail: true,
removeOnComplete: true
}
);
};
queueService.start(QueueName.AccessTokenStatusUpdate, async (job) => {
// for identity token update
if (job.name === QueueJobs.IdentityAccessTokenStatusUpdate && "identityAccessTokenId" in job.data) {
const { identityAccessTokenId } = job.data;
const tokenDetails = { lastUpdatedAt: new Date(job.timestamp), numberOfUses: job.data.numberOfUses };
const tokenDetailsInCache = await getIdentityTokenDetailsInCache(identityAccessTokenId);
if (tokenDetailsInCache) {
tokenDetails.numberOfUses = tokenDetailsInCache.numberOfUses;
tokenDetails.lastUpdatedAt = new Date(tokenDetailsInCache.lastUpdatedAt);
}
await identityAccessTokenDAL.updateById(identityAccessTokenId, {
accessTokenLastUsedAt: tokenDetails.lastUpdatedAt,
accessTokenNumUses: tokenDetails.numberOfUses
});
return;
}
// for service token
if (job.name === QueueJobs.ServiceTokenStatusUpdate && "serviceTokenId" in job.data) {
const { serviceTokenId } = job.data;
const tokenDetailsInCache = await keyStore.getItem(KeyStorePrefixes.ServiceTokenStatusUpdate(serviceTokenId));
let lastUsed = new Date(job.timestamp);
if (tokenDetailsInCache) {
const tokenDetails = await AccessTokenStatusSchema.pick({ lastUpdatedAt: true }).parseAsync(
JSON.parse(tokenDetailsInCache)
);
lastUsed = new Date(tokenDetails.lastUpdatedAt);
}
await serviceTokenDAL.updateById(serviceTokenId, {
lastUsed
});
}
});
queueService.listen(QueueName.AccessTokenStatusUpdate, "failed", (_, err) => {
logger.error(err, `${QueueName.AccessTokenStatusUpdate}: Failed to updated access token status`);
});
return { updateIdentityAccessTokenStatus, updateServiceTokenStatus, getIdentityTokenDetailsInCache };
};

View File

@ -583,7 +583,13 @@ export const authLoginServiceFactory = ({
} else { } else {
const isLinkingRequired = !user?.authMethods?.includes(authMethod); const isLinkingRequired = !user?.authMethods?.includes(authMethod);
if (isLinkingRequired) { if (isLinkingRequired) {
user = await userDAL.updateById(user.id, { authMethods: [...(user.authMethods || []), authMethod] }); // we update the names here because upon org invitation, the names are set to be NULL
// if user is signing up with SSO after invitation, their names should be set based on their SSO profile
user = await userDAL.updateById(user.id, {
authMethods: [...(user.authMethods || []), authMethod],
firstName: !user.isAccepted ? firstName : undefined,
lastName: !user.isAccepted ? lastName : undefined
});
} }
} }

View File

@ -368,7 +368,6 @@ export const certificateAuthorityServiceFactory = ({
); );
if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" }); if (ca.type === CaType.ROOT) throw new BadRequestError({ message: "Root CA cannot generate CSR" });
if (ca.activeCaCertId) throw new BadRequestError({ message: "CA already has a certificate installed" });
const { caPrivateKey, caPublicKey } = await getCaCredentials({ const { caPrivateKey, caPublicKey } = await getCaCredentials({
caId, caId,
@ -407,7 +406,8 @@ export const certificateAuthorityServiceFactory = ({
/** /**
* Renew certificate for CA with id [caId] * Renew certificate for CA with id [caId]
* Note: Currently implements CA renewal with same key-pair only * Note 1: This CA renewal method is only applicable to CAs with internal parent CAs
* Note 2: Currently implements CA renewal with same key-pair only
*/ */
const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => { const renewCaCert = async ({ caId, notAfter, actorId, actorAuthMethod, actor, actorOrgId }: TRenewCaCertDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
@ -888,9 +888,9 @@ export const certificateAuthorityServiceFactory = ({
}; };
/** /**
* Import certificate for (un-installed) CA with id [caId]. * Import certificate for CA with id [caId].
* Note: Can be used to import an external certificate and certificate chain * Note: Can be used to import an external certificate and certificate chain
* to be installed into the CA. * to be into an installed or uninstalled CA.
*/ */
const importCertToCa = async ({ const importCertToCa = async ({
caId, caId,
@ -917,7 +917,18 @@ export const certificateAuthorityServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
if (ca.activeCaCertId) throw new BadRequestError({ message: "CA has already imported a certificate" }); if (ca.parentCaId) {
/**
* re-evaluate in the future if we should allow users to import a new CA certificate for an intermediate
* CA chained to an internal parent CA. Doing so would allow users to re-chain the CA to a different
* internal CA.
*/
throw new BadRequestError({
message: "Cannot import certificate to intermediate CA chained to internal parent CA"
});
}
const caCert = ca.activeCaCertId ? await certificateAuthorityCertDAL.findById(ca.activeCaCertId) : undefined;
const certObj = new x509.X509Certificate(certificate); const certObj = new x509.X509Certificate(certificate);
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength; const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
@ -988,7 +999,7 @@ export const certificateAuthorityServiceFactory = ({
caId: ca.id, caId: ca.id,
encryptedCertificate, encryptedCertificate,
encryptedCertificateChain, encryptedCertificateChain,
version: 1, version: caCert ? caCert.version + 1 : 1,
caSecretId: caSecret.id caSecretId: caSecret.id
}, },
tx tx

View File

@ -5,6 +5,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip"; import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
import { AuthTokenType } from "../auth/auth-type"; import { AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
@ -13,19 +14,24 @@ import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity
type TIdentityAccessTokenServiceFactoryDep = { type TIdentityAccessTokenServiceFactoryDep = {
identityAccessTokenDAL: TIdentityAccessTokenDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory;
accessTokenQueue: Pick<
TAccessTokenQueueServiceFactory,
"updateIdentityAccessTokenStatus" | "getIdentityTokenDetailsInCache"
>;
}; };
export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>; export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>;
export const identityAccessTokenServiceFactory = ({ export const identityAccessTokenServiceFactory = ({
identityAccessTokenDAL, identityAccessTokenDAL,
identityOrgMembershipDAL identityOrgMembershipDAL,
accessTokenQueue
}: TIdentityAccessTokenServiceFactoryDep) => { }: TIdentityAccessTokenServiceFactoryDep) => {
const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => { const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => {
const { const {
id: tokenId, id: tokenId,
accessTokenTTL,
accessTokenNumUses, accessTokenNumUses,
accessTokenTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
accessTokenLastRenewedAt, accessTokenLastRenewedAt,
createdAt: accessTokenCreatedAt createdAt: accessTokenCreatedAt
@ -83,7 +89,12 @@ export const identityAccessTokenServiceFactory = ({
}); });
if (!identityAccessToken) throw new UnauthorizedError(); if (!identityAccessToken) throw new UnauthorizedError();
await validateAccessTokenExp(identityAccessToken); let { accessTokenNumUses } = identityAccessToken;
const tokenStatusInCache = await accessTokenQueue.getIdentityTokenDetailsInCache(identityAccessToken.id);
if (tokenStatusInCache) {
accessTokenNumUses = tokenStatusInCache.numberOfUses;
}
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
const { accessTokenMaxTTL, createdAt: accessTokenCreatedAt, accessTokenTTL } = identityAccessToken; const { accessTokenMaxTTL, createdAt: accessTokenCreatedAt, accessTokenTTL } = identityAccessToken;
@ -164,14 +175,14 @@ export const identityAccessTokenServiceFactory = ({
throw new UnauthorizedError({ message: "Identity does not belong to any organization" }); throw new UnauthorizedError({ message: "Identity does not belong to any organization" });
} }
await validateAccessTokenExp(identityAccessToken); let { accessTokenNumUses } = identityAccessToken;
const tokenStatusInCache = await accessTokenQueue.getIdentityTokenDetailsInCache(identityAccessToken.id);
if (tokenStatusInCache) {
accessTokenNumUses = tokenStatusInCache.numberOfUses;
}
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
await identityAccessTokenDAL.updateById(identityAccessToken.id, { await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1);
accessTokenLastUsedAt: new Date(),
$incr: {
accessTokenNumUses: 1
}
});
return { ...identityAccessToken, orgId: identityOrgMembership.orgId }; return { ...identityAccessToken, orgId: identityOrgMembership.orgId };
}; };

View File

@ -65,7 +65,7 @@ export const identityAwsAuthServiceFactory = ({
} }
}: { data: TGetCallerIdentityResponse } = await axios({ }: { data: TGetCallerIdentityResponse } = await axios({
method: iamHttpRequestMethod, method: iamHttpRequestMethod,
url: identityAwsAuth.stsEndpoint, url: headers?.Host ? `https://${headers.Host}` : identityAwsAuth.stsEndpoint,
headers, headers,
data: body data: body
}); });

View File

@ -8,6 +8,7 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { TAccessTokenQueueServiceFactory } from "../access-token-queue/access-token-queue";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
@ -26,6 +27,7 @@ type TServiceTokenServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
projectDAL: Pick<TProjectDALFactory, "findById">; projectDAL: Pick<TProjectDALFactory, "findById">;
accessTokenQueue: Pick<TAccessTokenQueueServiceFactory, "updateServiceTokenStatus">;
}; };
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>; export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
@ -35,7 +37,8 @@ export const serviceTokenServiceFactory = ({
userDAL, userDAL,
permissionService, permissionService,
projectEnvDAL, projectEnvDAL,
projectDAL projectDAL,
accessTokenQueue
}: TServiceTokenServiceFactoryDep) => { }: TServiceTokenServiceFactoryDep) => {
const createServiceToken = async ({ const createServiceToken = async ({
iv, iv,
@ -166,11 +169,9 @@ export const serviceTokenServiceFactory = ({
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceToken.secretHash); const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceToken.secretHash);
if (!isMatch) throw new UnauthorizedError(); if (!isMatch) throw new UnauthorizedError();
const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, { await accessTokenQueue.updateServiceTokenStatus(serviceToken.id);
lastUsed: new Date()
});
return { ...serviceToken, lastUsed: updatedToken.lastUsed, orgId: project.orgId }; return { ...serviceToken, lastUsed: new Date(), orgId: project.orgId };
}; };
return { return {

View File

@ -24,8 +24,8 @@ graph TD
A typical workflow for setting up a Private CA hierarchy consists of the following steps: A typical workflow for setting up a Private CA hierarchy consists of the following steps:
1. Configuring a root CA with details like name, validity period, and path length. 1. Configuring an Infisical root CA with details like name, validity period, and path length — This step is optional if you wish to use an external root CA.
2. Configuring and chaining intermediate CA(s) with details like name, validity period, path length, and imported certificate. 2. Configuring and chaining intermediate CA(s) with details like name, validity period, path length, and imported certificate to your Root CA.
3. Managing the CA lifecycle events such as CA succession. 3. Managing the CA lifecycle events such as CA succession.
<Note> <Note>
@ -39,19 +39,21 @@ A typical workflow for setting up a Private CA hierarchy consists of the followi
## Guide to Creating a CA Hierarchy ## Guide to Creating a CA Hierarchy
In the following steps, we explore how to create a simple Private CA hierarchy In the following steps, we explore how to create a simple Private CA hierarchy
consisting of a root CA and an intermediate CA. consisting of an (optional) root CA and an intermediate CA.
<Tabs> <Tabs>
<Tab title="Infisical UI"> <Tab title="Infisical UI">
<Steps> <Steps>
<Step title="Creating a root CA"> <Step title="Creating a root CA">
If you wish to use an external root CA, you can skip this step and head to step 2 to create an intermediate CA.
To create a root CA, head to your Project > Internal PKI > Certificate Authorities and press **Create CA**. To create a root CA, head to your Project > Internal PKI > Certificate Authorities and press **Create CA**.
![pki create ca](/images/platform/pki/ca-create.png) ![pki create ca](/images/platform/pki/ca/ca-create.png)
Here, set the **CA Type** to **Root** and fill out details for the root CA. Here, set the **CA Type** to **Root** and fill out details for the root CA.
![pki create root ca](/images/platform/pki/ca-create-root.png) ![pki create root ca](/images/platform/pki/ca/ca-create-root.png)
Here's some guidance on each field: Here's some guidance on each field:
@ -71,17 +73,19 @@ consisting of a root CA and an intermediate CA.
</Note> </Note>
</Step> </Step>
<Step title="Creating an intermediate CA"> <Step title="Creating an intermediate CA">
1.1. To create an intermediate CA, press **Create CA** again but this time specifying the **CA Type** to be **Intermediate**. Fill out the details for the intermediate CA. 2.1. To create an intermediate CA, press **Create CA** again but this time specifying the **CA Type** to be **Intermediate**. Fill out the details for the intermediate CA.
![pki create intermediate ca](/images/platform/pki/ca-create-intermediate.png) ![pki create intermediate ca](/images/platform/pki/ca/ca-create-intermediate.png)
1.2. Next, press the **Install Certificate** option on the intermediate CA from step 1.1. 2.2. Next, press the **Install Certificate** option on the intermediate CA from step 1.1.
![pki install cert opt](/images/platform/pki/ca-install-intermediate-opt.png) ![pki install cert opt](/images/platform/pki/ca/ca-install-intermediate-opt.png)
Here, set the **Parent CA** to the root CA created in step 1 and configure the intended **Valid Until** and **Path Length** fields on the intermediate CA; feel free to use the prefilled values. 2.3a. If you created a root CA in step 1, select **Infisical CA** for the **Parent CA Type** field.
![pki install cert](/images/platform/pki/ca-install-intermediate.png) Next, set the **Parent CA** to the root CA created in step 1 and configure the intended **Valid Until** and **Path Length** fields on the intermediate CA; feel free to use the prefilled values.
![pki install cert](/images/platform/pki/ca/ca-install-intermediate.png)
Here's some guidance on each field: Here's some guidance on each field:
@ -91,17 +95,30 @@ consisting of a root CA and an intermediate CA.
Finally, press **Install** to chain the intermediate CA to the root CA; this creates a Certificate Signing Request (CSR) for the intermediate CA, creates an intermediate certificate using the root CA private key and CSR, and imports the signed certificate back to the intermediate CA. Finally, press **Install** to chain the intermediate CA to the root CA; this creates a Certificate Signing Request (CSR) for the intermediate CA, creates an intermediate certificate using the root CA private key and CSR, and imports the signed certificate back to the intermediate CA.
![pki cas](/images/platform/pki/cas.png) ![pki cas](/images/platform/pki/ca/cas.png)
Great! You've successfully created a Private CA hierarchy with a root CA and an intermediate CA. Great! You've successfully created a Private CA hierarchy with a root CA and an intermediate CA.
Now check out the [Certificates](/documentation/platform/pki/certificates) page to learn more about how to issue X.509 certificates using the intermediate CA. Now check out the [Certificates](/documentation/platform/pki/certificates) page to learn more about how to issue X.509 certificates using the intermediate CA.
2.3b. If you have an external root CA, select **External CA** for the **Parent CA Type** field.
Next, use the provided intermediate CSR to generate a certificate from your external root CA and paste the PEM-encoded certificate back into the **Certificate Body** field; the PEM-encoded external root CA certificate should be pasted under the **Certificate Chain** field.
![pki ca csr](/images/platform/pki/ca/ca-install-intermediate-csr.png)
Finally, press **Install** to import the certificate and certificate chain as part of the installation step for the intermediate CA
Great! You've successfully created a Private CA hierarchy with an intermediate CA chained to an external root CA.
Now check out the [Certificates](/documentation/platform/pki/certificates) page to learn more about how to issue X.509 certificates using the intermediate CA.
</Step> </Step>
</Steps> </Steps>
</Tab> </Tab>
<Tab title="API"> <Tab title="API">
<Steps> <Steps>
<Step title="Creating a root CA"> <Step title="Creating a root CA">
If you wish to use an external root CA, you can skip this step and head to step 2 to create an intermediate CA.
To create a root CA, make an API request to the [Create CA](/api-reference/endpoints/certificate-authorities/create) API endpoint, specifying the `type` as `root`. To create a root CA, make an API request to the [Create CA](/api-reference/endpoints/certificate-authorities/create) API endpoint, specifying the `type` as `root`.
### Sample request ### Sample request
@ -181,6 +198,8 @@ consisting of a root CA and an intermediate CA.
} }
``` ```
If using an external root CA, then use the CSR to generate a certificate for the intermediate CA using your external root CA and skip to step 2.4.
2.3. Next, create an intermediate certificate by making an API request to the [Sign Intermediate](/api-reference/endpoints/certificate-authorities/sign-intermediate) API endpoint 2.3. Next, create an intermediate certificate by making an API request to the [Sign Intermediate](/api-reference/endpoints/certificate-authorities/sign-intermediate) API endpoint
containing the CSR from step 2.2, referencing the root CA created in step 1. containing the CSR from step 2.2, referencing the root CA created in step 1.
@ -212,6 +231,8 @@ consisting of a root CA and an intermediate CA.
2.4. Finally, import the intermediate certificate and certificate chain from step 2.3 back to the intermediate CA by making an API request to the [Import Certificate](/api-reference/endpoints/certificate-authorities/import-cert) API endpoint. 2.4. Finally, import the intermediate certificate and certificate chain from step 2.3 back to the intermediate CA by making an API request to the [Import Certificate](/api-reference/endpoints/certificate-authorities/import-cert) API endpoint.
If using an external root CA, then import the generated certificate and root CA certificate under certificate chain back into the intermediate CA.
### Sample request ### Sample request
```bash Request ```bash Request
@ -242,7 +263,17 @@ consisting of a root CA and an intermediate CA.
## Guide to CA Renewal ## Guide to CA Renewal
In the following steps, we explore how to renew a CA certificate via same key pair. In the following steps, we explore how to renew a CA certificate.
<Note>
If renewing an intermediate CA chained to an Infisical CA, then Infisical will
automate the process of generating a new certificate for the intermediate CA for you.
If renewing an intermediate CA chained to an external parent CA, you'll be
required to generate a new certificate from the external parent CA and manually import
the certificate back to the intermediate CA.
</Note>
<Tabs> <Tabs>
<Tab title="Infisical UI"> <Tab title="Infisical UI">
@ -296,4 +327,10 @@ In the following steps, we explore how to renew a CA certificate via same key pa
At the moment, Infisical only supports CA renewal via same key pair. We At the moment, Infisical only supports CA renewal via same key pair. We
anticipate supporting CA renewal via new key pair in the coming month. anticipate supporting CA renewal via new key pair in the coming month.
</Accordion> </Accordion>
<Accordion title="Does Infisical support chaining an Intermediate CA to an external Root CA?">
Yes. You may obtain a CSR from the Intermediate CA and use it to generate a
certificate from your external Root CA. The certificate, along with the Root
CA certificate, can be imported back to the Intermediate CA as part of the
CA installation step.
</Accordion>
</AccordionGroup> </AccordionGroup>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 417 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 492 KiB

View File

@ -11,7 +11,7 @@ type Props = {
}; };
const textAreaVariants = cva( const textAreaVariants = cva(
"textarea w-full p-2 focus:ring-2 ring-primary-800 outline-none border border-solid text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50", "textarea w-full p-2 focus:ring-2 ring-primary-800 outline-none border text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50",
{ {
variants: { variants: {
size: { size: {
@ -25,13 +25,13 @@ const textAreaVariants = cva(
false: "" false: ""
}, },
variant: { variant: {
filled: ["bg-bunker-800", "text-gray-400"], filled: ["bg-mineshaft-900", "text-gray-400"],
outline: ["bg-transparent"], outline: ["bg-transparent"],
plain: "bg-transparent outline-none" plain: "bg-transparent outline-none"
}, },
isError: { isError: {
true: "focus:ring-red/50 placeholder-red-300 border-red", true: "focus:ring-red/50 placeholder-red-300 border-red",
false: "focus:ring-primary/50 border-mineshaft-400" false: "focus:ring-primary-400/50 focus:ring-1 border-mineshaft-500"
} }
}, },
compoundVariants: [ compoundVariants: [

View File

@ -22,7 +22,6 @@ import { usePopUp } from "@app/hooks/usePopUp";
import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal"; import { CaModal } from "@app/views/Project/CertificatesPage/components/CaTab/components/CaModal";
import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal"; import { CaInstallCertModal } from "../CertificatesPage/components/CaTab/components/CaInstallCertModal";
import { TabSections } from "../Types";
import { CaCertificatesSection, CaDetailsSection, CaRenewalModal } from "./components"; import { CaCertificatesSection, CaDetailsSection, CaRenewalModal } from "./components";
export const CaPage = withProjectPermission( export const CaPage = withProjectPermission(

View File

@ -6,7 +6,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Tooltip } from "@app/components/v2"; import { Button, IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useTimedReset } from "@app/hooks"; import { useTimedReset } from "@app/hooks";
import { CaStatus, useGetCaById } from "@app/hooks/api"; import { CaStatus, CaType, useGetCaById } from "@app/hooks/api";
import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants"; import { caStatusToNameMap, caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants"; import { certKeyAlgorithmToNameMap } from "@app/hooks/api/certificates/constants";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
@ -35,6 +35,10 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3> <h3 className="text-lg font-semibold text-mineshaft-100">CA Details</h3>
</div> </div>
<div className="pt-4"> <div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA Type</p>
<p className="text-sm text-mineshaft-300">{caTypeToNameMap[ca.type]}</p>
</div>
<div className="mb-4"> <div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA ID</p> <p className="text-sm font-semibold text-mineshaft-300">CA ID</p>
<div className="group flex align-top"> <div className="group flex align-top">
@ -56,26 +60,30 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
</div> </div>
</div> </div>
</div> </div>
{ca.parentCaId && ( {ca.type === CaType.INTERMEDIATE && ca.status !== CaStatus.PENDING_CERTIFICATE && (
<div className="mb-4"> <div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Parent CA ID</p> <p className="text-sm font-semibold text-mineshaft-300">Parent CA ID</p>
<div className="group flex align-top"> <div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{ca.parentCaId}</p> <p className="text-sm text-mineshaft-300">
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100"> {ca.parentCaId ? ca.parentCaId : "N/A - External Parent CA"}
<Tooltip content={copyTextParentId}> </p>
<IconButton {ca.parentCaId && (
ariaLabel="copy icon" <div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
variant="plain" <Tooltip content={copyTextParentId}>
className="group relative ml-2" <IconButton
onClick={() => { ariaLabel="copy icon"
navigator.clipboard.writeText(ca.parentCaId as string); variant="plain"
setCopyTextParentId("Copied"); className="group relative ml-2"
}} onClick={() => {
> navigator.clipboard.writeText(ca.parentCaId as string);
<FontAwesomeIcon icon={isCopyingParentId ? faCheck : faCopy} /> setCopyTextParentId("Copied");
</IconButton> }}
</Tooltip> >
</div> <FontAwesomeIcon icon={isCopyingParentId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
)}
</div> </div>
</div> </div>
)} )}
@ -83,10 +91,6 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
<p className="text-sm font-semibold text-mineshaft-300">Friendly Name</p> <p className="text-sm font-semibold text-mineshaft-300">Friendly Name</p>
<p className="text-sm text-mineshaft-300">{ca.friendlyName}</p> <p className="text-sm text-mineshaft-300">{ca.friendlyName}</p>
</div> </div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">CA Type</p>
<p className="text-sm text-mineshaft-300">{caTypeToNameMap[ca.type]}</p>
</div>
<div className="mb-4"> <div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Status</p> <p className="text-sm font-semibold text-mineshaft-300">Status</p>
<p className="text-sm text-mineshaft-300">{caStatusToNameMap[ca.status]}</p> <p className="text-sm text-mineshaft-300">{caStatusToNameMap[ca.status]}</p>
@ -124,6 +128,15 @@ export const CaDetailsSection = ({ caId, handlePopUpOpen }: Props) => {
colorSchema="primary" colorSchema="primary"
type="submit" type="submit"
onClick={() => { onClick={() => {
if (ca.type === CaType.INTERMEDIATE && !ca.parentCaId) {
// intermediate CA with external parent CA
handlePopUpOpen("installCaCert", {
caId,
isParentCaExternal: true
});
return;
}
handlePopUpOpen("renewCa", { handlePopUpOpen("renewCa", {
caId caId
}); });

View File

@ -1,53 +1,10 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
import {
// DatePicker,
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
CaStatus,
useGetCaById,
useGetCaCsr,
useImportCaCertificate,
useListWorkspaceCas,
useSignIntermediate
} from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const isValidDate = (dateString: string) => { import { ExternalCaInstallForm } from "./ExternalCaInstallForm";
const date = new Date(dateString); import { InternalCaInstallForm } from "./InternalCaInstallForm";
return !Number.isNaN(date.getTime());
};
const getMiddleDate = (date1: Date, date2: Date) => {
const timestamp1 = date1.getTime();
const timestamp2 = date2.getTime();
const middleTimestamp = (timestamp1 + timestamp2) / 2;
return new Date(middleTimestamp);
};
const schema = z.object({
parentCaId: z.string(),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
maxPathLength: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = { type Props = {
popUp: UsePopUpState<["installCaCert"]>; popUp: UsePopUpState<["installCaCert"]>;
@ -60,234 +17,23 @@ enum ParentCaType {
} }
export const CaInstallCertModal = ({ popUp, handlePopUpToggle }: Props) => { export const CaInstallCertModal = ({ popUp, handlePopUpToggle }: Props) => {
const [parentCaType] = useState<ParentCaType>(ParentCaType.Internal); const popupData = popUp?.installCaCert?.data;
const { currentWorkspace } = useWorkspace(); const caId = popupData?.caId || "";
const caId = (popUp?.installCaCert?.data as { caId: string })?.caId || ""; const isParentCaExternal = popupData?.isParentCaExternal || false;
const [parentCaType, setParentCaType] = useState<ParentCaType>(ParentCaType.Internal);
// const [isStartDatePickerOpen, setIsStartDatePickerOpen] = useState(false);
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "",
status: CaStatus.ACTIVE
});
const { data: ca } = useGetCaById(caId);
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: signIntermediate } = useSignIntermediate();
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
setValue,
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
maxPathLength: "0"
}
});
useEffect(() => { useEffect(() => {
if (cas?.length) { if (popupData?.isParentCaExternal) {
setValue("parentCaId", cas[0].id); setParentCaType(ParentCaType.External);
} }
}, [cas, setValue]); }, [popupData]);
const parentCaId = watch("parentCaId");
const { data: parentCa } = useGetCaById(parentCaId);
useEffect(() => {
if (parentCa?.maxPathLength) {
setValue(
"maxPathLength",
(parentCa.maxPathLength === -1 ? 3 : parentCa.maxPathLength - 1).toString()
);
}
if (parentCa?.notAfter) {
const parentCaNotAfter = new Date(parentCa.notAfter);
const middleDate = getMiddleDate(new Date(), parentCaNotAfter);
setValue("notAfter", format(middleDate, "yyyy-MM-dd"));
}
}, [parentCa]);
const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
const { certificate, certificateChain } = await signIntermediate({
caId: parentCaId,
csr,
maxPathLength: Number(maxPathLength),
notAfter,
notBefore: new Date().toISOString()
});
await importCaCertificate({
caId,
projectSlug: currentWorkspace?.slug,
certificate,
certificateChain
});
reset();
createNotification({
text: "Successfully installed certificate for CA",
type: "success"
});
handlePopUpToggle("installCaCert", false);
} catch (err) {
createNotification({
text: "Failed to install certificate for CA",
type: "error"
});
}
};
function generatePathLengthOpts(parentCaMaxPathLength: number): number[] {
if (parentCaMaxPathLength === -1) {
return [-1, 0, 1, 2, 3];
}
return Array.from({ length: parentCaMaxPathLength }, (_, index) => index);
}
const renderForm = (parentCaTypeInput: ParentCaType) => { const renderForm = (parentCaTypeInput: ParentCaType) => {
switch (parentCaTypeInput) { switch (parentCaTypeInput) {
case ParentCaType.Internal: case ParentCaType.Internal:
return ( return <InternalCaInstallForm caId={caId} handlePopUpToggle={handlePopUpToggle} />;
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="parentCaId"
defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Parent CA"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
isRequired
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{(cas || [])
.filter((c) => {
const isParentCaNotSelf = c.id !== ca?.id;
const isParentCaActive = c.status === CaStatus.ACTIVE;
const isParentCaAllowedChildrenCas =
c.maxPathLength && c.maxPathLength !== 0;
return (
isParentCaNotSelf && isParentCaActive && isParentCaAllowedChildrenCas
);
})
.map(({ id, type, dn }) => (
<SelectItem value={id} key={`parent-ca-${id}`}>
{`${caTypeToNameMap[type]}: ${dn}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{/* <Controller
name="notAfter"
control={control}
defaultValue={getDefaultNotAfterDate()}
render={({ field: { onChange, ...field }, fieldState: { error } }) => {
return (
<FormControl
label="Validity"
errorText={error?.message}
isError={Boolean(error)}
className="mr-4"
>
<DatePicker
value={field.value || undefined}
onChange={(date) => {
onChange(date);
setIsStartDatePickerOpen(false);
}}
popUpProps={{
open: isStartDatePickerOpen,
onOpenChange: setIsStartDatePickerOpen
}}
popUpContentProps={{}}
/>
</FormControl>
);
}}
/> */}
<Controller
control={control}
name="notAfter"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Valid Until"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="YYYY-MM-DD" />
</FormControl>
)}
/>
<Controller
control={control}
name="maxPathLength"
// defaultValue="0"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Path Length"
errorText={error?.message}
isError={Boolean(error)}
className="mt-4"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{generatePathLengthOpts(parentCa?.maxPathLength || 0).map((value) => (
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
{`${value}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Install
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("installCaCert", false)}
>
Cancel
</Button>
</div>
</form>
);
default: default:
return <div>External TODO</div>; return <ExternalCaInstallForm caId={caId} handlePopUpToggle={handlePopUpToggle} />;
} }
}; };
@ -296,31 +42,32 @@ export const CaInstallCertModal = ({ popUp, handlePopUpToggle }: Props) => {
isOpen={popUp?.installCaCert?.isOpen} isOpen={popUp?.installCaCert?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("installCaCert", isOpen); handlePopUpToggle("installCaCert", isOpen);
reset();
}} }}
> >
<ModalContent title="Install Intermediate CA certificate"> <ModalContent
{/* <FormControl label="Parent CA Type" className="mt-4"> title={`${isParentCaExternal ? "Renew" : "Install"} Intermediate CA certificate`}
>
<FormControl label="Parent CA Type">
<Select <Select
defaultValue={ParentCaType.Internal}
value={parentCaType} value={parentCaType}
onValueChange={(e) => setParentCaType(e as ParentCaType)} onValueChange={(e) => setParentCaType(e as ParentCaType)}
className="w-full" className="w-full"
isDisabled={isParentCaExternal}
> >
<SelectItem <SelectItem
value={ParentCaType.Internal} value={ParentCaType.Internal}
key={`parent-ca-type-${ParentCaType.Internal}`} key={`parent-ca-type-${ParentCaType.Internal}`}
> >
Infisical Private CA Infisical CA
</SelectItem> </SelectItem>
<SelectItem <SelectItem
value={ParentCaType.External} value={ParentCaType.External}
key={`parent-ca-type-${ParentCaType.External}`} key={`parent-ca-type-${ParentCaType.External}`}
> >
External Private CA External CA
</SelectItem> </SelectItem>
</Select> </Select>
</FormControl> */} </FormControl>
{renderForm(parentCaType)} {renderForm(parentCaType)}
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@ -0,0 +1,172 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import FileSaver from "file-saver";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton,TextArea, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetCaCsr, useImportCaCertificate } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
certificate: z.string().min(1),
certificateChain: z.string().min(1)
});
export type FormData = z.infer<typeof schema>;
type Props = {
caId: string;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["installCaCert"]>, state?: boolean) => void;
};
export const ExternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const [copyTextCaCsr, isCopyingCaCsr, setCopyTextCaCsr] = useTimedReset<string>({
initialState: "Copy to clipboard"
});
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema)
});
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
useEffect(() => {
reset();
}, []);
const onFormSubmit = async ({ certificate, certificateChain }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
await importCaCertificate({
caId,
projectSlug: currentWorkspace?.slug,
certificate,
certificateChain
});
reset();
createNotification({
text: "Successfully installed certificate for CA",
type: "success"
});
handlePopUpToggle("installCaCert", false);
} catch (err) {
createNotification({
text: "Failed to install certificate for CA",
type: "error"
});
}
};
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
{csr && (
<>
<div className="my-4 flex items-center justify-between">
<h2>CSR for this CA</h2>
<div className="flex">
<Tooltip content={copyTextCaCsr}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(csr);
setCopyTextCaCsr("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingCaCsr ? faCheck : faCopy} />
</IconButton>
</Tooltip>
<Tooltip content="Download">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("csr.pem", csr);
}}
>
<FontAwesomeIcon icon={faDownload} />
</IconButton>
</Tooltip>
</div>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 whitespace-pre-wrap break-all">{csr}</p>
</div>
</>
)}
<Controller
control={control}
name="certificate"
render={({ field, fieldState: { error } }) => (
<FormControl label="Certificate Body" errorText={error?.message} isError={Boolean(error)}>
<TextArea
{...field}
placeholder="PEM-encoded certificate..."
reSize="none"
className="h-48"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="certificateChain"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Certificate Chain"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea
{...field}
placeholder="PEM-encoded certificate chain..."
reSize="none"
className="h-48"
/>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Install
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("installCaCert", false)}
>
Cancel
</Button>
</div>
</form>
);
};

View File

@ -0,0 +1,236 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input,Select, SelectItem } from "@app/components/v2";
import { useWorkspace } from "@app/context";
import {
CaStatus,
useGetCaById,
useGetCaCsr,
useImportCaCertificate,
useListWorkspaceCas,
useSignIntermediate
} from "@app/hooks/api";
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
import { UsePopUpState } from "@app/hooks/usePopUp";
const isValidDate = (dateString: string) => {
const date = new Date(dateString);
return !Number.isNaN(date.getTime());
};
const getMiddleDate = (date1: Date, date2: Date) => {
const timestamp1 = date1.getTime();
const timestamp2 = date2.getTime();
const middleTimestamp = (timestamp1 + timestamp2) / 2;
return new Date(middleTimestamp);
};
const schema = z.object({
parentCaId: z.string(),
notAfter: z.string().trim().refine(isValidDate, { message: "Invalid date format" }),
maxPathLength: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = {
caId: string;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["installCaCert"]>, state?: boolean) => void;
};
export const InternalCaInstallForm = ({ caId, handlePopUpToggle }: Props) => {
const { currentWorkspace } = useWorkspace();
const { data: cas } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "",
status: CaStatus.ACTIVE
});
const { data: ca } = useGetCaById(caId);
const { data: csr } = useGetCaCsr(caId);
const { mutateAsync: signIntermediate } = useSignIntermediate();
const { mutateAsync: importCaCertificate } = useImportCaCertificate();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting },
setValue,
watch
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
maxPathLength: "0"
}
});
useEffect(() => {
reset();
}, []);
useEffect(() => {
if (cas?.length) {
setValue("parentCaId", cas[0].id);
}
}, [cas, setValue]);
const parentCaId = watch("parentCaId");
const { data: parentCa } = useGetCaById(parentCaId);
useEffect(() => {
if (parentCa?.maxPathLength) {
setValue(
"maxPathLength",
(parentCa.maxPathLength === -1 ? 3 : parentCa.maxPathLength - 1).toString()
);
}
if (parentCa?.notAfter) {
const parentCaNotAfter = new Date(parentCa.notAfter);
const middleDate = getMiddleDate(new Date(), parentCaNotAfter);
setValue("notAfter", format(middleDate, "yyyy-MM-dd"));
}
}, [parentCa]);
const onFormSubmit = async ({ notAfter, maxPathLength }: FormData) => {
try {
if (!csr || !caId || !currentWorkspace?.slug) return;
const { certificate, certificateChain } = await signIntermediate({
caId: parentCaId,
csr,
maxPathLength: Number(maxPathLength),
notAfter,
notBefore: new Date().toISOString()
});
await importCaCertificate({
caId,
projectSlug: currentWorkspace?.slug,
certificate,
certificateChain
});
reset();
createNotification({
text: "Successfully installed certificate for CA",
type: "success"
});
handlePopUpToggle("installCaCert", false);
} catch (err) {
createNotification({
text: "Failed to install certificate for CA",
type: "error"
});
}
};
function generatePathLengthOpts(parentCaMaxPathLength: number): number[] {
if (parentCaMaxPathLength === -1) {
return [-1, 0, 1, 2, 3];
}
return Array.from({ length: parentCaMaxPathLength }, (_, index) => index);
}
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="parentCaId"
// defaultValue=""
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl
label="Parent CA"
errorText={error?.message}
isError={Boolean(error)}
isRequired
>
<Select
// defaultValue={field.value}
{...field}
onValueChange={onChange}
className="w-full"
>
{(cas || [])
.filter((c) => {
const isParentCaNotSelf = c.id !== ca?.id;
const isParentCaActive = c.status === CaStatus.ACTIVE;
const isParentCaAllowedChildrenCas = c.maxPathLength && c.maxPathLength !== 0;
return isParentCaNotSelf && isParentCaActive && isParentCaAllowedChildrenCas;
})
.map(({ id, type, dn }) => (
<SelectItem value={id} key={`parent-ca-${id}`}>
{`${caTypeToNameMap[type]}: ${dn}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="notAfter"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Valid Until"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="YYYY-MM-DD" />
</FormControl>
)}
/>
<Controller
control={control}
name="maxPathLength"
// defaultValue="0"
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Path Length" errorText={error?.message} isError={Boolean(error)}>
<Select
// defaultValue={field.value}
{...field}
onValueChange={onChange}
className="w-full"
>
{generatePathLengthOpts(parentCa?.maxPathLength || 0).map((value) => (
<SelectItem value={String(value)} key={`ca-path-length-${value}`}>
{`${value}`}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<div className="flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Install
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpToggle("installCaCert", false)}
>
Cancel
</Button>
</div>
</form>
);
};

View File

@ -1,5 +1,5 @@
import { faWarning } from "@fortawesome/free-solid-svg-icons"; // import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; // import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { ServiceTokenSection } from "./components"; import { ServiceTokenSection } from "./components";
@ -14,7 +14,7 @@ export const ServiceTokenTab = () => {
exit={{ opacity: 0, translateX: 30 }} exit={{ opacity: 0, translateX: 30 }}
> >
<div className="space-y-3"> <div className="space-y-3">
<div className="flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white"> {/* <div className="flex w-full flex-row items-center rounded-md border border-primary-600/70 bg-primary/[.07] p-4 text-base text-white">
<FontAwesomeIcon icon={faWarning} className="pr-6 text-4xl text-white/80" /> <FontAwesomeIcon icon={faWarning} className="pr-6 text-4xl text-white/80" />
<div className="flex w-full flex-col text-sm"> <div className="flex w-full flex-col text-sm">
<span className="mb-4 text-lg font-semibold">Deprecation Notice</span> <span className="mb-4 text-lg font-semibold">Deprecation Notice</span>
@ -41,7 +41,7 @@ export const ServiceTokenTab = () => {
</a> </a>
</p> </p>
</div> </div>
</div> </div> */}
<ServiceTokenSection /> <ServiceTokenSection />
</div> </div>
</motion.div> </motion.div>