Compare commits

..

47 Commits

Author SHA1 Message Date
Daniel Hougaard
a6b852fab9 Fix: Type errors / cleanup 2024-08-26 15:13:18 +04:00
Daniel Hougaard
2a043afe11 Cleanup 2024-08-26 15:13:18 +04:00
Daniel Hougaard
df8f2cf9ab Update integration-sync-secret.ts 2024-08-26 15:13:18 +04:00
Daniel Hougaard
a18015b1e5 Fix: Use unique parameter for passing devops org name
Used to be teamId, now it's azureDevopsOrgName.
2024-08-26 15:13:18 +04:00
Daniel Hougaard
8b80622d2f Cleanup 2024-08-26 15:13:18 +04:00
Daniel Hougaard
c0fd0a56f3 Update integration-list.ts 2024-08-26 15:13:18 +04:00
Daniel Hougaard
326764dd41 Feat: Azure DevOps Integration 2024-08-26 15:13:18 +04:00
Daniel Hougaard
c130fbddd9 Merge pull request #2321 from Infisical/daniel/specify-roles-and-projects
Feat: Select roles & projects when inviting members to organization
2024-08-26 15:00:39 +04:00
Maidul Islam
10a97f4522 update python docs to point to new repo 2024-08-25 18:02:51 -04:00
Daniel Hougaard
a2b994ab23 Requested changes 2024-08-24 11:00:05 +04:00
Maidul Islam
c4715124dc Merge pull request #2327 from Infisical/fix/resolve-name-null-null
fix: this pr addresses null null name issue with invited users
2024-08-23 14:01:27 -04:00
BlackMagiq
68b1984a76 Merge pull request #2325 from Infisical/crl-update
CRL Distribution Point URLs + Support for Multiple CRLs per CA
2024-08-22 23:56:55 -07:00
Tuan Dang
ba45e83880 Clean 2024-08-22 23:37:36 -07:00
Daniel Hougaard
28ecc37163 Update org-service.ts 2024-08-23 03:10:14 +04:00
Daniel Hougaard
a6a2e2bae0 Update AddOrgMemberModal.tsx 2024-08-23 02:25:15 +04:00
Daniel Hougaard
d8bbfacae0 UI improvements 2024-08-23 02:25:15 +04:00
Daniel Hougaard
58549c398f Update project-service.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
842ed62bec Rename 2024-08-23 02:25:15 +04:00
Daniel Hougaard
06d8800ee0 Feat: Specify organization role and projects when inviting users to org 2024-08-23 02:25:15 +04:00
Daniel Hougaard
2ecfd1bb7e Update auth-signup-type.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
783d4c7bd6 Update org-dal.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
fbf3f26abd Refactored org invites to allow for multiple users and to handle project invites 2024-08-23 02:25:15 +04:00
Daniel Hougaard
1d09693041 Update org-types.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
626e37e3d0 Moved project membership creation to project membership fns 2024-08-23 02:25:15 +04:00
Daniel Hougaard
07fd67b328 Add metadata to SMTP email 2024-08-23 02:25:15 +04:00
Daniel Hougaard
3f1f018adc Update telemetry-types.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
fe04e6d20c Remove *.*.posthog.com 2024-08-23 02:25:15 +04:00
Daniel Hougaard
d7171a1617 Removed unused code 2024-08-23 02:25:15 +04:00
Daniel Hougaard
384a0daa31 Update types.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
c5c949e034 Multi user org invites 2024-08-23 02:25:15 +04:00
Daniel Hougaard
c2c9edf156 Update types.ts 2024-08-23 02:25:15 +04:00
Daniel Hougaard
c8248ef4e9 Fix: Skip org selection when user only has one org 2024-08-23 02:25:15 +04:00
Daniel Hougaard
9f6a6a7b7c Automatic timed toggle 2024-08-23 02:25:15 +04:00
Daniel Hougaard
121b642d50 Added new metadata parameter for signup 2024-08-23 02:25:15 +04:00
Daniel Hougaard
59b16f647e Update AddOrgMemberModal.tsx 2024-08-23 02:25:15 +04:00
Daniel Hougaard
2ab5932693 Update OrgMembersSection.tsx 2024-08-23 02:25:15 +04:00
Daniel Hougaard
8dfcef3900 Seperate component for Org Invite Links 2024-08-23 02:25:15 +04:00
Daniel Hougaard
8ca70eec44 Refactor add users to org handlers 2024-08-23 02:25:14 +04:00
Daniel Hougaard
60df59c7f0 Multi-user organization invites structure 2024-08-23 02:25:14 +04:00
Daniel Hougaard
e231c531a6 Update index.ts 2024-08-23 02:25:14 +04:00
Daniel Hougaard
d48bb910fa JWT invite lifetime (1 day) 2024-08-23 02:25:14 +04:00
Tuan Dang
288f47f4bd Update API reference CRL docs 2024-08-22 12:30:48 -07:00
Tuan Dang
b090ebfd41 Update API reference CRL docs 2024-08-22 12:26:13 -07:00
Tuan Dang
67773bff5e Update wording on external parent ca 2024-08-22 12:18:00 -07:00
Tuan Dang
8ef1cfda04 Update docs for CRL 2024-08-22 12:16:37 -07:00
Tuan Dang
2a79d5ba36 Fix merge conflicts 2024-08-22 12:01:43 -07:00
Tuan Dang
0cb95f36ff Finish updating CRL impl 2024-08-22 11:55:19 -07:00
85 changed files with 1929 additions and 805 deletions

View File

@@ -29,7 +29,7 @@
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.10.0", "@peculiar/x509": "^1.12.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4", "@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@team-plain/typescript-sdk": "^4.6.1", "@team-plain/typescript-sdk": "^4.6.1",
@@ -5029,9 +5029,9 @@
} }
}, },
"node_modules/@peculiar/x509": { "node_modules/@peculiar/x509": {
"version": "1.10.0", "version": "1.12.1",
"resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.10.0.tgz", "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.12.1.tgz",
"integrity": "sha512-gdH6H8gWjAYoM4Yr6wPnRbzU77nU7xq/jipqYyyv5/AHTrulN2Z5DlnOSq9jjKrB+Ya0D6YJ2cGGtwkWDK75jA==", "integrity": "sha512-2T9t2viNP9m20mky50igPTpn2ByhHl5NlT6wW4Tp4BejQaQ5XDNZgfsabYwYysLXhChABlgtTCpp2gM3JBZRKA==",
"dependencies": { "dependencies": {
"@peculiar/asn1-cms": "^2.3.8", "@peculiar/asn1-cms": "^2.3.8",
"@peculiar/asn1-csr": "^2.3.8", "@peculiar/asn1-csr": "^2.3.8",

View File

@@ -126,7 +126,7 @@
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8",
"@peculiar/x509": "^1.10.0", "@peculiar/x509": "^1.12.1",
"@serdnam/pino-cloudwatch-transport": "^1.0.4", "@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@sindresorhus/slugify": "1.1.0", "@sindresorhus/slugify": "1.1.0",
"@team-plain/typescript-sdk": "^4.6.1", "@team-plain/typescript-sdk": "^4.6.1",

View File

@@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthorityCrl)) {
const hasCaSecretIdColumn = await knex.schema.hasColumn(TableName.CertificateAuthorityCrl, "caSecretId");
if (!hasCaSecretIdColumn) {
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.uuid("caSecretId").nullable();
t.foreign("caSecretId").references("id").inTable(TableName.CertificateAuthoritySecret).onDelete("CASCADE");
});
await knex.raw(`
UPDATE "${TableName.CertificateAuthorityCrl}" crl
SET "caSecretId" = (
SELECT sec.id
FROM "${TableName.CertificateAuthoritySecret}" sec
WHERE sec."caId" = crl."caId"
)
`);
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.uuid("caSecretId").notNullable().alter();
});
}
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateAuthorityCrl)) {
await knex.schema.alterTable(TableName.CertificateAuthorityCrl, (t) => {
t.dropColumn("caSecretId");
});
}
}

View File

@@ -9,6 +9,7 @@ import { TImmutableDBKeys } from "./models";
export const AccessApprovalRequestsReviewersSchema = z.object({ export const AccessApprovalRequestsReviewersSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
member: z.string().uuid().nullable().optional(),
status: z.string(), status: z.string(),
requestId: z.string().uuid(), requestId: z.string().uuid(),
createdAt: z.date(), createdAt: z.date(),

View File

@@ -11,6 +11,7 @@ export const AccessApprovalRequestsSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
policyId: z.string().uuid(), policyId: z.string().uuid(),
privilegeId: z.string().uuid().nullable().optional(), privilegeId: z.string().uuid().nullable().optional(),
requestedBy: z.string().uuid().nullable().optional(),
isTemporary: z.boolean(), isTemporary: z.boolean(),
temporaryRange: z.string().nullable().optional(), temporaryRange: z.string().nullable().optional(),
permissions: z.unknown(), permissions: z.unknown(),

View File

@@ -14,7 +14,8 @@ export const CertificateAuthorityCrlSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
caId: z.string().uuid(), caId: z.string().uuid(),
encryptedCrl: zodBuffer encryptedCrl: zodBuffer,
caSecretId: z.string().uuid()
}); });
export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>; export type TCertificateAuthorityCrl = z.infer<typeof CertificateAuthorityCrlSchema>;

View File

