mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-24 20:43:19 +00:00
Compare commits
47 Commits
fix/resolv
...
daniel/azu
Author | SHA1 | Date | |
---|---|---|---|
|
a6b852fab9 | ||
|
2a043afe11 | ||
|
df8f2cf9ab | ||
|
a18015b1e5 | ||
|
8b80622d2f | ||
|
c0fd0a56f3 | ||
|
326764dd41 | ||
|
c130fbddd9 | ||
|
10a97f4522 | ||
|
a2b994ab23 | ||
|
c4715124dc | ||
|
68b1984a76 | ||
|
ba45e83880 | ||
|
28ecc37163 | ||
|
a6a2e2bae0 | ||
|
d8bbfacae0 | ||
|
58549c398f | ||
|
842ed62bec | ||
|
06d8800ee0 | ||
|
2ecfd1bb7e | ||
|
783d4c7bd6 | ||
|
fbf3f26abd | ||
|
1d09693041 | ||
|
626e37e3d0 | ||
|
07fd67b328 | ||
|
3f1f018adc | ||
|
fe04e6d20c | ||
|
d7171a1617 | ||
|
384a0daa31 | ||
|
c5c949e034 | ||
|
c2c9edf156 | ||
|
c8248ef4e9 | ||
|
9f6a6a7b7c | ||
|
121b642d50 | ||
|
59b16f647e | ||
|
2ab5932693 | ||
|
8dfcef3900 | ||
|
8ca70eec44 | ||
|
60df59c7f0 | ||
|
e231c531a6 | ||
|
d48bb910fa | ||
|
288f47f4bd | ||
|
b090ebfd41 | ||
|
67773bff5e | ||
|
8ef1cfda04 | ||
|
2a79d5ba36 | ||
|
0cb95f36ff |
8
backend/package-lock.json
generated
8
backend/package-lock.json
generated
@@ -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",
|
||||||
|
@@ -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",
|
||||||
|
@@ -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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -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(),
|
||||||
|
@@ -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(),
|
||||||
|
@@ -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>;
|
||||||
|
@@ -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(),
|
||||||
|
@@ -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"
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
};
|
};
|
||||||
|
@@ -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" }
|
||||||
);
|
);
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -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">;
|
||||||
|
@@ -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,
|
||||||
|
@@ -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",
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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"
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// });
|
||||||
};
|
};
|
||||||
|
@@ -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: {
|
||||||
|
@@ -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(", ")}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -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 };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -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({
|
||||||
|
@@ -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;
|
||||||
|
};
|
||||||
|
@@ -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 },
|
||||||
|
@@ -37,4 +37,5 @@ export type TCompleteAccountInviteDTO = {
|
|||||||
ip: string;
|
ip: string;
|
||||||
userAgent: string;
|
userAgent: string;
|
||||||
authorization: string;
|
authorization: string;
|
||||||
|
tokenMetadata?: string;
|
||||||
};
|
};
|
||||||
|
@@ -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,
|
||||||
|
@@ -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,
|
||||||
|
@@ -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" });
|
||||||
}
|
}
|
||||||
|
@@ -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
|
||||||
});
|
});
|
||||||
|
@@ -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;
|
||||||
|
};
|
||||||
|
@@ -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: ""
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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);
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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 = {
|
||||||
|
@@ -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
|
||||||
|
};
|
||||||
|
};
|
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
52
backend/src/services/project-role/project-role-fns.ts
Normal file
52
backend/src/services/project-role/project-role-fns.ts
Normal 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));
|
||||||
|
};
|
@@ -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,
|
||||||
|
@@ -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,
|
||||||
|
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
@@ -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>
|
||||||
|
@@ -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;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
---
|
---
|
||||||
|
@@ -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.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
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:
|
||||||
|
@@ -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 |
@@ -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"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -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. */}
|
||||||
|
@@ -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">
|
||||||
|
@@ -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;
|
||||||
|
@@ -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 = {
|
||||||
|
@@ -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;
|
|
@@ -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"
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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 & {
|
||||||
|
@@ -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";
|
||||||
|
@@ -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)
|
||||||
});
|
});
|
||||||
|
@@ -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
|
||||||
|
@@ -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`
|
||||||
);
|
);
|
||||||
|
@@ -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";
|
||||||
|
@@ -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;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -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[]>>(
|
||||||
|
@@ -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 = {
|
||||||
|
@@ -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 }];
|
||||||
};
|
};
|
||||||
|
80
frontend/src/pages/integrations/azure-devops/authorize.tsx
Normal file
80
frontend/src/pages/integrations/azure-devops/authorize.tsx
Normal 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;
|
150
frontend/src/pages/integrations/azure-devops/create.tsx
Normal file
150
frontend/src/pages/integrations/azure-devops/create.tsx
Normal 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;
|
@@ -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]);
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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;
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
@@ -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}
|
||||||
|
@@ -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"
|
||||||
|
@@ -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"
|
||||||
|
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1 @@
|
|||||||
|
export { CaCrlsSection } from "./CaCrlsSection";
|
@@ -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";
|
||||||
|
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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}
|
||||||
|
@@ -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}
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user