mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-07 08:38:28 +00:00
Compare commits
60 Commits
pki-teleme
...
infisical-
Author | SHA1 | Date | |
---|---|---|---|
68d07f0136 | |||
10a3658328 | |||
e8ece6be3f | |||
c765c20539 | |||
5cdabd3e61 | |||
75ca093b24 | |||
6c0889f117 | |||
8f49d45309 | |||
a4a162ab65 | |||
5b11232325 | |||
8b53f63d69 | |||
6c0975554d | |||
697543e4a2 | |||
73b5ca5b4f | |||
a1318d54b1 | |||
44afe2fc1d | |||
956d0f6c5d | |||
c376add0fa | |||
977c02357b | |||
d4125443a3 | |||
8e3ac6ca29 | |||
fa9bdd21ff | |||
accf42de2e | |||
348a412cda | |||
c5a5ad93a8 | |||
67a0e5ae68 | |||
2af515c486 | |||
cdfec32195 | |||
8d6bd5d537 | |||
6ebc766308 | |||
6f9a66a0d7 | |||
cca7b68dd0 | |||
ab39f13e03 | |||
07bd527cc1 | |||
fa7843983f | |||
2d5b7afda7 | |||
4f08801ae8 | |||
cfe2bbe125 | |||
29dcf229d8 | |||
3741201b87 | |||
63d325c208 | |||
2149c0a9d1 | |||
430f8458cb | |||
bdb7cb4cbf | |||
54d002d718 | |||
dc2358bbaa | |||
fc651f6645 | |||
cc2c4b16bf | |||
1b05b7cf2c | |||
dcc3509a33 | |||
9dbe45a730 | |||
7875bcc067 | |||
9c702b27b2 | |||
db8a4bd26d | |||
2b7e1b465f | |||
b7b294f024 | |||
a3fb7c9f00 | |||
5ed164de24 | |||
596378208e | |||
943d0ddb69 |
@ -8,3 +8,7 @@ frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/S
|
||||
docs/mint.json:generic-api-key:651
|
||||
backend/src/ee/services/hsm/hsm-service.ts:generic-api-key:134
|
||||
docs/documentation/platform/audit-log-streams/audit-log-streams.mdx:generic-api-key:104
|
||||
docs/cli/commands/bootstrap.mdx:jwt:86
|
||||
docs/documentation/platform/audit-log-streams/audit-log-streams.mdx:generic-api-key:102
|
||||
docs/self-hosting/guides/automated-bootstrapping.mdx:jwt:74
|
||||
frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretDetailSidebar.tsx:generic-api-key:72
|
||||
|
@ -16,7 +16,7 @@ const createAuditLogPartition = async (knex: Knex, startDate: Date, endDate: Dat
|
||||
const startDateStr = formatPartitionDate(startDate);
|
||||
const endDateStr = formatPartitionDate(endDate);
|
||||
|
||||
const partitionName = `${TableName.AuditLog}_${startDateStr.replace(/-/g, "")}_${endDateStr.replace(/-/g, "")}`;
|
||||
const partitionName = `${TableName.AuditLog}_${startDateStr.replaceAll("-", "")}_${endDateStr.replaceAll("-", "")}`;
|
||||
|
||||
await knex.schema.raw(
|
||||
`CREATE TABLE ${partitionName} PARTITION OF ${TableName.AuditLog} FOR VALUES FROM ('${startDateStr}') TO ('${endDateStr}')`
|
||||
|
@ -0,0 +1,30 @@
|
||||
import { Knex } from "knex";
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.Organization, "shouldUseNewPrivilegeSystem"))) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.boolean("shouldUseNewPrivilegeSystem");
|
||||
t.string("privilegeUpgradeInitiatedByUsername");
|
||||
t.dateTime("privilegeUpgradeInitiatedAt");
|
||||
});
|
||||
|
||||
await knex(TableName.Organization).update({
|
||||
shouldUseNewPrivilegeSystem: false
|
||||
});
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.boolean("shouldUseNewPrivilegeSystem").defaultTo(true).notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.Organization, "shouldUseNewPrivilegeSystem")) {
|
||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
||||
t.dropColumn("shouldUseNewPrivilegeSystem");
|
||||
t.dropColumn("privilegeUpgradeInitiatedByUsername");
|
||||
t.dropColumn("privilegeUpgradeInitiatedAt");
|
||||
});
|
||||
}
|
||||
}
|
@ -23,6 +23,9 @@ export const OrganizationsSchema = z.object({
|
||||
defaultMembershipRole: z.string().default("member"),
|
||||
enforceMfa: z.boolean().default(false),
|
||||
selectedMfaMethod: z.string().nullable().optional(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean().default(true),
|
||||
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
||||
allowSecretSharingOutsideOrganization: z.boolean().default(true).nullable().optional()
|
||||
});
|
||||
|
||||
|
@ -16,7 +16,7 @@ export const registerCertificateEstRouter = async (server: FastifyZodProvider) =
|
||||
// for CSRs sent in PEM, we leave them as is
|
||||
// for CSRs sent in base64, we preprocess them to remove new lines and spaces
|
||||
if (!csrBody.includes("BEGIN CERTIFICATE REQUEST")) {
|
||||
csrBody = csrBody.replace(/\n/g, "").replace(/ /g, "");
|
||||
csrBody = csrBody.replaceAll("\n", "").replaceAll(" ", "");
|
||||
}
|
||||
|
||||
done(null, csrBody);
|
||||
|
@ -61,8 +61,8 @@ export const registerLdapRouter = async (server: FastifyZodProvider) => {
|
||||
if (ldapConfig.groupSearchBase) {
|
||||
const groupFilter = "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))";
|
||||
const groupSearchFilter = (ldapConfig.groupSearchFilter || groupFilter)
|
||||
.replace(/{{\.Username}}/g, user.uid)
|
||||
.replace(/{{\.UserDN}}/g, user.dn);
|
||||
.replaceAll("{{.Username}}", user.uid)
|
||||
.replaceAll("{{.UserDN}}", user.dn);
|
||||
|
||||
if (!isValidLdapFilter(groupSearchFilter)) {
|
||||
throw new Error("Generated LDAP search filter is invalid.");
|
||||
|
@ -45,7 +45,6 @@ export const auditLogStreamServiceFactory = ({
|
||||
}: TCreateAuditLogStreamDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID attached to authentication token" });
|
||||
|
||||
const appCfg = getConfig();
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.auditLogStreams) {
|
||||
throw new BadRequestError({
|
||||
@ -62,9 +61,8 @@ export const auditLogStreamServiceFactory = ({
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Settings);
|
||||
|
||||
if (appCfg.isCloud) {
|
||||
blockLocalAndPrivateIpAddresses(url);
|
||||
}
|
||||
const appCfg = getConfig();
|
||||
if (appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
const totalStreams = await auditLogStreamDAL.find({ orgId: actorOrgId });
|
||||
if (totalStreams.length >= plan.auditLogStreamLimit) {
|
||||
@ -135,9 +133,8 @@ export const auditLogStreamServiceFactory = ({
|
||||
const { orgId } = logStream;
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Settings);
|
||||
|
||||
const appCfg = getConfig();
|
||||
if (url && appCfg.isCloud) blockLocalAndPrivateIpAddresses(url);
|
||||
if (url && appCfg.isCloud) await blockLocalAndPrivateIpAddresses(url);
|
||||
|
||||
// testing connection first
|
||||
const streamHeaders: RawAxiosRequestHeaders = { "Content-Type": "application/json" };
|
||||
|
@ -968,6 +968,7 @@ interface LoginIdentityOidcAuthEvent {
|
||||
identityId: string;
|
||||
identityOidcAuthId: string;
|
||||
identityAccessTokenId: string;
|
||||
oidcClaimsReceived: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||
import { BadRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { isCertChainValid } from "@app/services/certificate/certificate-fns";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
@ -67,9 +68,7 @@ export const certificateEstServiceFactory = ({
|
||||
|
||||
const certTemplate = await certificateTemplateDAL.findById(certificateTemplateId);
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new UnauthorizedError({ message: "Missing client certificate" });
|
||||
@ -88,10 +87,7 @@ export const certificateEstServiceFactory = ({
|
||||
const verifiedChains = await Promise.all(
|
||||
caCertChains.map((chain) => {
|
||||
const caCert = new x509.X509Certificate(chain.certificate);
|
||||
const caChain =
|
||||
chain.certificateChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((c) => new x509.X509Certificate(c)) || [];
|
||||
const caChain = extractX509CertFromChain(chain.certificateChain)?.map((c) => new x509.X509Certificate(c)) || [];
|
||||
|
||||
return isCertChainValid([cert, caCert, ...caChain]);
|
||||
})
|
||||
@ -172,19 +168,15 @@ export const certificateEstServiceFactory = ({
|
||||
}
|
||||
|
||||
if (!estConfig.disableBootstrapCertValidation) {
|
||||
const caCerts = estConfig.caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
const caCerts = extractX509CertFromChain(estConfig.caChain)?.map((cert) => {
|
||||
return new x509.X509Certificate(cert);
|
||||
});
|
||||
|
||||
if (!caCerts) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
|
||||
const leafCertificate = decodeURIComponent(sslClientCert).match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
|
||||
)?.[0];
|
||||
const leafCertificate = extractX509CertFromChain(decodeURIComponent(sslClientCert))?.[0];
|
||||
|
||||
if (!leafCertificate) {
|
||||
throw new BadRequestError({ message: "Missing client certificate" });
|
||||
@ -250,13 +242,7 @@ export const certificateEstServiceFactory = ({
|
||||
kmsService
|
||||
});
|
||||
|
||||
const certificates = caCertChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
}
|
||||
const certificates = extractX509CertFromChain(caCertChain).map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
const caCertificate = new x509.X509Certificate(caCert);
|
||||
return convertRawCertsToPkcs7([caCertificate.rawData, ...certificates.map((cert) => cert.rawData)]);
|
||||
|
@ -95,7 +95,7 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
password
|
||||
});
|
||||
|
||||
const queries = creationStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
||||
const queries = creationStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||
|
||||
for await (const query of queries) {
|
||||
// If it's an adduser query, we need to first call sp_addlogin on the MASTER database.
|
||||
@ -116,7 +116,7 @@ export const SapAseProvider = (): TDynamicProviderFns => {
|
||||
username
|
||||
});
|
||||
|
||||
const queries = revokeStatement.trim().replace(/\n/g, "").split(";").filter(Boolean);
|
||||
const queries = revokeStatement.trim().replaceAll("\n", "").split(";").filter(Boolean);
|
||||
|
||||
const client = await $getClient(providerInputs);
|
||||
const masterClient = await $getClient(providerInputs, true);
|
||||
|
@ -3,8 +3,7 @@ import slugify from "@sindresorhus/slugify";
|
||||
|
||||
import { OrgMembershipRole, TOrgRoles } from "@app/db/schemas";
|
||||
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||
import { TOrgDALFactory } from "@app/services/org/org-dal";
|
||||
@ -14,7 +13,8 @@ import { TProjectKeyDALFactory } from "@app/services/project-key/project-key-dal
|
||||
import { TUserDALFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { OrgPermissionGroupActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TGroupDALFactory } from "./group-dal";
|
||||
import { addUsersToGroupByUserIds, removeUsersFromGroupByUserIds } from "./group-fns";
|
||||
@ -67,14 +67,14 @@ export const groupServiceFactory = ({
|
||||
const createGroup = async ({ name, slug, role, actor, actorId, actorAuthMethod, actorOrgId }: TCreateGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.groups)
|
||||
@ -87,14 +87,26 @@ export const groupServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
const isCustomRole = Boolean(customRole);
|
||||
if (role !== OrgMembershipRole.NoAccess) {
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Groups,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to create a more privileged group",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to create group",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Groups
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
}
|
||||
|
||||
const group = await groupDAL.transaction(async (tx) => {
|
||||
const existingGroup = await groupDAL.findOne({ orgId: actorOrgId, name }, tx);
|
||||
@ -133,14 +145,15 @@ export const groupServiceFactory = ({
|
||||
}: TUpdateGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
if (!plan.groups)
|
||||
@ -161,11 +174,21 @@ export const groupServiceFactory = ({
|
||||
);
|
||||
|
||||
const isCustomRole = Boolean(customOrgRole);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Groups,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update a more privileged group",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update group",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Groups
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
if (isCustomRole) customRole = customOrgRole;
|
||||
@ -215,7 +238,7 @@ export const groupServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups);
|
||||
|
||||
const plan = await licenseService.getPlan(actorOrgId);
|
||||
|
||||
@ -242,7 +265,7 @@ export const groupServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
|
||||
|
||||
const group = await groupDAL.findById(id);
|
||||
if (!group) {
|
||||
@ -275,7 +298,7 @@ export const groupServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
|
||||
|
||||
const group = await groupDAL.findOne({
|
||||
orgId: actorOrgId,
|
||||
@ -303,14 +326,14 @@ export const groupServiceFactory = ({
|
||||
const addUserToGroup = async ({ id, username, actor, actorId, actorAuthMethod, actorOrgId }: TAddUserToGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
// check if group with slug exists
|
||||
const group = await groupDAL.findOne({
|
||||
@ -338,11 +361,22 @@ export const groupServiceFactory = ({
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.AddMembers,
|
||||
OrgPermissionSubjects.Groups,
|
||||
permission,
|
||||
groupRolePermission
|
||||
);
|
||||
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to add user to more privileged group",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to add user to more privileged group",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.AddMembers,
|
||||
OrgPermissionSubjects.Groups
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -374,14 +408,14 @@ export const groupServiceFactory = ({
|
||||
}: TRemoveUserFromGroupDTO) => {
|
||||
if (!actorOrgId) throw new UnauthorizedError({ message: "No organization ID provided in request" });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
|
||||
|
||||
// check if group with slug exists
|
||||
const group = await groupDAL.findOne({
|
||||
@ -409,11 +443,21 @@ export const groupServiceFactory = ({
|
||||
const { permission: groupRolePermission } = await permissionService.getOrgPermissionByRole(group.role, actorOrgId);
|
||||
|
||||
// check if user has broader or equal to privileges than group
|
||||
const permissionBoundary = validatePermissionBoundary(permission, groupRolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.RemoveMembers,
|
||||
OrgPermissionSubjects.Groups,
|
||||
permission,
|
||||
groupRolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to delete user from more privileged group",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to delete user from more privileged group",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionGroupActions.RemoveMembers,
|
||||
OrgPermissionSubjects.Groups
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
|
@ -2,8 +2,7 @@ import { ForbiddenError, subject } from "@casl/ability";
|
||||
import { packRules } from "@casl/ability/extra";
|
||||
|
||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
import { unpackPermissions } from "@app/server/routes/sanitizedSchema/permission";
|
||||
@ -11,8 +10,9 @@ import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TIdentityProjectAdditionalPrivilegeV2DALFactory } from "./identity-project-additional-privilege-v2-dal";
|
||||
import {
|
||||
IdentityProjectAdditionalPrivilegeTemporaryMode,
|
||||
@ -65,10 +65,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
|
||||
const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({
|
||||
actor: ActorType.IDENTITY,
|
||||
actorId: identityId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -80,11 +80,21 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
targetIdentityPermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
validateHandlebarTemplate("Identity Additional Privilege Create", JSON.stringify(customPermission || []), {
|
||||
@ -154,10 +164,10 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||
);
|
||||
const { permission: targetIdentityPermission } = await permissionService.getProjectPermission({
|
||||
const { permission: targetIdentityPermission, membership } = await permissionService.getProjectPermission({
|
||||
actor: ActorType.IDENTITY,
|
||||
actorId: identityProjectMembership.identityId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -169,11 +179,21 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
targetIdentityPermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -235,7 +255,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
message: `Failed to find identity with membership ${identityPrivilege.projectMembershipId}`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -244,7 +264,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||
);
|
||||
const { permission: identityRolePermission } = await permissionService.getProjectPermission({
|
||||
@ -255,11 +275,21 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
identityRolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -295,7 +325,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||
);
|
||||
|
||||
@ -330,7 +360,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||
);
|
||||
|
||||
@ -366,7 +396,7 @@ export const identityProjectAdditionalPrivilegeV2ServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId: identityProjectMembership.identityId })
|
||||
);
|
||||
|
||||
|
@ -2,8 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf, subject } from "@casl/ability"
|
||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||
@ -11,8 +10,13 @@ import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import {
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub
|
||||
} from "../permission/project-permission";
|
||||
import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal";
|
||||
import {
|
||||
IdentityProjectAdditionalPrivilegeTemporaryMode,
|
||||
@ -64,7 +68,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
if (!identityProjectMembership)
|
||||
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -72,8 +76,9 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
@ -89,11 +94,21 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(customPermission));
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
targetIdentityPermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -155,7 +170,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
if (!identityProjectMembership)
|
||||
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -165,7 +180,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
@ -181,11 +196,21 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetIdentityPermission.update(targetIdentityPermission.rules.concat(data.permissions || []));
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetIdentityPermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
targetIdentityPermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -263,7 +288,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
if (!identityProjectMembership)
|
||||
throw new NotFoundError({ message: `Failed to find identity with id ${identityId}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
@ -272,7 +297,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
@ -284,11 +309,21 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
identityRolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to edit more privileged identity",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to edit more privileged identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -335,7 +370,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
@ -379,7 +414,7 @@ export const identityProjectAdditionalPrivilegeServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
|
@ -4,8 +4,9 @@ import crypto, { KeyObject } from "crypto";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { BadRequestError, InternalServerError, NotFoundError } from "@app/lib/errors";
|
||||
import { isValidHostname, isValidIp } from "@app/lib/ip";
|
||||
import { isValidIp } from "@app/lib/ip";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
import { constructPemChainFromCerts } from "@app/services/certificate/certificate-fns";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
@ -665,7 +666,7 @@ export const kmipServiceFactory = ({
|
||||
.split(",")
|
||||
.map((name) => name.trim())
|
||||
.map((altName) => {
|
||||
if (isValidHostname(altName)) {
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
return {
|
||||
type: "dns",
|
||||
value: altName
|
||||
|
@ -97,12 +97,14 @@ export const searchGroups = async (
|
||||
|
||||
res.on("searchEntry", (entry) => {
|
||||
const dn = entry.dn.toString();
|
||||
const regex = /cn=([^,]+)/;
|
||||
const match = dn.match(regex);
|
||||
// parse the cn from the dn
|
||||
const cn = (match && match[1]) as string;
|
||||
const cnStartIndex = dn.indexOf("cn=");
|
||||
|
||||
groups.push({ dn, cn });
|
||||
if (cnStartIndex !== -1) {
|
||||
const valueStartIndex = cnStartIndex + 3;
|
||||
const commaIndex = dn.indexOf(",", valueStartIndex);
|
||||
const cn = dn.substring(valueStartIndex, commaIndex === -1 ? undefined : commaIndex);
|
||||
groups.push({ dn, cn });
|
||||
}
|
||||
});
|
||||
res.on("error", (error) => {
|
||||
ldapClient.unbind();
|
||||
|
@ -44,6 +44,28 @@ export enum OrgPermissionGatewayActions {
|
||||
DeleteGateways = "delete-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionIdentityActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges",
|
||||
RevokeAuth = "revoke-auth",
|
||||
CreateToken = "create-token",
|
||||
GetToken = "get-token",
|
||||
DeleteToken = "delete-token"
|
||||
}
|
||||
|
||||
export enum OrgPermissionGroupActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges",
|
||||
AddMembers = "add-members",
|
||||
RemoveMembers = "remove-members"
|
||||
}
|
||||
|
||||
export enum OrgPermissionSubjects {
|
||||
Workspace = "workspace",
|
||||
Role = "role",
|
||||
@ -80,10 +102,10 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Sso]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Scim]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Ldap]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Groups]
|
||||
| [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Billing]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
@ -256,20 +278,28 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Ldap);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.Create, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.Edit, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.Delete, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.GrantPrivileges, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.AddMembers, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.RemoveMembers, OrgPermissionSubjects.Groups);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Billing);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Billing);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.GrantPrivileges, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.RevokeAuth, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.CreateToken, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.GetToken, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.DeleteToken, OrgPermissionSubjects.Identity);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Kms);
|
||||
@ -316,7 +346,7 @@ const buildMemberPermission = () => {
|
||||
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Workspace);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Member);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Role);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Settings);
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Billing);
|
||||
@ -327,10 +357,10 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.SecretScanning);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.SecretScanning);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
|
||||
|
||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||
|
||||
|
@ -49,6 +49,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
.join(TableName.Organization, `${TableName.Organization}.id`, `${TableName.OrgMembership}.orgId`)
|
||||
.select(
|
||||
selectAllTableCols(TableName.OrgMembership),
|
||||
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization),
|
||||
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.OrgRoles),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
@ -70,7 +71,8 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
OrgMembershipsSchema.extend({
|
||||
permissions: z.unknown(),
|
||||
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||
customRoleSlug: z.string().optional().nullable()
|
||||
customRoleSlug: z.string().optional().nullable(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean()
|
||||
}).parse(el),
|
||||
childrenMapper: [
|
||||
{
|
||||
@ -118,7 +120,9 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.IdentityOrgMembership))
|
||||
.select(db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"))
|
||||
.select("permissions")
|
||||
.select(db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization))
|
||||
.first();
|
||||
|
||||
return membership;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "GetOrgIdentityPermission" });
|
||||
@ -668,7 +672,8 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
db.ref("type").withSchema(TableName.Project).as("projectType"),
|
||||
db.ref("id").withSchema(TableName.Project).as("projectId")
|
||||
db.ref("id").withSchema(TableName.Project).as("projectId"),
|
||||
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization)
|
||||
);
|
||||
|
||||
const [userPermission] = sqlNestRelationships({
|
||||
@ -684,7 +689,8 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
groupMembershipCreatedAt,
|
||||
groupMembershipUpdatedAt,
|
||||
membershipUpdatedAt,
|
||||
projectType
|
||||
projectType,
|
||||
shouldUseNewPrivilegeSystem
|
||||
}) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
@ -694,7 +700,8 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
projectType,
|
||||
id: membershipId || groupMembershipId,
|
||||
createdAt: membershipCreatedAt || groupMembershipCreatedAt,
|
||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt
|
||||
updatedAt: membershipUpdatedAt || groupMembershipUpdatedAt,
|
||||
shouldUseNewPrivilegeSystem
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
@ -995,6 +1002,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
`${TableName.IdentityProjectMembership}.projectId`,
|
||||
`${TableName.Project}.id`
|
||||
)
|
||||
.join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`)
|
||||
.leftJoin(TableName.IdentityMetadata, (queryBuilder) => {
|
||||
void queryBuilder
|
||||
.on(`${TableName.Identity}.id`, `${TableName.IdentityMetadata}.identityId`)
|
||||
@ -1012,6 +1020,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"),
|
||||
db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.ProjectRoles),
|
||||
db.ref("shouldUseNewPrivilegeSystem").withSchema(TableName.Organization),
|
||||
db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"),
|
||||
db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"),
|
||||
db
|
||||
@ -1045,7 +1054,8 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
membershipUpdatedAt,
|
||||
orgId,
|
||||
identityName,
|
||||
projectType
|
||||
projectType,
|
||||
shouldUseNewPrivilegeSystem
|
||||
}) => ({
|
||||
id: membershipId,
|
||||
identityId,
|
||||
@ -1055,6 +1065,7 @@ export const permissionDALFactory = (db: TDbClient) => {
|
||||
updatedAt: membershipUpdatedAt,
|
||||
orgId,
|
||||
projectType,
|
||||
shouldUseNewPrivilegeSystem,
|
||||
// just a prefilled value
|
||||
orgAuthEnforced: false
|
||||
}),
|
||||
|
@ -3,9 +3,11 @@ import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/abilit
|
||||
import { z } from "zod";
|
||||
|
||||
import { TOrganizations } from "@app/db/schemas";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
|
||||
|
||||
import { OrgPermissionSet } from "./org-permission";
|
||||
import {
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSet,
|
||||
@ -145,4 +147,57 @@ const escapeHandlebarsMissingDict = (obj: Record<string, string>, key: string) =
|
||||
return new Proxy(obj, handler);
|
||||
};
|
||||
|
||||
export { escapeHandlebarsMissingDict, isAuthMethodSaml, validateOrgSSO };
|
||||
// This function serves as a transition layer between the old and new privilege management system
|
||||
// the old privilege management system is based on the actor having more privileges than the managed permission
|
||||
// the new privilege management system is based on the actor having the appropriate permission to perform the privilege change,
|
||||
// regardless of the actor's privilege level.
|
||||
const validatePrivilegeChangeOperation = (
|
||||
shouldUseNewPrivilegeSystem: boolean,
|
||||
opAction: OrgPermissionSet[0] | ProjectPermissionSet[0],
|
||||
opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1],
|
||||
actorPermission: MongoAbility,
|
||||
managedPermission: MongoAbility
|
||||
) => {
|
||||
if (shouldUseNewPrivilegeSystem) {
|
||||
if (actorPermission.can(opAction, opSubject)) {
|
||||
return {
|
||||
isValid: true,
|
||||
missingPermissions: []
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
missingPermissions: [
|
||||
{
|
||||
action: opAction,
|
||||
subject: opSubject
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// if not, we check if the actor is indeed more privileged than the managed permission - this is the old system
|
||||
return validatePermissionBoundary(actorPermission, managedPermission);
|
||||
};
|
||||
|
||||
const constructPermissionErrorMessage = (
|
||||
baseMessage: string,
|
||||
shouldUseNewPrivilegeSystem: boolean,
|
||||
opAction: OrgPermissionSet[0] | ProjectPermissionSet[0],
|
||||
opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1]
|
||||
) => {
|
||||
return `${baseMessage}${
|
||||
shouldUseNewPrivilegeSystem
|
||||
? `. Actor is missing permission ${opAction as string} on ${opSubject as string}`
|
||||
: ". Actor privilege level is not high enough to perform this action"
|
||||
}`;
|
||||
};
|
||||
|
||||
export {
|
||||
constructPermissionErrorMessage,
|
||||
escapeHandlebarsMissingDict,
|
||||
isAuthMethodSaml,
|
||||
validateOrgSSO,
|
||||
validatePrivilegeChangeOperation
|
||||
};
|
||||
|
@ -397,14 +397,18 @@ export const permissionServiceFactory = ({
|
||||
const scopes = ServiceTokenScopes.parse(serviceToken.scopes || []);
|
||||
return {
|
||||
permission: buildServiceTokenProjectPermission(scopes, serviceToken.permissions),
|
||||
membership: undefined
|
||||
membership: {
|
||||
shouldUseNewPrivilegeSystem: true
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
type TProjectPermissionRT<T extends ActorType> = T extends ActorType.SERVICE
|
||||
? {
|
||||
permission: MongoAbility<ProjectPermissionSet, MongoQuery>;
|
||||
membership: undefined;
|
||||
membership: {
|
||||
shouldUseNewPrivilegeSystem: boolean;
|
||||
};
|
||||
hasRole: (arg: string) => boolean;
|
||||
} // service token doesn't have both membership and roles
|
||||
: {
|
||||
@ -413,6 +417,7 @@ export const permissionServiceFactory = ({
|
||||
orgAuthEnforced: boolean | null | undefined;
|
||||
orgId: string;
|
||||
roles: Array<{ role: string }>;
|
||||
shouldUseNewPrivilegeSystem: boolean;
|
||||
};
|
||||
hasRole: (role: string) => boolean;
|
||||
};
|
||||
|
@ -43,6 +43,30 @@ export enum ProjectPermissionDynamicSecretActions {
|
||||
Lease = "lease"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionIdentityActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionMemberActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionGroupActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
Edit = "edit",
|
||||
Delete = "delete",
|
||||
GrantPrivileges = "grant-privileges"
|
||||
}
|
||||
|
||||
export enum ProjectPermissionSecretSyncActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@ -150,8 +174,8 @@ export type ProjectPermissionSet =
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Role]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Tags]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Member]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
|
||||
| [ProjectPermissionMemberActions, ProjectPermissionSub.Member]
|
||||
| [ProjectPermissionGroupActions, ProjectPermissionSub.Groups]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
|
||||
@ -162,7 +186,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||
| [
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionIdentityActions,
|
||||
ProjectPermissionSub.Identity | (ForcedSubject<ProjectPermissionSub.Identity> & IdentityManagementSubjectFields)
|
||||
]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||
@ -290,13 +314,13 @@ const GeneralPermissionSchema = [
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Member).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionMemberActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Groups).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionGroupActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
@ -510,7 +534,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
||||
z.object({
|
||||
subject: z.literal(ProjectPermissionSub.Identity).describe("The entity this permission pertains to."),
|
||||
inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionIdentityActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
),
|
||||
conditions: IdentityManagementConditionSchema.describe(
|
||||
@ -531,12 +555,9 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretImports,
|
||||
ProjectPermissionSub.SecretApproval,
|
||||
ProjectPermissionSub.SecretRotation,
|
||||
ProjectPermissionSub.Member,
|
||||
ProjectPermissionSub.Groups,
|
||||
ProjectPermissionSub.Role,
|
||||
ProjectPermissionSub.Integrations,
|
||||
ProjectPermissionSub.Webhooks,
|
||||
ProjectPermissionSub.Identity,
|
||||
ProjectPermissionSub.ServiceTokens,
|
||||
ProjectPermissionSub.Settings,
|
||||
ProjectPermissionSub.Environments,
|
||||
@ -563,6 +584,39 @@ const buildAdminPermissionRules = () => {
|
||||
);
|
||||
});
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionMemberActions.Create,
|
||||
ProjectPermissionMemberActions.Edit,
|
||||
ProjectPermissionMemberActions.Delete,
|
||||
ProjectPermissionMemberActions.Read,
|
||||
ProjectPermissionMemberActions.GrantPrivileges
|
||||
],
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionGroupActions.Create,
|
||||
ProjectPermissionGroupActions.Edit,
|
||||
ProjectPermissionGroupActions.Delete,
|
||||
ProjectPermissionGroupActions.Read,
|
||||
ProjectPermissionGroupActions.GrantPrivileges
|
||||
],
|
||||
ProjectPermissionSub.Groups
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionIdentityActions.Create,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionIdentityActions.Delete,
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges
|
||||
],
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretActions.DescribeAndReadValue,
|
||||
@ -677,9 +731,9 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
|
||||
|
||||
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.Member);
|
||||
can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member);
|
||||
|
||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.Groups);
|
||||
can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups);
|
||||
|
||||
can(
|
||||
[
|
||||
@ -703,10 +757,10 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionActions.Delete
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
ProjectPermissionIdentityActions.Create,
|
||||
ProjectPermissionIdentityActions.Delete
|
||||
],
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
@ -820,12 +874,12 @@ const buildViewerPermissionRules = () => {
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRotation);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Integrations);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Webhooks);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
||||
|
@ -2,16 +2,20 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||
|
||||
import { ActionProjectType, TableName } from "@app/db/schemas";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { validateHandlebarTemplate } from "@app/lib/template/validate-handlebars";
|
||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
|
||||
import { constructPermissionErrorMessage, validatePrivilegeChangeOperation } from "../permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSet, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import {
|
||||
ProjectPermissionMemberActions,
|
||||
ProjectPermissionSet,
|
||||
ProjectPermissionSub
|
||||
} from "../permission/project-permission";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal";
|
||||
import {
|
||||
ProjectUserAdditionalPrivilegeTemporaryMode,
|
||||
@ -64,8 +68,8 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
const { permission: targetUserPermission } = await permissionService.getProjectPermission({
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
|
||||
const { permission: targetUserPermission, membership } = await permissionService.getProjectPermission({
|
||||
actor: ActorType.USER,
|
||||
actorId: projectMembership.userId,
|
||||
projectId: projectMembership.projectId,
|
||||
@ -77,11 +81,21 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetUserPermission.update(targetUserPermission.rules.concat(customPermission));
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member,
|
||||
permission,
|
||||
targetUserPermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged user",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged user",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -151,7 +165,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
message: `Project membership for user with ID '${userPrivilege.userId}' not found in project with ID '${userPrivilege.projectId}'`
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId: projectMembership.projectId,
|
||||
@ -159,7 +173,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
|
||||
const { permission: targetUserPermission } = await permissionService.getProjectPermission({
|
||||
actor: ActorType.USER,
|
||||
actorId: projectMembership.userId,
|
||||
@ -172,11 +186,21 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
// we need to validate that the privilege given is not higher than the assigning users permission
|
||||
// @ts-expect-error this is expected error because of one being really accurate rule definition other being a bit more broader. Both are valid casl rules
|
||||
targetUserPermission.update(targetUserPermission.rules.concat(dto.permissions || []));
|
||||
const permissionBoundary = validatePermissionBoundary(permission, targetUserPermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member,
|
||||
permission,
|
||||
targetUserPermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update more privileged identity",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update more privileged user",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -253,7 +277,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
|
||||
|
||||
const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id);
|
||||
return {
|
||||
@ -290,7 +314,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
return {
|
||||
...userPrivilege,
|
||||
@ -317,7 +341,7 @@ export const projectUserAdditionalPrivilegeServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
const userPrivileges = await projectUserAdditionalPrivilegeDAL.find(
|
||||
{
|
||||
|
@ -29,15 +29,9 @@ export const parseScimFilter = (filterToParse: string | undefined) => {
|
||||
attributeName = "name";
|
||||
}
|
||||
|
||||
return { [attributeName]: parsedValue.replace(/"/g, "") };
|
||||
return { [attributeName]: parsedValue.replaceAll('"', "") };
|
||||
};
|
||||
|
||||
export function extractScimValueFromPath(path: string): string | null {
|
||||
const regex = /members\[value eq "([^"]+)"\]/;
|
||||
const match = path.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export const buildScimUser = ({
|
||||
orgMembershipId,
|
||||
username,
|
||||
|
@ -14,16 +14,43 @@ import { verifyHostInputValidity } from "../../dynamic-secret/dynamic-secret-fns
|
||||
import { TAssignOp, TDbProviderClients, TDirectAssignOp, THttpProviderFunction } from "../templates/types";
|
||||
import { TSecretRotationData, TSecretRotationDbFn } from "./secret-rotation-queue-types";
|
||||
|
||||
const REGEX = /\${([^}]+)}/g;
|
||||
const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
|
||||
|
||||
const replaceTemplateVariables = (str: string, getValue: (key: string) => unknown) => {
|
||||
// Use array to collect pieces and join at the end (more efficient for large strings)
|
||||
const parts: string[] = [];
|
||||
let pos = 0;
|
||||
|
||||
while (pos < str.length) {
|
||||
const start = str.indexOf("${", pos);
|
||||
if (start === -1) {
|
||||
parts.push(str.slice(pos));
|
||||
break;
|
||||
}
|
||||
|
||||
parts.push(str.slice(pos, start));
|
||||
const end = str.indexOf("}", start + 2);
|
||||
|
||||
if (end === -1) {
|
||||
parts.push(str.slice(start));
|
||||
break;
|
||||
}
|
||||
|
||||
const varName = str.slice(start + 2, end);
|
||||
parts.push(String(getValue(varName)));
|
||||
pos = end + 1;
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
};
|
||||
|
||||
export const interpolate = (data: any, getValue: (key: string) => unknown) => {
|
||||
if (!data) return;
|
||||
|
||||
if (typeof data === "number") return data;
|
||||
|
||||
if (typeof data === "string") {
|
||||
return data.replace(REGEX, (_a, b) => getValue(b) as string);
|
||||
return replaceTemplateVariables(data, getValue);
|
||||
}
|
||||
|
||||
if (typeof data === "object" && Array.isArray(data)) {
|
||||
|
@ -8,7 +8,18 @@ type GetFullFolderPath = {
|
||||
|
||||
export const getFullFolderPath = async ({ folderDAL, folderId, envId }: GetFullFolderPath): Promise<string> => {
|
||||
// Helper function to remove duplicate slashes
|
||||
const removeDuplicateSlashes = (path: string) => path.replace(/\/{2,}/g, "/");
|
||||
const removeDuplicateSlashes = (path: string) => {
|
||||
const chars = [];
|
||||
let lastWasSlash = false;
|
||||
|
||||
for (let i = 0; i < path.length; i += 1) {
|
||||
const char = path[i];
|
||||
if (char !== "/" || !lastWasSlash) chars.push(char);
|
||||
lastWasSlash = char === "/";
|
||||
}
|
||||
|
||||
return chars.join("");
|
||||
};
|
||||
|
||||
// Fetch all folders at once based on environment ID to avoid multiple queries
|
||||
const folders = await folderDAL.find({ envId });
|
||||
|
@ -1,14 +1,34 @@
|
||||
import { isIP } from "net";
|
||||
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
|
||||
// Validates usernames or wildcard (*)
|
||||
export const isValidUserPattern = (value: string): boolean => {
|
||||
// Matches valid Linux usernames or a wildcard (*)
|
||||
const userRegex = /^(?:\*|[a-z_][a-z0-9_-]{0,31})$/;
|
||||
// Length check before regex to prevent ReDoS
|
||||
if (typeof value !== "string") return false;
|
||||
if (value.length > 32) return false; // Maximum Linux username length
|
||||
if (value === "*") return true; // Handle wildcard separately
|
||||
|
||||
// Simpler, more specific pattern for usernames
|
||||
const userRegex = /^[a-z_][a-z0-9_-]*$/i;
|
||||
return userRegex.test(value);
|
||||
};
|
||||
|
||||
// Validates hostnames, wildcard domains, or IP addresses
|
||||
export const isValidHostPattern = (value: string): boolean => {
|
||||
// Matches FQDNs, wildcard domains (*.example.com), IPv4, and IPv6 addresses
|
||||
const hostRegex =
|
||||
/^(?:\*|\*\.[a-z0-9-]+(?:\.[a-z0-9-]+)*|[a-z0-9-]+(?:\.[a-z0-9-]+)*|\d{1,3}(\.\d{1,3}){3}|([a-fA-F0-9:]+:+)+[a-fA-F0-9]+(?:%[a-zA-Z0-9]+)?)$/;
|
||||
return hostRegex.test(value);
|
||||
// Input validation
|
||||
if (typeof value !== "string") return false;
|
||||
|
||||
// Length check
|
||||
if (value.length > 255) return false;
|
||||
|
||||
// Handle the wildcard case separately
|
||||
if (value === "*") return true;
|
||||
|
||||
// Check for IP addresses using Node.js built-in functions
|
||||
if (isIP(value)) return true;
|
||||
|
||||
return isFQDN(value, {
|
||||
allow_wildcard: true
|
||||
});
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ import { promisify } from "util";
|
||||
import { TSshCertificateTemplates } from "@app/db/schemas";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { CertKeyAlgorithm } from "@app/services/certificate/certificate-types";
|
||||
|
||||
import {
|
||||
@ -18,6 +19,7 @@ import { SshCertType, TCreateSshCertDTO } from "./ssh-certificate-authority-type
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const EXEC_TIMEOUT_MS = 10000; // 10 seconds
|
||||
/* eslint-disable no-bitwise */
|
||||
export const createSshCertSerialNumber = () => {
|
||||
const randomBytes = crypto.randomBytes(8); // 8 bytes = 64 bits
|
||||
@ -64,7 +66,9 @@ export const createSshKeyPair = async (keyAlgorithm: CertKeyAlgorithm) => {
|
||||
// Generate the SSH key pair
|
||||
// The "-N ''" sets an empty passphrase
|
||||
// The keys are created in the temporary directory
|
||||
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""]);
|
||||
await execFileAsync("ssh-keygen", ["-t", keyType, "-b", keyBits, "-f", privateKeyFile, "-N", ""], {
|
||||
timeout: EXEC_TIMEOUT_MS
|
||||
});
|
||||
|
||||
// Read the generated keys
|
||||
const publicKey = await fs.readFile(publicKeyFile, "utf8");
|
||||
@ -87,7 +91,10 @@ export const getSshPublicKey = async (privateKey: string) => {
|
||||
await fs.writeFile(privateKeyFile, privateKey, { mode: 0o600 });
|
||||
|
||||
// Run ssh-keygen to extract the public key
|
||||
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], { encoding: "utf8" });
|
||||
const { stdout } = await execFileAsync("ssh-keygen", ["-y", "-f", privateKeyFile], {
|
||||
encoding: "utf8",
|
||||
timeout: EXEC_TIMEOUT_MS
|
||||
});
|
||||
return stdout.trim();
|
||||
} finally {
|
||||
// Ensure that files and the temporary directory are cleaned up
|
||||
@ -143,7 +150,14 @@ export const validateSshCertificatePrincipals = (
|
||||
}
|
||||
|
||||
// restrict allowed characters to letters, digits, dot, underscore, and hyphen
|
||||
if (!/^[A-Za-z0-9._-]+$/.test(sanitized)) {
|
||||
if (
|
||||
!characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Period,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
])(sanitized)
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: `Principal '${sanitized}' contains invalid characters. Allowed: alphanumeric, '.', '_', '-'.`
|
||||
});
|
||||
@ -266,8 +280,8 @@ export const validateSshCertificateTtl = (template: TSshCertificateTemplates, tt
|
||||
* that it only contains alphanumeric characters with no spaces.
|
||||
*/
|
||||
export const validateSshCertificateKeyId = (keyId: string) => {
|
||||
const regex = /^[A-Za-z0-9-]+$/;
|
||||
if (!regex.test(keyId)) {
|
||||
const regex = characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen]);
|
||||
if (!regex(keyId)) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to validate Key ID because it can only contain alphanumeric characters and hyphens, with no spaces."
|
||||
@ -298,7 +312,7 @@ const validateSshPublicKey = async (publicKey: string) => {
|
||||
|
||||
try {
|
||||
await fs.writeFile(pubKeyFile, publicKey, { mode: 0o600 });
|
||||
await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile]);
|
||||
await execFileAsync("ssh-keygen", ["-l", "-f", pubKeyFile], { timeout: EXEC_TIMEOUT_MS });
|
||||
} catch (error) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to validate SSH public key format: could not be parsed."
|
||||
@ -363,7 +377,7 @@ export const createSshCert = async ({
|
||||
await fs.writeFile(privateKeyFile, caPrivateKey, { mode: 0o600 });
|
||||
|
||||
// Execute the signing process
|
||||
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8" });
|
||||
await execFileAsync("ssh-keygen", sshKeygenArgs, { encoding: "utf8", timeout: EXEC_TIMEOUT_MS });
|
||||
|
||||
// Read the signed public key from the generated cert file
|
||||
const signedPublicKey = await fs.readFile(signedPublicKeyFile, "utf8");
|
||||
|
@ -28,8 +28,8 @@ export const createDigestAuthRequestInterceptor = (
|
||||
nc += 1;
|
||||
const nonceCount = nc.toString(16).padStart(8, "0");
|
||||
const cnonce = crypto.randomBytes(24).toString("hex");
|
||||
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1].replace(/"/g, "");
|
||||
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1].replace(/"/g, "");
|
||||
const realm = authDetails.find((el) => el[0].toLowerCase().indexOf("realm") > -1)?.[1]?.replaceAll('"', "") || "";
|
||||
const nonce = authDetails.find((el) => el[0].toLowerCase().indexOf("nonce") > -1)?.[1]?.replaceAll('"', "") || "";
|
||||
const ha1 = crypto.createHash("md5").update(`${username}:${realm}:${password}`).digest("hex");
|
||||
const path = opts.url;
|
||||
|
||||
|
@ -1,26 +1,35 @@
|
||||
// Credit: https://github.com/miguelmota/is-base64
|
||||
export const isBase64 = (
|
||||
v: string,
|
||||
opts = { allowEmpty: false, mimeRequired: false, allowMime: true, paddingRequired: false }
|
||||
) => {
|
||||
if (opts.allowEmpty === false && v === "") {
|
||||
return false;
|
||||
type Base64Options = {
|
||||
urlSafe?: boolean;
|
||||
padding?: boolean;
|
||||
};
|
||||
|
||||
const base64WithPadding = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/;
|
||||
const base64WithoutPadding = /^[A-Za-z0-9+/]+$/;
|
||||
const base64UrlWithPadding = /^(?:[A-Za-z0-9_-]{4})*(?:[A-Za-z0-9_-]{2}==|[A-Za-z0-9_-]{3}=|[A-Za-z0-9_-]{4})$/;
|
||||
const base64UrlWithoutPadding = /^[A-Za-z0-9_-]+$/;
|
||||
|
||||
export const isBase64 = (str: string, options: Base64Options = {}): boolean => {
|
||||
if (typeof str !== "string") {
|
||||
throw new TypeError("Expected a string");
|
||||
}
|
||||
|
||||
let regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+/]{3}=)?";
|
||||
const mimeRegex = "(data:\\w+\\/[a-zA-Z\\+\\-\\.]+;base64,)";
|
||||
// Default padding to true unless urlSafe is true
|
||||
const opts: Base64Options = {
|
||||
urlSafe: false,
|
||||
padding: options.urlSafe === undefined ? true : !options.urlSafe,
|
||||
...options
|
||||
};
|
||||
|
||||
if (opts.mimeRequired === true) {
|
||||
regex = mimeRegex + regex;
|
||||
} else if (opts.allowMime === true) {
|
||||
regex = `${mimeRegex}?${regex}`;
|
||||
if (str === "") return true;
|
||||
|
||||
let regex;
|
||||
if (opts.urlSafe) {
|
||||
regex = opts.padding ? base64UrlWithPadding : base64UrlWithoutPadding;
|
||||
} else {
|
||||
regex = opts.padding ? base64WithPadding : base64WithoutPadding;
|
||||
}
|
||||
|
||||
if (opts.paddingRequired === false) {
|
||||
regex = "(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}(==)?|[A-Za-z0-9+\\/]{3}=?)?";
|
||||
}
|
||||
|
||||
return new RegExp(`^${regex}$`, "gi").test(v);
|
||||
return (!opts.padding || str.length % 4 === 0) && regex.test(str);
|
||||
};
|
||||
|
||||
export const getBase64SizeInBytes = (base64String: string) => {
|
||||
|
42
backend/src/lib/certificates/extract-certificate.test.ts
Normal file
42
backend/src/lib/certificates/extract-certificate.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { extractX509CertFromChain } from "./extract-certificate";
|
||||
|
||||
describe("Extract Certificate Payload", () => {
|
||||
test("Single chain", () => {
|
||||
const payload = `-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||
-----END CERTIFICATE-----`;
|
||||
const result = extractX509CertFromChain(payload);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.length).toBe(1);
|
||||
expect(result?.[0]).toEqual(payload);
|
||||
});
|
||||
|
||||
test("Multiple chain", () => {
|
||||
const payload = `-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----`;
|
||||
const result = extractX509CertFromChain(payload);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.length).toBe(3);
|
||||
expect(result).toEqual([
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz
|
||||
-----END CERTIFICATE-----`,
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----`,
|
||||
`-----BEGIN CERTIFICATE-----
|
||||
MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL
|
||||
-----END CERTIFICATE-----`
|
||||
]);
|
||||
});
|
||||
});
|
51
backend/src/lib/certificates/extract-certificate.ts
Normal file
51
backend/src/lib/certificates/extract-certificate.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { BadRequestError } from "../errors";
|
||||
|
||||
export const extractX509CertFromChain = (certificateChain: string): string[] => {
|
||||
if (!certificateChain) {
|
||||
throw new BadRequestError({
|
||||
message: "Certificate chain is empty or undefined"
|
||||
});
|
||||
}
|
||||
|
||||
const certificates: string[] = [];
|
||||
let currentPosition = 0;
|
||||
const chainLength = certificateChain.length;
|
||||
|
||||
while (currentPosition < chainLength) {
|
||||
// Find the start of a certificate
|
||||
const beginMarker = "-----BEGIN CERTIFICATE-----";
|
||||
const startIndex = certificateChain.indexOf(beginMarker, currentPosition);
|
||||
|
||||
if (startIndex === -1) {
|
||||
break; // No more certificates found
|
||||
}
|
||||
|
||||
// Find the end of the certificate
|
||||
const endMarker = "-----END CERTIFICATE-----";
|
||||
const endIndex = certificateChain.indexOf(endMarker, startIndex);
|
||||
|
||||
if (endIndex === -1) {
|
||||
throw new BadRequestError({
|
||||
message: "Malformed certificate chain: Found BEGIN marker without matching END marker"
|
||||
});
|
||||
}
|
||||
|
||||
// Extract the complete certificate including markers
|
||||
const completeEndIndex = endIndex + endMarker.length;
|
||||
const certificate = certificateChain.substring(startIndex, completeEndIndex);
|
||||
|
||||
// Add the extracted certificate to our results
|
||||
certificates.push(certificate);
|
||||
|
||||
// Move position to after this certificate
|
||||
currentPosition = completeEndIndex;
|
||||
}
|
||||
|
||||
if (certificates.length === 0) {
|
||||
throw new BadRequestError({
|
||||
message: "No valid certificates found in the chain"
|
||||
});
|
||||
}
|
||||
|
||||
return certificates;
|
||||
};
|
@ -68,6 +68,23 @@ export class ForbiddenRequestError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class PermissionBoundaryError extends ForbiddenRequestError {
|
||||
constructor({
|
||||
message,
|
||||
name,
|
||||
error,
|
||||
details
|
||||
}: {
|
||||
message?: string;
|
||||
name?: string;
|
||||
error?: unknown;
|
||||
details?: unknown;
|
||||
}) {
|
||||
super({ message, name, error, details });
|
||||
this.name = "PermissionBoundaryError";
|
||||
}
|
||||
}
|
||||
|
||||
export class BadRequestError extends Error {
|
||||
name: string;
|
||||
|
||||
|
@ -107,12 +107,6 @@ export const isValidIp = (ip: string) => {
|
||||
return net.isIPv4(ip) || net.isIPv6(ip);
|
||||
};
|
||||
|
||||
export const isValidHostname = (name: string) => {
|
||||
const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
||||
|
||||
return hostnameRegex.test(name);
|
||||
};
|
||||
|
||||
export type TIp = {
|
||||
ipAddress: string;
|
||||
type: IPType;
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { CharacterType, characterValidator } from "./validate-string";
|
||||
|
||||
// regex to allow only alphanumeric, dash, underscore
|
||||
export const isValidFolderName = (name: string) => /^[a-zA-Z0-9-_]+$/.test(name);
|
||||
export const isValidFolderName = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.Underscore
|
||||
]);
|
||||
|
||||
export const isValidSecretPath = (path: string) =>
|
||||
path
|
||||
|
23
backend/src/lib/validator/validate-string.test.ts
Normal file
23
backend/src/lib/validator/validate-string.test.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { CharacterType, characterValidator } from "./validate-string";
|
||||
|
||||
describe("validate-string", () => {
|
||||
test("Check alphabets", () => {
|
||||
expect(characterValidator([CharacterType.Alphabets])("hello")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.Alphabets])("hello world")).toBeFalsy();
|
||||
expect(characterValidator([CharacterType.Alphabets, CharacterType.Spaces])("hello world")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Check numbers", () => {
|
||||
expect(characterValidator([CharacterType.Numbers])("1234567890")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric])("helloWORLD1234567890")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric])("helloWORLD1234567890-")).toBeFalsy();
|
||||
});
|
||||
|
||||
test("Check special characters", () => {
|
||||
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])("Hello-World")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Plus])("Hello+World")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Underscore])("Hello_World")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Colon])("Hello:World")).toBeTruthy();
|
||||
expect(characterValidator([CharacterType.AlphaNumeric, CharacterType.Underscore])("Hello World")).toBeFalsy();
|
||||
});
|
||||
});
|
101
backend/src/lib/validator/validate-string.ts
Normal file
101
backend/src/lib/validator/validate-string.ts
Normal file
@ -0,0 +1,101 @@
|
||||
export enum CharacterType {
|
||||
Alphabets = "alphabets",
|
||||
Numbers = "numbers",
|
||||
AlphaNumeric = "alpha-numeric",
|
||||
Spaces = "spaces",
|
||||
SpecialCharacters = "specialCharacters",
|
||||
Punctuation = "punctuation",
|
||||
Period = "period", // .
|
||||
Underscore = "underscore", // _
|
||||
Colon = "colon", // :
|
||||
ForwardSlash = "forwardSlash", // /
|
||||
Equals = "equals", // =
|
||||
Plus = "plus", // +
|
||||
Hyphen = "hyphen", // -
|
||||
At = "at", // @
|
||||
// Additional individual characters that might be useful
|
||||
Asterisk = "asterisk", // *
|
||||
Ampersand = "ampersand", // &
|
||||
Question = "question", // ?
|
||||
Hash = "hash", // #
|
||||
Percent = "percent", // %
|
||||
Dollar = "dollar", // $
|
||||
Caret = "caret", // ^
|
||||
Backtick = "backtick", // `
|
||||
Pipe = "pipe", // |
|
||||
Backslash = "backslash", // \
|
||||
OpenParen = "openParen", // (
|
||||
CloseParen = "closeParen", // )
|
||||
OpenBracket = "openBracket", // [
|
||||
CloseBracket = "closeBracket", // ]
|
||||
OpenBrace = "openBrace", // {
|
||||
CloseBrace = "closeBrace", // }
|
||||
LessThan = "lessThan", // <
|
||||
GreaterThan = "greaterThan", // >
|
||||
SingleQuote = "singleQuote", // '
|
||||
DoubleQuote = "doubleQuote", // "
|
||||
Comma = "comma", // ,
|
||||
Semicolon = "semicolon", // ;
|
||||
Exclamation = "exclamation" // !
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if a string contains only specific types of characters
|
||||
*/
|
||||
export const characterValidator = (allowedCharacters: CharacterType[]) => {
|
||||
// Create a regex pattern based on allowed character types
|
||||
const patternMap: Record<CharacterType, string> = {
|
||||
[CharacterType.Alphabets]: "a-zA-Z",
|
||||
[CharacterType.Numbers]: "0-9",
|
||||
[CharacterType.AlphaNumeric]: "a-zA-Z0-9",
|
||||
[CharacterType.Spaces]: "\\s",
|
||||
[CharacterType.SpecialCharacters]: "!@#$%^&*()_+\\-=\\[\\]{}|;:'\",.<>/?\\\\",
|
||||
[CharacterType.Punctuation]: "\\.\\,\\;\\:\\!\\?",
|
||||
[CharacterType.Colon]: "\\:",
|
||||
[CharacterType.ForwardSlash]: "\\/",
|
||||
[CharacterType.Underscore]: "_",
|
||||
[CharacterType.Hyphen]: "\\-",
|
||||
[CharacterType.Period]: "\\.",
|
||||
[CharacterType.Equals]: "=",
|
||||
[CharacterType.Plus]: "\\+",
|
||||
[CharacterType.At]: "@",
|
||||
[CharacterType.Asterisk]: "\\*",
|
||||
[CharacterType.Ampersand]: "&",
|
||||
[CharacterType.Question]: "\\?",
|
||||
[CharacterType.Hash]: "#",
|
||||
[CharacterType.Percent]: "%",
|
||||
[CharacterType.Dollar]: "\\$",
|
||||
[CharacterType.Caret]: "\\^",
|
||||
[CharacterType.Backtick]: "`",
|
||||
[CharacterType.Pipe]: "\\|",
|
||||
[CharacterType.Backslash]: "\\\\",
|
||||
[CharacterType.OpenParen]: "\\(",
|
||||
[CharacterType.CloseParen]: "\\)",
|
||||
[CharacterType.OpenBracket]: "\\[",
|
||||
[CharacterType.CloseBracket]: "\\]",
|
||||
[CharacterType.OpenBrace]: "\\{",
|
||||
[CharacterType.CloseBrace]: "\\}",
|
||||
[CharacterType.LessThan]: "<",
|
||||
[CharacterType.GreaterThan]: ">",
|
||||
[CharacterType.SingleQuote]: "'",
|
||||
[CharacterType.DoubleQuote]: '\\"',
|
||||
[CharacterType.Comma]: ",",
|
||||
[CharacterType.Semicolon]: ";",
|
||||
[CharacterType.Exclamation]: "!"
|
||||
};
|
||||
|
||||
// Combine patterns from allowed characters
|
||||
const combinedPattern = allowedCharacters.map((char) => patternMap[char]).join("");
|
||||
|
||||
// Create a regex that matches only the allowed characters
|
||||
const regex = new RegExp(`^[${combinedPattern}]+$`);
|
||||
|
||||
/**
|
||||
* Validates if the input string contains only the allowed character types
|
||||
* @param input String to validate
|
||||
* @returns Boolean indicating if the string is valid
|
||||
*/
|
||||
return function validate(input: string): boolean {
|
||||
return regex.test(input);
|
||||
};
|
||||
};
|
15
backend/src/lib/validator/validate-url.test.ts
Normal file
15
backend/src/lib/validator/validate-url.test.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { isFQDN } from "./validate-url";
|
||||
|
||||
describe("isFQDN", () => {
|
||||
test("Non wildcard", () => {
|
||||
expect(isFQDN("www.example.com")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Wildcard", () => {
|
||||
expect(isFQDN("*.example.com", { allow_wildcard: true })).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Wildcard FQDN fails on option allow_wildcard false", () => {
|
||||
expect(isFQDN("*.example.com")).toBeFalsy();
|
||||
});
|
||||
});
|
@ -1,18 +1,117 @@
|
||||
import { getConfig } from "../config/env";
|
||||
import dns from "node:dns/promises";
|
||||
|
||||
import { isIPv4 } from "net";
|
||||
|
||||
import { BadRequestError } from "../errors";
|
||||
import { isPrivateIp } from "../ip/ipRange";
|
||||
|
||||
export const blockLocalAndPrivateIpAddresses = (url: string) => {
|
||||
export const blockLocalAndPrivateIpAddresses = async (url: string) => {
|
||||
const validUrl = new URL(url);
|
||||
const appCfg = getConfig();
|
||||
// on cloud local ips are not allowed
|
||||
if (
|
||||
appCfg.isCloud &&
|
||||
(validUrl.host === "host.docker.internal" ||
|
||||
validUrl.host.match(/^10\.\d+\.\d+\.\d+/) ||
|
||||
validUrl.host.match(/^192\.168\.\d+\.\d+/))
|
||||
)
|
||||
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||
|
||||
if (validUrl.host === "localhost" || validUrl.host === "127.0.0.1")
|
||||
throw new BadRequestError({ message: "Localhost not allowed" });
|
||||
const inputHostIps: string[] = [];
|
||||
if (isIPv4(validUrl.host)) {
|
||||
inputHostIps.push(validUrl.host);
|
||||
} else {
|
||||
if (validUrl.host === "localhost" || validUrl.host === "host.docker.internal") {
|
||||
throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||
}
|
||||
const resolvedIps = await dns.resolve4(validUrl.host);
|
||||
inputHostIps.push(...resolvedIps);
|
||||
}
|
||||
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
|
||||
if (isInternalIp) throw new BadRequestError({ message: "Local IPs not allowed as URL" });
|
||||
};
|
||||
|
||||
type FQDNOptions = {
|
||||
require_tld?: boolean;
|
||||
allow_underscores?: boolean;
|
||||
allow_trailing_dot?: boolean;
|
||||
allow_numeric_tld?: boolean;
|
||||
allow_wildcard?: boolean;
|
||||
ignore_max_length?: boolean;
|
||||
};
|
||||
|
||||
const defaultFqdnOptions: FQDNOptions = {
|
||||
require_tld: true,
|
||||
allow_underscores: false,
|
||||
allow_trailing_dot: false,
|
||||
allow_numeric_tld: false,
|
||||
allow_wildcard: false,
|
||||
ignore_max_length: false
|
||||
};
|
||||
|
||||
// credits: https://github.com/validatorjs/validator.js/blob/f5da7fb6ed59b94695e6fcb2e970c80029509919/src/lib/isFQDN.js#L13
|
||||
export const isFQDN = (str: string, options: FQDNOptions = {}): boolean => {
|
||||
if (typeof str !== "string") {
|
||||
throw new TypeError("Expected a string");
|
||||
}
|
||||
|
||||
// Apply default options
|
||||
const opts: FQDNOptions = {
|
||||
...defaultFqdnOptions,
|
||||
...options
|
||||
};
|
||||
|
||||
let testStr = str;
|
||||
/* Remove the optional trailing dot before checking validity */
|
||||
if (opts.allow_trailing_dot && str[str.length - 1] === ".") {
|
||||
testStr = testStr.substring(0, str.length - 1);
|
||||
}
|
||||
|
||||
/* Remove the optional wildcard before checking validity */
|
||||
if (opts.allow_wildcard === true && str.indexOf("*.") === 0) {
|
||||
testStr = testStr.substring(2);
|
||||
}
|
||||
|
||||
const parts = testStr.split(".");
|
||||
const tld = parts[parts.length - 1];
|
||||
|
||||
if (opts.require_tld) {
|
||||
// disallow fqdns without tld
|
||||
if (parts.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!opts.allow_numeric_tld &&
|
||||
!/^([a-z\u00A1-\u00A8\u00AA-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}|xn[a-z0-9-]{2,})$/i.test(tld)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// disallow spaces
|
||||
if (/\s/.test(tld)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// reject numeric TLDs
|
||||
if (!opts.allow_numeric_tld && /^\d+$/.test(tld)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parts.every((part) => {
|
||||
if (part.length > 63 && !opts.ignore_max_length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!/^[a-z_\u00a1-\uffff0-9-]+$/i.test(part)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// disallow full-width chars
|
||||
if (/[\uff01-\uff5e]/.test(part)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// disallow parts starting or ending with hyphen
|
||||
if (/^-|-$/.test(part)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!opts.allow_underscores && /_/.test(part)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
@ -1,6 +1,8 @@
|
||||
import slugify from "@sindresorhus/slugify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
|
||||
interface SlugSchemaInputs {
|
||||
min?: number;
|
||||
max?: number;
|
||||
@ -27,4 +29,13 @@ export const GenericResourceNameSchema = z
|
||||
.trim()
|
||||
.min(1, { message: "Name must be at least 1 character" })
|
||||
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||
.regex(/^[a-zA-Z0-9\-_\s]+$/, "Name can only contain alphanumeric characters, dashes, underscores, and spaces");
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Spaces
|
||||
])(val),
|
||||
"Name can only contain alphanumeric characters, dashes, underscores, and spaces"
|
||||
);
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
InternalServerError,
|
||||
NotFoundError,
|
||||
OidcAuthError,
|
||||
PermissionBoundaryError,
|
||||
RateLimitError,
|
||||
ScimRequestError,
|
||||
UnauthorizedError
|
||||
@ -117,7 +118,7 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
||||
conditions: el.conditions
|
||||
}))
|
||||
});
|
||||
} else if (error instanceof ForbiddenRequestError) {
|
||||
} else if (error instanceof ForbiddenRequestError || error instanceof PermissionBoundaryError) {
|
||||
void res.status(HttpStatusCodes.Forbidden).send({
|
||||
reqId: req.id,
|
||||
statusCode: HttpStatusCodes.Forbidden,
|
||||
|
@ -6,7 +6,6 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_AUTHORITIES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
@ -15,7 +14,6 @@ import {
|
||||
validateAltNamesField,
|
||||
validateCaDateField
|
||||
} from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -651,16 +649,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
caId: ca.id,
|
||||
commonName: req.body.commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
@ -719,7 +707,7 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } =
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
caId: req.params.caId,
|
||||
@ -743,16 +731,6 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
caId: ca.id,
|
||||
commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
|
@ -5,7 +5,6 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { CERTIFICATE_AUTHORITIES, CERTIFICATES } from "@app/lib/api-docs";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { CertExtendedKeyUsage, CertKeyUsage, CrlReason } from "@app/services/certificate/certificate-types";
|
||||
@ -13,7 +12,6 @@ import {
|
||||
validateAltNamesField,
|
||||
validateCaDateField
|
||||
} from "@app/services/certificate-authority/certificate-authority-validators";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -152,17 +150,6 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.IssueCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
caId: req.body.caId,
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
commonName: req.body.commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate,
|
||||
certificateChain,
|
||||
@ -241,7 +228,7 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca, commonName } =
|
||||
const { certificate, certificateChain, issuingCaCertificate, serialNumber, ca } =
|
||||
await server.services.certificateAuthority.signCertFromCa({
|
||||
isInternal: false,
|
||||
actor: req.permission.type,
|
||||
@ -264,17 +251,6 @@ export const registerCertRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.SignCert,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
properties: {
|
||||
caId: req.body.caId,
|
||||
certificateTemplateId: req.body.certificateTemplateId,
|
||||
commonName,
|
||||
...req.auditLogInfo
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificate: certificate.toString("pem"),
|
||||
certificateChain,
|
||||
|
@ -32,6 +32,8 @@ const IdentityOidcAuthResponseSchema = IdentityOidcAuthsSchema.pick({
|
||||
caCert: z.string()
|
||||
});
|
||||
|
||||
const MAX_OIDC_CLAIM_SIZE = 32_768;
|
||||
|
||||
export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
method: "POST",
|
||||
@ -55,7 +57,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||
const { identityOidcAuth, accessToken, identityAccessToken, identityMembershipOrg, oidcTokenData } =
|
||||
await server.services.identityOidcAuth.login({
|
||||
identityId: req.body.identityId,
|
||||
jwt: req.body.jwt
|
||||
@ -69,7 +71,11 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
|
||||
metadata: {
|
||||
identityId: identityOidcAuth.identityId,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
identityOidcAuthId: identityOidcAuth.id
|
||||
identityOidcAuthId: identityOidcAuth.id,
|
||||
oidcClaimsReceived:
|
||||
Buffer.from(JSON.stringify(oidcTokenData), "utf8").byteLength < MAX_OIDC_CLAIM_SIZE
|
||||
? oidcTokenData
|
||||
: { payload: "Error: Payload exceeds 32KB, provided oidc claim not recorded in audit log." }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -4,7 +4,6 @@ import {
|
||||
AuditLogsSchema,
|
||||
GroupsSchema,
|
||||
IncidentContactsSchema,
|
||||
OrganizationsSchema,
|
||||
OrgMembershipsSchema,
|
||||
OrgRolesSchema,
|
||||
UsersSchema
|
||||
@ -57,7 +56,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: OrganizationsSchema
|
||||
organization: sanitizedOrganizationSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -263,7 +262,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
organization: OrganizationsSchema
|
||||
organization: sanitizedOrganizationSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
OrganizationsSchema,
|
||||
OrgMembershipsSchema,
|
||||
ProjectMembershipsSchema,
|
||||
ProjectsSchema,
|
||||
@ -15,6 +14,7 @@ import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||
|
||||
export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -335,7 +335,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: OrganizationsSchema
|
||||
organization: sanitizedOrganizationSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
@ -365,7 +365,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: OrganizationsSchema,
|
||||
organization: sanitizedOrganizationSchema,
|
||||
accessToken: z.string()
|
||||
})
|
||||
}
|
||||
@ -396,4 +396,30 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
return { organization, accessToken: tokens.accessToken };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/privilege-system-upgrade",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
response: {
|
||||
200: z.object({
|
||||
organization: sanitizedOrganizationSchema
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const organization = await server.services.org.upgradePrivilegeSystem({
|
||||
actorId: req.permission.id,
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
orgId: req.permission.orgId
|
||||
});
|
||||
|
||||
return { organization };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -7,9 +7,11 @@ import { z } from "zod";
|
||||
import { ActionProjectType, ProjectType, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
@ -58,7 +60,6 @@ import {
|
||||
TSignIntermediateDTO,
|
||||
TUpdateCaDTO
|
||||
} from "./certificate-authority-types";
|
||||
import { hostnameRegex } from "./certificate-authority-validators";
|
||||
|
||||
type TCertificateAuthorityServiceFactoryDep = {
|
||||
certificateAuthorityDAL: Pick<
|
||||
@ -1017,9 +1018,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
const maxPathLength = certObj.getExtension(x509.BasicConstraintsExtension)?.pathLength;
|
||||
|
||||
// validate imported certificate and certificate chain
|
||||
const certificates = certificateChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
const certificates = extractX509CertFromChain(certificateChain)?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
|
||||
@ -1325,7 +1324,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
}
|
||||
|
||||
// check if the altName is a valid hostname
|
||||
if (hostnameRegex.test(altName)) {
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
return {
|
||||
type: "dns",
|
||||
value: altName
|
||||
@ -1702,7 +1701,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
}
|
||||
|
||||
// check if the altName is a valid hostname
|
||||
if (hostnameRegex.test(altName)) {
|
||||
if (isFQDN(altName, { allow_wildcard: true })) {
|
||||
return {
|
||||
type: "dns",
|
||||
value: altName
|
||||
@ -1819,8 +1818,7 @@ export const certificateAuthorityServiceFactory = ({
|
||||
certificateChain: `${issuingCaCertificate}\n${caCertChain}`.trim(),
|
||||
issuingCaCertificate,
|
||||
serialNumber,
|
||||
ca,
|
||||
commonName: cn
|
||||
ca
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { isValidIp } from "@app/lib/ip";
|
||||
import { isFQDN } from "@app/lib/validator/validate-url";
|
||||
|
||||
const isValidDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
@ -9,7 +10,6 @@ const isValidDate = (dateString: string) => {
|
||||
|
||||
export const validateCaDateField = z.string().trim().refine(isValidDate, { message: "Invalid date format" });
|
||||
|
||||
export const hostnameRegex = /^(?!:\/\/)(\*\.)?([a-zA-Z0-9-_]{1,63}\.?)+(?!:\/\/)([a-zA-Z]{2,63})$/;
|
||||
export const validateAltNamesField = z
|
||||
.string()
|
||||
.trim()
|
||||
@ -27,7 +27,7 @@ export const validateAltNamesField = z
|
||||
if (data === "") return true;
|
||||
// Split and validate each alt name
|
||||
return data.split(", ").every((name) => {
|
||||
return hostnameRegex.test(name) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||
return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name);
|
||||
});
|
||||
},
|
||||
{
|
||||
|
@ -11,6 +11,7 @@ export const validateCertificateDetailsAgainstTemplate = (
|
||||
},
|
||||
template: TCertificateTemplates
|
||||
) => {
|
||||
// these are validated in router using validateTemplateRegexField
|
||||
const commonNameRegex = new RegExp(template.commonName);
|
||||
if (!commonNameRegex.test(cert.commonName)) {
|
||||
throw new BadRequestError({
|
||||
|
@ -6,6 +6,7 @@ import { ActionProjectType, TCertificateTemplateEstConfigsUpdate } from "@app/db
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { extractX509CertFromChain } from "@app/lib/certificates/extract-certificate";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
@ -281,9 +282,7 @@ export const certificateTemplateServiceFactory = ({
|
||||
});
|
||||
|
||||
// validate CA chain
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
@ -379,9 +378,7 @@ export const certificateTemplateServiceFactory = ({
|
||||
};
|
||||
|
||||
if (caChain) {
|
||||
const certificates = caChain
|
||||
.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g)
|
||||
?.map((cert) => new x509.X509Certificate(cert));
|
||||
const certificates = extractX509CertFromChain(caChain)?.map((cert) => new x509.X509Certificate(cert));
|
||||
|
||||
if (!certificates) {
|
||||
throw new BadRequestError({ message: "Failed to parse certificate chain" });
|
||||
|
@ -1,13 +1,27 @@
|
||||
import safe from "safe-regex";
|
||||
import z from "zod";
|
||||
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
|
||||
export const validateTemplateRegexField = z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9 *@\-\\.\\]+$/, {
|
||||
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
})
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Spaces, // (space)
|
||||
CharacterType.Asterisk, // *
|
||||
CharacterType.At, // @
|
||||
CharacterType.Hyphen, // -
|
||||
CharacterType.Period, // .
|
||||
CharacterType.Backslash // \
|
||||
])(val),
|
||||
{
|
||||
message: "Invalid pattern: only alphanumeric characters, spaces, *, ., @, -, and \\ are allowed."
|
||||
}
|
||||
)
|
||||
// we ensure that the inputted pattern is computationally safe by limiting star height to 1
|
||||
.refine((v) => safe(v), {
|
||||
message: "Unsafe REGEX pattern"
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { ProjectPermissionGroupActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { decryptAsymmetric, encryptAsymmetric } from "@app/lib/crypto";
|
||||
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
import { isUuidV4 } from "@app/lib/validator";
|
||||
@ -70,7 +73,7 @@ export const groupProjectServiceFactory = ({
|
||||
if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` });
|
||||
if (project.version < 2) throw new BadRequestError({ message: `Failed to add group to E2EE project` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@ -78,7 +81,7 @@ export const groupProjectServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Create, ProjectPermissionSub.Groups);
|
||||
|
||||
let group: TGroups | null = null;
|
||||
if (isUuidV4(groupIdOrName)) {
|
||||
@ -102,11 +105,21 @@ export const groupProjectServiceFactory = ({
|
||||
project.id
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionGroupActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Groups,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to assign group to a more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to assign group to role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionGroupActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Groups
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
}
|
||||
@ -248,7 +261,7 @@ export const groupProjectServiceFactory = ({
|
||||
|
||||
if (!project) throw new NotFoundError({ message: `Failed to find project with ID ${projectId}` });
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@ -256,7 +269,7 @@ export const groupProjectServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Edit, ProjectPermissionSub.Groups);
|
||||
|
||||
const group = await groupDAL.findOne({ orgId: actorOrgId, id: groupId });
|
||||
if (!group) throw new NotFoundError({ message: `Failed to find group with ID ${groupId}` });
|
||||
@ -269,11 +282,21 @@ export const groupProjectServiceFactory = ({
|
||||
requestedRoleChange,
|
||||
project.id
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionGroupActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Groups,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to assign group to a more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to assign group to role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionGroupActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Groups
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
}
|
||||
@ -360,7 +383,7 @@ export const groupProjectServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Delete, ProjectPermissionSub.Groups);
|
||||
|
||||
const deletedProjectGroup = await groupProjectDAL.transaction(async (tx) => {
|
||||
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group.id, project.id, tx);
|
||||
@ -405,7 +428,7 @@ export const groupProjectServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
|
||||
|
||||
const groupMemberships = await groupProjectDAL.findByProjectId(project.id);
|
||||
return groupMemberships;
|
||||
@ -433,7 +456,7 @@ export const groupProjectServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionGroupActions.Read, ProjectPermissionSub.Groups);
|
||||
|
||||
const [groupMembership] = await groupProjectDAL.findByProjectId(project.id, {
|
||||
groupId
|
||||
|
@ -5,11 +5,14 @@ import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
@ -93,7 +96,8 @@ export const identityAwsAuthServiceFactory = ({
|
||||
.some((principalArn) => {
|
||||
// convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$"
|
||||
// considers exact matches + wildcard matches
|
||||
const regex = new RegExp(`^${principalArn.replace(/\*/g, ".*")}$`);
|
||||
// heavily validated in router
|
||||
const regex = new RegExp(`^${principalArn.replaceAll("*", ".*")}$`);
|
||||
return regex.test(extractPrincipalArn(Arn));
|
||||
});
|
||||
|
||||
@ -175,7 +179,7 @@ export const identityAwsAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
@ -254,7 +258,7 @@ export const identityAwsAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
@ -308,7 +312,7 @@ export const identityAwsAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
return { ...awsIdentityAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
@ -326,14 +330,14 @@ export const identityAwsAuthServiceFactory = ({
|
||||
message: "The identity does not have aws auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
@ -343,11 +347,22 @@ export const identityAwsAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to revoke aws auth of identity with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke aws auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import safe from "safe-regex";
|
||||
import { z } from "zod";
|
||||
|
||||
const twelveDigitRegex = /^\d{12}$/;
|
||||
// akhilmhdh: change this to a normal function later. Checked no redosable at the moment
|
||||
const arnRegex = /^arn:aws:iam::\d{12}:(user\/[a-zA-Z0-9_.@+*/-]+|role\/[a-zA-Z0-9_.@+*/-]+|\*)$/;
|
||||
|
||||
export const validateAccountIds = z
|
||||
@ -42,7 +44,8 @@ export const validatePrincipalArns = z
|
||||
// Split the string by commas to check each supposed ARN
|
||||
const arns = data.split(",");
|
||||
// Return true only if every item matches one of the allowed ARN formats
|
||||
return arns.every((arn) => arnRegex.test(arn.trim()));
|
||||
// and checks whether the provided regex is safe
|
||||
return arns.map((el) => el.trim()).every((arn) => safe(`^${arn.replaceAll("*", ".*")}$`) && arnRegex.test(arn));
|
||||
},
|
||||
{
|
||||
message:
|
||||
|
@ -3,11 +3,14 @@ import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
@ -147,7 +150,7 @@ export const identityAzureAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
@ -225,7 +228,7 @@ export const identityAzureAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
@ -281,7 +284,7 @@ export const identityAzureAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
return { ...identityAzureAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
@ -300,14 +303,14 @@ export const identityAzureAuthServiceFactory = ({
|
||||
message: "The identity does not have azure auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
@ -316,11 +319,21 @@ export const identityAzureAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to revoke azure auth of identity with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke azure auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
|
@ -3,11 +3,14 @@ import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
@ -188,7 +191,7 @@ export const identityGcpAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
@ -268,7 +271,7 @@ export const identityGcpAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
@ -326,7 +329,7 @@ export const identityGcpAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
return { ...identityGcpAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
@ -346,14 +349,14 @@ export const identityGcpAuthServiceFactory = ({
|
||||
message: "The identity does not have gcp auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
@ -362,11 +365,21 @@ export const identityGcpAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to revoke gcp auth of identity with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke gcp auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
|
@ -5,11 +5,20 @@ import { JwksClient } from "jwks-rsa";
|
||||
|
||||
import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import {
|
||||
BadRequestError,
|
||||
ForbiddenRequestError,
|
||||
NotFoundError,
|
||||
PermissionBoundaryError,
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
||||
|
||||
@ -278,7 +287,7 @@ export const identityJwtAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
@ -381,7 +390,7 @@ export const identityJwtAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
@ -470,7 +479,7 @@ export const identityJwtAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
||||
|
||||
@ -504,7 +513,7 @@ export const identityJwtAuthServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
@ -512,7 +521,7 @@ export const identityJwtAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
@ -522,11 +531,21 @@ export const identityJwtAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to revoke jwt auth of identity with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke jwt auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
|
@ -5,11 +5,14 @@ import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod, TIdentityKubernetesAuthsUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
@ -257,7 +260,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
@ -350,7 +353,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
@ -446,7 +449,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.Organization,
|
||||
@ -483,14 +486,14 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
message: "The identity does not have kubernetes auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
@ -499,11 +502,21 @@ export const identityKubernetesAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to revoke kubernetes auth of identity with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke kubernetes auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
|
@ -6,11 +6,20 @@ import { JwksClient } from "jwks-rsa";
|
||||
|
||||
import { IdentityAuthMethod, TIdentityOidcAuthsUpdate } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import {
|
||||
BadRequestError,
|
||||
ForbiddenRequestError,
|
||||
NotFoundError,
|
||||
PermissionBoundaryError,
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
||||
|
||||
@ -204,7 +213,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
}
|
||||
);
|
||||
|
||||
return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg };
|
||||
return { accessToken, identityOidcAuth, identityAccessToken, identityMembershipOrg, oidcTokenData: tokenData };
|
||||
};
|
||||
|
||||
const attachOidcAuth = async ({
|
||||
@ -249,7 +258,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
@ -341,7 +350,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
@ -414,7 +423,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
|
||||
|
||||
@ -442,7 +451,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
@ -450,7 +459,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
@ -460,11 +469,22 @@ export const identityOidcAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to revoke oidc auth of identity with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke oidc auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
|
@ -1,14 +1,16 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, ProjectMembershipRole } from "@app/db/schemas";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ProjectPermissionIdentityActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { ProjectUserMembershipTemporaryMode } from "../project-membership/project-membership-types";
|
||||
@ -54,7 +56,7 @@ export const identityProjectServiceFactory = ({
|
||||
projectId,
|
||||
roles
|
||||
}: TCreateProjectIdentityDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@ -63,7 +65,7 @@ export const identityProjectServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionIdentityActions.Create,
|
||||
subject(ProjectPermissionSub.Identity, {
|
||||
identityId
|
||||
})
|
||||
@ -91,11 +93,21 @@ export const identityProjectServiceFactory = ({
|
||||
projectId
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to assign to a more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to assign to role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
}
|
||||
@ -162,7 +174,7 @@ export const identityProjectServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: TUpdateProjectIdentityDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@ -171,7 +183,7 @@ export const identityProjectServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionIdentityActions.Edit,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
@ -187,11 +199,22 @@ export const identityProjectServiceFactory = ({
|
||||
projectId
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to change to a more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to change role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionIdentityActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
}
|
||||
@ -271,26 +294,10 @@ export const identityProjectServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionIdentityActions.Delete,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
const { permission: identityRolePermission } = await permissionService.getProjectPermission({
|
||||
actor: ActorType.IDENTITY,
|
||||
actorId: identityId,
|
||||
projectId: identityProjectMembership.projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to remove more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
const [deletedIdentity] = await identityProjectDAL.delete({ identityId, projectId });
|
||||
return deletedIdentity;
|
||||
};
|
||||
@ -315,7 +322,10 @@ export const identityProjectServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
ProjectPermissionSub.Identity
|
||||
);
|
||||
|
||||
const identityMemberships = await identityProjectDAL.findByProjectId(projectId, {
|
||||
limit,
|
||||
@ -348,7 +358,7 @@ export const identityProjectServiceFactory = ({
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionIdentityActions.Read,
|
||||
subject(ProjectPermissionSub.Identity, { identityId })
|
||||
);
|
||||
|
||||
|
@ -3,11 +3,14 @@ import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod, TableName } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
@ -85,7 +88,7 @@ export const identityTokenAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => {
|
||||
@ -161,7 +164,7 @@ export const identityTokenAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedAccessTokenTrustedIps = accessTokenTrustedIps?.map((accessTokenTrustedIp) => {
|
||||
@ -215,7 +218,7 @@ export const identityTokenAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
return { ...identityTokenAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
@ -245,9 +248,9 @@ export const identityTokenAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
const { permission: rolePermission, membership } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityMembershipOrg.identityId,
|
||||
identityMembershipOrg.orgId,
|
||||
@ -255,11 +258,21 @@ export const identityTokenAuthServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to revoke token auth of identity with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke token auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -301,20 +314,31 @@ export const identityTokenAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
const { permission: rolePermission, membership } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityMembershipOrg.identityId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.CreateToken,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to create token for identity with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to create token for identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.CreateToken,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -383,7 +407,7 @@ export const identityTokenAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const tokens = await identityAccessTokenDAL.find(
|
||||
{
|
||||
@ -429,20 +453,30 @@ export const identityTokenAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
const { permission: rolePermission, membership } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityMembershipOrg.identityId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.CreateToken,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update token for identity with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update token for identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.CreateToken,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -496,7 +530,7 @@ export const identityTokenAuthServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const [revokedToken] = await identityAccessTokenDAL.update(
|
||||
{
|
||||
|
@ -6,11 +6,14 @@ import jwt from "jsonwebtoken";
|
||||
|
||||
import { IdentityAuthMethod } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
@ -184,7 +187,7 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => {
|
||||
@ -278,7 +281,7 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
|
||||
const reformattedClientSecretTrustedIps = clientSecretTrustedIps?.map((clientSecretTrustedIp) => {
|
||||
@ -350,7 +353,7 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
return { ...uaIdentityAuth, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
@ -376,20 +379,30 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
const { permission: rolePermission, membership } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityMembershipOrg.identityId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to revoke universal auth of identity with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke universal auth of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.RevokeAuth,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -419,14 +432,14 @@ export const identityUaServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
@ -435,11 +448,21 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.CreateToken,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to create client secret for a more privileged identity.",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to create client secret for identity.",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.CreateToken,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -481,14 +504,14 @@ export const identityUaServiceFactory = ({
|
||||
message: "The identity does not have universal auth"
|
||||
});
|
||||
}
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
@ -498,11 +521,21 @@ export const identityUaServiceFactory = ({
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.GetToken,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to get identity client secret with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to get identity client secret with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.GetToken,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -534,14 +567,14 @@ export const identityUaServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
@ -550,11 +583,21 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.GetToken,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to read identity client secret of identity with more privileged role",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to read identity client secret of identity with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.GetToken,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -579,14 +622,14 @@ export const identityUaServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityMembershipOrg.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
@ -595,17 +638,30 @@ export const identityUaServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to revoke identity client secret with more privileged role",
|
||||
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.DeleteToken,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid) {
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to revoke identity client secret with more privileged role",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.DeleteToken,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
}
|
||||
|
||||
const clientSecret = await identityUaClientSecretDAL.updateById(clientSecretId, {
|
||||
isClientSecretRevoked: true
|
||||
});
|
||||
|
||||
return { ...clientSecret, identityId, orgId: identityMembershipOrg.orgId };
|
||||
};
|
||||
|
||||
|
@ -2,13 +2,15 @@ import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { OrgMembershipRole, TableName, TOrgRoles } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
||||
import { TIdentityDALFactory } from "./identity-dal";
|
||||
import { TIdentityMetadataDALFactory } from "./identity-metadata-dal";
|
||||
@ -51,19 +53,35 @@ export const identityServiceFactory = ({
|
||||
actorOrgId,
|
||||
metadata
|
||||
}: TCreateIdentityDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.Identity);
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: rolePermission, role: customRole } = await permissionService.getOrgPermissionByRole(
|
||||
role,
|
||||
orgId
|
||||
);
|
||||
const isCustomRole = Boolean(customRole);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to create a more privileged identity",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to create identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
@ -121,29 +139,14 @@ export const identityServiceFactory = ({
|
||||
const identityOrgMembership = await identityOrgMembershipDAL.findOne({ identityId: id });
|
||||
if (!identityOrgMembership) throw new NotFoundError({ message: `Failed to find identity with id ${id}` });
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
const { permission, membership } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
identityOrgMembership.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
const { permission: identityRolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
id,
|
||||
identityOrgMembership.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to update a more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||
|
||||
let customRole: TOrgRoles | undefined;
|
||||
if (role) {
|
||||
@ -153,11 +156,21 @@ export const identityServiceFactory = ({
|
||||
);
|
||||
|
||||
const isCustomRole = Boolean(customOrgRole);
|
||||
const appliedRolePermissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const appliedRolePermissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Identity,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!appliedRolePermissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to create a more privileged identity",
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to update identity",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
OrgPermissionIdentityActions.GrantPrivileges,
|
||||
OrgPermissionSubjects.Identity
|
||||
),
|
||||
details: { missingPermissions: appliedRolePermissionBoundary.missingPermissions }
|
||||
});
|
||||
if (isCustomRole) customRole = customOrgRole;
|
||||
@ -209,7 +222,7 @@ export const identityServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
return identity;
|
||||
};
|
||||
|
||||
@ -233,21 +246,8 @@ export const identityServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.Identity);
|
||||
const { permission: identityRolePermission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
id,
|
||||
identityOrgMembership.orgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
const permissionBoundary = validatePermissionBoundary(permission, identityRolePermission);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: "Failed to delete more privileged identity",
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
|
||||
|
||||
const deletedIdentity = await identityDAL.deleteById(id);
|
||||
|
||||
@ -269,7 +269,7 @@ export const identityServiceFactory = ({
|
||||
search
|
||||
}: TListOrgIdentitiesByOrgIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const identityMemberships = await identityOrgMembershipDAL.find({
|
||||
[`${TableName.IdentityOrgMembership}.orgId` as "orgId"]: orgId,
|
||||
@ -305,7 +305,7 @@ export const identityServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Identity);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||
|
||||
const identityMemberships = await identityProjectDAL.findByIdentityId(identityId);
|
||||
return identityMemberships;
|
||||
|
@ -584,7 +584,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
}[] = [];
|
||||
|
||||
Object.keys(secrets).forEach((key) => {
|
||||
const hyphenatedKey = key.replace(/_/g, "-");
|
||||
const hyphenatedKey = key.replaceAll("_", "-");
|
||||
if (!(hyphenatedKey in res)) {
|
||||
// case: secret has been created
|
||||
setSecrets.push({
|
||||
@ -603,7 +603,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
const deleteSecrets: AzureKeyVaultSecret[] = [];
|
||||
|
||||
Object.keys(res).forEach((key) => {
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
const underscoredKey = key.replaceAll("-", "_");
|
||||
if (!(underscoredKey in secrets)) {
|
||||
deleteSecrets.push(res[key]);
|
||||
}
|
||||
@ -617,7 +617,7 @@ const syncSecretsAzureKeyVault = async ({
|
||||
if (!integration.lastUsed) {
|
||||
Object.keys(res).forEach((key) => {
|
||||
// first time using integration
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
const underscoredKey = key.replaceAll("-", "_");
|
||||
|
||||
// -> apply initial sync behavior
|
||||
switch (metadata.initialSyncBehavior) {
|
||||
@ -3578,7 +3578,7 @@ const syncSecretsTeamCity = async ({
|
||||
.filter((parameter) => !parameter.inherited)
|
||||
.reduce(
|
||||
(obj, secret) => {
|
||||
const secretName = secret.name.replace(/^env\./, "");
|
||||
const secretName = secret.name.startsWith(".env") ? secret.name.slice(4) : secret.name;
|
||||
return {
|
||||
...obj,
|
||||
[secretName]: secret.value
|
||||
@ -3635,7 +3635,7 @@ const syncSecretsTeamCity = async ({
|
||||
)
|
||||
).data.property.reduce(
|
||||
(obj, secret) => {
|
||||
const secretName = secret.name.replace(/^env\./, "");
|
||||
const secretName = secret.name.startsWith("env.") ? secret.name.slice(4) : secret.name;
|
||||
return {
|
||||
...obj,
|
||||
[secretName]: secret.value
|
||||
|
@ -43,7 +43,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.Organization))
|
||||
.select(
|
||||
db.raw(`
|
||||
CASE
|
||||
CASE
|
||||
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.SAML}'
|
||||
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.OIDC}'
|
||||
ELSE ''
|
||||
@ -80,7 +80,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.Organization))
|
||||
.select(
|
||||
db.raw(`
|
||||
CASE
|
||||
CASE
|
||||
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.SAML}'
|
||||
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN '${OrgAuthMethod.OIDC}'
|
||||
ELSE ''
|
||||
@ -119,7 +119,7 @@ export const orgDALFactory = (db: TDbClient) => {
|
||||
.select(selectAllTableCols(TableName.Organization))
|
||||
.select(
|
||||
db.raw(`
|
||||
CASE
|
||||
CASE
|
||||
WHEN ${TableName.SamlConfig}."orgId" IS NOT NULL THEN 'saml'
|
||||
WHEN ${TableName.OidcConfig}."orgId" IS NOT NULL THEN 'oidc'
|
||||
ELSE ''
|
||||
|
@ -12,5 +12,9 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||
kmsDefaultKeyId: true,
|
||||
defaultMembershipRole: true,
|
||||
enforceMfa: true,
|
||||
selectedMfaMethod: true
|
||||
selectedMfaMethod: true,
|
||||
allowSecretSharingOutsideOrganization: true,
|
||||
shouldUseNewPrivilegeSystem: true,
|
||||
privilegeUpgradeInitiatedByUsername: true,
|
||||
privilegeUpgradeInitiatedAt: true
|
||||
});
|
||||
|
@ -21,18 +21,29 @@ import { TLicenseServiceFactory } from "@app/ee/services/license/license-service
|
||||
import { TOidcConfigDALFactory } from "@app/ee/services/oidc/oidc-config-dal";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionGroupActions,
|
||||
OrgPermissionSecretShareAction,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/ee/services/permission/org-permission";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { TSamlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { generateAsymmetricKeyPair } from "@app/lib/crypto";
|
||||
import { generateSymmetricKey, infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||
import {
|
||||
BadRequestError,
|
||||
ForbiddenRequestError,
|
||||
NotFoundError,
|
||||
PermissionBoundaryError,
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
import { isDisposableEmail } from "@app/lib/validator";
|
||||
@ -76,6 +87,7 @@ import {
|
||||
TResendOrgMemberInvitationDTO,
|
||||
TUpdateOrgDTO,
|
||||
TUpdateOrgMembershipDTO,
|
||||
TUpgradePrivilegeSystemDTO,
|
||||
TVerifyUserToOrgDTO
|
||||
} from "./org-types";
|
||||
|
||||
@ -187,7 +199,7 @@ export const orgServiceFactory = ({
|
||||
|
||||
const getOrgGroups = async ({ actor, actorId, orgId, actorAuthMethod, actorOrgId }: TGetOrgGroupsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.Groups);
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionGroupActions.Read, OrgPermissionSubjects.Groups);
|
||||
const groups = await groupDAL.findByOrgId(orgId);
|
||||
return groups;
|
||||
};
|
||||
@ -281,6 +293,45 @@ export const orgServiceFactory = ({
|
||||
};
|
||||
};
|
||||
|
||||
const upgradePrivilegeSystem = async ({
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
}: TUpgradePrivilegeSystemDTO) => {
|
||||
const { membership } = await permissionService.getUserOrgPermission(actorId, orgId, actorAuthMethod, actorOrgId);
|
||||
|
||||
if (membership.role !== OrgMembershipRole.Admin) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Insufficient privileges - only the organization admin can upgrade the privilege system."
|
||||
});
|
||||
}
|
||||
|
||||
return orgDAL.transaction(async (tx) => {
|
||||
const org = await orgDAL.findById(actorOrgId, tx);
|
||||
if (org.shouldUseNewPrivilegeSystem) {
|
||||
throw new BadRequestError({
|
||||
message: "Privilege system already upgraded"
|
||||
});
|
||||
}
|
||||
|
||||
const user = await userDAL.findById(actorId, tx);
|
||||
if (!user) {
|
||||
throw new NotFoundError({ message: `User with ID '${actorId}' not found` });
|
||||
}
|
||||
|
||||
return orgDAL.updateById(
|
||||
actorOrgId,
|
||||
{
|
||||
shouldUseNewPrivilegeSystem: true,
|
||||
privilegeUpgradeInitiatedAt: new Date(),
|
||||
privilegeUpgradeInitiatedByUsername: user.username
|
||||
},
|
||||
tx
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
* Update organization details
|
||||
* */
|
||||
@ -856,7 +907,7 @@ export const orgServiceFactory = ({
|
||||
// if there exist no project membership we set is as given by the request
|
||||
for await (const project of projectsToInvite) {
|
||||
const projectId = project.id;
|
||||
const { permission: projectPermission } = await permissionService.getProjectPermission({
|
||||
const { permission: projectPermission, membership } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@ -865,7 +916,7 @@ export const orgServiceFactory = ({
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(projectPermission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionMemberActions.Create,
|
||||
ProjectPermissionSub.Member
|
||||
);
|
||||
const existingMembers = await projectMembershipDAL.find(
|
||||
@ -888,6 +939,34 @@ export const orgServiceFactory = ({
|
||||
ProjectMembershipRole.Member
|
||||
];
|
||||
|
||||
for await (const invitedRole of invitedProjectRoles) {
|
||||
const { permission: rolePermission } = await permissionService.getProjectPermissionByRole(
|
||||
invitedRole,
|
||||
projectId
|
||||
);
|
||||
|
||||
if (invitedRole !== ProjectMembershipRole.NoAccess) {
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member,
|
||||
projectPermission,
|
||||
rolePermission
|
||||
);
|
||||
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
"Failed to invite user to the project",
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const customProjectRoles = invitedProjectRoles.filter(
|
||||
(role) => !Object.values(ProjectMembershipRole).includes(role as ProjectMembershipRole)
|
||||
);
|
||||
@ -1021,9 +1100,9 @@ export const orgServiceFactory = ({
|
||||
const sanitizedProjectMembershipRoles: TProjectUserMembershipRolesInsert[] = [];
|
||||
invitedProjectRoles.forEach((projectRole) => {
|
||||
const isCustomRole = Boolean(customRolesGroupBySlug?.[projectRole]?.[0]);
|
||||
projectMemberships.forEach((membership) => {
|
||||
projectMemberships.forEach((membershipEntry) => {
|
||||
sanitizedProjectMembershipRoles.push({
|
||||
projectMembershipId: membership.id,
|
||||
projectMembershipId: membershipEntry.id,
|
||||
role: isCustomRole ? ProjectMembershipRole.Custom : projectRole,
|
||||
customRoleId: customRolesGroupBySlug[projectRole] ? customRolesGroupBySlug[projectRole][0].id : null
|
||||
});
|
||||
@ -1304,6 +1383,7 @@ export const orgServiceFactory = ({
|
||||
getOrgGroups,
|
||||
listProjectMembershipsByOrgMembershipId,
|
||||
findOrgBySlug,
|
||||
resendOrgMemberInvitation
|
||||
resendOrgMemberInvitation,
|
||||
upgradePrivilegeSystem
|
||||
};
|
||||
};
|
||||
|
@ -76,6 +76,8 @@ export type TUpdateOrgDTO = {
|
||||
}>;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TUpgradePrivilegeSystemDTO = Omit<TOrgPermission, "actor">;
|
||||
|
||||
export type TGetOrgGroupsDTO = TOrgPermission;
|
||||
|
||||
export type TListProjectMembershipsByOrgMembershipIdDTO = {
|
||||
|
@ -2,7 +2,7 @@ import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
@ -40,7 +40,7 @@ export const projectKeyServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
|
||||
|
||||
const receiverMembership = await projectMembershipDAL.findOne({
|
||||
userId: receiverId,
|
||||
@ -89,7 +89,7 @@ export const projectKeyServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
return projectKeyDAL.findAllProjectUserPubKeys(projectId);
|
||||
};
|
||||
|
||||
|
@ -3,12 +3,15 @@ import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { ActionProjectType, ProjectMembershipRole, ProjectVersion, TableName } from "@app/db/schemas";
|
||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||
import {
|
||||
constructPermissionErrorMessage,
|
||||
validatePrivilegeChangeOperation
|
||||
} from "@app/ee/services/permission/permission-fns";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { ProjectPermissionMemberActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||
import { TProjectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal";
|
||||
import { validatePermissionBoundary } from "@app/lib/casl/boundary";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { ms } from "@app/lib/ms";
|
||||
|
||||
@ -86,7 +89,7 @@ export const projectMembershipServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
|
||||
@ -130,7 +133,7 @@ export const projectMembershipServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { username });
|
||||
if (!membership) throw new NotFoundError({ message: `Project membership not found for user '${username}'` });
|
||||
@ -153,7 +156,7 @@ export const projectMembershipServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Read, ProjectPermissionSub.Member);
|
||||
|
||||
const [membership] = await projectMembershipDAL.findAllProjectMembers(projectId, { id });
|
||||
if (!membership) throw new NotFoundError({ message: `Project membership not found for user ${id}` });
|
||||
@ -180,7 +183,7 @@ export const projectMembershipServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Create, ProjectPermissionSub.Member);
|
||||
const orgMembers = await orgDAL.findMembership({
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: project.orgId,
|
||||
$in: {
|
||||
@ -253,7 +256,7 @@ export const projectMembershipServiceFactory = ({
|
||||
membershipId,
|
||||
roles
|
||||
}: TUpdateProjectMembershipDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
const { permission, membership } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
@ -261,7 +264,7 @@ export const projectMembershipServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Edit, ProjectPermissionSub.Member);
|
||||
|
||||
const membershipUser = await userDAL.findUserByProjectMembershipId(membershipId);
|
||||
if (membershipUser?.isGhost || membershipUser?.projectId !== projectId) {
|
||||
@ -274,11 +277,21 @@ export const projectMembershipServiceFactory = ({
|
||||
projectId
|
||||
);
|
||||
|
||||
const permissionBoundary = validatePermissionBoundary(permission, rolePermission);
|
||||
const permissionBoundary = validatePrivilegeChangeOperation(
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member,
|
||||
permission,
|
||||
rolePermission
|
||||
);
|
||||
if (!permissionBoundary.isValid)
|
||||
throw new ForbiddenRequestError({
|
||||
name: "PermissionBoundaryError",
|
||||
message: `Failed to change to a more privileged role ${requestedRoleChange}`,
|
||||
throw new PermissionBoundaryError({
|
||||
message: constructPermissionErrorMessage(
|
||||
`Failed to change role ${requestedRoleChange}`,
|
||||
membership.shouldUseNewPrivilegeSystem,
|
||||
ProjectPermissionMemberActions.GrantPrivileges,
|
||||
ProjectPermissionSub.Member
|
||||
),
|
||||
details: { missingPermissions: permissionBoundary.missingPermissions }
|
||||
});
|
||||
}
|
||||
@ -361,7 +374,7 @@ export const projectMembershipServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member);
|
||||
|
||||
const member = await userDAL.findUserByProjectMembershipId(membershipId);
|
||||
|
||||
@ -397,7 +410,7 @@ export const projectMembershipServiceFactory = ({
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.Any
|
||||
});
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Member);
|
||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionMemberActions.Delete, ProjectPermissionSub.Member);
|
||||
|
||||
const project = await projectDAL.findById(projectId);
|
||||
|
||||
|
@ -7,14 +7,16 @@ import { groupBy, removeTrailingSlash } from "@app/lib/fn";
|
||||
import { ormify, selectAllTableCols } from "@app/lib/knex";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { isValidSecretPath } from "@app/lib/validator";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { SecretsOrderBy } from "@app/services/secret/secret-types";
|
||||
|
||||
import { TFindFoldersDeepByParentIdsDTO } from "./secret-folder-types";
|
||||
|
||||
export const validateFolderName = (folderName: string) => {
|
||||
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
|
||||
return validNameRegex.test(folderName);
|
||||
};
|
||||
export const validateFolderName = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.Underscore
|
||||
]);
|
||||
|
||||
const sqlFindMultipleFolderByEnvPathQuery = (db: Knex, query: Array<{ envId: string; secretPath: string }>) => {
|
||||
// this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2
|
||||
@ -188,9 +190,9 @@ const sqlFindSecretPathByFolderId = (db: Knex, projectId: string, folderIds: str
|
||||
// the root folder check is used to avoid last / and also root name in folders
|
||||
depth: db.raw("parent.depth + 1"),
|
||||
path: db.raw(
|
||||
`CONCAT( CASE
|
||||
WHEN ${TableName.SecretFolder}."parentId" is NULL THEN ''
|
||||
ELSE CONCAT('/', secret_folders.name)
|
||||
`CONCAT( CASE
|
||||
WHEN ${TableName.SecretFolder}."parentId" is NULL THEN ''
|
||||
ELSE CONCAT('/', secret_folders.name)
|
||||
END, parent.path )`
|
||||
),
|
||||
child: db.raw("COALESCE(parent.child, parent.id)"),
|
||||
@ -464,7 +466,7 @@ export const secretFolderDALFactory = (db: TDbClient) => {
|
||||
db.raw("parents.depth + 1 as depth"),
|
||||
db.raw(
|
||||
`CONCAT(
|
||||
CASE WHEN parents.path = '/' THEN '' ELSE parents.path END,
|
||||
CASE WHEN parents.path = '/' THEN '' ELSE parents.path END,
|
||||
CASE WHEN ${TableName.SecretFolder}."parentId" is NULL THEN '' ELSE CONCAT('/', secret_folders.name) END
|
||||
)`
|
||||
),
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import {
|
||||
@ -10,6 +11,25 @@ import {
|
||||
} from "@app/services/secret-sync/secret-sync-schemas";
|
||||
import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
const tagFieldCharacterValidator = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Spaces,
|
||||
CharacterType.Period,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Colon,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Equals,
|
||||
CharacterType.Plus,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.At
|
||||
]);
|
||||
|
||||
const pathCharacterValidator = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
]);
|
||||
|
||||
const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||
region: z.nativeEnum(AWSRegion).describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.region),
|
||||
path: z
|
||||
@ -17,35 +37,54 @@ const AwsParameterStoreSyncDestinationConfigSchema = z.object({
|
||||
.trim()
|
||||
.min(1, "Parameter Store Path required")
|
||||
.max(2048, "Cannot exceed 2048 characters")
|
||||
.regex(/^\/([/]|(([\w-]+\/)+))?$/, 'Invalid path - must follow "/example/path/" format')
|
||||
.refine(
|
||||
(val) =>
|
||||
val.startsWith("/") &&
|
||||
val.endsWith("/") &&
|
||||
val
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.every((el) => pathCharacterValidator(el)),
|
||||
'Invalid path - must follow "/example/path/" format'
|
||||
)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_PARAMETER_STORE.path)
|
||||
});
|
||||
|
||||
const AwsParameterStoreSyncOptionsSchema = z.object({
|
||||
keyId: z
|
||||
.string()
|
||||
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
||||
.min(1, "Invalid KMS Key ID")
|
||||
.max(256, "Invalid KMS Key ID")
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Colon,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
])(val),
|
||||
"Invalid KMS Key ID"
|
||||
)
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_PARAMETER_STORE.keyId),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Resource tag key required")
|
||||
.max(128, "Resource tag key cannot exceed 128 characters"),
|
||||
.max(128, "Resource tag key cannot exceed 128 characters")
|
||||
.refine(
|
||||
(val) => tagFieldCharacterValidator(val),
|
||||
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
.max(256, "Resource tag value cannot exceed 256 characters")
|
||||
.refine(
|
||||
(val) => tagFieldCharacterValidator(val),
|
||||
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.max(256, "Resource tag value cannot exceed 256 characters")
|
||||
})
|
||||
.array()
|
||||
.max(50)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretSyncs } from "@app/lib/api-docs";
|
||||
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
|
||||
import { AppConnection, AWSRegion } from "@app/services/app-connection/app-connection-enums";
|
||||
import { AwsSecretsManagerSyncMappingBehavior } from "@app/services/secret-sync/aws-secrets-manager/aws-secrets-manager-sync-enums";
|
||||
import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
@ -24,12 +25,23 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.mappingBehavior),
|
||||
secretName: z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-zA-Z0-9/_+=.@-]+$/,
|
||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||
)
|
||||
|
||||
.min(1, "Secret name is required")
|
||||
.max(256, "Secret name cannot exceed 256 characters")
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Plus,
|
||||
CharacterType.Equals,
|
||||
CharacterType.Period,
|
||||
CharacterType.At,
|
||||
CharacterType.Hyphen
|
||||
])(val),
|
||||
"Secret name must contain only alphanumeric characters and the characters /_+=.@-"
|
||||
)
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.AWS_SECRETS_MANAGER.secretName)
|
||||
})
|
||||
])
|
||||
@ -39,31 +51,54 @@ const AwsSecretsManagerSyncDestinationConfigSchema = z
|
||||
})
|
||||
);
|
||||
|
||||
const tagFieldCharacterValidator = characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Spaces,
|
||||
CharacterType.Period,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Colon,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Equals,
|
||||
CharacterType.Plus,
|
||||
CharacterType.Hyphen,
|
||||
CharacterType.At
|
||||
]);
|
||||
|
||||
const AwsSecretsManagerSyncOptionsSchema = z.object({
|
||||
keyId: z
|
||||
.string()
|
||||
.regex(/^([a-zA-Z0-9:/_-]+)$/, "Invalid KMS Key ID")
|
||||
.min(1, "Invalid KMS Key ID")
|
||||
.max(256, "Invalid KMS Key ID")
|
||||
.refine(
|
||||
(val) =>
|
||||
characterValidator([
|
||||
CharacterType.AlphaNumeric,
|
||||
CharacterType.Colon,
|
||||
CharacterType.ForwardSlash,
|
||||
CharacterType.Underscore,
|
||||
CharacterType.Hyphen
|
||||
])(val),
|
||||
"Invalid KMS Key ID"
|
||||
)
|
||||
.optional()
|
||||
.describe(SecretSyncs.ADDITIONAL_SYNC_OPTIONS.AWS_SECRETS_MANAGER.keyId),
|
||||
tags: z
|
||||
.object({
|
||||
key: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.min(1, "Tag key required")
|
||||
.max(128, "Tag key cannot exceed 128 characters"),
|
||||
.max(128, "Tag key cannot exceed 128 characters")
|
||||
.refine(
|
||||
(val) => tagFieldCharacterValidator(val),
|
||||
"Invalid resource tag key: keys can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
),
|
||||
value: z
|
||||
.string()
|
||||
.regex(
|
||||
/^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$/u,
|
||||
"Invalid tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
.max(256, "Tag value cannot exceed 256 characters")
|
||||
.refine(
|
||||
(val) => tagFieldCharacterValidator(val),
|
||||
"Invalid resource tag value: tag values can only contain Unicode letters, digits, white space and any of the following: _.:/=+@-"
|
||||
)
|
||||
})
|
||||
.array()
|
||||
.max(50)
|
||||
|
@ -100,7 +100,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
||||
const deleteSecrets: string[] = [];
|
||||
|
||||
Object.keys(secretMap).forEach((infisicalKey) => {
|
||||
const hyphenatedKey = infisicalKey.replace(/_/g, "-");
|
||||
const hyphenatedKey = infisicalKey.replaceAll("_", "-");
|
||||
if (!(hyphenatedKey in vaultSecrets)) {
|
||||
// case: secret has been created
|
||||
setSecrets.push({
|
||||
@ -117,7 +117,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
||||
});
|
||||
|
||||
Object.keys(vaultSecrets).forEach((key) => {
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
const underscoredKey = key.replaceAll("-", "_");
|
||||
if (!(underscoredKey in secretMap)) {
|
||||
deleteSecrets.push(key);
|
||||
}
|
||||
@ -211,7 +211,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
||||
);
|
||||
|
||||
for await (const [key] of Object.entries(vaultSecrets)) {
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
const underscoredKey = key.replaceAll("-", "_");
|
||||
|
||||
if (underscoredKey in secretMap) {
|
||||
if (!disabledAzureKeyVaultSecretKeys.includes(underscoredKey)) {
|
||||
@ -237,7 +237,7 @@ export const azureKeyVaultSyncFactory = ({ kmsService, appConnectionDAL }: TAzur
|
||||
|
||||
Object.keys(vaultSecrets).forEach((key) => {
|
||||
if (!disabledAzureKeyVaultSecretKeys.includes(key)) {
|
||||
const underscoredKey = key.replace(/-/g, "_");
|
||||
const underscoredKey = key.replaceAll("-", "_");
|
||||
secretMap[underscoredKey] = {
|
||||
value: vaultSecrets[key].value
|
||||
};
|
||||
|
@ -463,7 +463,7 @@ export const recursivelyGetSecretPaths = async ({
|
||||
const formatMultiValueEnv = (val?: string) => {
|
||||
if (!val) return "";
|
||||
if (!val.match("\n")) return val;
|
||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
||||
return `"${val.replaceAll("\n", "\\n")}"`;
|
||||
};
|
||||
|
||||
type TSecretReferenceTraceNode = {
|
||||
|
@ -207,7 +207,7 @@ export const recursivelyGetSecretPaths = ({
|
||||
const formatMultiValueEnv = (val?: string) => {
|
||||
if (!val) return "";
|
||||
if (!val.match("\n")) return val;
|
||||
return `"${val.replace(/\n/g, "\\n")}"`;
|
||||
return `"${val.replaceAll("\n", "\\n")}"`;
|
||||
};
|
||||
|
||||
type TInterpolateSecretArg = {
|
||||
@ -218,7 +218,7 @@ type TInterpolateSecretArg = {
|
||||
};
|
||||
|
||||
const MAX_SECRET_REFERENCE_DEPTH = 5;
|
||||
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
|
||||
const INTERPOLATION_SYNTAX_REG = /\${([a-zA-Z0-9-_.]+)}/g;
|
||||
export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderDAL }: TInterpolateSecretArg) => {
|
||||
const secretCache: Record<string, Record<string, string>> = {};
|
||||
const getCacheUniqueKey = (environment: string, secretPath: string) => `${environment}-${secretPath}`;
|
||||
|
@ -17,9 +17,7 @@ export enum PostHogEventTypes {
|
||||
SecretRequestCreated = "Secret Request Created",
|
||||
SecretRequestDeleted = "Secret Request Deleted",
|
||||
SignSshKey = "Sign SSH Key",
|
||||
IssueSshCreds = "Issue SSH Credentials",
|
||||
SignCert = "Sign PKI Certificate",
|
||||
IssueCert = "Issue PKI Certificate"
|
||||
IssueSshCreds = "Issue SSH Credentials"
|
||||
}
|
||||
|
||||
export type TSecretModifiedEvent = {
|
||||
@ -161,26 +159,6 @@ export type TIssueSshCredsEvent = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TSignCertificateEvent = {
|
||||
event: PostHogEventTypes.SignCert;
|
||||
properties: {
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
commonName: string;
|
||||
userAgent?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TIssueCertificateEvent = {
|
||||
event: PostHogEventTypes.IssueCert;
|
||||
properties: {
|
||||
caId?: string;
|
||||
certificateTemplateId?: string;
|
||||
commonName: string;
|
||||
userAgent?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TPostHogEvent = { distinctId: string } & (
|
||||
| TSecretModifiedEvent
|
||||
| TAdminInitEvent
|
||||
@ -195,6 +173,4 @@ export type TPostHogEvent = { distinctId: string } & (
|
||||
| TSecretRequestDeletedEvent
|
||||
| TSignSshKeyEvent
|
||||
| TIssueSshCredsEvent
|
||||
| TSignCertificateEvent
|
||||
| TIssueCertificateEvent
|
||||
);
|
||||
|
@ -12,7 +12,7 @@ require (
|
||||
github.com/fatih/semgroup v1.2.0
|
||||
github.com/gitleaks/go-gitdiff v0.8.0
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/infisical/go-sdk v0.4.8
|
||||
github.com/infisical/go-sdk v0.5.1
|
||||
github.com/infisical/infisical-kmip v0.3.5
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
|
||||
@ -34,6 +34,7 @@ require (
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/term v0.30.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
@ -125,7 +126,6 @@ require (
|
||||
google.golang.org/grpc v1.64.1 // indirect
|
||||
google.golang.org/protobuf v1.36.1 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
|
@ -277,8 +277,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
|
||||
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/infisical/go-sdk v0.4.8 h1:aphRnaauC5//PkP1ZbY9RSK2RiT1LjPS5o4CbX0x5OQ=
|
||||
github.com/infisical/go-sdk v0.4.8/go.mod h1:bMO9xSaBeXkDBhTIM4FkkREAfw2V8mv5Bm7lvo4+uDk=
|
||||
github.com/infisical/go-sdk v0.5.1 h1:bl0D4A6CmvfL8RwEQTcZh39nsxC6q3HSs76/4J8grWY=
|
||||
github.com/infisical/go-sdk v0.5.1/go.mod h1:ExjqFLRz7LSpZpGluqDLvFl6dFBLq5LKyLW7GBaMAIs=
|
||||
github.com/infisical/infisical-kmip v0.3.5 h1:QM3s0e18B+mYv3a9HQNjNAlbwZJBzXq5BAJM2scIeiE=
|
||||
github.com/infisical/infisical-kmip v0.3.5/go.mod h1:bO1M4YtKyutNg1bREPmlyZspC5duSR7hyQ3lPmLzrIs=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
@ -858,4 +858,4 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
@ -29,7 +29,6 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/config"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -514,7 +513,10 @@ type NewAgentMangerOptions struct {
|
||||
}
|
||||
|
||||
func NewAgentManager(options NewAgentMangerOptions) *AgentManager {
|
||||
|
||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get custom headers")
|
||||
}
|
||||
return &AgentManager{
|
||||
filePaths: options.FileDeposits,
|
||||
templates: options.Templates,
|
||||
@ -529,6 +531,7 @@ func NewAgentManager(options NewAgentMangerOptions) *AgentManager {
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT, // ? Should we perhaps use a different user agent for the Agent for better analytics?
|
||||
AutoTokenRefresh: false,
|
||||
CustomHeaders: customHeaders,
|
||||
}),
|
||||
}
|
||||
|
||||
@ -716,7 +719,11 @@ func (tm *AgentManager) FetchNewAccessToken() error {
|
||||
|
||||
// Refreshes the existing access token
|
||||
func (tm *AgentManager) RefreshAccessToken() error {
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpClient.SetRetryCount(10000).
|
||||
SetRetryMaxWaitTime(20 * time.Second).
|
||||
SetRetryWaitTime(5 * time.Second)
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -70,8 +69,12 @@ var bootstrapCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
httpClient := resty.New().
|
||||
SetHeader("Accept", "application/json")
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
log.Error().Msgf("Failed to get resty client with custom headers: %v", err)
|
||||
return
|
||||
}
|
||||
httpClient.SetHeader("Accept", "application/json")
|
||||
|
||||
bootstrapResponse, err := api.CallBootstrapInstance(httpClient, api.BootstrapInstanceRequest{
|
||||
Domain: util.AppendAPIEndpoint(domain),
|
||||
|
@ -14,7 +14,6 @@ import (
|
||||
// "github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
// "github.com/Infisical/infisical-merge/packages/visualize"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/posthog/posthog-go"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@ -56,7 +55,10 @@ func getDynamicSecretList(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
@ -85,10 +87,16 @@ func getDynamicSecretList(cmd *cobra.Command, args []string) {
|
||||
|
||||
httpClient.SetAuthToken(infisicalToken)
|
||||
|
||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get custom headers")
|
||||
}
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
CustomHeaders: customHeaders,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
@ -164,7 +172,10 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
@ -193,10 +204,16 @@ func createDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
|
||||
httpClient.SetAuthToken(infisicalToken)
|
||||
|
||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get custom headers")
|
||||
}
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
CustomHeaders: customHeaders,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
@ -286,7 +303,10 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
@ -315,10 +335,16 @@ func renewDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
|
||||
httpClient.SetAuthToken(infisicalToken)
|
||||
|
||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get custom headers")
|
||||
}
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
CustomHeaders: customHeaders,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
@ -384,7 +410,10 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
@ -413,10 +442,16 @@ func revokeDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
|
||||
httpClient.SetAuthToken(infisicalToken)
|
||||
|
||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get custom headers")
|
||||
}
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
CustomHeaders: customHeaders,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
@ -481,7 +516,10 @@ func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
|
||||
var infisicalToken string
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
@ -510,10 +548,16 @@ func listDynamicSecretLeaseByName(cmd *cobra.Command, args []string) {
|
||||
|
||||
httpClient.SetAuthToken(infisicalToken)
|
||||
|
||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get custom headers")
|
||||
}
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
CustomHeaders: customHeaders,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/posthog/posthog-go"
|
||||
"github.com/rs/zerolog/log"
|
||||
@ -50,7 +49,10 @@ var initCmd = &cobra.Command{
|
||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||
}
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
httpClient.SetAuthToken(userCreds.UserCredentials.JTWToken)
|
||||
|
||||
organizationResponse, err := api.CallGetAllOrganizations(httpClient)
|
||||
@ -81,7 +83,10 @@ var initCmd = &cobra.Command{
|
||||
for i < 6 {
|
||||
mfaVerifyCode := askForMFACode(tokenResponse.MfaMethod)
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
httpClient.SetAuthToken(tokenResponse.Token)
|
||||
verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{
|
||||
Email: userCreds.UserCredentials.Email,
|
||||
|
@ -27,7 +27,6 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/srp"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/posthog/posthog-go"
|
||||
"github.com/rs/cors"
|
||||
@ -178,10 +177,16 @@ var loginCmd = &cobra.Command{
|
||||
return
|
||||
}
|
||||
|
||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get custom headers")
|
||||
}
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
CustomHeaders: customHeaders,
|
||||
})
|
||||
|
||||
loginMethod, err := cmd.Flags().GetString("method")
|
||||
@ -359,7 +364,10 @@ func cliDefaultLogin(userCredentialsToBeStored *models.UserCredentials) {
|
||||
for i < 6 {
|
||||
mfaVerifyCode := askForMFACode("email")
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
httpClient.SetAuthToken(loginTwoResponse.Token)
|
||||
verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{
|
||||
Email: email,
|
||||
@ -726,7 +734,10 @@ func askForLoginCredentials() (email string, password string, err error) {
|
||||
|
||||
func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2Response, *api.GetLoginTwoV2Response, error) {
|
||||
log.Debug().Msg(fmt.Sprint("getFreshUserCredentials: ", "email", email, "password: ", password))
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
httpClient.SetRetryCount(5)
|
||||
|
||||
params := srp.GetParams(4096)
|
||||
@ -776,7 +787,10 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R
|
||||
func GetJwtTokenWithOrganizationId(oldJwtToken string, email string) string {
|
||||
log.Debug().Msg(fmt.Sprint("GetJwtTokenWithOrganizationId: ", "oldJwtToken", oldJwtToken))
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
httpClient.SetAuthToken(oldJwtToken)
|
||||
|
||||
organizationResponse, err := api.CallGetAllOrganizations(httpClient)
|
||||
@ -811,7 +825,10 @@ func GetJwtTokenWithOrganizationId(oldJwtToken string, email string) string {
|
||||
for i < 6 {
|
||||
mfaVerifyCode := askForMFACode(selectedOrgRes.MfaMethod)
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
httpClient.SetAuthToken(selectedOrgRes.Token)
|
||||
verifyMFAresponse, mfaErrorResponse, requestError := api.CallVerifyMfaToken(httpClient, api.VerifyMfaTokenRequest{
|
||||
Email: email,
|
||||
@ -913,7 +930,14 @@ func askToPasteJwtToken(success chan models.UserCredentials, failure chan error)
|
||||
}
|
||||
|
||||
// verify JTW
|
||||
httpClient := resty.New().
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
failure <- err
|
||||
fmt.Println("Error getting resty client with custom headers", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
httpClient.
|
||||
SetAuthToken(userCredentials.JTWToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
|
@ -5,6 +5,7 @@ package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
@ -13,7 +14,6 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/Infisical/infisical-merge/packages/visualize"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/posthog/posthog-go"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -139,7 +139,7 @@ var secretsGenerateExampleEnvCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var secretsSetCmd = &cobra.Command{
|
||||
Example: `secrets set <secretName=secretValue> <secretName=secretValue>..."`,
|
||||
Example: `secrets set <secretName=secretValue> <secretName=secretValue> <secretName=@/path/to/file>..."`,
|
||||
Short: "Used set secrets",
|
||||
Use: "set [secrets]",
|
||||
DisableFlagsInUseLine: true,
|
||||
@ -185,6 +185,30 @@ var secretsSetCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse secret type")
|
||||
}
|
||||
|
||||
processedArgs := []string{}
|
||||
for _, arg := range args {
|
||||
splitKeyValue := strings.SplitN(arg, "=", 2)
|
||||
if len(splitKeyValue) != 2 {
|
||||
util.HandleError(fmt.Errorf("invalid argument format: %s. Expected format: key=value or key=@filepath", arg), "")
|
||||
}
|
||||
|
||||
key := splitKeyValue[0]
|
||||
value := splitKeyValue[1]
|
||||
|
||||
if strings.HasPrefix(value, "\\@") {
|
||||
value = "@" + value[2:]
|
||||
} else if strings.HasPrefix(value, "@") {
|
||||
filePath := strings.TrimPrefix(value, "@")
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
util.HandleError(err, fmt.Sprintf("Unable to read file %s", filePath))
|
||||
}
|
||||
value = string(content)
|
||||
}
|
||||
|
||||
processedArgs = append(processedArgs, fmt.Sprintf("%s=%s", key, value))
|
||||
}
|
||||
|
||||
file, err := cmd.Flags().GetString("file")
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
@ -216,7 +240,7 @@ var secretsSetCmd = &cobra.Command{
|
||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||
}
|
||||
|
||||
secretOperations, err = util.SetRawSecrets(args, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{
|
||||
secretOperations, err = util.SetRawSecrets(processedArgs, secretType, environmentName, secretsPath, projectId, &models.TokenDetails{
|
||||
Type: "",
|
||||
Token: loggedInUserDetails.UserCredentials.JTWToken,
|
||||
}, file)
|
||||
@ -274,8 +298,12 @@ var secretsDeleteCmd = &cobra.Command{
|
||||
util.HandleError(err, "Unable to parse flag")
|
||||
}
|
||||
|
||||
httpClient := resty.New().
|
||||
SetHeader("Accept", "application/json")
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
|
||||
httpClient.SetHeader("Accept", "application/json")
|
||||
|
||||
if projectId == "" {
|
||||
workspaceFile, err := util.GetWorkSpaceFromFile()
|
||||
|
@ -315,10 +315,16 @@ func issueCredentials(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get custom headers")
|
||||
}
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
CustomHeaders: customHeaders,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
@ -555,10 +561,16 @@ func signKey(cmd *cobra.Command, args []string) {
|
||||
signedKeyPath = outFilePath
|
||||
}
|
||||
|
||||
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get custom headers")
|
||||
}
|
||||
|
||||
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
||||
SiteUrl: config.INFISICAL_URL,
|
||||
UserAgent: api.USER_AGENT,
|
||||
AutoTokenRefresh: false,
|
||||
CustomHeaders: customHeaders,
|
||||
})
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
|
||||
|
@ -13,7 +13,6 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/crypto"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -136,7 +135,11 @@ var tokensCreateCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
// make a call to the api to save the encrypted symmetric key details
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
util.HandleError(err, "Unable to get resty client with custom headers")
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/systemd"
|
||||
"github.com/Infisical/infisical-merge/packages/util"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/pion/dtls/v3"
|
||||
"github.com/pion/logging"
|
||||
@ -40,7 +41,11 @@ type Gateway struct {
|
||||
}
|
||||
|
||||
func NewGateway(identityToken string) (Gateway, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return Gateway{}, fmt.Errorf("unable to get client with custom headers [err=%v]", err)
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(identityToken)
|
||||
|
||||
return Gateway{
|
||||
|
@ -4,8 +4,11 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/config"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
func GetHomeDir() (string, error) {
|
||||
@ -27,3 +30,88 @@ func ValidateInfisicalAPIConnection() (ok bool) {
|
||||
_, err := http.Get(fmt.Sprintf("%v/status", config.INFISICAL_URL))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func GetRestyClientWithCustomHeaders() (*resty.Client, error) {
|
||||
httpClient := resty.New()
|
||||
customHeaders := os.Getenv("INFISICAL_CUSTOM_HEADERS")
|
||||
if customHeaders != "" {
|
||||
headers, err := GetInfisicalCustomHeadersMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient.SetHeaders(headers)
|
||||
}
|
||||
return httpClient, nil
|
||||
}
|
||||
|
||||
func GetInfisicalCustomHeadersMap() (map[string]string, error) {
|
||||
customHeaders := os.Getenv("INFISICAL_CUSTOM_HEADERS")
|
||||
if customHeaders == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
headers := map[string]string{}
|
||||
|
||||
pos := 0
|
||||
for pos < len(customHeaders) {
|
||||
for pos < len(customHeaders) && unicode.IsSpace(rune(customHeaders[pos])) {
|
||||
pos++
|
||||
}
|
||||
|
||||
if pos >= len(customHeaders) {
|
||||
break
|
||||
}
|
||||
|
||||
keyStart := pos
|
||||
for pos < len(customHeaders) && customHeaders[pos] != '=' && !unicode.IsSpace(rune(customHeaders[pos])) {
|
||||
pos++
|
||||
}
|
||||
|
||||
if pos >= len(customHeaders) || customHeaders[pos] != '=' {
|
||||
return nil, fmt.Errorf("invalid custom header format. Expected \"headerKey1=value1 headerKey2=value2 ....\" but got %v", customHeaders)
|
||||
}
|
||||
|
||||
key := customHeaders[keyStart:pos]
|
||||
pos++
|
||||
|
||||
for pos < len(customHeaders) && unicode.IsSpace(rune(customHeaders[pos])) {
|
||||
pos++
|
||||
}
|
||||
|
||||
var value string
|
||||
|
||||
if pos < len(customHeaders) {
|
||||
if customHeaders[pos] == '"' || customHeaders[pos] == '\'' {
|
||||
quoteChar := customHeaders[pos]
|
||||
pos++
|
||||
valueStart := pos
|
||||
|
||||
for pos < len(customHeaders) &&
|
||||
(customHeaders[pos] != quoteChar ||
|
||||
(pos > 0 && customHeaders[pos-1] == '\\')) {
|
||||
pos++
|
||||
}
|
||||
|
||||
if pos < len(customHeaders) {
|
||||
value = customHeaders[valueStart:pos]
|
||||
pos++
|
||||
} else {
|
||||
value = customHeaders[valueStart:]
|
||||
}
|
||||
} else {
|
||||
valueStart := pos
|
||||
for pos < len(customHeaders) && !unicode.IsSpace(rune(customHeaders[pos])) {
|
||||
pos++
|
||||
}
|
||||
value = customHeaders[valueStart:pos]
|
||||
}
|
||||
}
|
||||
|
||||
if key != "" && !strings.EqualFold(key, "User-Agent") && !strings.EqualFold(key, "Accept") {
|
||||
headers[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return headers, nil
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/config"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/zalando/go-keyring"
|
||||
)
|
||||
|
||||
@ -85,7 +84,12 @@ func GetCurrentLoggedInUserDetails(setConfigVariables bool) (LoggedInUserDetails
|
||||
}
|
||||
|
||||
// check to to see if the JWT is still valid
|
||||
httpClient := resty.New().
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return LoggedInUserDetails{}, fmt.Errorf("getCurrentLoggedInUserDetails: unable to get client with custom headers [err=%s]", err)
|
||||
}
|
||||
|
||||
httpClient.
|
||||
SetAuthToken(userCreds.JTWToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
@ -65,7 +64,11 @@ func GetAllFolders(params models.GetAllFoldersParameters) ([]models.SingleFolder
|
||||
|
||||
func GetFoldersViaJTW(JTWToken string, workspaceId string, environmentName string, foldersPath string) ([]models.SingleFolder, error) {
|
||||
// set up resty client
|
||||
httpClient := resty.New()
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(JTWToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
@ -100,7 +103,10 @@ func GetFoldersViaServiceToken(fullServiceToken string, workspaceId string, envi
|
||||
|
||||
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get client with custom headers [err=%v]", err)
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(serviceToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
@ -143,7 +149,11 @@ func GetFoldersViaServiceToken(fullServiceToken string, workspaceId string, envi
|
||||
}
|
||||
|
||||
func GetFoldersViaMachineIdentity(accessToken string, workspaceId string, envSlug string, foldersPath string) ([]models.SingleFolder, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(accessToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
@ -191,9 +201,12 @@ func CreateFolder(params models.CreateFolderParameters) (models.SingleFolder, er
|
||||
}
|
||||
|
||||
// set up resty client
|
||||
httpClient := resty.New()
|
||||
httpClient.
|
||||
SetAuthToken(params.InfisicalToken).
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return models.SingleFolder{}, err
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(params.InfisicalToken).
|
||||
SetHeader("Accept", "application/json").
|
||||
SetHeader("Content-Type", "application/json")
|
||||
|
||||
@ -238,9 +251,12 @@ func DeleteFolder(params models.DeleteFolderParameters) ([]models.SingleFolder,
|
||||
}
|
||||
|
||||
// set up resty client
|
||||
httpClient := resty.New()
|
||||
httpClient.
|
||||
SetAuthToken(params.InfisicalToken).
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(params.InfisicalToken).
|
||||
SetHeader("Accept", "application/json").
|
||||
SetHeader("Content-Type", "application/json")
|
||||
|
||||
|
@ -16,7 +16,6 @@ import (
|
||||
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@ -120,7 +119,11 @@ func GetInfisicalToken(cmd *cobra.Command) (token *models.TokenDetails, err erro
|
||||
}
|
||||
|
||||
func UniversalAuthLogin(clientId string, clientSecret string) (api.UniversalAuthLoginResponse, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return api.UniversalAuthLoginResponse{}, err
|
||||
}
|
||||
|
||||
httpClient.SetRetryCount(10000).
|
||||
SetRetryMaxWaitTime(20 * time.Second).
|
||||
SetRetryWaitTime(5 * time.Second)
|
||||
@ -135,7 +138,11 @@ func UniversalAuthLogin(clientId string, clientSecret string) (api.UniversalAuth
|
||||
|
||||
func RenewMachineIdentityAccessToken(accessToken string) (string, error) {
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
httpClient.SetRetryCount(10000).
|
||||
SetRetryMaxWaitTime(20 * time.Second).
|
||||
SetRetryWaitTime(5 * time.Second)
|
||||
|
@ -14,7 +14,6 @@ import (
|
||||
"github.com/Infisical/infisical-merge/packages/api"
|
||||
"github.com/Infisical/infisical-merge/packages/crypto"
|
||||
"github.com/Infisical/infisical-merge/packages/models"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/zalando/go-keyring"
|
||||
"gopkg.in/yaml.v3"
|
||||
@ -28,7 +27,10 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
|
||||
|
||||
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
|
||||
|
||||
httpClient := resty.New()
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get client with custom headers [err=%v]", err)
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(serviceToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
@ -79,7 +81,11 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str
|
||||
}
|
||||
|
||||
func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool, tagSlugs string, expandSecretReferences bool) (models.PlaintextSecretResult, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return models.PlaintextSecretResult{}, err
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(accessToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
@ -122,7 +128,11 @@ func GetPlainTextSecretsV3(accessToken string, workspaceId string, environmentNa
|
||||
}
|
||||
|
||||
func GetSinglePlainTextSecretByNameV3(accessToken string, workspaceId string, environmentName string, secretsPath string, secretName string) (models.SingleEnvironmentVariable, string, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return models.SingleEnvironmentVariable{}, "", err
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(accessToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
@ -153,7 +163,11 @@ func GetSinglePlainTextSecretByNameV3(accessToken string, workspaceId string, en
|
||||
}
|
||||
|
||||
func CreateDynamicSecretLease(accessToken string, projectSlug string, environmentName string, secretsPath string, slug string, ttl string) (models.DynamicSecretLease, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return models.DynamicSecretLease{}, err
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(accessToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
@ -525,7 +539,11 @@ func GetEnvelopmentBasedOnGitBranch(workspaceFile models.WorkspaceConfigFile) st
|
||||
}
|
||||
|
||||
func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey string, workspaceId string) ([]byte, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: unable to get client with custom headers [err=%v]", err)
|
||||
}
|
||||
|
||||
httpClient.SetAuthToken(authenticationToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
@ -672,9 +690,12 @@ func SetRawSecrets(secretArgs []string, secretType string, environmentName strin
|
||||
getAllEnvironmentVariablesRequest.InfisicalToken = tokenDetails.Token
|
||||
}
|
||||
|
||||
httpClient := resty.New().
|
||||
SetAuthToken(tokenDetails.Token).
|
||||
SetHeader("Accept", "application/json")
|
||||
httpClient, err := GetRestyClientWithCustomHeaders()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get client with custom headers [err=%v]", err)
|
||||
}
|
||||
|
||||
httpClient.SetHeader("Accept", "application/json")
|
||||
|
||||
// pull current secrets
|
||||
secrets, err := GetAllEnvironmentVariables(getAllEnvironmentVariablesRequest, "")
|
||||
|
@ -186,12 +186,28 @@ This command allows you to set or update secrets in your environment. If the sec
|
||||
If the secret key does not exist, a new secret will be created using both the key and value provided.
|
||||
|
||||
```bash
|
||||
$ infisical secrets set <key1=value1> <key2=value2>...
|
||||
$ infisical secrets set <key1=value1> <key2=value2> <key3=@/path/to/file>...
|
||||
|
||||
## Example
|
||||
$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe
|
||||
$ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jebhfbwe SECRET_PEM_KEY=@secret.pem
|
||||
```
|
||||
|
||||
<Tip>
|
||||
When setting secret values:
|
||||
- Use `secretName=@path/to/file` to load the secret value from a file
|
||||
- Use `secretName=\@value` if you need the literal '@' character at the beginning of your value
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
# Set a secret with the value loaded from a certificate file
|
||||
$ secrets set CERTIFICATE=@/path/to/certificate.pem
|
||||
|
||||
# Set a secret with the literal value "@example.com"
|
||||
$ secrets set email="\@example.com"
|
||||
```
|
||||
</Tip>
|
||||
|
||||
### Flags
|
||||
|
||||
<Accordion title="--env">
|
||||
|
@ -33,3 +33,28 @@ Yes. This is simply a configuration file and contains no sensitive data.
|
||||
https://app.infisical.com/project/<your_project_id>/settings
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="How do I use custom headers with the Infisical CLI?">
|
||||
|
||||
The Infisical CLI supports custom HTTP headers for requests to servers that require additional authentication. Set these headers using the `INFISICAL_CUSTOM_HEADERS` environment variable:
|
||||
|
||||
```bash
|
||||
export INFISICAL_CUSTOM_HEADERS="Access-Client-Id=your-client-id Access-Client-Secret=your-client-secret"
|
||||
```
|
||||
|
||||
After setting this environment variable, run your Infisical commands as usual.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Why would I need to use custom headers?">
|
||||
|
||||
Custom headers are necessary when your Infisical server is protected by services like Cloudflare Access or other reverse proxies that require specific authentication headers. Without this feature, you would need to implement security workarounds that might compromise your security posture.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="What format should I use for the custom headers?">
|
||||
|
||||
Custom headers should be specified in the format `headername1=headervalue1 headername2=headervalue2`, with spaces separating each header-value pair. For example:
|
||||
|
||||
```bash
|
||||
export INFISICAL_CUSTOM_HEADERS="Header1=value1 Header2=value2 Header3=value3"
|
||||
```
|
||||
</Accordion>
|
@ -120,6 +120,22 @@ The CLI is designed for a variety of secret management applications ranging from
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Tip>
|
||||
## Custom Request Headers
|
||||
|
||||
The Infisical CLI supports custom HTTP headers for requests to servers protected by authentication services such as Cloudflare Access. Configure these headers using the `INFISICAL_CUSTOM_HEADERS` environment variable:
|
||||
|
||||
```bash
|
||||
# Syntax: headername1=headervalue1 headername2=headervalue2
|
||||
export INFISICAL_CUSTOM_HEADERS="Access-Client-Id=your-client-id Access-Client-Secret=your-client-secret"
|
||||
|
||||
# Execute Infisical commands after setting the environment variable
|
||||
infisical secrets ls
|
||||
```
|
||||
|
||||
This functionality enables secure interaction with Infisical instances that require specific authentication headers.
|
||||
</Tip>
|
||||
|
||||
## History
|
||||
|
||||
Your terminal keeps a history with the commands you run. When you create Infisical secrets directly from your terminal, they'll stay there for a while.
|
||||
|
@ -124,7 +124,7 @@ When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="leaseTTL">
|
||||
The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret.
|
||||
The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret.
|
||||
|
||||
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
|
||||
|
||||
|
@ -1,213 +0,0 @@
|
||||
---
|
||||
title: "Permissions"
|
||||
description: "Infisical's permissions system provides granular access control."
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Infisical permissions system is based on a role-based access control (RBAC) model. The system allows you to define roles and assign them to users and machines. Each role has a set of permissions that define what actions a user can perform.
|
||||
|
||||
Permissions are built on a subject-action-object model. The subject is the resource the permission is being applied to, the action is what the permission allows.
|
||||
An example of a subject/action combination would be `secrets/read`. This permission allows the subject to read secrets.
|
||||
|
||||
Refer to the table below for a list of subjects and the actions they support.
|
||||
|
||||
## Subjects and Actions
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Project Permissions">
|
||||
|
||||
<Note>
|
||||
Not all actions are applicable to all subjects. As an example, the
|
||||
`secrets-rollback` subject only supports `read`, and `create` as actions.
|
||||
While `secrets` support `read`, `create`, `edit`, `delete`.
|
||||
</Note>
|
||||
|
||||
| Subject | Actions |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `integrations` | `read`, `create`, `edit`, `delete` |
|
||||
| `webhooks` | `read`, `create`, `edit`, `delete` |
|
||||
| `service-tokens` | `read`, `create`, `edit`, `delete` |
|
||||
| `environments` | `read`, `create`, `edit`, `delete` |
|
||||
| `tags` | `read`, `create`, `edit`, `delete` |
|
||||
| `audit-logs` | `read`, `create`, `edit`, `delete` |
|
||||
| `ip-allowlist` | `read`, `create`, `edit`, `delete` |
|
||||
| `workspace` | `edit`, `delete` |
|
||||
| `secrets` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-folders` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-imports` | `read`, `create`, `edit`, `delete` |
|
||||
| `dynamic-secrets` | `read-root-credential`, `create-root-credential`, `edit-root-credential`, `delete-root-credential`, `lease` |
|
||||
| `secret-rollback` | `read`, `create` |
|
||||
| `secret-approval` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-rotation` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificate-authorities` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificates` | `read`, `create`, `edit`, `delete` |
|
||||
| `certificate-templates` | `read`, `create`, `edit`, `delete` |
|
||||
| `pki-alerts` | `read`, `create`, `edit`, `delete` |
|
||||
| `pki-collections` | `read`, `create`, `edit`, `delete` |
|
||||
| `kms` | `edit` |
|
||||
| `cmek` | `read`, `create`, `edit`, `delete`, `encrypt`, `decrypt` |
|
||||
| `secret-syncs` | `read`, `create`, `edit`, `delete`, `sync-secrets`, `import-secrets`, `remove-secrets` |
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="Organization Permissions">
|
||||
|
||||
<Note>
|
||||
Not all actions are applicable to all subjects. As an example, the `workspace`
|
||||
subject only supports `read`, and `create` as actions. While `member` support
|
||||
`read`, `create`, `edit`, `delete`.
|
||||
</Note>
|
||||
|
||||
| Subject | Actions |
|
||||
| --------------------- | ------------------------------------------------ |
|
||||
| `workspace` | `read`, `create` |
|
||||
| `role` | `read`, `create`, `edit`, `delete` |
|
||||
| `member` | `read`, `create`, `edit`, `delete` |
|
||||
| `secret-scanning` | `read`, `create`, `edit`, `delete` |
|
||||
| `settings` | `read`, `create`, `edit`, `delete` |
|
||||
| `incident-account` | `read`, `create`, `edit`, `delete` |
|
||||
| `sso` | `read`, `create`, `edit`, `delete` |
|
||||
| `scim` | `read`, `create`, `edit`, `delete` |
|
||||
| `ldap` | `read`, `create`, `edit`, `delete` |
|
||||
| `groups` | `read`, `create`, `edit`, `delete` |
|
||||
| `billing` | `read`, `create`, `edit`, `delete` |
|
||||
| `identity` | `read`, `create`, `edit`, `delete` |
|
||||
| `project-templates` | `read`, `create`, `edit`, `delete` |
|
||||
| `app-connections` | `read`, `create`, `edit`, `delete`, `connect` |
|
||||
| `kms` | `read` |
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Inversion
|
||||
|
||||
Permission inversion allows you to explicitly deny actions instead of allowing them. This is supported for the following subjects:
|
||||
|
||||
- secrets
|
||||
- secret-folders
|
||||
- secret-imports
|
||||
- dynamic-secrets
|
||||
|
||||
When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example:
|
||||
|
||||
```typescript
|
||||
// Regular permission - allows reading secrets
|
||||
{
|
||||
subject: "secrets",
|
||||
action: ["read"]
|
||||
}
|
||||
|
||||
// Inverted permission - denies reading secrets
|
||||
{
|
||||
subject: "secrets",
|
||||
action: ["read"],
|
||||
inverted: true
|
||||
}
|
||||
```
|
||||
|
||||
## Conditions
|
||||
|
||||
Conditions allow you to create more granular permissions by specifying criteria that must be met for the permission to apply. This is supported for the following subjects:
|
||||
|
||||
- secrets
|
||||
- secret-folders
|
||||
- secret-imports
|
||||
- dynamic-secrets
|
||||
|
||||
### Properties
|
||||
|
||||
Conditions can be applied to the following properties:
|
||||
|
||||
- `environment`: Control access based on environment slugs
|
||||
- `secretPath`: Control access based on secret paths
|
||||
- `secretName`: Control access based on secret names
|
||||
- `secretTags`: Control access based on tags (only supports $in operator)
|
||||
|
||||
### Operators
|
||||
|
||||
The following operators are available for conditions:
|
||||
|
||||
| Operator | Description | Example |
|
||||
| -------- | ---------------------------------- | ----------------------------------------------------- |
|
||||
| `$eq` | Equal | `{ environment: { $eq: "production" } }` |
|
||||
| `$ne` | Not equal | `{ environment: { $ne: "development" } }` |
|
||||
| `$in` | Matches any value in array | `{ environment: { $in: ["staging", "production"] } }` |
|
||||
| `$glob` | Pattern matching using glob syntax | `{ secretPath: { $glob: "/app/\*" } }` |
|
||||
|
||||
These details are especially useful if you're using the API to [create new project roles](../api-reference/endpoints/project-roles/create).
|
||||
The rules outlined on this page, also apply when using our Terraform Provider to manage your Infisical project roles, or any other of our clients that manage project roles.
|
||||
|
||||
## Migrating from permission V1 to permission V2
|
||||
|
||||
When upgrading to V2 permissions (i.e. when moving from using the `permissions` to `permissions_v2` field in your Terraform configurations, or upgrading to the V2 permission API), you'll need to update your permission structure as follows:
|
||||
|
||||
Any permissions for `secrets` should be expanded to include equivalent permissions for:
|
||||
|
||||
- `secret-imports`
|
||||
- `secret-folders` (except for read permissions)
|
||||
- `dynamic-secrets`
|
||||
|
||||
For dynamic secrets, the actions need to be mapped differently:
|
||||
|
||||
- `read` → `read-root-credential`
|
||||
- `create` → `create-root-credential`
|
||||
- `edit` → `edit-root-credential` (also adds `lease` permission)
|
||||
- `delete` → `delete-root-credential`
|
||||
|
||||
Example:
|
||||
|
||||
```hcl
|
||||
# Old V1 configuration
|
||||
resource "infisical_project_role" "example" {
|
||||
name = "example"
|
||||
permissions = [
|
||||
{
|
||||
subject = "secrets"
|
||||
action = "read"
|
||||
},
|
||||
{
|
||||
subject = "secrets"
|
||||
action = "edit"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# New V2 configuration
|
||||
resource "infisical_project_role" "example" {
|
||||
name = "example"
|
||||
permissions_v2 = [
|
||||
# Original secrets permission
|
||||
{
|
||||
subject = "secrets"
|
||||
action = ["read", "edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add equivalent secret-imports permission
|
||||
{
|
||||
subject = "secret-imports"
|
||||
action = ["read", "edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add secret-folders permission (without read)
|
||||
{
|
||||
subject = "secret-folders"
|
||||
action = ["edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add dynamic-secrets permission with mapped actions
|
||||
{
|
||||
subject = "dynamic-secrets"
|
||||
action = ["read-root-credential", "edit-root-credential", "lease"]
|
||||
inverted = false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note: When moving to V2 permissions, make sure to include all the necessary expanded permissions based on your original `secrets` permissions.
|
118
docs/internals/permissions/migration.mdx
Normal file
118
docs/internals/permissions/migration.mdx
Normal file
@ -0,0 +1,118 @@
|
||||
---
|
||||
title: "Migration Guide"
|
||||
description: "Guide for migrating permissions in Infisical"
|
||||
---
|
||||
|
||||
# Migrating from Permission V1 to Permission V2
|
||||
|
||||
This guide provides instructions for upgrading from the legacy V1 permissions system to the more powerful V2 permissions system in Infisical.
|
||||
|
||||
## Why Upgrade to V2?
|
||||
|
||||
The V2 permissions system offers several advantages over V1:
|
||||
|
||||
- **More granular control**: Separate permissions for different secret-related resources
|
||||
- **Explicit deny rules**: Support for permission inversion
|
||||
- **Conditional permissions**: Apply permissions based on specific criteria
|
||||
- **Array-based actions**: Cleaner syntax for multiple actions
|
||||
|
||||
## Migration Steps
|
||||
|
||||
When upgrading to V2 permissions (i.e., when moving from using the `permissions` to `permissions_v2` field in your Terraform configurations, or upgrading to the V2 permission API), you'll need to update your permission structure as follows:
|
||||
|
||||
### 1. Expand Secret Permissions
|
||||
|
||||
Any permissions for `secrets` should be expanded to include equivalent permissions for:
|
||||
|
||||
- `secret-imports`
|
||||
- `secret-folders` (except for read permissions)
|
||||
- `dynamic-secrets`
|
||||
|
||||
### 2. Map Dynamic Secret Actions
|
||||
|
||||
For dynamic secrets, the actions need to be mapped differently:
|
||||
|
||||
| V1 Action | V2 Action |
|
||||
| --------- | ----------------------------------------------------- |
|
||||
| `read` | `read-root-credential` |
|
||||
| `create` | `create-root-credential` |
|
||||
| `edit` | `edit-root-credential` (also adds `lease` permission) |
|
||||
| `delete` | `delete-root-credential` |
|
||||
|
||||
### 3. Update Configuration Format
|
||||
|
||||
V2 permissions use a different syntax, with actions stored in arrays and an optional `inverted` flag:
|
||||
|
||||
```typescript
|
||||
// V1 format (single action)
|
||||
{
|
||||
subject: "secrets",
|
||||
action: "read"
|
||||
}
|
||||
|
||||
// V2 format (array of actions)
|
||||
{
|
||||
subject: "secrets",
|
||||
action: ["read"],
|
||||
inverted: false // Optional, defaults to false
|
||||
}
|
||||
```
|
||||
|
||||
## Example Migration
|
||||
|
||||
Here's a complete example showing how to migrate a role from V1 to V2:
|
||||
|
||||
```hcl
|
||||
# Old V1 configuration
|
||||
resource "infisical_project_role" "example" {
|
||||
name = "example"
|
||||
permissions = [
|
||||
{
|
||||
subject = "secrets"
|
||||
action = "read"
|
||||
},
|
||||
{
|
||||
subject = "secrets"
|
||||
action = "edit"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# New V2 configuration
|
||||
resource "infisical_project_role" "example" {
|
||||
name = "example"
|
||||
permissions_v2 = [
|
||||
# Original secrets permission
|
||||
{
|
||||
subject = "secrets"
|
||||
action = ["read", "edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add equivalent secret-imports permission
|
||||
{
|
||||
subject = "secret-imports"
|
||||
action = ["read", "edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add secret-folders permission (without read)
|
||||
{
|
||||
subject = "secret-folders"
|
||||
action = ["edit"]
|
||||
inverted = false
|
||||
},
|
||||
# Add dynamic-secrets permission with mapped actions
|
||||
{
|
||||
subject = "dynamic-secrets"
|
||||
action = ["read-root-credential", "edit-root-credential", "lease"]
|
||||
inverted = false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Important Considerations
|
||||
|
||||
- When moving to V2 permissions, make sure to include all the necessary expanded permissions based on your original `secrets` permissions.
|
||||
- V2 permissions give you the ability to use conditions and inversion, which are not available in V1.
|
||||
- During migration, review your existing roles and consider if more granular permissions would better fit your security requirements.
|
||||
- Test your migrated permissions thoroughly in a non-production environment before deploying to production.
|
220
docs/internals/permissions/organization-permissions.mdx
Normal file
220
docs/internals/permissions/organization-permissions.mdx
Normal file
@ -0,0 +1,220 @@
|
||||
---
|
||||
title: "Organization Permissions"
|
||||
description: "Comprehensive guide to Infisical's organization-level permissions"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Infisical's organization permissions system follows a role-based access control (RBAC) model built on a subject-action-object framework. At the organization level, these permissions determine what actions users/machines can perform on various resources across the entire organization.
|
||||
|
||||
Each permission consists of:
|
||||
|
||||
- **Subject**: The resource the permission applies to (e.g., workspaces, members, billing)
|
||||
- **Action**: The operation that can be performed (e.g., read, create, edit, delete)
|
||||
|
||||
Some organization-level resources—specifically `app-connections`—support conditional permissions and permission inversion for more granular access control.
|
||||
|
||||
## Available Organization Permissions
|
||||
|
||||
Below is a comprehensive list of all available organization-level subjects and their supported actions, organized by functional area.
|
||||
|
||||
### Workspace Management
|
||||
|
||||
#### Subject: `workspace`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | --------------------- |
|
||||
| `create` | Create new workspaces |
|
||||
|
||||
### Role Management
|
||||
|
||||
#### Subject: `role`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ------------------------------------------------------ |
|
||||
| `read` | View organization roles and their assigned permissions |
|
||||
| `create` | Create new organization roles |
|
||||
| `edit` | Modify existing organization roles |
|
||||
| `delete` | Remove organization roles |
|
||||
|
||||
### User Management
|
||||
|
||||
#### Subject: `member`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ------------------------------------ |
|
||||
| `read` | View organization members |
|
||||
| `create` | Add new members to the organization |
|
||||
| `edit` | Modify member details |
|
||||
| `delete` | Remove members from the organization |
|
||||
|
||||
#### Subject: `groups`
|
||||
|
||||
| Action | Description |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| `read` | View organization groups |
|
||||
| `create` | Create new groups in the organization |
|
||||
| `edit` | Modify existing groups |
|
||||
| `delete` | Remove groups from the organization |
|
||||
| `grant-privileges` | Change permission levels for organization groups |
|
||||
| `add-members` | Add members to groups |
|
||||
| `remove-members` | Remove members from groups |
|
||||
|
||||
#### Subject: `identity`
|
||||
|
||||
| Action | Description |
|
||||
| ------------------ | --------------------------------------------------- |
|
||||
| `read` | View organization identities |
|
||||
| `create` | Add new identities to organization |
|
||||
| `edit` | Modify organization identities |
|
||||
| `delete` | Remove identities from organization |
|
||||
| `grant-privileges` | Change permission levels of organization identities |
|
||||
| `revoke-auth` | Revoke authentication for identities |
|
||||
| `create-token` | Create new authentication tokens |
|
||||
| `delete-token` | Delete authentication tokens |
|
||||
| `get-token` | Retrieve authentication tokens |
|
||||
|
||||
### Security & Compliance
|
||||
|
||||
#### Subject: `secret-scanning`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ----------------------------------------- |
|
||||
| `read` | View secret scanning results and settings |
|
||||
| `create` | Configure secret scanning |
|
||||
| `edit` | Modify secret scanning settings |
|
||||
| `delete` | Remove secret scanning configuration |
|
||||
|
||||
#### Subject: `settings`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ----------------------------------------- |
|
||||
| `read` | View organization settings |
|
||||
| `create` | Setup and configure organization settings |
|
||||
| `edit` | Modify organization settings |
|
||||
| `delete` | Remove organization settings |
|
||||
|
||||
#### Subject: `incident-contact`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | -------------------------------- |
|
||||
| `read` | View incident contacts |
|
||||
| `create` | Set up new incident contacts |
|
||||
| `edit` | Modify incident contact settings |
|
||||
| `delete` | Remove incident contacts |
|
||||
|
||||
#### Subject: `audit-logs`
|
||||
|
||||
| Action | Description |
|
||||
| ------ | ---------------------------- |
|
||||
| `read` | View organization audit logs |
|
||||
|
||||
### Identity Provider Integration
|
||||
|
||||
#### Subject: `sso`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ---------------------------------- |
|
||||
| `read` | View Single Sign-On configurations |
|
||||
| `create` | Set up new SSO integrations |
|
||||
| `edit` | Modify existing SSO settings |
|
||||
| `delete` | Remove SSO configurations |
|
||||
|
||||
#### Subject: `scim`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ----------------------------- |
|
||||
| `read` | View SCIM configurations |
|
||||
| `create` | Set up new SCIM provisioning |
|
||||
| `edit` | Modify existing SCIM settings |
|
||||
| `delete` | Remove SCIM configurations |
|
||||
|
||||
#### Subject: `ldap`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ----------------------------- |
|
||||
| `read` | View LDAP configurations |
|
||||
| `create` | Set up new LDAP integrations |
|
||||
| `edit` | Modify existing LDAP settings |
|
||||
| `delete` | Remove LDAP configurations |
|
||||
|
||||
### Billing & Subscriptions
|
||||
|
||||
#### Subject: `billing`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ------------------------------------------------ |
|
||||
| `read` | View billing information and subscription status |
|
||||
| `create` | Set up new payment methods or subscriptions |
|
||||
| `edit` | Modify billing details or subscription plans |
|
||||
| `delete` | Remove payment methods or cancel subscriptions |
|
||||
|
||||
### Templates & Automation
|
||||
|
||||
#### Subject: `project-templates`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | --------------------------------- |
|
||||
| `read` | View project templates |
|
||||
| `create` | Create new project templates |
|
||||
| `edit` | Modify existing project templates |
|
||||
| `delete` | Remove project templates |
|
||||
|
||||
### Integrations
|
||||
|
||||
#### Subject: `app-connections`
|
||||
|
||||
Supports conditions and permission inversion
|
||||
|
||||
| Action | Description |
|
||||
| --------- | ---------------------------------- |
|
||||
| `read` | View app connection configurations |
|
||||
| `create` | Create new app connections |
|
||||
| `edit` | Modify existing app connections |
|
||||
| `delete` | Remove app connections |
|
||||
| `connect` | Use app connections |
|
||||
|
||||
### Key Management
|
||||
|
||||
#### Subject: `kms`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ------------------------------------ |
|
||||
| `read` | View organization KMS configurations |
|
||||
| `create` | Set up new KMS configurations |
|
||||
| `edit` | Modify KMS settings |
|
||||
| `delete` | Remove KMS configurations |
|
||||
|
||||
#### Subject: `kmip`
|
||||
|
||||
| Action | Description |
|
||||
| ------- | ---------------------------------- |
|
||||
| `setup` | Configure KMIP server settings |
|
||||
| `proxy` | Act as a proxy for KMIP operations |
|
||||
|
||||
### Admin Tools
|
||||
|
||||
#### Subject: `organization-admin-console`
|
||||
|
||||
| Action | Description |
|
||||
| --------------------- | ------------------------------------------- |
|
||||
| `access-all-projects` | Access all projects within the organization |
|
||||
|
||||
### Secure Share
|
||||
|
||||
#### Subject: `secret-share`
|
||||
|
||||
| Action | Description |
|
||||
| ----------------- | ---------------------------- |
|
||||
| `manage-settings` | Manage secret share settings |
|
||||
|
||||
### Gateway Management
|
||||
|
||||
#### Subject: `gateway`
|
||||
|
||||
| Action | Description |
|
||||
| ----------------- | --------------------------------- |
|
||||
| `list-gateways` | View all organization gateways |
|
||||
| `create-gateways` | Add new gateways to organization |
|
||||
| `edit-gateways` | Modify existing gateway settings |
|
||||
| `delete-gateways` | Remove gateways from organization |
|
89
docs/internals/permissions/overview.mdx
Normal file
89
docs/internals/permissions/overview.mdx
Normal file
@ -0,0 +1,89 @@
|
||||
---
|
||||
title: "Overview"
|
||||
description: "Infisical's permissions system provides granular access control."
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Infisical permissions system is based on a role-based access control (RBAC) model. The system allows you to define roles and assign them to users and machines. Each role has a set of permissions that define what actions a user can perform.
|
||||
|
||||
Permissions are built on a subject-action-object model. The subject is the resource the permission is being applied to, the action is what the permission allows.
|
||||
An example of a subject/action combination would be `secrets/read`. This permission allows the subject to read secrets.
|
||||
|
||||
## Permission Scope Levels
|
||||
|
||||
Infisical's permission system operates at two distinct levels, providing comprehensive and flexible access control across your entire security infrastructure:
|
||||
|
||||
### Project Permissions
|
||||
|
||||
Project permissions control access to resources within a specific project, including secrets management, PKI, KMS, and SSH certificate functionality.
|
||||
|
||||
For a comprehensive list of all project-level subjects, actions, and detailed descriptions, please refer to the [Project Permissions](/internals/permissions/project-permissions) documentation.
|
||||
|
||||
### Organization Permissions
|
||||
|
||||
Organization permissions control access to organization-wide resources and settings such as workspaces, billing, identity providers, and more.
|
||||
|
||||
For a comprehensive list of all organization-level subjects, actions, and detailed descriptions, please refer to the [Organization Permissions](/internals/permissions/organization-permissions) documentation.
|
||||
|
||||
## Inversion
|
||||
|
||||
Permission inversion allows you to explicitly deny actions instead of allowing them. This is supported for the following subjects:
|
||||
|
||||
- secrets
|
||||
- secret-folders
|
||||
- secret-imports
|
||||
- dynamic-secrets
|
||||
|
||||
When a permission is inverted, it changes from an "allow" rule to a "deny" rule. For example:
|
||||
|
||||
```typescript
|
||||
// Regular permission - allows reading secrets
|
||||
{
|
||||
subject: "secrets",
|
||||
action: ["read"]
|
||||
}
|
||||
|
||||
// Inverted permission - denies reading secrets
|
||||
{
|
||||
subject: "secrets",
|
||||
action: ["read"],
|
||||
inverted: true
|
||||
}
|
||||
```
|
||||
|
||||
**Important:** The order of permissions matters when using inversion. For inverted (deny) permissions to be effective, there
|
||||
typically needs to be a corresponding allow permission somewhere in the chain. Permissions are evaluated in sequence,
|
||||
so the relative positioning of allow and deny rules determines the final access outcome.
|
||||
|
||||
## Conditions
|
||||
|
||||
Conditions allow you to create more granular permissions by specifying criteria that must be met for the permission to apply. This is supported for the following subjects:
|
||||
|
||||
- secrets
|
||||
- secret-folders
|
||||
- secret-imports
|
||||
- dynamic-secrets
|
||||
|
||||
### Properties
|
||||
|
||||
Conditions can be applied to the following properties:
|
||||
|
||||
- `environment`: Control access based on environment slugs
|
||||
- `secretPath`: Control access based on secret paths
|
||||
- `secretName`: Control access based on secret names
|
||||
- `secretTags`: Control access based on tags (only supports $in operator)
|
||||
|
||||
### Operators
|
||||
|
||||
The following operators are available for conditions:
|
||||
|
||||
| Operator | Description | Example |
|
||||
| -------- | ---------------------------------- | ----------------------------------------------------- |
|
||||
| `$eq` | Equal | `{ environment: { $eq: "production" } }` |
|
||||
| `$ne` | Not equal | `{ environment: { $ne: "development" } }` |
|
||||
| `$in` | Matches any value in array | `{ environment: { $in: ["staging", "production"] } }` |
|
||||
| `$glob` | Pattern matching using glob syntax | `{ secretPath: { $glob: "/app/\*" } }` |
|
||||
|
||||
These details are especially useful if you're using the API to [create new project roles](../api-reference/endpoints/project-roles/create).
|
||||
The rules outlined on this page, also apply when using our Terraform Provider to manage your Infisical project roles, or any other of our clients that manage project roles.
|
312
docs/internals/permissions/project-permissions.mdx
Normal file
312
docs/internals/permissions/project-permissions.mdx
Normal file
@ -0,0 +1,312 @@
|
||||
---
|
||||
title: "Project Permissions"
|
||||
description: "Comprehensive guide to Infisical's project-level permissions"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Infisical's project permissions system follows a role-based access control (RBAC) model built on a subject-action-object framework. At the project level, these permissions determine what actions users/machines can perform on various resources within a specific project.
|
||||
|
||||
Each permission consists of:
|
||||
|
||||
- **Subject**: The resource the permission applies to (e.g., secrets, members, settings)
|
||||
- **Action**: The operation that can be performed (e.g., read, create, edit, delete)
|
||||
|
||||
Some project-level resources—specifically `secrets`, `secret-folders`, `secret-imports`, and `dynamic-secrets`—support conditional permissions and permission inversion for more granular access control. Conditions allow you to specify criteria (like environment, secret path, or tags) that must be met for the permission to apply.
|
||||
|
||||
## Available Project Permissions
|
||||
|
||||
Below is a comprehensive list of all available project-level subjects and their supported actions.
|
||||
|
||||
### Core Platform & Access Control
|
||||
|
||||
#### Subject: `role`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ------------------------------------------------- |
|
||||
| `read` | View project roles and their assigned permissions |
|
||||
| `create` | Create new project roles |
|
||||
| `edit` | Modify existing project roles |
|
||||
| `delete` | Remove project roles |
|
||||
|
||||
#### Subject: `member`
|
||||
|
||||
| Action | Description |
|
||||
| ------------------ | ------------------------------------------- |
|
||||
| `read` | View project members |
|
||||
| `create` | Add new members to the project |
|
||||
| `edit` | Modify member details |
|
||||
| `delete` | Remove members from the project |
|
||||
| `grant-privileges` | Change permission levels of project members |
|
||||
|
||||
#### Subject: `groups`
|
||||
|
||||
| Action | Description |
|
||||
| ------------------ | ------------------------------------------ |
|
||||
| `read` | View project groups |
|
||||
| `create` | Create new groups within the project |
|
||||
| `edit` | Modify existing groups |
|
||||
| `delete` | Remove groups from the project |
|
||||
| `grant-privileges` | Change permission levels of project groups |
|
||||
|
||||
#### Subject: `identity`
|
||||
|
||||
| Action | Description |
|
||||
| ------------------ | ---------------------------------------------- |
|
||||
| `read` | View project identities |
|
||||
| `create` | Add new identities to project |
|
||||
| `edit` | Modify project identities |
|
||||
| `delete` | Remove identities from project |
|
||||
| `grant-privileges` | Change permission levels of project identities |
|
||||
|
||||
#### Subject: `settings`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | -------------------------------------- |
|
||||
| `read` | View project settings |
|
||||
| `create` | Add new project configuration settings |
|
||||
| `edit` | Modify project settings |
|
||||
| `delete` | Remove project settings |
|
||||
|
||||
#### Subject: `environments`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ------------------------------------ |
|
||||
| `read` | View project environments |
|
||||
| `create` | Add new environments to the project |
|
||||
| `edit` | Modify existing environments |
|
||||
| `delete` | Remove environments from the project |
|
||||
|
||||
#### Subject: `tags`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ---------------------------------------- |
|
||||
| `read` | View project tags |
|
||||
| `create` | Create new tags for organizing resources |
|
||||
| `edit` | Modify existing tags |
|
||||
| `delete` | Remove tags from the project |
|
||||
|
||||
#### Subject: `workspace`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ------------------------- |
|
||||
| `edit` | Modify workspace settings |
|
||||
| `delete` | Delete the workspace |
|
||||
|
||||
#### Subject: `ip-allowlist`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | -------------------------------------------- |
|
||||
| `read` | View IP allowlists |
|
||||
| `create` | Add new IP addresses or ranges to allowlists |
|
||||
| `edit` | Modify existing IP allowlist entries |
|
||||
| `delete` | Remove IP addresses from allowlists |
|
||||
|
||||
#### Subject: `audit-logs`
|
||||
|
||||
| Action | Description |
|
||||
| ------ | ------------------------------------------------------- |
|
||||
| `read` | View audit logs of actions performed within the project |
|
||||
|
||||
#### Subject: `integrations`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | -------------------------------- |
|
||||
| `read` | View configured integrations |
|
||||
| `create` | Add new third-party integrations |
|
||||
| `edit` | Modify integration settings |
|
||||
| `delete` | Remove integrations |
|
||||
|
||||
#### Subject: `webhooks`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ------------------------------------ |
|
||||
| `read` | View webhook configurations |
|
||||
| `create` | Add new webhooks |
|
||||
| `edit` | Modify webhook endpoints or triggers |
|
||||
| `delete` | Remove webhooks |
|
||||
|
||||
#### Subject: `service-tokens`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ---------------------------------------- |
|
||||
| `read` | View service tokens |
|
||||
| `create` | Create new service tokens for API access |
|
||||
| `edit` | Modify token properties |
|
||||
| `delete` | Revoke or remove service tokens |
|
||||
|
||||
### Secrets Management
|
||||
|
||||
#### Subject: `secrets`
|
||||
|
||||
Supports conditions and permission inversion
|
||||
| Action | Description | Notes |
|
||||
| -------- | ------------------------------- | ----- |
|
||||
| `read` | View secrets and their values | This action is the equivalent of granting both `describeSecret` and `readValue`. The `read` action is considered **legacy**. You should use the `describeSecret` and/or `readValue` actions instead. |
|
||||
| `describeSecret` | View secret details such as key, path, metadata, tags, and more | If you are using the API, you can pass `viewSecretValue: false` to the API call to retrieve secrets without their values. |
|
||||
| `readValue` | View the value of a secret.| In order to read secret values, the `describeSecret` action must also be granted. |
|
||||
| `create` | Add new secrets to the project | |
|
||||
| `edit` | Modify existing secret values | |
|
||||
| `delete` | Remove secrets from the project | |
|
||||
|
||||
#### Subject: `secret-folders`
|
||||
|
||||
Supports conditions and permission inversion
|
||||
| Action | Description |
|
||||
| -------- | ------------------------ |
|
||||
| `read` | View secret folders |
|
||||
| `create` | Create new folders |
|
||||
| `edit` | Modify folder properties |
|
||||
| `delete` | Remove secret folders |
|
||||
|
||||
#### Subject: `secret-imports`
|
||||
|
||||
Supports conditions and permission inversion
|
||||
| Action | Description |
|
||||
| -------- | --------------------- |
|
||||
| `read` | View secret imports |
|
||||
| `create` | Create secret imports |
|
||||
| `edit` | Modify secret imports |
|
||||
| `delete` | Remove secret imports |
|
||||
|
||||
#### Subject: `secret-rollback`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ---------------------------------- |
|
||||
| `read` | View secret versions and snapshots |
|
||||
| `create` | Roll back secrets to snapshots |
|
||||
|
||||
#### Subject: `secret-approval`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ----------------------------------- |
|
||||
| `read` | View approval policies and requests |
|
||||
| `create` | Create new approval policies |
|
||||
| `edit` | Modify approval policies |
|
||||
| `delete` | Remove approval policies |
|
||||
|
||||
#### Subject: `secret-rotation`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ------------------------------------- |
|
||||
| `read` | View secret rotation policies |
|
||||
| `create` | Set up automatic secret rotation |
|
||||
| `edit` | Modify rotation schedules or policies |
|
||||
| `delete` | Remove rotation policies |
|
||||
|
||||
#### Subject: `secret-syncs`
|
||||
|
||||
| Action | Description |
|
||||
| ---------------- | -------------------------------------------------- |
|
||||
| `read` | View secret synchronization configurations |
|
||||
| `create` | Create new sync configurations |
|
||||
| `edit` | Modify existing sync settings |
|
||||
| `delete` | Remove sync configurations |
|
||||
| `sync-secrets` | Execute synchronization of secrets between systems |
|
||||
| `import-secrets` | Import secrets from sync sources |
|
||||
| `remove-secrets` | Remove secrets from sync destinations |
|
||||
|
||||
#### Subject: `dynamic-secrets`
|
||||
|
||||
Supports conditions and permission inversion
|
||||
| Action | Description |
|
||||
| ------------------------ | ---------------------------------- |
|
||||
| `read-root-credential` | View dynamic secret configurations |
|
||||
| `create-root-credential` | Create dynamic secrets |
|
||||
| `edit-root-credential` | Edit dynamic secrets |
|
||||
| `delete-root-credential` | Remove dynamic secrets |
|
||||
| `lease` | Create dynamic secret leases |
|
||||
|
||||
### Key Management Service (KMS)
|
||||
|
||||
#### Subject: `kms`
|
||||
|
||||
| Action | Description |
|
||||
| ------ | --------------------------- |
|
||||
| `edit` | Modify project KMS settings |
|
||||
|
||||
#### Subject: `cmek`
|
||||
|
||||
| Action | Description |
|
||||
| --------- | ------------------------------------- |
|
||||
| `read` | View Customer-Managed Encryption Keys |
|
||||
| `create` | Add new encryption keys |
|
||||
| `edit` | Modify key properties |
|
||||
| `delete` | Remove encryption keys |
|
||||
| `encrypt` | Use keys for encryption operations |
|
||||
| `decrypt` | Use keys for decryption operations |
|
||||
|
||||
### Public Key Infrastructure (PKI)
|
||||
|
||||
#### Subject: `certificate-authorities`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ---------------------------------- |
|
||||
| `read` | View certificate authorities |
|
||||
| `create` | Create new certificate authorities |
|
||||
| `edit` | Modify CA configurations |
|
||||
| `delete` | Remove certificate authorities |
|
||||
|
||||
#### Subject: `certificates`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ----------------------------- |
|
||||
| `read` | View certificates |
|
||||
| `create` | Issue new certificates |
|
||||
| `delete` | Revoke or remove certificates |
|
||||
|
||||
#### Subject: `certificate-templates`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | -------------------------------- |
|
||||
| `read` | View certificate templates |
|
||||
| `create` | Create new certificate templates |
|
||||
| `edit` | Modify template configurations |
|
||||
| `delete` | Remove certificate templates |
|
||||
|
||||
#### Subject: `pki-alerts`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ------------------------------------------------------------ |
|
||||
| `read` | View PKI alert configurations |
|
||||
| `create` | Create new alerts for certificate expiry or other PKI events |
|
||||
| `edit` | Modify alert settings |
|
||||
| `delete` | Remove PKI alerts |
|
||||
|
||||
#### Subject: `pki-collections`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | --------------------------------------------------- |
|
||||
| `read` | View PKI resource collections |
|
||||
| `create` | Create new collections for organizing PKI resources |
|
||||
| `edit` | Modify collection properties |
|
||||
| `delete` | Remove PKI collections |
|
||||
|
||||
### SSH Certificate Management
|
||||
|
||||
#### Subject: `ssh-certificate-authorities`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | -------------------------------------- |
|
||||
| `read` | View SSH certificate authorities |
|
||||
| `create` | Create new SSH certificate authorities |
|
||||
| `edit` | Modify SSH CA configurations |
|
||||
| `delete` | Remove SSH certificate authorities |
|
||||
|
||||
#### Subject: `ssh-certificates`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | --------------------------------- |
|
||||
| `read` | View SSH certificates |
|
||||
| `create` | Issue new SSH certificates |
|
||||
| `edit` | Modify SSH certificate properties |
|
||||
| `delete` | Revoke or remove SSH certificates |
|
||||
|
||||
#### Subject: `ssh-certificate-templates`
|
||||
|
||||
| Action | Description |
|
||||
| -------- | ------------------------------------ |
|
||||
| `read` | View SSH certificate templates |
|
||||
| `create` | Create new SSH certificate templates |
|
||||
| `edit` | Modify SSH template configurations |
|
||||
| `delete` | Remove SSH certificate templates |
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user