@@ -10,6 +10,7 @@ import { TImmutableDBKeys } from "./models";
export const ProjectUserAdditionalPrivilegeSchema = z.object({ export const ProjectUserAdditionalPrivilegeSchema = z.object({
id: z.string().uuid(), id: z.string().uuid(),
slug: z.string(), slug: z.string(),
projectMembershipId: z.string().uuid().nullable().optional(),
isTemporary: z.boolean().default(false), isTemporary: z.boolean().default(false),
temporaryMode: z.string().nullable().optional(), temporaryMode: z.string().nullable().optional(),
temporaryRange: z.string().nullable().optional(), temporaryRange: z.string().nullable().optional(),

View File

@@ -1,86 +1,31 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import { z } from "zod"; import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { CA_CRLS } from "@app/lib/api-docs";
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
export const registerCaCrlRouter = async (server: FastifyZodProvider) => { export const registerCaCrlRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
method: "GET", method: "GET",
url: "/:caId/crl", url: "/:crlId",
config: { config: {
rateLimit: readLimit rateLimit: readLimit
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: { schema: {
description: "Get CRL of the CA", description: "Get CRL in DER format",
params: z.object({ params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRL.caId) crlId: z.string().trim().describe(CA_CRLS.GET.crlId)
}), }),
response: { response: {
200: z.object({ 200: z.instanceof(Buffer)
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRL.crl)
})
} }
}, },
handler: async (req) => { handler: async (req, res) => {
const { crl, ca } = await server.services.certificateAuthorityCrl.getCaCrl({ const { crl } = await server.services.certificateAuthorityCrl.getCrlById(req.params.crlId);
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({ res.header("Content-Type", "application/pkix-crl");
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_CA_CRL,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
return { return Buffer.from(crl);
crl
};
} }
}); });
// server.route({
// method: "GET",
// url: "/:caId/crl/rotate",
// config: {
// rateLimit: writeLimit
// },
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
// schema: {
// description: "Rotate CRL of the CA",
// params: z.object({
// caId: z.string().trim()
// }),
// response: {
// 200: z.object({
// message: z.string()
// })
// }
// },
// handler: async (req) => {
// await server.services.certificateAuthority.rotateCaCrl({
// caId: req.params.caId,
// actor: req.permission.type,
// actorId: req.permission.id,
// actorAuthMethod: req.permission.authMethod,
// actorOrgId: req.permission.orgId
// });
// return {
// message: "Successfully rotated CA CRL"
// };
// }
// });
}; };

View File

@@ -61,7 +61,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register( await server.register(
async (pkiRouter) => { async (pkiRouter) => {
await pkiRouter.register(registerCaCrlRouter, { prefix: "/ca" }); await pkiRouter.register(registerCaCrlRouter, { prefix: "/crl" });
}, },
{ prefix: "/pki" } { prefix: "/pki" }
); );

View File

@@ -137,7 +137,7 @@ export enum EventType {
GET_CA_CERT = "get-certificate-authority-cert", GET_CA_CERT = "get-certificate-authority-cert",
SIGN_INTERMEDIATE = "sign-intermediate", SIGN_INTERMEDIATE = "sign-intermediate",
IMPORT_CA_CERT = "import-certificate-authority-cert", IMPORT_CA_CERT = "import-certificate-authority-cert",
GET_CA_CRL = "get-certificate-authority-crl", GET_CA_CRLS = "get-certificate-authority-crls",
ISSUE_CERT = "issue-cert", ISSUE_CERT = "issue-cert",
SIGN_CERT = "sign-cert", SIGN_CERT = "sign-cert",
GET_CERT = "get-cert", GET_CERT = "get-cert",
@@ -1163,8 +1163,8 @@ interface ImportCaCert {
}; };
} }
interface GetCaCrl { interface GetCaCrls {
type: EventType.GET_CA_CRL; type: EventType.GET_CA_CRLS;
metadata: { metadata: {
caId: string; caId: string;
dn: string; dn: string;
@@ -1518,7 +1518,7 @@ export type Event =
| GetCaCert | GetCaCert
| SignIntermediate | SignIntermediate
| ImportCaCert | ImportCaCert
| GetCaCrl | GetCaCrls
| IssueCert | IssueCert
| SignCert | SignCert
| GetCert | GetCert

View File

@@ -2,24 +2,24 @@ import { ForbiddenError } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; // import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns"; import { getProjectKmsCertificateKeyId } from "@app/services/project/project-fns";
import { TGetCrl } from "./certificate-authority-crl-types"; import { TGetCaCrlsDTO, TGetCrlById } from "./certificate-authority-crl-types";
type TCertificateAuthorityCrlServiceFactoryDep = { type TCertificateAuthorityCrlServiceFactoryDep = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findById">;
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">; certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "find" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; // licenseService: Pick<TLicenseServiceFactory, "getPlan">;
}; };
export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>; export type TCertificateAuthorityCrlServiceFactory = ReturnType<typeof certificateAuthorityCrlServiceFactory>;
@@ -29,13 +29,42 @@ export const certificateAuthorityCrlServiceFactory = ({
certificateAuthorityCrlDAL, certificateAuthorityCrlDAL,
projectDAL, projectDAL,
kmsService, kmsService,
permissionService, permissionService // licenseService
licenseService
}: TCertificateAuthorityCrlServiceFactoryDep) => { }: TCertificateAuthorityCrlServiceFactoryDep) => {
/** /**
* Return the Certificate Revocation List (CRL) for CA with id [caId] * Return CRL with id [crlId]
*/ */
const getCaCrl = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCrl) => { const getCrlById = async (crlId: TGetCrlById) => {
const caCrl = await certificateAuthorityCrlDAL.findById(crlId);
if (!caCrl) throw new NotFoundError({ message: "CRL not found" });
const ca = await certificateAuthorityDAL.findById(caCrl.caId);
const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId,
projectDAL,
kmsService
});
const kmsDecryptor = await kmsService.decryptWithKmsKey({
kmsId: keyId
});
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
return {
ca,
caCrl,
crl: crl.rawData
};
};
/**
* Returns a list of CRL ids for CA with id [caId]
*/
const getCaCrls = async ({ caId, actorId, actorAuthMethod, actor, actorOrgId }: TGetCaCrlsDTO) => {
const ca = await certificateAuthorityDAL.findById(caId); const ca = await certificateAuthorityDAL.findById(caId);
if (!ca) throw new BadRequestError({ message: "CA not found" }); if (!ca) throw new BadRequestError({ message: "CA not found" });
@@ -52,15 +81,14 @@ export const certificateAuthorityCrlServiceFactory = ({
ProjectPermissionSub.CertificateAuthorities ProjectPermissionSub.CertificateAuthorities
); );
const plan = await licenseService.getPlan(actorOrgId); // const plan = await licenseService.getPlan(actorOrgId);
if (!plan.caCrl) // if (!plan.caCrl)
throw new BadRequestError({ // throw new BadRequestError({
message: // message:
"Failed to get CA certificate revocation list (CRL) due to plan restriction. Upgrade plan to get the CA CRL." // "Failed to get CA certificate revocation lists (CRLs) due to plan restriction. Upgrade plan to get the CA CRL."
}); // });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caId: ca.id }); const caCrls = await certificateAuthorityCrlDAL.find({ caId: ca.id }, { sort: [["createdAt", "desc"]] });
if (!caCrl) throw new BadRequestError({ message: "CRL not found" });
const keyId = await getProjectKmsCertificateKeyId({ const keyId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -72,15 +100,23 @@ export const certificateAuthorityCrlServiceFactory = ({
kmsId: keyId kmsId: keyId
}); });
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl }); const decryptedCrls = await Promise.all(
const crl = new x509.X509Crl(decryptedCrl); caCrls.map(async (caCrl) => {
const decryptedCrl = await kmsDecryptor({ cipherTextBlob: caCrl.encryptedCrl });
const crl = new x509.X509Crl(decryptedCrl);
const base64crl = crl.toString("base64"); const base64crl = crl.toString("base64");
const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`; const crlPem = `-----BEGIN X509 CRL-----\n${base64crl.match(/.{1,64}/g)?.join("\n")}\n-----END X509 CRL-----`;
return {
id: caCrl.id,
crl: crlPem
};
})
);
return { return {
crl: crlPem, ca,
ca crls: decryptedCrls
}; };
}; };
@@ -166,7 +202,8 @@ export const certificateAuthorityCrlServiceFactory = ({
// }; // };
return { return {
getCaCrl getCrlById,
getCaCrls
// rotateCaCrl // rotateCaCrl
}; };
}; };

View File

@@ -1,5 +1,7 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
export type TGetCrl = { export type TGetCrlById = string;
export type TGetCaCrlsDTO = {
caId: string; caId: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;

View File

@@ -98,6 +98,7 @@ export const dynamicSecretServiceFactory = ({
if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" }); if (!isConnected) throw new BadRequestError({ message: "Provider connection failed" });
const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs)); const encryptedInput = infisicalSymmetricEncypt(JSON.stringify(inputs));
const dynamicSecretCfg = await dynamicSecretDAL.create({ const dynamicSecretCfg = await dynamicSecretDAL.create({
type: provider.type, type: provider.type,
version: 1, version: 1,

View File

@@ -1120,9 +1120,10 @@ export const CERTIFICATE_AUTHORITIES = {
certificateChain: "The certificate chain of the issued certificate", certificateChain: "The certificate chain of the issued certificate",
serialNumber: "The serial number of the issued certificate" serialNumber: "The serial number of the issued certificate"
}, },
GET_CRL: { GET_CRLS: {
caId: "The ID of the CA to get the certificate revocation list (CRL) for", caId: "The ID of the CA to get the certificate revocation lists (CRLs) for",
crl: "The certificate revocation list (CRL) of the CA" id: "The ID of certificate revocation list (CRL)",
crl: "The certificate revocation list (CRL)"
} }
}; };
@@ -1174,6 +1175,13 @@ export const CERTIFICATE_TEMPLATES = {
} }
}; };
export const CA_CRLS = {
GET: {
crlId: "The ID of the certificate revocation list (CRL) to get",
crl: "The certificate revocation list (CRL)"
}
};
export const ALERTS = { export const ALERTS = {
CREATE: { CREATE: {
projectId: "The ID of the project to create the alert in", projectId: "The ID of the project to create the alert in",

View File

@@ -74,6 +74,7 @@ const envSchema = z
JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")), JWT_AUTH_LIFETIME: zpStr(z.string().default("10d")),
JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")), JWT_SIGNUP_LIFETIME: zpStr(z.string().default("15m")),
JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")), JWT_REFRESH_LIFETIME: zpStr(z.string().default("90d")),
JWT_INVITE_LIFETIME: zpStr(z.string().default("1d")),
JWT_MFA_LIFETIME: zpStr(z.string().default("5m")), JWT_MFA_LIFETIME: zpStr(z.string().default("5m")),
JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")), JWT_PROVIDER_AUTH_LIFETIME: zpStr(z.string().default("15m")),
// Oauth // Oauth

View File

@@ -477,9 +477,12 @@ export const registerRoutes = async (
orgRoleDAL, orgRoleDAL,
permissionService, permissionService,
orgDAL, orgDAL,
userGroupMembershipDAL,
projectBotDAL,
incidentContactDAL, incidentContactDAL,
tokenService, tokenService,
projectUserAdditionalPrivilegeDAL, projectUserAdditionalPrivilegeDAL,
projectUserMembershipRoleDAL,
projectDAL, projectDAL,
projectMembershipDAL, projectMembershipDAL,
orgMembershipDAL, orgMembershipDAL,
@@ -499,6 +502,8 @@ export const registerRoutes = async (
projectDAL, projectDAL,
projectBotDAL, projectBotDAL,
groupProjectDAL, groupProjectDAL,
projectMembershipDAL,
projectUserMembershipRoleDAL,
orgDAL, orgDAL,
orgService, orgService,
licenseService licenseService
@@ -646,8 +651,8 @@ export const registerRoutes = async (
certificateAuthorityCrlDAL, certificateAuthorityCrlDAL,
projectDAL, projectDAL,
kmsService, kmsService,
permissionService, permissionService
licenseService // licenseService
}); });
const certificateTemplateService = certificateTemplateServiceFactory({ const certificateTemplateService = certificateTemplateServiceFactory({
@@ -683,6 +688,7 @@ export const registerRoutes = async (
orgDAL, orgDAL,
orgService, orgService,
projectMembershipDAL, projectMembershipDAL,
projectRoleDAL,
folderDAL, folderDAL,
licenseService, licenseService,
certificateAuthorityDAL, certificateAuthorityDAL,

View File

@@ -698,4 +698,83 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
}; };
} }
}); });
server.route({
method: "GET",
url: "/:caId/crls",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Get list of CRLs of the CA",
params: z.object({
caId: z.string().trim().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.caId)
}),
response: {
200: z.array(
z.object({
id: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.id),
crl: z.string().describe(CERTIFICATE_AUTHORITIES.GET_CRLS.crl)
})
)
}
},
handler: async (req) => {
const { ca, crls } = await server.services.certificateAuthorityCrl.getCaCrls({
caId: req.params.caId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: ca.projectId,
event: {
type: EventType.GET_CA_CRLS,
metadata: {
caId: ca.id,
dn: ca.dn
}
}
});
return crls;
}
});
// TODO: implement this endpoint in the future
// server.route({
// method: "GET",
// url: "/:caId/crl/rotate",
// config: {
// rateLimit: writeLimit
// },
// onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
// schema: {
// description: "Rotate CRLs of the CA",
// params: z.object({
// caId: z.string().trim()
// }),
// response: {
// 200: z.object({
// message: z.string()
// })
// }
// },
// handler: async (req) => {
// await server.services.certificateAuthority.rotateCaCrl({
// caId: req.params.caId,
// actor: req.permission.type,
// actorId: req.permission.id,
// actorAuthMethod: req.permission.authMethod,
// actorOrgId: req.permission.orgId
// });
// return {
// message: "Successfully rotated CA CRL"
// };
// }
// });
}; };

View File

@@ -293,6 +293,7 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider)
}), }),
querystring: z.object({ querystring: z.object({
teamId: z.string().trim().optional(), teamId: z.string().trim().optional(),
azureDevOpsOrgName: z.string().trim().optional(),
workspaceSlug: z.string().trim().optional() workspaceSlug: z.string().trim().optional()
}), }),
response: { response: {

View File

@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { UsersSchema } from "@app/db/schemas"; import { OrgMembershipRole, ProjectMembershipRole, UsersSchema } from "@app/db/schemas";
import { inviteUserRateLimit } from "@app/server/config/rateLimiter"; import { inviteUserRateLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -16,23 +16,37 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
method: "POST", method: "POST",
schema: { schema: {
body: z.object({ body: z.object({
inviteeEmail: z.string().trim().email(), inviteeEmails: z.array(z.string().trim().email()),
organizationId: z.string().trim() organizationId: z.string().trim(),
projectIds: z.array(z.string().trim()).optional(),
projectRoleSlug: z.nativeEnum(ProjectMembershipRole).optional(),
organizationRoleSlug: z.nativeEnum(OrgMembershipRole)
}), }),
response: { response: {
200: z.object({ 200: z.object({
message: z.string(), message: z.string(),
completeInviteLink: z.string().optional() completeInviteLinks: z
.array(
z.object({
email: z.string(),
link: z.string()
})
)
.optional()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
if (req.auth.actor !== ActorType.USER) return; if (req.auth.actor !== ActorType.USER) return;
const completeInviteLink = await server.services.org.inviteUserToOrganization({
const completeInviteLinks = await server.services.org.inviteUserToOrganization({
orgId: req.body.organizationId, orgId: req.body.organizationId,
userId: req.permission.id, userId: req.permission.id,
inviteeEmail: req.body.inviteeEmail, inviteeEmails: req.body.inviteeEmails,
projectIds: req.body.projectIds,
projectRoleSlug: req.body.projectRoleSlug,
organizationRoleSlug: req.body.organizationRoleSlug,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId actorOrgId: req.permission.orgId
}); });
@@ -41,14 +55,15 @@ export const registerInviteOrgRouter = async (server: FastifyZodProvider) => {
event: PostHogEventTypes.UserOrgInvitation, event: PostHogEventTypes.UserOrgInvitation,
distinctId: getTelemetryDistinctId(req), distinctId: getTelemetryDistinctId(req),
properties: { properties: {
inviteeEmail: req.body.inviteeEmail, inviteeEmails: req.body.inviteeEmails,
organizationRoleSlug: req.body.organizationRoleSlug,
...req.auditLogInfo ...req.auditLogInfo
} }
}); });
return { return {
completeInviteLink, completeInviteLinks,
message: `Send an invite link to ${req.body.inviteeEmail}` message: `Send an invite link to ${req.body.inviteeEmails.join(", ")}`
}; };
} }
}); });

View File

@@ -1,6 +1,12 @@
import { z } from "zod"; import { z } from "zod";
import { IntegrationsSchema, ProjectMembershipsSchema, UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import {
IntegrationsSchema,
ProjectMembershipsSchema,
ProjectRolesSchema,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -122,15 +128,31 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
rateLimit: readLimit rateLimit: readLimit
}, },
schema: { schema: {
querystring: z.object({
includeRoles: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
}),
response: { response: {
200: z.object({ 200: z.object({
workspaces: projectWithEnv.array() workspaces: projectWithEnv
.extend({
roles: ProjectRolesSchema.array().optional()
})
.array()
}) })
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.API_KEY]),
handler: async (req) => { handler: async (req) => {
const workspaces = await server.services.project.getProjects(req.permission.id); const workspaces = await server.services.project.getProjects({
includeRoles: req.query.includeRoles,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actor: req.permission.type,
actorOrgId: req.permission.orgId
});
return { workspaces }; return { workspaces };
} }
}); });

View File

@@ -179,7 +179,8 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
encryptedPrivateKeyIV: z.string().trim(), encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(), encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(), salt: z.string().trim(),
verifier: z.string().trim() verifier: z.string().trim(),
tokenMetadata: z.string().optional()
}), }),
response: { response: {
200: z.object({ 200: z.object({

View File

@@ -1,3 +1,5 @@
import { ProjectMembershipRole } from "@app/db/schemas";
export enum TokenType { export enum TokenType {
TOKEN_EMAIL_CONFIRMATION = "emailConfirmation", TOKEN_EMAIL_CONFIRMATION = "emailConfirmation",
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
@@ -49,3 +51,19 @@ export type TIssueAuthTokenDTO = {
ip: string; ip: string;
userAgent: string; userAgent: string;
}; };
export enum TokenMetadataType {
InviteToProjects = "projects-invite"
}
export type TTokenInviteToProjectsMetadataPayload = {
projectIds: string[];
projectRoleSlug: ProjectMembershipRole;
userId: string;
orgId: string;
};
export type TTokenMetadata = {
type: TokenMetadataType.InviteToProjects;
payload: TTokenInviteToProjectsMetadataPayload;
};

View File

@@ -9,7 +9,7 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal"; import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -17,9 +17,12 @@ import { TProjectBotDALFactory } from "@app/services/project-bot/project-bot-dal
import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal"; import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service"; import { TOrgServiceFactory } from "../org/org-service";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { addMembersToProject } from "../project-membership/project-membership-fns";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TAuthDALFactory } from "./auth-dal"; import { TAuthDALFactory } from "./auth-dal";
@@ -32,10 +35,14 @@ type TAuthSignupDep = {
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
"find" | "transaction" | "insertMany" | "deletePendingUserGroupMembershipsByUserIds" | "find"
| "transaction"
| "insertMany"
| "deletePendingUserGroupMembershipsByUserIds"
| "findUserGroupMembershipsInProject"
>; >;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgService: Pick<TOrgServiceFactory, "createOrganization">; orgService: Pick<TOrgServiceFactory, "createOrganization">;
@@ -43,6 +50,8 @@ type TAuthSignupDep = {
tokenService: TAuthTokenServiceFactory; tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService; smtpService: TSmtpService;
licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">; licenseService: Pick<TLicenseServiceFactory, "updateSubscriptionOrgMemberCount">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
}; };
export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>; export type TAuthSignupFactory = ReturnType<typeof authSignupServiceFactory>;
@@ -58,6 +67,8 @@ export const authSignupServiceFactory = ({
smtpService, smtpService,
orgService, orgService,
orgDAL, orgDAL,
projectMembershipDAL,
projectUserMembershipRoleDAL,
licenseService licenseService
}: TAuthSignupDep) => { }: TAuthSignupDep) => {
// first step of signup. create user and send email // first step of signup. create user and send email
@@ -301,7 +312,8 @@ export const authSignupServiceFactory = ({
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
authorization authorization,
tokenMetadata
}: TCompleteAccountInviteDTO) => { }: TCompleteAccountInviteDTO) => {
const user = await userDAL.findUserByUsername(email); const user = await userDAL.findUserByUsername(email);
if (!user || (user && user.isAccepted)) { if (!user || (user && user.isAccepted)) {
@@ -358,6 +370,45 @@ export const authSignupServiceFactory = ({
tx tx
); );
if (tokenMetadata) {
const metadataObj = jwt.verify(tokenMetadata, appCfg.AUTH_SECRET) as TTokenMetadata;
if (
metadataObj?.payload?.userId !== user.id ||
metadataObj?.payload?.orgId !== orgMembership.orgId ||
metadataObj?.type !== TokenMetadataType.InviteToProjects
) {
throw new UnauthorizedError({
message: "Malformed or invalid metadata token"
});
}
for await (const projectId of metadataObj.payload.projectIds) {
await addMembersToProject({
orgDAL,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
userGroupMembershipDAL,
projectBotDAL,
projectUserMembershipRoleDAL,
smtpService
}).addMembersToNonE2EEProject(
{
emails: [user.email!],
usernames: [],
projectId,
projectMembershipRole: metadataObj.payload.projectRoleSlug,
sendEmails: false
},
{
tx,
throwOnProjectNotFound: false
}
);
}
}
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: email, status: OrgMembershipStatus.Invited }, { inviteEmail: email, status: OrgMembershipStatus.Invited },
{ userId: us.id, status: OrgMembershipStatus.Accepted }, { userId: us.id, status: OrgMembershipStatus.Accepted },

View File

@@ -37,4 +37,5 @@ export type TCompleteAccountInviteDTO = {
ip: string; ip: string;
userAgent: string; userAgent: string;
authorization: string; authorization: string;
tokenMetadata?: string;
}; };

View File

@@ -13,6 +13,13 @@ import {
TRebuildCaCrlDTO TRebuildCaCrlDTO
} from "./certificate-authority-types"; } from "./certificate-authority-types";
/* eslint-disable no-bitwise */
export const createSerialNumber = () => {
const randomBytes = crypto.randomBytes(32);
randomBytes[0] &= 0x7f; // ensure the first bit is 0
return randomBytes.toString("hex");
};
export const createDistinguishedName = (parts: TDNParts) => { export const createDistinguishedName = (parts: TDNParts) => {
const dnParts = []; const dnParts = [];
if (parts.country) dnParts.push(`C=${parts.country}`); if (parts.country) dnParts.push(`C=${parts.country}`);
@@ -284,12 +291,11 @@ export const rebuildCaCrl = async ({
thisUpdate: new Date(), thisUpdate: new Date(),
nextUpdate: new Date("2025/12/12"), nextUpdate: new Date("2025/12/12"),
entries: revokedCerts.map((revokedCert) => { entries: revokedCerts.map((revokedCert) => {
const revocationDate = new Date(revokedCert.revokedAt as Date);
return { return {
serialNumber: revokedCert.serialNumber, serialNumber: revokedCert.serialNumber,
revocationDate: new Date(revokedCert.revokedAt as Date), revocationDate,
reason: revokedCert.revocationReason as number, reason: revokedCert.revocationReason as number
invalidity: new Date("2022/01/01"),
issuer: ca.dn
}; };
}), }),
signingAlgorithm: alg, signingAlgorithm: alg,

View File

@@ -8,6 +8,7 @@ import { z } from "zod";
import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
@@ -25,6 +26,7 @@ import { TCertificateAuthorityCertDALFactory } from "./certificate-authority-cer
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal"; import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
import { import {
createDistinguishedName, createDistinguishedName,
createSerialNumber,
getCaCertChain, // TODO: consider rename getCaCertChain, // TODO: consider rename
getCaCertChains, getCaCertChains,
getCaCredentials, getCaCredentials,
@@ -147,7 +149,7 @@ export const certificateAuthorityServiceFactory = ({
? new Date(notAfter) ? new Date(notAfter)
: new Date(new Date().setFullYear(new Date().getFullYear() + 10)); : new Date(new Date().setFullYear(new Date().getFullYear() + 10));
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = createSerialNumber();
const ca = await certificateAuthorityDAL.create( const ca = await certificateAuthorityDAL.create(
{ {
@@ -263,7 +265,8 @@ export const certificateAuthorityServiceFactory = ({
await certificateAuthorityCrlDAL.create( await certificateAuthorityCrlDAL.create(
{ {
caId: ca.id, caId: ca.id,
encryptedCrl encryptedCrl,
caSecretId: caSecret.id
}, },
tx tx
); );
@@ -433,7 +436,7 @@ export const certificateAuthorityServiceFactory = ({
// get latest CA certificate // get latest CA certificate
const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId); const caCert = await certificateAuthorityCertDAL.findById(ca.activeCaCertId);
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = createSerialNumber();
const certificateManagerKmsId = await getProjectKmsCertificateKeyId({ const certificateManagerKmsId = await getProjectKmsCertificateKeyId({
projectId: ca.projectId, projectId: ca.projectId,
@@ -846,7 +849,7 @@ export const certificateAuthorityServiceFactory = ({
kmsService kmsService
}); });
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = createSerialNumber();
const intermediateCert = await x509.X509CertificateGenerator.create({ const intermediateCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
subject: csrObj.subject, subject: csrObj.subject,
@@ -1142,7 +1145,7 @@ export const certificateAuthorityServiceFactory = ({
attributes: [new x509.ChallengePasswordAttribute("password")] attributes: [new x509.ChallengePasswordAttribute("password")]
}); });
const { caPrivateKey } = await getCaCredentials({ const { caPrivateKey, caSecret } = await getCaCredentials({
caId: ca.id, caId: ca.id,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateAuthoritySecretDAL, certificateAuthoritySecretDAL,
@@ -1150,9 +1153,15 @@ export const certificateAuthorityServiceFactory = ({
kmsService kmsService
}); });
const caCrl = await certificateAuthorityCrlDAL.findOne({ caSecretId: caSecret.id });
const appCfg = getConfig();
const distributionPointUrl = `${appCfg.SITE_URL}/api/v1/pki/crl/${caCrl.id}`;
const extensions: x509.Extension[] = [ const extensions: x509.Extension[] = [
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true), new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment, true),
new x509.BasicConstraintsExtension(false), new x509.BasicConstraintsExtension(false),
new x509.CRLDistributionPointsExtension([distributionPointUrl]),
await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false), await x509.AuthorityKeyIdentifierExtension.create(caCertObj, false),
await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey) await x509.SubjectKeyIdentifierExtension.create(csrObj.publicKey)
]; ];
@@ -1203,7 +1212,7 @@ export const certificateAuthorityServiceFactory = ({
); );
} }
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({ const leafCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
subject: csrObj.subject, subject: csrObj.subject,
@@ -1462,7 +1471,7 @@ export const certificateAuthorityServiceFactory = ({
); );
} }
const serialNumber = crypto.randomBytes(32).toString("hex"); const serialNumber = createSerialNumber();
const leafCert = await x509.X509CertificateGenerator.create({ const leafCert = await x509.X509CertificateGenerator.create({
serialNumber, serialNumber,
subject: csrObj.subject, subject: csrObj.subject,

View File

@@ -1030,11 +1030,31 @@ const getAppsCloud66 = async ({ accessToken }: { accessToken: string }) => {
return apps; return apps;
}; };
const getAppsAzureDevOps = async ({ accessToken, orgName }: { accessToken: string; orgName: string }) => {
const res = (
await request.get<{ count: number; value: Record<string, string>[] }>(
`${IntegrationUrls.AZURE_DEVOPS_API_URL}/${orgName}/_apis/projects?api-version=7.2-preview.2`,
{
headers: {
Authorization: `Basic ${accessToken}`
}
}
)
).data;
const apps = res.value.map((a) => ({
name: a.name,
appId: a.id
}));
return apps;
};
export const getApps = async ({ export const getApps = async ({
integration, integration,
accessToken, accessToken,
accessId, accessId,
teamId, teamId,
azureDevOpsOrgName,
workspaceSlug, workspaceSlug,
url url
}: { }: {
@@ -1042,6 +1062,7 @@ export const getApps = async ({
accessToken: string; accessToken: string;
accessId?: string; accessId?: string;
teamId?: string | null; teamId?: string | null;
azureDevOpsOrgName?: string | null;
workspaceSlug?: string; workspaceSlug?: string;
url?: string | null; url?: string | null;
}): Promise<App[]> => { }): Promise<App[]> => {
@@ -1184,6 +1205,12 @@ export const getApps = async ({
accessToken accessToken
}); });
case Integrations.AZURE_DEVOPS:
return getAppsAzureDevOps({
accessToken,
orgName: azureDevOpsOrgName as string
});
default: default:
throw new BadRequestError({ message: "integration not found" }); throw new BadRequestError({ message: "integration not found" });
} }

View File

@@ -440,6 +440,7 @@ export const integrationAuthServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
teamId, teamId,
azureDevOpsOrgName,
id, id,
workspaceSlug workspaceSlug
}: TIntegrationAuthAppsDTO) => { }: TIntegrationAuthAppsDTO) => {
@@ -462,6 +463,7 @@ export const integrationAuthServiceFactory = ({
accessToken, accessToken,
accessId, accessId,
teamId, teamId,
azureDevOpsOrgName,
workspaceSlug, workspaceSlug,
url: integrationAuth.url url: integrationAuth.url
}); });

View File

@@ -1,3 +1,4 @@
import { TIntegrations } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
export type TGetIntegrationAuthDTO = { export type TGetIntegrationAuthDTO = {
@@ -28,6 +29,7 @@ export type TDeleteIntegrationAuthsDTO = TProjectPermission & {
export type TIntegrationAuthAppsDTO = { export type TIntegrationAuthAppsDTO = {
id: string; id: string;
teamId?: string; teamId?: string;
azureDevOpsOrgName?: string;
workspaceSlug?: string; workspaceSlug?: string;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -163,3 +165,13 @@ export type TTeamCityBuildConfig = {
href: string; href: string;
webUrl: string; webUrl: string;
}; };
export type TIntegrationsWithEnvironment = TIntegrations & {
environment?:
| {
id?: string | null | undefined;
name?: string | null | undefined;
}
| null
| undefined;
};

View File

@@ -31,7 +31,8 @@ export enum Integrations {
CLOUD_66 = "cloud-66", CLOUD_66 = "cloud-66",
NORTHFLANK = "northflank", NORTHFLANK = "northflank",
HASURA_CLOUD = "hasura-cloud", HASURA_CLOUD = "hasura-cloud",
RUNDECK = "rundeck" RUNDECK = "rundeck",
AZURE_DEVOPS = "azure-devops"
} }
export enum IntegrationType { export enum IntegrationType {
@@ -88,6 +89,7 @@ export enum IntegrationUrls {
CLOUD_66_API_URL = "https://app.cloud66.com/api", CLOUD_66_API_URL = "https://app.cloud66.com/api",
NORTHFLANK_API_URL = "https://api.northflank.com", NORTHFLANK_API_URL = "https://api.northflank.com",
HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql", HASURA_CLOUD_API_URL = "https://data.pro.hasura.io/v1/graphql",
AZURE_DEVOPS_API_URL = "https://dev.azure.com",
GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com", GCP_SECRET_MANAGER_SERVICE_NAME = "secretmanager.googleapis.com",
GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`, GCP_SECRET_MANAGER_URL = `https://${GCP_SECRET_MANAGER_SERVICE_NAME}`,
@@ -378,6 +380,15 @@ export const getIntegrationOptions = async () => {
type: "pat", type: "pat",
clientId: "", clientId: "",
docsLink: "" docsLink: ""
},
{
name: "Azure DevOps",
slug: "azure-devops",
image: "Microsoft Azure.png",
isAvailable: true,
type: "pat",
clientId: "",
docsLink: ""
} }
]; ];

View File

@@ -35,6 +35,7 @@ import { TCreateManySecretsRawFn, TUpdateManySecretsRawFn } from "@app/services/
import { TIntegrationDALFactory } from "../integration/integration-dal"; import { TIntegrationDALFactory } from "../integration/integration-dal";
import { IntegrationMetadataSchema } from "../integration/integration-schema"; import { IntegrationMetadataSchema } from "../integration/integration-schema";
import { TIntegrationsWithEnvironment } from "./integration-auth-types";
import { import {
IntegrationInitialSyncBehavior, IntegrationInitialSyncBehavior,
IntegrationMappingBehavior, IntegrationMappingBehavior,
@@ -2075,6 +2076,116 @@ const syncSecretsTravisCI = async ({
} }
}; };
/**
* Sync/push [secrets] to GitLab repo with name [integration.app]
*/
const syncSecretsAzureDevops = async ({
integrationAuth,
integration,
secrets,
accessToken
}: {
integrationAuth: TIntegrationAuths;
integration: TIntegrationsWithEnvironment;
secrets: Record<string, { value: string; comment?: string }>;
accessToken: string;
}) => {
if (!integration.appId || !integration.app) {
throw new Error("Azure DevOps: orgId and projectId are required");
}
if (!integration.environment || !integration.environment.name) {
throw new Error("Azure DevOps: environment is required");
}
const headers = {
Authorization: `Basic ${accessToken}`
};
const azureDevopsApiUrl = integrationAuth.url ? `${integrationAuth.url}` : IntegrationUrls.AZURE_DEVOPS_API_URL;
const getEnvGroupId = async (orgId: string, project: string, env: string) => {
let groupId;
const url: string | null =
`${azureDevopsApiUrl}/${orgId}/${project}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`;
const response = await request.get(url, { headers });
for (const group of response.data.value) {
const groupName = group.name;
if (groupName === env) {
groupId = group.id;
return { groupId, groupName };
}
}
return { groupId: "", groupName: "" };
};
const { groupId, groupName } = await getEnvGroupId(integration.app, integration.appId, integration.environment.name);
const variables: Record<string, { value: string }> = {};
for (const key of Object.keys(secrets)) {
variables[key] = { value: secrets[key].value };
}
if (!groupId) {
// create new variable group if not present
const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups?api-version=7.2-preview.2`;
const config = {
method: "POST",
url,
data: {
name: integration.environment.name,
description: integration.environment.name,
type: "Vsts",
owner: "Library",
variables,
variableGroupProjectReferences: [
{
name: integration.environment.name,
projectReference: {
name: integration.appId
}
}
]
},
headers: {
headers
}
};
const res = await request.post(url, config.data, config.headers);
if (res.status !== 200) {
throw new Error(`Azure DevOps: Failed to create variable group: ${res.statusText}`);
}
} else {
// sync variables for pre-existing variable group
const url = `${azureDevopsApiUrl}/${integration.app}/_apis/distributedtask/variablegroups/${groupId}?api-version=7.2-preview.2`;
const config = {
method: "PUT",
url,
data: {
name: groupName,
description: groupName,
type: "Vsts",
owner: "Library",
variables,
variableGroupProjectReferences: [
{
name: groupName,
projectReference: {
name: integration.appId
}
}
]
},
headers: {
headers
}
};
const res = await request.put(url, config.data, config.headers);
if (res.status !== 200) {
throw new Error(`Azure DevOps: Failed to update variable group: ${res.statusText}`);
}
}
};
/** /**
* Sync/push [secrets] to GitLab repo with name [integration.app] * Sync/push [secrets] to GitLab repo with name [integration.app]
*/ */
@@ -3714,6 +3825,15 @@ export const syncIntegrationSecrets = async ({
updateManySecretsRawFn updateManySecretsRawFn
}); });
break; break;
case Integrations.AZURE_DEVOPS:
await syncSecretsAzureDevops({
integrationAuth,
integration,
secrets,
accessToken
});
break;
case Integrations.AWS_PARAMETER_STORE: case Integrations.AWS_PARAMETER_STORE:
response = await syncSecretsAWSParameterStore({ response = await syncSecretsAWSParameterStore({
integration, integration,

View File

@@ -114,10 +114,11 @@ export const orgDALFactory = (db: TDbClient) => {
} }
}; };
const findOrgMembersByUsername = async (orgId: string, usernames: string[]) => { const findOrgMembersByUsername = async (orgId: string, usernames: string[], tx?: Knex) => {
try { try {
const members = await db const conn = tx || db;
.replicaNode()(TableName.OrgMembership) const members = await conn(TableName.OrgMembership)
// .replicaNode()(TableName.OrgMembership)
.where(`${TableName.OrgMembership}.orgId`, orgId) .where(`${TableName.OrgMembership}.orgId`, orgId)
.join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.OrgMembership}.userId`, `${TableName.Users}.id`)
.leftJoin<TUserEncryptionKeys>( .leftJoin<TUserEncryptionKeys>(
@@ -126,18 +127,18 @@ export const orgDALFactory = (db: TDbClient) => {
`${TableName.Users}.id` `${TableName.Users}.id`
) )
.select( .select(
db.ref("id").withSchema(TableName.OrgMembership), conn.ref("id").withSchema(TableName.OrgMembership),
db.ref("inviteEmail").withSchema(TableName.OrgMembership), conn.ref("inviteEmail").withSchema(TableName.OrgMembership),
db.ref("orgId").withSchema(TableName.OrgMembership), conn.ref("orgId").withSchema(TableName.OrgMembership),
db.ref("role").withSchema(TableName.OrgMembership), conn.ref("role").withSchema(TableName.OrgMembership),
db.ref("roleId").withSchema(TableName.OrgMembership), conn.ref("roleId").withSchema(TableName.OrgMembership),
db.ref("status").withSchema(TableName.OrgMembership), conn.ref("status").withSchema(TableName.OrgMembership),
db.ref("username").withSchema(TableName.Users), conn.ref("username").withSchema(TableName.Users),
db.ref("email").withSchema(TableName.Users), conn.ref("email").withSchema(TableName.Users),
db.ref("firstName").withSchema(TableName.Users), conn.ref("firstName").withSchema(TableName.Users),
db.ref("lastName").withSchema(TableName.Users), conn.ref("lastName").withSchema(TableName.Users),
db.ref("id").withSchema(TableName.Users).as("userId"), conn.ref("id").withSchema(TableName.Users).as("userId"),
db.ref("publicKey").withSchema(TableName.UserEncryptionKey) conn.ref("publicKey").withSchema(TableName.UserEncryptionKey)
) )
.where({ isGhost: false }) .where({ isGhost: false })
.whereIn("username", usernames); .whereIn("username", usernames);

View File

@@ -4,9 +4,17 @@ import crypto from "crypto";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { Knex } from "knex"; import { Knex } from "knex";
import { OrgMembershipRole, OrgMembershipStatus, TableName } from "@app/db/schemas"; import {
OrgMembershipRole,
OrgMembershipStatus,
ProjectMembershipRole,
ProjectVersion,
TableName,
TUsers
} from "@app/db/schemas";
import { TProjects } from "@app/db/schemas/projects"; import { TProjects } from "@app/db/schemas/projects";
import { TGroupDALFactory } from "@app/ee/services/group/group-dal"; import { TGroupDALFactory } from "@app/ee/services/group/group-dal";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
@@ -24,10 +32,14 @@ import { TUserAliasDALFactory } from "@app/services/user-alias/user-alias-dal";
import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType, AuthMethod, AuthTokenType } from "../auth/auth-type";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service"; import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenMetadataType, TokenType, TTokenMetadata } from "../auth-token/auth-token-types";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { verifyProjectVersions } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { addMembersToProject } from "../project-membership/project-membership-fns";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TIncidentContactsDALFactory } from "./incident-contacts-dal"; import { TIncidentContactsDALFactory } from "./incident-contacts-dal";
@@ -56,8 +68,11 @@ type TOrgServiceFactoryDep = {
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
groupDAL: TGroupDALFactory; groupDAL: TGroupDALFactory;
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findProjectMembershipsByUserId" | "delete">; projectMembershipDAL: Pick<
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete">; TProjectMembershipDALFactory,
"findProjectMembershipsByUserId" | "delete" | "create" | "find" | "insertMany" | "transaction"
>;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "insertMany" | "findLatestProjectKey">;
orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">; orgMembershipDAL: Pick<TOrgMembershipDALFactory, "findOrgMembershipById" | "findOne">;
incidentContactDAL: TIncidentContactsDALFactory; incidentContactDAL: TIncidentContactsDALFactory;
samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">; samlConfigDAL: Pick<TSamlConfigDALFactory, "findOne" | "findEnforceableSamlCfg">;
@@ -69,6 +84,9 @@ type TOrgServiceFactoryDep = {
"getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer" "getPlan" | "updateSubscriptionOrgMemberCount" | "generateOrgCustomerId" | "removeOrgCustomer"
>; >;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
}; };
export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>; export type TOrgServiceFactory = ReturnType<typeof orgServiceFactory>;
@@ -90,7 +108,10 @@ export const orgServiceFactory = ({
tokenService, tokenService,
orgBotDAL, orgBotDAL,
licenseService, licenseService,
samlConfigDAL samlConfigDAL,
userGroupMembershipDAL,
projectBotDAL,
projectUserMembershipRoleDAL
}: TOrgServiceFactoryDep) => { }: TOrgServiceFactoryDep) => {
/* /*
* Get organization details by the organization id * Get organization details by the organization id
@@ -420,10 +441,15 @@ export const orgServiceFactory = ({
const inviteUserToOrganization = async ({ const inviteUserToOrganization = async ({
orgId, orgId,
userId, userId,
inviteeEmail, inviteeEmails,
organizationRoleSlug,
projectRoleSlug,
projectIds,
actorAuthMethod, actorAuthMethod,
actorOrgId actorOrgId
}: TInviteUserToOrgDTO) => { }: TInviteUserToOrgDTO) => {
const appCfg = getConfig();
const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId); const { permission } = await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
@@ -450,98 +476,203 @@ export const orgServiceFactory = ({
}); });
} }
const invitee = await orgDAL.transaction(async (tx) => { if (projectIds?.length) {
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx); const projects = await projectDAL.find({
if (inviteeUser) { orgId,
// if user already exist means its already part of infisical $in: {
// Thus the signup flow is not needed anymore id: projectIds
const [inviteeMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
},
{ tx }
);
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
throw new BadRequestError({
message: "Failed to invite an existing member of org",
name: "Invite user to org"
});
} }
});
if (!inviteeMembership) { // if its not v3, throw an error
await orgDAL.createMembership( if (!verifyProjectVersions(projects, ProjectVersion.V3)) {
{
userId: inviteeUser.id,
inviteEmail: inviteeEmail,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
}
return inviteeUser;
}
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
if (isEmailInvalid) {
throw new BadRequestError({ throw new BadRequestError({
message: "Provided a disposable email", message: "One or more selected projects are not compatible with this operation. Please upgrade your projects."
name: "Org invite"
}); });
} }
// not invited before }
const user = await userDAL.create(
{
username: inviteeEmail,
email: inviteeEmail,
isAccepted: false,
authMethods: [AuthMethod.EMAIL],
isGhost: false
},
tx
);
await orgDAL.createMembership(
{
inviteEmail: inviteeEmail,
orgId,
userId: user.id,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
return user;
});
const token = await tokenService.createTokenForUser({ const inviteeUsers = await orgDAL.transaction(async (tx) => {
type: TokenType.TOKEN_EMAIL_ORG_INVITATION, const users: Pick<
userId: invitee.id, TUsers & { orgId: string },
orgId "id" | "firstName" | "lastName" | "email" | "orgId" | "username"
>[] = [];
for await (const inviteeEmail of inviteeEmails) {
const inviteeUser = await userDAL.findUserByUsername(inviteeEmail, tx);
if (inviteeUser) {
// if user already exist means its already part of infisical
// Thus the signup flow is not needed anymore
const [inviteeMembership] = await orgDAL.findMembership(
{
[`${TableName.OrgMembership}.orgId` as "orgId"]: orgId,
[`${TableName.OrgMembership}.userId` as "userId"]: inviteeUser.id
},
{ tx }
);
if (inviteeMembership && inviteeMembership.status === OrgMembershipStatus.Accepted) {
throw new BadRequestError({
message: `Failed to invite members because ${inviteeEmail} is already part of the organization`,
name: "Invite user to org"
});
}
if (!inviteeMembership) {
await orgDAL.createMembership(
{
userId: inviteeUser.id,
inviteEmail: inviteeEmail,
orgId,
role: OrgMembershipRole.Member,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
if (projectIds?.length) {
if (
organizationRoleSlug === OrgMembershipRole.Custom ||
projectRoleSlug === ProjectMembershipRole.Custom
) {
throw new BadRequestError({
message: "Custom roles are not supported for inviting users to projects and organizations"
});
}
if (!projectRoleSlug) {
throw new BadRequestError({
message: "Selecting a project role is required to invite users to projects"
});
}
await projectMembershipDAL.insertMany(
projectIds.map((id) => ({ projectId: id, userId: inviteeUser.id })),
tx
);
for await (const projectId of projectIds) {
await addMembersToProject({
orgDAL,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
userGroupMembershipDAL,
projectBotDAL,
projectUserMembershipRoleDAL,
smtpService
}).addMembersToNonE2EEProject(
{
emails: [inviteeEmail],
usernames: [],
projectId,
projectMembershipRole: projectRoleSlug,
sendEmails: false
},
{
tx
}
);
}
}
}
return [{ ...inviteeUser, orgId }];
}
const isEmailInvalid = await isDisposableEmail(inviteeEmail);
if (isEmailInvalid) {
throw new BadRequestError({
message: "Provided a disposable email",
name: "Org invite"
});
}
// not invited before
const user = await userDAL.create(
{
username: inviteeEmail,
email: inviteeEmail,
isAccepted: false,
authMethods: [AuthMethod.EMAIL],
isGhost: false
},
tx
);
await orgDAL.createMembership(
{
inviteEmail: inviteeEmail,
orgId,
userId: user.id,
role: organizationRoleSlug,
status: OrgMembershipStatus.Invited,
isActive: true
},
tx
);
users.push({
...user,
orgId
});
}
return users;
}); });
const user = await userDAL.findById(userId); const user = await userDAL.findById(userId);
const appCfg = getConfig();
await smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation",
recipients: [inviteeEmail],
substitutions: {
inviterFirstName: user.firstName,
inviterUsername: user.username,
organizationName: org?.name,
email: inviteeEmail,
organizationId: org?.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
const signupTokens: { email: string; link: string }[] = [];
if (inviteeUsers) {
for await (const invitee of inviteeUsers) {
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_ORG_INVITATION,
userId: invitee.id,
orgId
});
let inviteMetadata: string = "";
if (projectIds && projectIds?.length > 0) {
inviteMetadata = jwt.sign(
{
type: TokenMetadataType.InviteToProjects,
payload: {
projectIds,
projectRoleSlug: projectRoleSlug!, // Implicitly checked inside transaction if projectRoleSlug is undefined
userId: invitee.id,
orgId
}
} satisfies TTokenMetadata,
appCfg.AUTH_SECRET,
{
expiresIn: appCfg.JWT_INVITE_LIFETIME
}
);
}
signupTokens.push({
email: invitee.email || invitee.username,
link: `${appCfg.SITE_URL}/signupinvite?token=${token}${
inviteMetadata ? `&metadata=${inviteMetadata}` : ""
}&to=${invitee.email || invitee.username}&organization_id=${org?.id}`
});
await smtpService.sendMail({
template: SmtpTemplates.OrgInvite,
subjectLine: "Infisical organization invitation",
recipients: [invitee.email || invitee.username],
substitutions: {
metadata: inviteMetadata,
inviterFirstName: user.firstName,
inviterUsername: user.username,
organizationName: org?.name,
email: invitee.email || invitee.username,
organizationId: org?.id.toString(),
token,
callback_url: `${appCfg.SITE_URL}/signupinvite`
}
});
}
}
await licenseService.updateSubscriptionOrgMemberCount(orgId); await licenseService.updateSubscriptionOrgMemberCount(orgId);
if (!appCfg.isSmtpConfigured) { if (!appCfg.isSmtpConfigured) {
return `${appCfg.SITE_URL}/signupinvite?token=${token}&to=${inviteeEmail}&organization_id=${org?.id}`; return signupTokens;
} }
}; };

View File

@@ -1,3 +1,4 @@
import { OrgMembershipRole, ProjectMembershipRole } from "@app/db/schemas";
import { TOrgPermission } from "@app/lib/types"; import { TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { ActorAuthMethod, ActorType } from "../auth/auth-type";
@@ -29,7 +30,10 @@ export type TInviteUserToOrgDTO = {
orgId: string; orgId: string;
actorOrgId: string | undefined; actorOrgId: string | undefined;
actorAuthMethod: ActorAuthMethod; actorAuthMethod: ActorAuthMethod;
inviteeEmail: string; inviteeEmails: string[];
organizationRoleSlug: OrgMembershipRole;
projectIds?: string[];
projectRoleSlug?: ProjectMembershipRole;
}; };
export type TVerifyUserToOrgDTO = { export type TVerifyUserToOrgDTO = {

View File

@@ -0,0 +1,190 @@
import { Knex } from "knex";
import { ProjectMembershipRole, SecretKeyEncoding, TProjectMemberships } from "@app/db/schemas";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TProjectMembershipDALFactory } from "./project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "./project-user-membership-role-dal";
type TAddMembersToProjectArg = {
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "transaction" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectById" | "findProjectGhostUser">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "insertMany">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "findUserGroupMembershipsInProject">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany">;
smtpService: Pick<TSmtpService, "sendMail">;
};
type AddMembersToNonE2EEProjectDTO = {
emails: string[];
usernames: string[];
projectId: string;
projectMembershipRole: ProjectMembershipRole;
sendEmails?: boolean;
};
type AddMembersToNonE2EEProjectOptions = {
tx?: Knex;
throwOnProjectNotFound?: boolean;
};
export const addMembersToProject = ({
orgDAL,
projectDAL,
projectMembershipDAL,
projectKeyDAL,
projectBotDAL,
userGroupMembershipDAL,
projectUserMembershipRoleDAL,
smtpService
}: TAddMembersToProjectArg) => {
// Can create multiple memberships for a singular project, based on user email / username
const addMembersToNonE2EEProject = async (
{ emails, usernames, projectId, projectMembershipRole, sendEmails }: AddMembersToNonE2EEProjectDTO,
options: AddMembersToNonE2EEProjectOptions = { throwOnProjectNotFound: true }
) => {
const processTransaction = async (tx: Knex) => {
const usernamesAndEmails = [...emails, ...usernames];
const project = await projectDAL.findProjectById(projectId);
if (!project) {
if (options.throwOnProjectNotFound) {
throw new BadRequestError({ message: "Project not found when attempting to add user to project" });
}
return [];
}
const orgMembers = await orgDAL.findOrgMembersByUsername(
project.orgId,
[...new Set(usernamesAndEmails.map((element) => element.toLowerCase()))],
tx
);
if (orgMembers.length !== usernamesAndEmails.length)
throw new BadRequestError({ message: "Some users are not part of org" });
if (!orgMembers.length) return [];
const existingMembers = await projectMembershipDAL.find({
projectId,
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) }
});
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const newWsMembers = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: orgMembers.map((membership) => ({
orgMembershipId: membership.id,
projectMembershipRole,
userPublicKey: membership.user.publicKey
}))
});
const members: TProjectMemberships[] = [];
const userIdsToExcludeForProjectKeyAddition = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
);
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({
projectId,
userId: user.id
})),
tx
);
await projectUserMembershipRoleDAL.insertMany(
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: projectMembershipRole })),
tx
);
members.push(...projectMemberships);
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
orgMembers
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
.map(({ user, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
})),
tx
);
if (sendEmails) {
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
const appCfg = getConfig();
if (recipients.length) {
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical project invitation",
recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
}
}
return members;
};
if (options.tx) {
return processTransaction(options.tx);
}
return projectMembershipDAL.transaction(processTransaction);
};
return {
addMembersToNonE2EEProject
};
};

View File

@@ -2,19 +2,12 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import ms from "ms"; import ms from "ms";
import { import { ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas";
ProjectMembershipRole,
ProjectVersion,
SecretKeyEncoding,
TableName,
TProjectMemberships
} from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
@@ -23,13 +16,13 @@ import { ActorType } from "../auth/auth-type";
import { TGroupProjectDALFactory } from "../group-project/group-project-dal"; import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TProjectMembershipDALFactory } from "./project-membership-dal"; import { TProjectMembershipDALFactory } from "./project-membership-dal";
import { addMembersToProject } from "./project-membership-fns";
import { import {
ProjectUserMembershipTemporaryMode, ProjectUserMembershipTemporaryMode,
TAddUsersToWorkspaceDTO, TAddUsersToWorkspaceDTO,
@@ -53,7 +46,7 @@ type TProjectMembershipServiceFactoryDep = {
userGroupMembershipDAL: TUserGroupMembershipDALFactory; userGroupMembershipDAL: TUserGroupMembershipDALFactory;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">; orgDAL: Pick<TOrgDALFactory, "findMembership" | "findOrgMembersByUsername">;
projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findById" | "findProjectGhostUser" | "transaction" | "findProjectById">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "delete" | "insertMany">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan">;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
@@ -247,116 +240,23 @@ export const projectMembershipServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
const usernamesAndEmails = [...emails, ...usernames]; const members = await addMembersToProject({
orgDAL,
const orgMembers = await orgDAL.findOrgMembersByUsername(project.orgId, [ projectDAL,
...new Set(usernamesAndEmails.map((element) => element.toLowerCase())) projectMembershipDAL,
]); projectKeyDAL,
userGroupMembershipDAL,
if (orgMembers.length !== usernamesAndEmails.length) projectBotDAL,
throw new BadRequestError({ message: "Some users are not part of org" }); projectUserMembershipRoleDAL,
smtpService
if (!orgMembers.length) return []; }).addMembersToNonE2EEProject({
emails,
const existingMembers = await projectMembershipDAL.find({ usernames,
projectId, projectId,
$in: { userId: orgMembers.map(({ user }) => user.id).filter(Boolean) } projectMembershipRole: ProjectMembershipRole.Member,
}); sendEmails
if (existingMembers.length) throw new BadRequestError({ message: "Some users are already part of project" });
const ghostUser = await projectDAL.findProjectGhostUser(projectId);
if (!ghostUser) {
throw new BadRequestError({
message: "Failed to find sudo user"
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId);
if (!ghostUserLatestKey) {
throw new BadRequestError({
message: "Failed to find sudo user latest key"
});
}
const bot = await projectBotDAL.findOne({ projectId });
if (!bot) {
throw new BadRequestError({
message: "Failed to find bot"
});
}
const botPrivateKey = infisicalSymmetricDecrypt({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
}); });
const newWsMembers = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: orgMembers.map((membership) => ({
orgMembershipId: membership.id,
projectMembershipRole: ProjectMembershipRole.Member,
userPublicKey: membership.user.publicKey
}))
});
const members: TProjectMemberships[] = [];
const userIdsToExcludeForProjectKeyAddition = new Set(
await userGroupMembershipDAL.findUserGroupMembershipsInProject(usernamesAndEmails, projectId)
);
await projectMembershipDAL.transaction(async (tx) => {
const projectMemberships = await projectMembershipDAL.insertMany(
orgMembers.map(({ user }) => ({
projectId,
userId: user.id
})),
tx
);
await projectUserMembershipRoleDAL.insertMany(
projectMemberships.map(({ id }) => ({ projectMembershipId: id, role: ProjectMembershipRole.Member })),
tx
);
members.push(...projectMemberships);
const encKeyGroupByOrgMembId = groupBy(newWsMembers, (i) => i.orgMembershipId);
await projectKeyDAL.insertMany(
orgMembers
.filter(({ user }) => !userIdsToExcludeForProjectKeyAddition.has(user.id))
.map(({ user, id }) => ({
encryptedKey: encKeyGroupByOrgMembId[id][0].workspaceEncryptedKey,
nonce: encKeyGroupByOrgMembId[id][0].workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
})),
tx
);
});
if (sendEmails) {
const recipients = orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string);
const appCfg = getConfig();
if (recipients.length) {
await smtpService.sendMail({
template: SmtpTemplates.WorkspaceInvite,
subjectLine: "Infisical project invitation",
recipients: orgMembers.filter((i) => i.user.email).map((i) => i.user.email as string),
substitutions: {
workspaceName: project.name,
callback_url: `${appCfg.SITE_URL}/login`
}
});
}
}
return members; return members;
}; };

View File

@@ -0,0 +1,52 @@
import { ProjectMembershipRole } from "@app/db/schemas";
import {
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
projectViewerPermission
} from "@app/ee/services/permission/project-permission";
export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
return [
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
projectId,
name: "Admin",
slug: ProjectMembershipRole.Admin,
permissions: projectAdminPermissions,
description: "Full administrative access over a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
projectId,
name: "Developer",
slug: ProjectMembershipRole.Member,
permissions: projectMemberPermissions,
description: "Limited read/write role in a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
projectId,
name: "Viewer",
slug: ProjectMembershipRole.Viewer,
permissions: projectViewerPermission,
description: "Only read role in a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
projectId,
name: "No Access",
slug: ProjectMembershipRole.NoAccess,
permissions: projectNoAccessPermissions,
description: "No access to any resources in the project",
createdAt: new Date(),
updatedAt: new Date()
}
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
};

View File

@@ -5,13 +5,9 @@ import { ProjectMembershipRole } from "@app/db/schemas";
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { import {
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
ProjectPermissionActions, ProjectPermissionActions,
ProjectPermissionSet, ProjectPermissionSet,
ProjectPermissionSub, ProjectPermissionSub
projectViewerPermission
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
@@ -20,6 +16,7 @@ import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/id
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "./project-role-dal"; import { TProjectRoleDALFactory } from "./project-role-dal";
import { getPredefinedRoles } from "./project-role-fns";
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types"; import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
type TProjectRoleServiceFactoryDep = { type TProjectRoleServiceFactoryDep = {
@@ -37,51 +34,6 @@ const unpackPermissions = (permissions: unknown) =>
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]) unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
); );
const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
return [
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
projectId,
name: "Admin",
slug: ProjectMembershipRole.Admin,
permissions: projectAdminPermissions,
description: "Full administrative access over a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
projectId,
name: "Developer",
slug: ProjectMembershipRole.Member,
permissions: projectMemberPermissions,
description: "Limited read/write role in a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
projectId,
name: "Viewer",
slug: ProjectMembershipRole.Viewer,
permissions: projectViewerPermission,
description: "Only read role in a project",
createdAt: new Date(),
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
projectId,
name: "No Access",
slug: ProjectMembershipRole.NoAccess,
permissions: projectNoAccessPermissions,
description: "No access to any resources in the project",
createdAt: new Date(),
updatedAt: new Date()
}
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
};
export const projectRoleServiceFactory = ({ export const projectRoleServiceFactory = ({
projectRoleDAL, projectRoleDAL,
permissionService, permissionService,

View File

@@ -1,5 +1,6 @@
import crypto from "crypto"; import crypto from "crypto";
import { ProjectVersion, TProjects } from "@app/db/schemas";
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto"; import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -53,6 +54,16 @@ export const createProjectKey = ({ publicKey, privateKey, plainProjectKey }: TCr
return { key: encryptedProjectKey, iv: encryptedProjectKeyIv }; return { key: encryptedProjectKey, iv: encryptedProjectKeyIv };
}; };
export const verifyProjectVersions = (projects: Pick<TProjects, "version">[], version: ProjectVersion) => {
for (const project of projects) {
if (project.version !== version) {
return false;
}
}
return true;
};
export const getProjectKmsCertificateKeyId = async ({ export const getProjectKmsCertificateKeyId = async ({
projectId, projectId,
projectDAL, projectDAL,

View File

@@ -10,6 +10,7 @@ import { TKeyStoreFactory } from "@app/keystore/keystore";
import { isAtLeastAsPrivileged } from "@app/lib/casl"; import { isAtLeastAsPrivileged } from "@app/lib/casl";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
@@ -30,6 +31,8 @@ import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
import { getPredefinedRoles } from "../project-role/project-role-fns";
import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { ROOT_FOLDER_NAME, TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TProjectDALFactory } from "./project-dal"; import { TProjectDALFactory } from "./project-dal";
@@ -44,6 +47,7 @@ import {
TListProjectCasDTO, TListProjectCasDTO,
TListProjectCertificateTemplatesDTO, TListProjectCertificateTemplatesDTO,
TListProjectCertsDTO, TListProjectCertsDTO,
TListProjectsDTO,
TLoadProjectKmsBackupDTO, TLoadProjectKmsBackupDTO,
TToggleProjectAutoCapitalizationDTO, TToggleProjectAutoCapitalizationDTO,
TUpdateAuditLogsRetentionDTO, TUpdateAuditLogsRetentionDTO,
@@ -84,6 +88,7 @@ type TProjectServiceFactoryDep = {
orgDAL: Pick<TOrgDALFactory, "findOne">; orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">; keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">; projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
kmsService: Pick< kmsService: Pick<
TKmsServiceFactory, TKmsServiceFactory,
| "updateProjectSecretManagerKmsKey" | "updateProjectSecretManagerKmsKey"
@@ -112,6 +117,7 @@ export const projectServiceFactory = ({
projectEnvDAL, projectEnvDAL,
licenseService, licenseService,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
projectRoleDAL,
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateDAL, certificateDAL,
@@ -389,8 +395,34 @@ export const projectServiceFactory = ({
return deletedProject; return deletedProject;
}; };
const getProjects = async (actorId: string) => { const getProjects = async ({ actorId, includeRoles, actorAuthMethod, actorOrgId }: TListProjectsDTO) => {
const workspaces = await projectDAL.findAllProjects(actorId); const workspaces = await projectDAL.findAllProjects(actorId);
if (includeRoles) {
const { permission } = await permissionService.getUserOrgPermission(actorId, actorOrgId, actorAuthMethod);
// `includeRoles` is specifically used by organization admins when inviting new users to the organizations to avoid looping redundant api calls.
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Member);
const customRoles = await projectRoleDAL.find({
$in: {
projectId: workspaces.map((workspace) => workspace.id)
}
});
const workspaceMappedToRoles = groupBy(customRoles, (role) => role.projectId);
const workspacesWithRoles = await Promise.all(
workspaces.map(async (workspace) => {
return {
...workspace,
roles: [...(workspaceMappedToRoles[workspace.id] || []), ...getPredefinedRoles(workspace.id)]
};
})
);
return workspacesWithRoles;
}
return workspaces; return workspaces;
}; };

View File

@@ -75,6 +75,10 @@ export type TDeleteProjectDTO = {
actorOrgId: string | undefined; actorOrgId: string | undefined;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TListProjectsDTO = {
includeRoles: boolean;
} & Omit<TProjectPermission, "projectId">;
export type TUpgradeProjectDTO = { export type TUpgradeProjectDTO = {
userPrivateKey: string; userPrivateKey: string;
} & TProjectPermission; } & TProjectPermission;

View File

@@ -9,7 +9,7 @@
<body> <body>
<h2>Join your organization on Infisical</h2> <h2>Join your organization on Infisical</h2>
<p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}</p> <p>{{inviterFirstName}} ({{inviterUsername}}) has invited you to their Infisical organization — {{organizationName}}</p>
<a href="{{callback_url}}?token={{token}}&to={{email}}&organization_id={{organizationId}}">Join now</a> <a href="{{callback_url}}?token={{token}}{{#if metadata}}&metadata={{metadata}}{{/if}}&to={{email}}&organization_id={{organizationId}}">Join now</a>
<h3>What is Infisical?</h3> <h3>What is Infisical?</h3>
<p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p> <p>Infisical is an easy-to-use end-to-end encrypted tool that enables developers to sync and manage their secrets and configs.</p>
</body> </body>

View File

@@ -100,7 +100,9 @@ export type TIntegrationCreatedEvent = {
export type TUserOrgInvitedEvent = { export type TUserOrgInvitedEvent = {
event: PostHogEventTypes.UserOrgInvitation; event: PostHogEventTypes.UserOrgInvitation;
properties: { properties: {
inviteeEmail: string; inviteeEmails: string[];
projectIds?: string[];
organizationRoleSlug?: string;
}; };
}; };

View File

@@ -1,4 +1,4 @@
--- ---
title: "Retrieve CRL" title: "List CRLs"
openapi: "GET /api/v1/pki/ca/{caId}/crl" openapi: "GET /api/v1/pki/ca/{caId}/crls"
--- ---

View File

@@ -151,18 +151,24 @@ In the following steps, we explore how to revoke a X.509 certificate under a CA
</Step> </Step>
<Step title="Obtaining a CRL"> <Step title="Obtaining a CRL">
In order to check the revocation status of a certificate, you can check it In order to check the revocation status of a certificate, you can check it
against the CRL of a CA by selecting the **View CRL** option under the against the CRL of a CA by heading to its Issuing CA and downloading the CRL.
issuing CA and downloading the CRL file.
![pki view crl](/images/platform/pki/ca-crl.png) ![pki view crl](/images/platform/pki/ca-crl.png)
![pki download crl](/images/platform/pki/ca-crl-modal.png)
To verify a certificate against the To verify a certificate against the
downloaded CRL with OpenSSL, you can use the following command: downloaded CRL with OpenSSL, you can use the following command:
```bash ```bash
openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem
```
Note that you can also obtain the CRL from the certificate itself by
referencing the CRL distribution point extension on the certificate itself.
To check a certificate against the CRL distribution point specified within it with OpenSSL, you can use the following command:
```bash
openssl verify -verbose -crl_check -crl_download -CAfile chain.pem cert.pem
``` ```
</Step> </Step>
@@ -197,21 +203,25 @@ openssl verify -crl_check -CAfile chain.pem -CRLfile crl.pem cert.pem
</Step> </Step>
<Step title="Obtaining a CRL"> <Step title="Obtaining a CRL">
In order to check the revocation status of a certificate, you can check it against the CRL of the issuing CA. In order to check the revocation status of a certificate, you can check it against the CRL of the issuing CA.
To obtain the CRL of the CA, make an API request to the [Get CRL](/api-reference/endpoints/certificate-authorities/crl) API endpoint. To obtain the CRLs of the CA, make an API request to the [List CRLs](/api-reference/endpoints/certificate-authorities/crls) API endpoint.
### Sample request ### Sample request
```bash Request ```bash Request
curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/crl' \ curl --location --request GET 'https://app.infisical.com/api/v1/pki/ca/<ca-id>/crls' \
--header 'Authorization: Bearer <access-token>' --header 'Authorization: Bearer <access-token>'
``` ```
### Sample response ### Sample response
```bash Response ```bash Response
{ [
crl: "..." {
} id: "...",
crl: "..."
},
...
]
``` ```
To verify a certificate against the CRL with OpenSSL, you can use the following command: To verify a certificate against the CRL with OpenSSL, you can use the following command:

View File

@@ -327,10 +327,10 @@ the certificate back to the intermediate CA.
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?"> <Accordion title="Does Infisical support chaining an Intermediate CA to an external CA?">
Yes. You may obtain a CSR from the Intermediate CA and use it to generate a 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 certificate from your external CA. The certificate, along with the external
CA certificate, can be imported back to the Intermediate CA as part of the CA certificate chain, can be imported back to the Intermediate CA as part of
CA installation step. the CA installation step.
</Accordion> </Accordion>
</AccordionGroup> </AccordionGroup>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 638 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 649 KiB

After

Width:  |  Height:  |  Size: 833 KiB

View File

@@ -692,7 +692,7 @@
"api-reference/endpoints/certificate-authorities/import-cert", "api-reference/endpoints/certificate-authorities/import-cert",
"api-reference/endpoints/certificate-authorities/issue-cert", "api-reference/endpoints/certificate-authorities/issue-cert",
"api-reference/endpoints/certificate-authorities/sign-cert", "api-reference/endpoints/certificate-authorities/sign-cert",
"api-reference/endpoints/certificate-authorities/crl" "api-reference/endpoints/certificate-authorities/crls"
] ]
}, },
{ {

View File

@@ -1,10 +1,11 @@
--- ---
title: "Infisical Python SDK" title: "Infisical Python SDK"
sidebarTitle: "Python" sidebarTitle: "Python"
url: "https://github.com/Infisical/python-sdk-official?tab=readme-ov-file#infisical-python-sdk"
icon: "python" icon: "python"
--- ---
If you're working with Python, the official [infisical-python](https://github.com/Infisical/sdk/edit/main/crates/infisical-py) package is the easiest way to fetch and work with secrets for your application. {/* If you're working with Python, the official [infisical-python](https://github.com/Infisical/sdk/edit/main/crates/infisical-py) package is the easiest way to fetch and work with secrets for your application.
- [PyPi Package](https://pypi.org/project/infisical-python/) - [PyPi Package](https://pypi.org/project/infisical-python/)
- [Github Repository](https://github.com/Infisical/sdk/edit/main/crates/infisical-py) - [Github Repository](https://github.com/Infisical/sdk/edit/main/crates/infisical-py)
@@ -529,4 +530,4 @@ decryptedString = client.decryptSymmetric(decryptOptions)
#### Returns (string) #### Returns (string)
`plaintext` (string): The decrypted plaintext. `plaintext` (string): The decrypted plaintext. */}

View File

@@ -13,7 +13,7 @@ From local development to production, Infisical SDKs provide the easiest way for
<Card title="Node" href="/sdks/languages/node" icon="node" color="#68a063"> <Card title="Node" href="/sdks/languages/node" icon="node" color="#68a063">
Manage secrets for your Node application on demand Manage secrets for your Node application on demand
</Card> </Card>
<Card href="/sdks/languages/python" title="Python" icon="python" color="#4c8abe"> <Card href="https://github.com/Infisical/python-sdk-official" title="Python" icon="python" color="#4c8abe">
Manage secrets for your Python application on demand Manage secrets for your Python application on demand
</Card> </Card>
<Card href="/sdks/languages/java" title="Java" icon="java" color="#e41f23"> <Card href="/sdks/languages/java" title="Java" icon="java" color="#e41f23">

View File

@@ -2,7 +2,7 @@ const path = require("path");
const ContentSecurityPolicy = ` const ContentSecurityPolicy = `
default-src 'self'; default-src 'self';
script-src 'self' https://*.posthog.com https://*.*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval'; script-src 'self' https://*.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com https://hcaptcha.com https://*.hcaptcha.com 'unsafe-inline' 'unsafe-eval';
style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com; style-src 'self' https://rsms.me 'unsafe-inline' https://hcaptcha.com https://*.hcaptcha.com;
child-src https://api.stripe.com; child-src https://api.stripe.com;
frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com; frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/ https://hcaptcha.com https://*.hcaptcha.com;

View File

@@ -33,7 +33,8 @@ const integrationSlugNameMapping: Mapping = {
windmill: "Windmill", windmill: "Windmill",
"gcp-secret-manager": "GCP Secret Manager", "gcp-secret-manager": "GCP Secret Manager",
"hasura-cloud": "Hasura Cloud", "hasura-cloud": "Hasura Cloud",
rundeck: "Rundeck" rundeck: "Rundeck",
"azure-devops": "Azure DevOps"
}; };
const envMapping: Mapping = { const envMapping: Mapping = {

View File

@@ -1,116 +0,0 @@
import { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import Button from "../buttons/Button";
import InputField from "../InputField";
type Props = {
isOpen: boolean;
closeModal: () => void;
submitModal: (email: string) => void;
email: string;
setEmail: (email: string) => void;
orgName: string;
};
const AddUserDialog = ({ isOpen, closeModal, submitModal, email, setEmail, orgName }: Props) => {
const submit = () => {
submitModal(email);
};
return (
<div className="z-50">
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-70" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded-md border border-gray-700 bg-bunker-800 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="z-50 text-lg font-medium leading-6 text-gray-400"
>
Invite others to {orgName}
</Dialog.Title>
<div className="mt-2 mb-4">
<p className="text-sm text-gray-500">
An invite is specific to an email address and expires after 1 day. For
security reasons, you will need to separately add members to projects.
</p>
</div>
<div className="max-h-28">
<InputField
label="Email"
onChangeHandler={setEmail}
type="varName"
value={email}
placeholder=""
isRequired
/>
</div>
<div className="mt-4 max-w-max">
<Button onButtonPressed={submit} color="mineshaft" text="Invite" size="md" />
</div>
</Dialog.Panel>
{/* <Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-md bg-bunker-800 border border-gray-700 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-xl font-medium leading-6 text-gray-300 z-50"
>
Unleash Infisical's Full Power
</Dialog.Title>
<div className="mt-4 mb-4">
<p className="text-sm text-gray-400 mb-2">
You have exceeded the number of members in a free organization.
</p>
<p className="text-sm text-gray-400">
Upgrade now and get access to adding more members, as well as to other powerful enhancements.
</p>
</div>
<div className="mt-6">
<button
type="button"
className="inline-flex justify-center rounded-md border border-transparent bg-primary px-4 py-2 text-sm font-medium text-black hover:opacity-80 hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={() => router.push("/settings/billing/" + router.query.id)}
>
Upgrade Now
</button>
<button
type="button"
className="ml-2 inline-flex justify-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-sm font-medium text-gray-400 hover:bg-gray-500 hover:text-black hover:text-semibold duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
onClick={closeModal}
>
Maybe Later
</button>
</div>
</Dialog.Panel> */}
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</div>
);
};
export default AddUserDialog;

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { useAddUserToOrg } from "@app/hooks/api"; import { useAddUsersToOrg } from "@app/hooks/api";
import { useFetchServerStatus } from "@app/hooks/api/serverDetails"; import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
@@ -17,7 +17,7 @@ export default function TeamInviteStep(): JSX.Element {
const [emails, setEmails] = useState(""); const [emails, setEmails] = useState("");
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
const { mutateAsync } = useAddUserToOrg(); const { mutateAsync } = useAddUsersToOrg();
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const); const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
// Redirect user to the getting started page // Redirect user to the getting started page
@@ -31,8 +31,9 @@ export default function TeamInviteStep(): JSX.Element {
.map((email) => email.trim()) .map((email) => email.trim())
.map(async (email) => { .map(async (email) => {
mutateAsync({ mutateAsync({
inviteeEmail: email, inviteeEmails: [email],
organizationId: String(localStorage.getItem("orgData.id")) organizationId: String(localStorage.getItem("orgData.id")),
organizationRoleSlug: "member"
}); });
}); });

View File

@@ -93,6 +93,7 @@ export type CompleteAccountDTO = {
salt: string; salt: string;
verifier: string; verifier: string;
password: string; password: string;
tokenMetadata?: string;
}; };
export type CompleteAccountSignupDTO = CompleteAccountDTO & { export type CompleteAccountSignupDTO = CompleteAccountDTO & {

View File

@@ -1,4 +1,4 @@
export { CaRenewalType,CaStatus, CaType } from "./enums"; export { CaRenewalType, CaStatus, CaType } from "./enums";
export { export {
useCreateCa, useCreateCa,
useCreateCertificate, useCreateCertificate,
@@ -6,5 +6,6 @@ export {
useImportCaCertificate, useImportCaCertificate,
useRenewCa, useRenewCa,
useSignIntermediate, useSignIntermediate,
useUpdateCa} from "./mutations"; useUpdateCa
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrl, useGetCaCsr } from "./queries"; } from "./mutations";
export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCrls,useGetCaCsr } from "./queries";

View File

@@ -7,6 +7,7 @@ import { TCertificateAuthority } from "./types";
export const caKeys = { export const caKeys = {
getCaById: (caId: string) => [{ caId }, "ca"], getCaById: (caId: string) => [{ caId }, "ca"],
getCaCerts: (caId: string) => [{ caId }, "ca-cert"], getCaCerts: (caId: string) => [{ caId }, "ca-cert"],
getCaCrls: (caId: string) => [{ caId }, "ca-crls"],
getCaCert: (caId: string) => [{ caId }, "ca-cert"], getCaCert: (caId: string) => [{ caId }, "ca-cert"],
getCaCsr: (caId: string) => [{ caId }, "ca-csr"], getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
getCaCrl: (caId: string) => [{ caId }, "ca-crl"] getCaCrl: (caId: string) => [{ caId }, "ca-crl"]
@@ -73,16 +74,17 @@ export const useGetCaCsr = (caId: string) => {
}); });
}; };
export const useGetCaCrl = (caId: string) => { export const useGetCaCrls = (caId: string) => {
return useQuery({ return useQuery({
queryKey: caKeys.getCaCrl(caId), queryKey: caKeys.getCaCrls(caId),
queryFn: async () => { queryFn: async () => {
const { const { data } = await apiRequest.get<
data: { crl } {
} = await apiRequest.get<{ id: string;
crl: string; crl: string;
}>(`/api/v1/pki/ca/${caId}/crl`); }[]
return crl; >(`/api/v1/pki/ca/${caId}/crls`);
return data;
}, },
enabled: Boolean(caId) enabled: Boolean(caId)
}); });

View File

@@ -120,16 +120,22 @@ const fetchIntegrationAuthById = async (integrationAuthId: string) => {
const fetchIntegrationAuthApps = async ({ const fetchIntegrationAuthApps = async ({
integrationAuthId, integrationAuthId,
teamId, teamId,
azureDevOpsOrgName,
workspaceSlug workspaceSlug
}: { }: {
integrationAuthId: string; integrationAuthId: string;
teamId?: string; teamId?: string;
azureDevOpsOrgName?: string;
workspaceSlug?: string; workspaceSlug?: string;
}) => { }) => {
const params: Record<string, string> = {}; const params: Record<string, string> = {};
if (teamId) { if (teamId) {
params.teamId = teamId; params.teamId = teamId;
} }
if (azureDevOpsOrgName) {
params.azureDevOpsOrgName = azureDevOpsOrgName;
}
if (workspaceSlug) { if (workspaceSlug) {
params.workspaceSlug = workspaceSlug; params.workspaceSlug = workspaceSlug;
} }
@@ -452,10 +458,12 @@ export const useGetIntegrationAuthById = (integrationAuthId: string) => {
export const useGetIntegrationAuthApps = ({ export const useGetIntegrationAuthApps = ({
integrationAuthId, integrationAuthId,
teamId, teamId,
azureDevOpsOrgName,
workspaceSlug workspaceSlug
}: { }: {
integrationAuthId: string; integrationAuthId: string;
teamId?: string; teamId?: string;
azureDevOpsOrgName?: string;
workspaceSlug?: string; workspaceSlug?: string;
}) => { }) => {
return useQuery({ return useQuery({
@@ -464,6 +472,7 @@ export const useGetIntegrationAuthApps = ({
fetchIntegrationAuthApps({ fetchIntegrationAuthApps({
integrationAuthId, integrationAuthId,
teamId, teamId,
azureDevOpsOrgName,
workspaceSlug workspaceSlug
}), }),
enabled: true enabled: true

View File

@@ -47,7 +47,7 @@ export const roleQueryKeys = {
["user-project-permissions", { workspaceId }] as const ["user-project-permissions", { workspaceId }] as const
}; };
const getProjectRoles = async (projectId: string) => { export const getProjectRoles = async (projectId: string) => {
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>( const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
`/api/v1/workspace/${projectId}/roles` `/api/v1/workspace/${projectId}/roles`
); );

View File

@@ -6,7 +6,7 @@ export {
} from "./mutation"; } from "./mutation";
export { export {
fetchOrgUsers, fetchOrgUsers,
useAddUserToOrg, useAddUsersToOrg,
useCreateAPIKey, useCreateAPIKey,
useDeleteAPIKey, useDeleteAPIKey,
useDeleteMe, useDeleteMe,
@@ -26,4 +26,5 @@ export {
useRevokeMySessions, useRevokeMySessions,
useUpdateMfaEnabled, useUpdateMfaEnabled,
useUpdateOrgMembership, useUpdateOrgMembership,
useUpdateUserAuthMethods} from "./queries"; useUpdateUserAuthMethods
} from "./queries";

View File

@@ -157,12 +157,15 @@ export const useGetOrgUsers = (orgId: string) =>
// mutation // mutation
// TODO(akhilmhdh): move all mutation to mutation file // TODO(akhilmhdh): move all mutation to mutation file
export const useAddUserToOrg = () => { export const useAddUsersToOrg = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
type Response = { type Response = {
data: { data: {
message: string; message: string;
completeInviteLink: string | undefined; completeInviteLinks?: {
email: string;
link: string;
}[];
}; };
}; };

View File

@@ -149,7 +149,10 @@ export type DeletOrgMembershipDTO = {
}; };
export type AddUserToOrgDTO = { export type AddUserToOrgDTO = {
inviteeEmail: string; inviteeEmails: string[];
projectIds?: string[];
projectRoleSlug?: string;
organizationRoleSlug: string;
organizationId: string; organizationId: string;
}; };

View File

@@ -138,8 +138,12 @@ export const useGetUpgradeProjectStatus = ({
}); });
}; };
const fetchUserWorkspaces = async () => { const fetchUserWorkspaces = async (includeRoles?: boolean) => {
const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace"); const { data } = await apiRequest.get<{ workspaces: Workspace[] }>("/api/v1/workspace", {
params: {
includeRoles
}
});
return data.workspaces; return data.workspaces;
}; };
@@ -171,8 +175,8 @@ export const useGetWorkspaceById = (
}); });
}; };
export const useGetUserWorkspaces = () => export const useGetUserWorkspaces = (includeRoles?: boolean) =>
useQuery(workspaceKeys.getAllUserWorkspace, fetchUserWorkspaces); useQuery(workspaceKeys.getAllUserWorkspace, () => fetchUserWorkspaces(includeRoles));
const fetchUserWorkspaceMemberships = async (orgId: string) => { const fetchUserWorkspaceMemberships = async (orgId: string) => {
const { data } = await apiRequest.get<Record<string, Workspace[]>>( const { data } = await apiRequest.get<Record<string, Workspace[]>>(

View File

@@ -1,3 +1,5 @@
import { TProjectRole } from "../roles/types";
export enum ProjectVersion { export enum ProjectVersion {
V1 = 1, V1 = 1,
V2 = 2, V2 = 2,
@@ -22,6 +24,8 @@ export type Workspace = {
auditLogsRetentionDays: number; auditLogsRetentionDays: number;
slug: string; slug: string;
createdAt: string; createdAt: string;
roles?: TProjectRole[];
}; };
export type WorkspaceEnv = { export type WorkspaceEnv = {

View File

@@ -8,6 +8,7 @@ type UseToggleReturn = [
on: VoidFn; on: VoidFn;
off: VoidFn; off: VoidFn;
toggle: VoidFn; toggle: VoidFn;
timedToggle: (timeout?: number) => void;
} }
]; ];
@@ -26,5 +27,13 @@ export const useToggle = (initialState = false): UseToggleReturn => {
setValue((prev) => (typeof isOpen === "boolean" ? isOpen : !prev)); setValue((prev) => (typeof isOpen === "boolean" ? isOpen : !prev));
}, []); }, []);
return [value, { on, off, toggle }]; const timedToggle = useCallback((timeout = 2000) => {
setValue((prev) => !prev);
setTimeout(() => {
setValue(false);
}, timeout);
}, []);
return [value, { on, off, toggle, timedToggle }];
}; };

View File

@@ -0,0 +1,80 @@
import { useState } from "react";
import { useRouter } from "next/router";
import { useSaveIntegrationAccessToken } from "@app/hooks/api";
import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2";
export default function AzureDevopsCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useSaveIntegrationAccessToken();
const [apiKey, setApiKey] = useState("");
const [devopsOrgName, setDevopsOrgName] = useState("");
const [apiKeyErrorText, setApiKeyErrorText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handleButtonClick = async () => {
try {
setApiKeyErrorText("");
if (apiKey.length === 0) {
setApiKeyErrorText("API Key cannot be blank");
return;
}
setIsLoading(true);
localStorage.setItem("azure-devops-org-name", devopsOrgName);
const integrationAuth = await mutateAsync({
workspaceId: localStorage.getItem("projectData.id"),
integration: "azure-devops",
accessToken: btoa(`:${apiKey}`) // This is a base64 encoding of the API key without any username
});
setIsLoading(false);
router.push(`/integrations/azure-devops/create?integrationAuthId=${integrationAuth.id}`);
} catch (err) {
console.error(err);
}
};
return (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">AzureDevops Integration</CardTitle>
<FormControl
label="AzureDevops API Token"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input placeholder="" value={apiKey} onChange={(e) => setApiKey(e.target.value)} />
</FormControl>
<FormControl
label="Azure DevOps Organization Name"
tooltipText="This is the slug of the organization. An example would be 'my-acme-org'"
errorText={apiKeyErrorText}
isError={apiKeyErrorText !== "" ?? false}
>
<Input
placeholder=""
value={devopsOrgName}
onChange={(e) => setDevopsOrgName(e.target.value)}
/>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
>
Connect to AzureDevOps
</Button>
</Card>
</div>
);
}
AzureDevopsCreateIntegrationPage.requireAuth = true;

View File

@@ -0,0 +1,150 @@
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import queryString from "query-string";
import { useCreateIntegration } from "@app/hooks/api";
import {
Button,
Card,
CardTitle,
FormControl,
Input,
Select,
SelectItem
} from "../../../components/v2";
import {
useGetIntegrationAuthApps,
useGetIntegrationAuthById
} from "../../../hooks/api/integrationAuth";
import { useGetWorkspaceById } from "../../../hooks/api/workspace";
export default function AzureDevopsCreateIntegrationPage() {
const router = useRouter();
const { mutateAsync } = useCreateIntegration();
const { integrationAuthId } = queryString.parse(router.asPath.split("?")[1]);
const { data: workspace } = useGetWorkspaceById(localStorage.getItem("projectData.id") ?? "");
const { data: integrationAuth } = useGetIntegrationAuthById((integrationAuthId as string) ?? "");
const { data: integrationAuthApps } = useGetIntegrationAuthApps({
integrationAuthId: (integrationAuthId as string) ?? "",
azureDevOpsOrgName: localStorage.getItem("azure-devops-org-name") ?? ""
});
const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState("");
const [secretPath, setSecretPath] = useState("/");
const [targetApp, setTargetApp] = useState("");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (workspace) {
setSelectedSourceEnvironment(workspace.environments[0].slug);
}
}, [workspace]);
useEffect(() => {
if (integrationAuthApps) {
if (integrationAuthApps.length > 0) {
setTargetApp(integrationAuthApps[0].name);
} else {
setTargetApp("none");
}
}
}, [integrationAuthApps]);
const handleButtonClick = async () => {
try {
if (!integrationAuth?.id) return;
setIsLoading(true);
await mutateAsync({
integrationAuthId: integrationAuth?.id,
isActive: true,
app: localStorage.getItem("azure-devops-org-name") || "",
appId: targetApp,
sourceEnvironment: selectedSourceEnvironment,
secretPath
});
setIsLoading(false);
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
} catch (err) {
console.error(err);
}
};
return integrationAuth &&
workspace &&
selectedSourceEnvironment &&
integrationAuthApps &&
targetApp ? (
<div className="flex h-full w-full items-center justify-center">
<Card className="max-w-md rounded-md p-8">
<CardTitle className="text-center">AzureDevops Integration</CardTitle>
<FormControl label="Project Environment" className="mt-4">
<Select
value={selectedSourceEnvironment}
onValueChange={(val) => setSelectedSourceEnvironment(val)}
className="w-full border border-mineshaft-500"
>
{workspace?.environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
<FormControl label="Secrets Path">
<Input
value={secretPath}
onChange={(evt) => setSecretPath(evt.target.value)}
placeholder="Provide a path, default is /"
/>
</FormControl>
<FormControl label="AzureDevops Project" className="mt-4">
<Select
value={targetApp}
onValueChange={(val) => setTargetApp(val)}
className="w-full border border-mineshaft-500"
isDisabled={integrationAuthApps.length === 0}
>
{integrationAuthApps.length > 0 ? (
integrationAuthApps.map((integrationAuthApp) => (
<SelectItem
value={integrationAuthApp.name}
key={`target-environment-${integrationAuthApp.name}`}
>
{integrationAuthApp.name}
</SelectItem>
))
) : (
<SelectItem value="none" key="target-app-none">
No projects found
</SelectItem>
)}
</Select>
</FormControl>
<Button
onClick={handleButtonClick}
color="mineshaft"
className="mt-4"
isLoading={isLoading}
isDisabled={integrationAuthApps.length === 0}
>
Create Integration
</Button>
</Card>
</div>
) : (
<div />
);
}
AzureDevopsCreateIntegrationPage.requireAuth = true;

View File

@@ -38,6 +38,7 @@ export default function LoginPage() {
const { user, isLoading: userLoading } = useUser(); const { user, isLoading: userLoading } = useUser();
const queryParams = new URLSearchParams(window.location.search); const queryParams = new URLSearchParams(window.location.search);
const callbackPort = queryParams.get("callback_port");
const logout = useLogoutUser(true); const logout = useLogoutUser(true);
const handleLogout = useCallback(async () => { const handleLogout = useCallback(async () => {
@@ -52,8 +53,6 @@ export default function LoginPage() {
const handleSelectOrganization = useCallback( const handleSelectOrganization = useCallback(
async (organization: Organization) => { async (organization: Organization) => {
const callbackPort = queryParams.get("callback_port");
if (organization.authEnforced) { if (organization.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML) // org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO // -> logout + redirect to SAML SSO
@@ -116,9 +115,8 @@ export default function LoginPage() {
[selectOrg] [selectOrg]
); );
useEffect(() => { const handleCliRedirect = useCallback(() => {
const authToken = getAuthToken(); const authToken = getAuthToken();
const callbackPort = queryParams.get("callback_port");
if (authToken && !callbackPort) { if (authToken && !callbackPort) {
const decodedJwt = jwt_decode(authToken) as any; const decodedJwt = jwt_decode(authToken) as any;
@@ -131,13 +129,27 @@ export default function LoginPage() {
if (!isLoggedIn()) { if (!isLoggedIn()) {
router.push("/login"); router.push("/login");
} }
}, []);
useEffect(() => {
if (callbackPort) {
handleCliRedirect();
}
}, [router]); }, [router]);
// Case: User has no organizations. // Case: User has no organizations.
// This can happen if the user was previously a member, but the organization was deleted or the user was removed. // This can happen if the user was previously a member, but the organization was deleted or the user was removed.
useEffect(() => { useEffect(() => {
if (!organizations.isLoading && organizations.data?.length === 0) { if (organizations.isLoading || !organizations.data) return;
if (organizations.data.length === 0) {
router.push("/org/none"); router.push("/org/none");
} else if (organizations.data.length === 1) {
if (callbackPort) {
handleCliRedirect();
} else {
handleSelectOrganization(organizations.data[0]);
}
} }
}, [organizations.isLoading, organizations.data]); }, [organizations.isLoading, organizations.data]);

View File

@@ -64,6 +64,10 @@ export default function SignupInvite() {
const email = (parsedUrl.to as string)?.replace(" ", "+").trim(); const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
const { config } = useServerConfig(); const { config } = useServerConfig();
const queryParams = new URLSearchParams(window.location.search);
const metadata = queryParams.get("metadata") || undefined;
const { mutateAsync: selectOrganization } = useSelectOrganization(); const { mutateAsync: selectOrganization } = useSelectOrganization();
useEffect(() => { useEffect(() => {
@@ -160,7 +164,8 @@ export default function SignupInvite() {
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt: result.salt, salt: result.salt,
verifier: result.verifier verifier: result.verifier,
tokenMetadata: metadata
}); });
// unset temporary signup JWT token and set JWT token // unset temporary signup JWT token and set JWT token

View File

@@ -131,6 +131,9 @@ export const redirectForProviderAuth = (integrationOption: TCloudIntegration) =>
case "rundeck": case "rundeck":
link = `${window.location.origin}/integrations/rundeck/authorize`; link = `${window.location.origin}/integrations/rundeck/authorize`;
break; break;
case "azure-devops":
link = `${window.location.origin}/integrations/azure-devops/authorize`;
break;
default: default:
break; break;
} }

View File

@@ -1,65 +1,144 @@
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; import {
faCheckCircle,
faChevronDown,
faExclamationCircle
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup"; import { zodResolver } from "@hookform/resolvers/zod";
import * as yup from "yup"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, Modal, ModalContent } from "@app/components/v2"; import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
FormControl,
Modal,
ModalContent,
Select,
SelectItem,
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization } from "@app/context"; import { useOrganization } from "@app/context";
import { useToggle } from "@app/hooks"; import {
import { useAddUserToOrg, useFetchServerStatus } from "@app/hooks/api"; useAddUsersToOrg,
useFetchServerStatus,
useGetOrgRoles,
useGetUserWorkspaces
} from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { ProjectVersion } from "@app/hooks/api/workspace/types";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
const addMemberFormSchema = yup.object({ import { OrgInviteLink } from "./OrgInviteLink";
email: yup.string().email().required().label("Email").trim().lowercase()
const DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG = "member";
const EmailSchema = z.string().email().min(1).trim().toLowerCase();
const addMemberFormSchema = z.object({
emails: z.string().min(1).trim().toLowerCase(),
projectIds: z.array(z.string().min(1).trim().toLowerCase()).default([]),
projectRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG),
organizationRoleSlug: z.string().min(1).default(DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG)
}); });
type TAddMemberForm = yup.InferType<typeof addMemberFormSchema>; type TAddMemberForm = z.infer<typeof addMemberFormSchema>;
type Props = { type Props = {
popUp: UsePopUpState<["addMember"]>; popUp: UsePopUpState<["addMember"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void; handlePopUpToggle: (popUpName: keyof UsePopUpState<["addMember"]>, state?: boolean) => void;
completeInviteLink: string; completeInviteLinks: Array<{
setCompleteInviteLink: (link: string) => void; email: string;
link: string;
}> | null;
setCompleteInviteLinks: (links: Array<{ email: string; link: string }> | null) => void;
}; };
export const AddOrgMemberModal = ({ export const AddOrgMemberModal = ({
popUp, popUp,
handlePopUpToggle, handlePopUpToggle,
completeInviteLink, completeInviteLinks,
setCompleteInviteLink setCompleteInviteLinks
}: Props) => { }: Props) => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const { data: organizationRoles } = useGetOrgRoles(currentOrg?.id ?? "");
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg(); const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
const { data: projects } = useGetUserWorkspaces(true);
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const { const {
control, control,
handleSubmit, handleSubmit,
watch,
reset, reset,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<TAddMemberForm>({ resolver: yupResolver(addMemberFormSchema) }); } = useForm<TAddMemberForm>({ resolver: zodResolver(addMemberFormSchema) });
const onAddMember = async ({ email }: TAddMemberForm) => { const selectedProjectIds = watch("projectIds", []);
const onAddMembers = async ({
emails,
organizationRoleSlug,
projectIds,
projectRoleSlug
}: TAddMemberForm) => {
if (!currentOrg?.id) return; if (!currentOrg?.id) return;
const selectedProjects = projects?.filter((project) => projectIds.includes(String(project.id)));
if (selectedProjects?.length) {
// eslint-disable-next-line no-restricted-syntax
for (const project of selectedProjects) {
if (project.version !== ProjectVersion.V3) {
createNotification({
type: "error",
text: `Cannot add users to project "${project.name}" because it's incompatible. Please upgrade the project.`
});
return;
}
}
}
try { try {
const { data } = await addUserMutateAsync({ const parsedEmails = emails
.replace(/\s/g, "")
.split(",")
.map((email) => {
if (EmailSchema.safeParse(email).success) {
return email.trim();
}
return null;
});
if (parsedEmails.includes(null)) {
createNotification({
text: "Invalid email addresses provided.",
type: "error"
});
return;
}
const { data } = await addUsersMutateAsync({
organizationId: currentOrg?.id, organizationId: currentOrg?.id,
inviteeEmail: email inviteeEmails: emails.split(",").map((email) => email.trim()),
organizationRoleSlug,
projectIds,
projectRoleSlug
}); });
setCompleteInviteLink(data?.completeInviteLink ?? ""); setCompleteInviteLinks(data?.completeInviteLinks ?? null);
// only show this notification when email is configured. // only show this notification when email is configured.
// A [completeInviteLink] will not be sent if smtp is configured // A [completeInviteLink] will not be sent if smtp is configured
if (!data.completeInviteLink) { if (!data.completeInviteLinks) {
createNotification({ createNotification({
text: "Successfully invited user to the organization.", text: "Successfully invited user to the organization.",
type: "success" type: "success"
@@ -80,47 +159,196 @@ export const AddOrgMemberModal = ({
reset(); reset();
}; };
const copyTokenToClipboard = () => {
navigator.clipboard.writeText(completeInviteLink as string);
setInviteLinkCopied.on();
};
return ( return (
<Modal <Modal
isOpen={popUp?.addMember?.isOpen} isOpen={popUp?.addMember?.isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
handlePopUpToggle("addMember", isOpen); handlePopUpToggle("addMember", isOpen);
setCompleteInviteLink(""); setCompleteInviteLinks(null);
}} }}
> >
<ModalContent <ModalContent
title={`Invite others to ${currentOrg?.name}`} title={`Invite others to ${currentOrg?.name}`}
subTitle={ subTitle={
<div> <div>
{!completeInviteLink && ( {!completeInviteLinks && (
<div> <div>An invite is specific to an email address and expires after 1 day.</div>
An invite is specific to an email address and expires after 1 day.
<br />
For security reasons, you will need to separately add members to projects.
</div>
)} )}
{completeInviteLink && {completeInviteLinks &&
"This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"} "This Infisical instance does not have a email provider setup. Please share this invite link with the invitee manually"}
</div> </div>
} }
> >
{!completeInviteLink && ( {!completeInviteLinks && (
<form onSubmit={handleSubmit(onAddMember)}> <form onSubmit={handleSubmit(onAddMembers)}>
<Controller <Controller
control={control} control={control}
defaultValue="" name="emails"
name="email"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl label="Email" isError={Boolean(error)} errorText={error?.message}> <FormControl label="Emails" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} /> <TextArea
{...field}
className="mt-1 h-20 w-full min-w-[30rem] rounded-md border border-mineshaft-500 bg-mineshaft-900/70 py-1 px-2 text-sm text-bunker-300 outline-none ring-primary-800 ring-opacity-70 transition-all placeholder:text-bunker-400 focus:ring-2"
placeholder="email@example.com, email2@example.com..."
/>
</FormControl> </FormControl>
)} )}
/> />
<Controller
control={control}
name="organizationRoleSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="Select which organization role you want to assign to the user."
label="Assign organization role"
isError={Boolean(error)}
errorText={error?.message}
>
<div>
<Select
className="w-full"
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
{...field}
onValueChange={(val) => field.onChange(val)}
>
{organizationRoles?.map((role) => (
<SelectItem key={role.id} value={role.slug}>
{role.name}
</SelectItem>
))}
</Select>
</div>
</FormControl>
)}
/>
<div className="flex items-center justify-between gap-2">
<div className="w-full">
<Controller
control={control}
name="projectIds"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Assign users to projects (optional)"
isError={Boolean(error?.message)}
errorText={error?.message}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{projects && projects.length > 0 ? (
<div className="inline-flex w-full cursor-pointer items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
{/* eslint-disable-next-line no-nested-ternary */}
{selectedProjectIds.length === 1
? projects.find((project) => project.id === selectedProjectIds[0])
?.name
: selectedProjectIds.length === 0
? "No projects selected"
: `${selectedProjectIds.length} projects selected`}
<FontAwesomeIcon icon={faChevronDown} className="text-xs" />
</div>
) : (
<div className="inline-flex w-full cursor-default items-center justify-between rounded-md border border-mineshaft-600 bg-mineshaft-900 px-3 py-2 font-inter text-sm font-normal text-bunker-200 outline-none data-[placeholder]:text-mineshaft-200">
No projects found
</div>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
className="thin-scrollbar z-[100] max-h-80"
>
{projects && projects.length > 0 ? (
projects.map((project) => {
const isSelected = selectedProjectIds.includes(String(project.id));
return (
<DropdownMenuItem
onSelect={(event) =>
projects.length > 1 && event.preventDefault()
}
onClick={() => {
if (selectedProjectIds.includes(String(project.id))) {
field.onChange(
selectedProjectIds.filter(
(projectId: string) => projectId !== String(project.id)
)
);
} else {
field.onChange([...selectedProjectIds, String(project.id)]);
}
}}
key={`project-id-${project.id}`}
icon={
isSelected ? (
<FontAwesomeIcon
icon={faCheckCircle}
className="pr-0.5 text-primary"
/>
) : (
<div className="pl-[1.01rem]" />
)
}
iconPos="left"
className="w-[28.4rem] text-sm"
>
<div className="flex items-center gap-2 capitalize">
{project.name}
{project.version !== ProjectVersion.V3 && (
<Tooltip content="Project is not compatible with this action, please upgrade this project.">
<FontAwesomeIcon
icon={faExclamationCircle}
className="text-xs opacity-50"
/>
</Tooltip>
)}
</div>
</DropdownMenuItem>
);
})
) : (
<div />
)}
</DropdownMenuContent>
</DropdownMenu>
</FormControl>
)}
/>
</div>
<div className="flex min-w-fit justify-end">
<Controller
control={control}
name="projectRoleSlug"
render={({ field, fieldState: { error } }) => (
<FormControl
tooltipText="Select which role to assign to the users in the selected projects."
label="Role"
isError={Boolean(error)}
errorText={error?.message}
>
<div>
<Select
isDisabled={selectedProjectIds.length === 0}
defaultValue={DEFAULT_ORG_AND_PROJECT_MEMBER_ROLE_SLUG}
{...field}
onValueChange={(val) => field.onChange(val)}
>
{Object.entries(ProjectMembershipRole).map(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([_, slug]) =>
slug !== "custom" && (
<SelectItem key={slug} value={slug}>
<span className="capitalize">{slug.replace("-", " ")}</span>
</SelectItem>
)
)}
</Select>
</div>
</FormControl>
)}
/>
</div>
</div>
<div className="mt-8 flex items-center"> <div className="mt-8 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
@@ -141,20 +369,11 @@ export const AddOrgMemberModal = ({
</div> </div>
</form> </form>
)} )}
{completeInviteLink && ( {completeInviteLinks && (
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400"> <div className="space-y-3">
<p className="mr-4 break-all">{completeInviteLink}</p> {completeInviteLinks.map((invite) => (
<IconButton <OrgInviteLink key={`invite-${invite.email}`} invite={invite} />
ariaLabel="copy icon" ))}
colorSchema="secondary"
className="group relative"
onClick={copyTokenToClipboard}
>
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
click to copy
</span>
</IconButton>
</div> </div>
)} )}
</ModalContent> </ModalContent>

View File

@@ -0,0 +1,49 @@
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { IconButton, Tooltip } from "@app/components/v2";
import { useToggle } from "@app/hooks";
type Props = {
invite: { email: string; link: string };
};
export const OrgInviteLink = ({ invite }: Props) => {
const [isInviteLinkCopied, setInviteLinkCopied] = useToggle(false);
const copyTokenToClipboard = () => {
if (isInviteLinkCopied) return;
navigator.clipboard.writeText(invite.link);
setInviteLinkCopied.timedToggle();
createNotification({
type: "info",
text: "Copied invitation link to clipboard"
});
};
return (
<div key={`invite-${invite.email}`}>
<p className="text-sm text-mineshaft-400">
Invite for <span className="font-medium">{invite.email}</span>
</p>
<div className="flex flex-col gap-1 rounded-md bg-white/[0.04] p-2 text-base text-gray-400">
<p className="line-clamp-1 mr-4 overflow-hidden text-ellipsis whitespace-nowrap ">
{invite.link}
</p>
<Tooltip content={`Copy invitation link for ${invite.email}`}>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => copyTokenToClipboard()}
>
<FontAwesomeIcon icon={isInviteLinkCopied ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
);
};

View File

@@ -27,7 +27,10 @@ export const OrgMembersSection = () => {
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const orgId = currentOrg?.id ?? ""; const orgId = currentOrg?.id ?? "";
const [completeInviteLink, setCompleteInviteLink] = useState<string>(""); const [completeInviteLinks, setCompleteInviteLinks] = useState<Array<{
email: string;
link: string;
}> | null>(null);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addMember", "addMember",
@@ -132,13 +135,13 @@ export const OrgMembersSection = () => {
</div> </div>
<OrgMembersTable <OrgMembersTable
handlePopUpOpen={handlePopUpOpen} handlePopUpOpen={handlePopUpOpen}
setCompleteInviteLink={setCompleteInviteLink} setCompleteInviteLinks={setCompleteInviteLinks}
/> />
<AddOrgMemberModal <AddOrgMemberModal
popUp={popUp} popUp={popUp}
handlePopUpToggle={handlePopUpToggle} handlePopUpToggle={handlePopUpToggle}
completeInviteLink={completeInviteLink} completeInviteLinks={completeInviteLinks}
setCompleteInviteLink={setCompleteInviteLink} setCompleteInviteLinks={setCompleteInviteLinks}
/> />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.removeMember.isOpen} isOpen={popUp.removeMember.isOpen}

View File

@@ -33,7 +33,7 @@ import {
useUser useUser
} from "@app/context"; } from "@app/context";
import { import {
useAddUserToOrg, useAddUsersToOrg,
useFetchServerStatus, useFetchServerStatus,
useGetOrgRoles, useGetOrgRoles,
useGetOrgUsers, useGetOrgUsers,
@@ -50,10 +50,10 @@ type Props = {
description?: string; description?: string;
} }
) => void; ) => void;
setCompleteInviteLink: (link: string) => void; setCompleteInviteLinks: (links: Array<{ email: string; link: string }> | null) => void;
}; };
export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Props) => { export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLinks }: Props) => {
const router = useRouter(); const router = useRouter();
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
@@ -68,7 +68,7 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId); const { data: members, isLoading: isMembersLoading } = useGetOrgUsers(orgId);
const { mutateAsync: addUserMutateAsync } = useAddUserToOrg(); const { mutateAsync: addUsersMutateAsync } = useAddUsersToOrg();
const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership(); const { mutateAsync: updateOrgMembership } = useUpdateOrgMembership();
const onRoleChange = async (membershipId: string, role: string) => { const onRoleChange = async (membershipId: string, role: string) => {
@@ -106,14 +106,15 @@ export const OrgMembersTable = ({ handlePopUpOpen, setCompleteInviteLink }: Prop
const onResendInvite = async (email: string) => { const onResendInvite = async (email: string) => {
try { try {
const { data } = await addUserMutateAsync({ const { data } = await addUsersMutateAsync({
organizationId: orgId, organizationId: orgId,
inviteeEmail: email inviteeEmails: [email],
organizationRoleSlug: "member"
}); });
setCompleteInviteLink(data?.completeInviteLink || ""); setCompleteInviteLinks(data?.completeInviteLinks || null);
if (!data.completeInviteLink) { if (!data.completeInviteLinks) {
createNotification({ createNotification({
text: `Successfully resent invite to ${email}`, text: `Successfully resent invite to ${email}`,
type: "success" type: "success"

View File

@@ -18,7 +18,7 @@ import {
} from "@app/context"; } from "@app/context";
import { useTimedReset } from "@app/hooks"; import { useTimedReset } from "@app/hooks";
import { import {
useAddUserToOrg, useAddUsersToOrg,
useFetchServerStatus, useFetchServerStatus,
useGetOrgMembership, useGetOrgMembership,
useGetOrgRoles useGetOrgRoles
@@ -44,18 +44,19 @@ export const UserDetailsSection = ({ membershipId, handlePopUpOpen }: Props) =>
const { data: roles } = useGetOrgRoles(orgId); const { data: roles } = useGetOrgRoles(orgId);
const { data: serverDetails } = useFetchServerStatus(); const { data: serverDetails } = useFetchServerStatus();
const { data: membership } = useGetOrgMembership(orgId, membershipId); const { data: membership } = useGetOrgMembership(orgId, membershipId);
const { mutateAsync: inviteUser, isLoading } = useAddUserToOrg(); const { mutateAsync: inviteUsers, isLoading } = useAddUsersToOrg();
const onResendInvite = async (email: string) => { const onResendInvite = async (email: string) => {
try { try {
const { data } = await inviteUser({ const { data } = await inviteUsers({
organizationId: orgId, organizationId: orgId,
inviteeEmail: email inviteeEmails: [email],
organizationRoleSlug: "member"
}); });
// setCompleteInviteLink(data?.completeInviteLink || ""); // setCompleteInviteLink(data?.completeInviteLink || "");
if (!data.completeInviteLink) { if (!data.completeInviteLinks) {
createNotification({ createNotification({
text: `Successfully resent invite to ${email}`, text: `Successfully resent invite to ${email}`,
type: "success" type: "success"

View File

@@ -22,7 +22,12 @@ 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 { CaCertificatesSection, CaDetailsSection, CaRenewalModal } from "./components"; import {
CaCertificatesSection,
CaCrlsSection,
CaDetailsSection,
CaRenewalModal
} from "./components";
export const CaPage = withProjectPermission( export const CaPage = withProjectPermission(
() => { () => {
@@ -118,7 +123,10 @@ export const CaPage = withProjectPermission(
<div className="mr-4 w-96"> <div className="mr-4 w-96">
<CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} /> <CaDetailsSection caId={caId} handlePopUpOpen={handlePopUpOpen} />
</div> </div>
<CaCertificatesSection caId={caId} /> <div className="w-full">
<CaCertificatesSection caId={caId} />
<CaCrlsSection caId={caId} />
</div>
</div> </div>
</div> </div>
)} )}

View File

@@ -0,0 +1,20 @@
import { CaCrlsTable } from "./CaCrlsTable";
type Props = {
caId: string;
};
export const CaCrlsSection = ({ caId }: Props) => {
return (
<div className="mt-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-semibold text-mineshaft-100">
CA Certificate Revocation Lists (CRLs)
</h3>
</div>
<div className="py-4">
<CaCrlsTable caId={caId} />
</div>
</div>
);
};

View File

@@ -0,0 +1,88 @@
import { faCertificate, faFileDownload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
// import * as x509 from "@peculiar/x509";
// import { format } from "date-fns";
import FileSaver from "file-saver";
import {
EmptyState,
IconButton,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { useGetCaCrls } from "@app/hooks/api";
type Props = {
caId: string;
};
export const CaCrlsTable = ({ caId }: Props) => {
const { data: caCrls, isLoading } = useGetCaCrls(caId);
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
FileSaver.saveAs(blob, filename);
};
return (
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Distribution Point URL</Th>
{/* <Th>This Update</Th> */}
{/* <Th>Next Update</Th> */}
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isLoading && <TableSkeleton columns={4} innerKey="ca-certificates" />}
{!isLoading &&
caCrls?.map(({ id, crl }) => {
// const caCrlObj = new x509.X509Crl(crl);
return (
<Tr key={`ca-crl-${id}`}>
<Td>
<div className="flex items-center">
{`${window.origin}/api/v1/pki/crl/${id}`}
</div>
</Td>
{/* <Td>{format(new Date(caCrlObj.thisUpdate), "yyyy-MM-dd")}</Td> */}
{/* <Td>
{caCrlObj.nextUpdate
? format(new Date(caCrlObj.nextUpdate), "yyyy-MM-dd")
: "-"}
</Td> */}
<Td>
<Tooltip content="Download CRL">
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={(e) => {
e.stopPropagation();
downloadTxtFile("crl.pem", crl);
}}
>
<FontAwesomeIcon icon={faFileDownload} />
</IconButton>
</Tooltip>
</Td>
</Tr>
);
})}
</TBody>
</Table>
{!isLoading && !caCrls?.length && (
<EmptyState title="This CA does not have any CRLs" icon={faCertificate} />
)}
</TableContainer>
);
};

View File

@@ -0,0 +1 @@
export { CaCrlsSection } from "./CaCrlsSection";

View File

@@ -1,3 +1,4 @@
export { CaCertificatesSection } from "./CaCertificatesSection/CaCertificatesSection"; export { CaCertificatesSection } from "./CaCertificatesSection/CaCertificatesSection";
export { CaCrlsSection } from "./CaCrlsSection";
export { CaDetailsSection } from "./CaDetailsSection"; export { CaDetailsSection } from "./CaDetailsSection";
export { CaRenewalModal } from "./CaRenewalModal"; export { CaRenewalModal } from "./CaRenewalModal";

View File

@@ -1,106 +0,0 @@
import { useEffect } from "react";
import { faCheck, faCopy, faDownload } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconButton, Modal, ModalContent } from "@app/components/v2";
import { useToggle } from "@app/hooks";
import { useGetCaCrl } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
popUp: UsePopUpState<["caCrl"]>;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["caCrl"]>, state?: boolean) => void;
};
export const CaCrlModal = ({ popUp, handlePopUpToggle }: Props) => {
const [isCrlCopied, setIsCrlCopied] = useToggle(false);
const { data: crl } = useGetCaCrl((popUp?.caCrl?.data as { caId: string })?.caId || "");
useEffect(() => {
let timer: NodeJS.Timeout;
if (isCrlCopied) {
timer = setTimeout(() => setIsCrlCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isCrlCopied]);
const downloadTxtFile = (filename: string, content: string) => {
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
return (
<Modal
isOpen={popUp?.caCrl?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("caCrl", isOpen);
}}
>
<ModalContent title="CA Certificate Revocation List (CRL)">
<div>
{crl && (
<>
{/* <div className="mb-4 flex items-center justify-between">
<h2>Manual CRL Rotation</h2>
<Button
// isLoading={isLoading}
// isDisabled={!isAllowed}
colorSchema="primary"
variant="outline_bg"
type="submit"
// onClick={() => handleAssignment(username, !isPartOfGroup)}
onClick={() => {}}
>
Rotate
</Button>
</div> */}
<div className="mb-4 flex items-center justify-between">
<h2>Certificate Revocation List</h2>
<div className="flex">
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(crl);
setIsCrlCopied.on();
}}
>
<FontAwesomeIcon icon={isCrlCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Copy
</span>
</IconButton>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative ml-2"
onClick={() => {
downloadTxtFile("crl.pem", crl);
}}
>
<FontAwesomeIcon icon={faDownload} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
Download
</span>
</IconButton>
</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">{crl}</p>
</div>
</>
)}
</div>
</ModalContent>
</Modal>
);
};

View File

@@ -9,7 +9,6 @@ import { CaStatus, useDeleteCa, useUpdateCa } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { CaCertModal } from "./CaCertModal"; import { CaCertModal } from "./CaCertModal";
import { CaCrlModal } from "./CaCrlModal";
import { CaInstallCertModal } from "./CaInstallCertModal"; import { CaInstallCertModal } from "./CaInstallCertModal";
import { CaModal } from "./CaModal"; import { CaModal } from "./CaModal";
import { CaTable } from "./CaTable"; import { CaTable } from "./CaTable";
@@ -25,7 +24,6 @@ export const CaSection = () => {
"installCaCert", "installCaCert",
"deleteCa", "deleteCa",
"caStatus", // enable / disable "caStatus", // enable / disable
"caCrl", // enable / disable
"upgradePlan" "upgradePlan"
] as const); ] as const);
@@ -95,7 +93,6 @@ export const CaSection = () => {
<CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CaModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CaInstallCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <CaCertModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaCrlModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<CaTable handlePopUpOpen={handlePopUpOpen} /> <CaTable handlePopUpOpen={handlePopUpOpen} />
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteCa.isOpen} isOpen={popUp.deleteCa.isOpen}

View File

@@ -4,7 +4,6 @@ import {
faCertificate, faCertificate,
faEllipsis, faEllipsis,
faEye, faEye,
faFile,
faTrash faTrash
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -29,12 +28,7 @@ import {
Tooltip, Tooltip,
Tr Tr
} from "@app/components/v2"; } from "@app/components/v2";
import { import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
ProjectPermissionActions,
ProjectPermissionSub,
useSubscription,
useWorkspace
} from "@app/context";
import { CaStatus, useListWorkspaceCas } from "@app/hooks/api"; import { CaStatus, useListWorkspaceCas } from "@app/hooks/api";
import { import {
caStatusToNameMap, caStatusToNameMap,
@@ -46,7 +40,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = { type Props = {
handlePopUpOpen: ( handlePopUpOpen: (
popUpName: keyof UsePopUpState< popUpName: keyof UsePopUpState<
["installCaCert", "caCert", "ca", "deleteCa", "caStatus", "caCrl", "upgradePlan"] ["installCaCert", "caCert", "ca", "deleteCa", "caStatus", "upgradePlan"]
>, >,
data?: { data?: {
caId?: string; caId?: string;
@@ -59,7 +53,6 @@ type Props = {
export const CaTable = ({ handlePopUpOpen }: Props) => { export const CaTable = ({ handlePopUpOpen }: Props) => {
const router = useRouter(); const router = useRouter();
const { subscription } = useSubscription();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data, isLoading } = useListWorkspaceCas({ const { data, isLoading } = useListWorkspaceCas({
projectSlug: currentWorkspace?.slug ?? "" projectSlug: currentWorkspace?.slug ?? ""
@@ -162,38 +155,6 @@ export const CaTable = ({ handlePopUpOpen }: Props) => {
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
)} )}
{ca.status !== CaStatus.PENDING_CERTIFICATE && (
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed &&
"pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={(e) => {
e.stopPropagation();
if (!subscription?.caCrl) {
handlePopUpOpen("upgradePlan", {
description:
"You can use the certificate revocation list (CRL) feature if you upgrade your Infisical plan."
});
} else {
handlePopUpOpen("caCrl", {
caId: ca.id
});
}
}}
disabled={!isAllowed}
icon={<FontAwesomeIcon icon={faFile} />}
>
View CRL
</DropdownMenuItem>
)}
</ProjectPermissionCan>
)}
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Read} I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.CertificateAuthorities} a={ProjectPermissionSub.CertificateAuthorities}

View File

@@ -32,6 +32,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useListWorkspaceCertificates } from "@app/hooks/api"; import { useListWorkspaceCertificates } from "@app/hooks/api";
import { CertStatus } from "@app/hooks/api/certificates/enums";
import { UsePopUpState } from "@app/hooks/usePopUp"; import { UsePopUpState } from "@app/hooks/usePopUp";
import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils"; import { getCertValidUntilBadgeDetails } from "./CertificatesTable.utils";
@@ -82,9 +83,11 @@ export const CertificatesTable = ({ handlePopUpOpen }: Props) => {
<Tr className="h-10" key={`certificate-${certificate.id}`}> <Tr className="h-10" key={`certificate-${certificate.id}`}>
<Td>{certificate.friendlyName}</Td> <Td>{certificate.friendlyName}</Td>
<Td> <Td>
<Badge className="" variant={variant}> {certificate.status === CertStatus.REVOKED ? (
{label} <Badge variant="danger">Revoked</Badge>
</Badge> ) : (
<Badge variant={variant}>{label}</Badge>
)}
</Td> </Td>
<Td> <Td>
{certificate.notBefore {certificate.notBefore