Compare commits

..

66 Commits

Author SHA1 Message Date
Maidul Islam
428c60880a Update jumpcloud.mdx 2025-05-09 00:28:20 -04:00
Maidul Islam
2179b9a4d7 Update general.mdx 2025-05-09 00:27:43 -04:00
Daniel Hougaard
8dfc0cfbe0 Merge pull request #3571 from Infisical/daniel/identities-ldap-docs
docs(identities): ldap auth
2025-05-09 04:15:11 +04:00
Daniel Hougaard
394bd6755f Merge pull request #3566 from Infisical/daniel/identity-ldap-auth
feat(identities): ldap auth
2025-05-08 23:53:47 +04:00
x032205
04a8931cf6 Merge pull request #3568 from Infisical/pki-merge-fix
small migration fix
2025-05-08 01:23:36 -04:00
x032205
ab0b8c0f10 migration tweak 2025-05-08 01:22:34 -04:00
x032205
258836a605 migration tweak 2025-05-08 01:17:47 -04:00
Maidul Islam
4d0275e589 Merge pull request #3565 from Infisical/remove-migration-folder
Remove unused migration folder
2025-05-07 20:53:51 -04:00
Maidul Islam
6ca7a990f3 unused folder remove 2025-05-07 20:34:01 -04:00
Scott Wilson
befd77eec2 Merge pull request #3563 from Infisical/policy-selection-modal
improvement(project-roles): Add Policy Selection Modal
2025-05-07 16:49:05 -07:00
Daniel Hougaard
1d44774913 Merge pull request #3564 from Infisical/daniel/generator-doc-imp
docs(k8s/generators): improve documentation
2025-05-08 03:20:30 +04:00
Maidul Islam
984552eea9 rephrase generator overview 2025-05-07 19:18:45 -04:00
Scott Wilson
b6a957a30d fix: select all apply to filtered policies only, skip replacing existing policies 2025-05-07 15:34:34 -07:00
Daniel Hougaard
2f4efad8ae Update infisical-push-secret-crd.mdx 2025-05-08 01:47:00 +04:00
Scott Wilson
16c476d78c fix: correct policies typos 2025-05-07 14:09:32 -07:00
Scott Wilson
68c549f1c6 improvement: add select polices modal 2025-05-07 13:50:27 -07:00
Scott Wilson
0610416677 Merge pull request #3550 from Infisical/project-specific-default-roles
Improvements: Refactor Project Templates and Project Type Policy Filtering/Specific Roles
2025-05-07 12:50:01 -07:00
Daniel Hougaard
4a37dc9cb7 Merge pull request #3561 from Infisical/helm-update-v0.9.2
Update Helm chart to version v0.9.2
2025-05-07 22:37:58 +04:00
DanielHougaard
7e432a4297 Update Helm chart to version v0.9.2 2025-05-07 18:27:13 +00:00
Scott Wilson
794fc9c2a2 improvements: address feedback 2025-05-07 11:23:51 -07:00
Daniel Hougaard
d4e5d2c7ed Merge pull request #3540 from Infisical/daniel/generators
feat(k8s): generator support
2025-05-07 22:10:22 +04:00
Sheen
0c2e0bb0f9 Merge pull request #3560 from Infisical/misc/add-default-old-space-config
misc: add default old space config
2025-05-08 01:46:46 +08:00
Sheen Capadngan
e2a414ffff misc: add default old space config 2025-05-08 01:39:56 +08:00
=
0ca3c2bb68 feat: added password generator crd to samples 2025-05-07 22:50:49 +05:30
Daniel Hougaard
083581b51a Merge pull request #3554 from Infisical/feat/new-project-properties-for-tf-management
feat: adjustments to properties and validation
2025-05-07 20:22:23 +04:00
x032205
40e976133c Merge pull request #3528 from Infisical/ENG-2647
feat(admin): Invalidate Cache
2025-05-07 11:50:57 -04:00
x032205
ad2f002822 Merge pull request #3558 from Infisical/pki-docs-patch
docs fix
2025-05-07 11:06:24 -04:00
x032205
8842dfe5d1 docs fix 2025-05-07 11:01:19 -04:00
Sheen
b1eea4ae9c Merge pull request #3556 from Infisical/misc/remove-unnecessary-key-encryption-for-service-token
misc: removed unnecessary key encryption for service token
2025-05-07 16:41:51 +08:00
Sheen Capadngan
a8e0a8aca3 misc: removed unnecessary key encryption for service token 2025-05-07 16:36:10 +08:00
=
b37058d0e2 feat: switched to is fetching 2025-05-07 11:30:31 +05:30
x032205
334a05d5f1 fix lint 2025-05-06 18:08:08 -04:00
x032205
12c813928c fix polling 2025-05-06 18:00:24 -04:00
x032205
521fef6fca Merge branch 'main' into ENG-2647 2025-05-06 17:00:40 -04:00
=
8f8236c445 feat: simplied the caching panel logic and fixed permission issue 2025-05-07 01:37:26 +05:30
x032205
3cf5c534ff Merge pull request #3553 from Infisical/pki-docs-patch
patch(docs): mint.json update
2025-05-06 15:54:31 -04:00
Sheen Capadngan
2b03c295f9 feat: adjustments to properties and validation 2025-05-07 03:51:22 +08:00
x032205
4fc7a52941 patch(docs): mint.json update 2025-05-06 15:38:10 -04:00
Scott Wilson
0ded2e51ba fix: filter project templates polices by type 2025-05-06 11:59:59 -07:00
Maidul Islam
0d2b3adec7 Merge pull request #3551 from Infisical/maidul98-patch-11
Add Conduct and Enforcement to bug bounty
2025-05-06 14:50:17 -04:00
Maidul Islam
e695203c05 Update docs/internals/bug-bounty.mdx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-06 14:49:38 -04:00
Maidul Islam
f9d76aae5d Update bug-bounty.mdx 2025-05-06 14:46:42 -04:00
Daniel Hougaard
1c280759d1 Merge pull request #3548 from Infisical/daniel/self-hosted-secret-scanning
docs: secret scanning self hosted documentation
2025-05-06 22:27:00 +04:00
Scott Wilson
4562f57b54 improvements: refactor project templates, filter policies by project type, project type specific roles 2025-05-06 11:26:09 -07:00
x032205
86bb2659b5 small ui tweaks 2025-05-05 16:07:04 -04:00
x032205
dc59f226b6 swapped polling to react query 2025-05-05 15:58:45 -04:00
x032205
9175c1dffa Merge branch 'main' into ENG-2647 2025-05-05 15:27:25 -04:00
Daniel Hougaard
1e4dfd0c7c fix(k8s/generators): update base crds 2025-05-05 02:35:57 +04:00
Daniel Hougaard
34b7d28e2f requested changes 2025-05-05 02:30:59 +04:00
Daniel Hougaard
245a348517 Update generators.go 2025-05-05 02:13:12 +04:00
Daniel Hougaard
e0fc582e2e docs(k8s/generators): docs and minor fix 2025-05-05 02:09:21 +04:00
Daniel Hougaard
68ef897b6a fix: logs and rbac 2025-05-05 01:39:30 +04:00
Daniel Hougaard
1b060e76de Update kustomization.yaml 2025-05-05 01:08:22 +04:00
Daniel Hougaard
9f7599b2a1 feat(k8s): generators 2025-05-05 00:59:11 +04:00
x
9cbe70a6f3 lint fixes 2025-05-02 20:10:30 -04:00
x
f49fb534ab review fixes 2025-05-02 19:50:55 -04:00
x
6eea4c8364 frontend tweaks 2025-05-02 19:20:02 -04:00
x
1e206ee441 Merge branch 'main' into ENG-2647 2025-05-02 19:03:08 -04:00
x
85c1a1081e checkpoint 2025-05-02 18:43:07 -04:00
x
877485b45a queue job 2025-05-02 15:23:35 -04:00
x
d13e685a81 emphasize that secrets cache is encrypted in frontend 2025-05-02 13:04:22 -04:00
x
9849a5f136 switched to applyJitter functions 2025-05-02 13:00:37 -04:00
x
26773a1444 merge 2025-05-02 12:57:28 -04:00
x
a6f280197b spelling fix 2025-05-01 17:37:54 -04:00
x
346d2f213e improvements + review fixes 2025-05-01 17:33:24 -04:00
x
9f1ac77afa invalidate cache 2025-05-01 16:34:29 -04:00
150 changed files with 3524 additions and 1965 deletions

View File

@@ -171,6 +171,7 @@ ENV NODE_ENV production
ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
ENV NODE_OPTIONS="--max-old-space-size=1024"
WORKDIR /backend

View File

@@ -168,6 +168,7 @@ ENV HTTPS_ENABLED false
ENV NODE_ENV production
ENV STANDALONE_BUILD true
ENV STANDALONE_MODE true
ENV NODE_OPTIONS="--max-old-space-size=1024"
WORKDIR /backend

View File

@@ -1,4 +1,8 @@
import RE2 from "re2";
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { applyJitter } from "@app/lib/dates";
import { delay as delayMs } from "@app/lib/delay";
import { Lock } from "@app/lib/red-lock";
export const mockKeyStore = (): TKeyStoreFactory => {
@@ -18,6 +22,27 @@ export const mockKeyStore = (): TKeyStoreFactory => {
delete store[key];
return 1;
},
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
let totalDeleted = 0;
const keys = Object.keys(store);
for (let i = 0; i < keys.length; i += batchSize) {
const batch = keys.slice(i, i + batchSize);
for (const key of batch) {
if (regex.test(key)) {
delete store[key];
totalDeleted += 1;
}
}
// eslint-disable-next-line no-await-in-loop
await delayMs(Math.max(0, applyJitter(delay, jitter)));
}
return totalDeleted;
},
getItem: async (key) => {
const value = store[key];
if (typeof value === "string") {

View File

@@ -3,7 +3,7 @@ import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (await knex.schema.hasTable(TableName.CertificateBody)) {
if (!(await knex.schema.hasColumn(TableName.CertificateBody, "encryptedCertificateChain"))) {
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
t.binary("encryptedCertificateChain").nullable();
});
@@ -25,7 +25,7 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTable(TableName.CertificateSecret);
}
if (await knex.schema.hasTable(TableName.CertificateBody)) {
if (await knex.schema.hasColumn(TableName.CertificateBody, "encryptedCertificateChain")) {
await knex.schema.alterTable(TableName.CertificateBody, (t) => {
t.dropColumn("encryptedCertificateChain");
});

View File

@@ -0,0 +1,22 @@
import { Knex } from "knex";
import { ProjectType, TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasColumn(TableName.ProjectTemplates, "type"))) {
await knex.schema.alterTable(TableName.ProjectTemplates, (t) => {
// defaulting to sm for migration to set existing, new ones will always be specified on creation
t.string("type").defaultTo(ProjectType.SecretManager).notNullable();
t.jsonb("environments").nullable().alter();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.ProjectTemplates, "type")) {
await knex.schema.alterTable(TableName.ProjectTemplates, (t) => {
t.dropColumn("type");
// not reverting nullable environments
});
}
}

View File

@@ -186,11 +186,16 @@ export enum OrgMembershipStatus {
}
export enum ProjectMembershipRole {
// general
Admin = "admin",
Member = "member",
Custom = "custom",
Viewer = "viewer",
NoAccess = "no-access"
NoAccess = "no-access",
// ssh
SshHostBootstrapper = "ssh-host-bootstrapper",
// kms
KmsCryptographicOperator = "cryptographic-operator"
}
export enum SecretEncryptionAlgo {

View File

@@ -12,10 +12,11 @@ export const ProjectTemplatesSchema = z.object({
name: z.string(),
description: z.string().nullable().optional(),
roles: z.unknown(),
environments: z.unknown(),
environments: z.unknown().nullable().optional(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
type: z.string().default("secret-manager")
});
export type TProjectTemplates = z.infer<typeof ProjectTemplatesSchema>;

View File

@@ -1,9 +1,8 @@
import { z } from "zod";
import { ProjectMembershipRole, ProjectTemplatesSchema } from "@app/db/schemas";
import { ProjectMembershipRole, ProjectTemplatesSchema, ProjectType } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import { ApiDocsTags, ProjectTemplates } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -35,6 +34,7 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
position: z.number().min(1)
})
.array()
.nullable()
});
const ProjectTemplateRolesSchema = z
@@ -104,6 +104,9 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
hide: false,
tags: [ApiDocsTags.ProjectTemplates],
description: "List project templates for the current organization.",
querystring: z.object({
type: z.nativeEnum(ProjectType).optional().describe(ProjectTemplates.LIST.type)
}),
response: {
200: z.object({
projectTemplates: SanitizedProjectTemplateSchema.array()
@@ -112,7 +115,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission);
const { type } = req.query;
const projectTemplates = await server.services.projectTemplate.listProjectTemplatesByOrg(req.permission, type);
const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name));
@@ -184,6 +188,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
tags: [ApiDocsTags.ProjectTemplates],
description: "Create a project template.",
body: z.object({
type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type),
name: slugSchema({ field: "name" })
.refine((val) => !isInfisicalProjectTemplate(val), {
message: `The requested project template name is reserved.`
@@ -191,9 +196,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
.describe(ProjectTemplates.CREATE.name),
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
ProjectTemplates.CREATE.environments
)
environments: ProjectTemplateEnvironmentsSchema.describe(ProjectTemplates.CREATE.environments).optional()
}),
response: {
200: z.object({

View File

@@ -0,0 +1,448 @@
import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability";
import {
ProjectPermissionActions,
ProjectPermissionCertificateActions,
ProjectPermissionCmekActions,
ProjectPermissionDynamicSecretActions,
ProjectPermissionGroupActions,
ProjectPermissionIdentityActions,
ProjectPermissionKmipActions,
ProjectPermissionMemberActions,
ProjectPermissionSecretActions,
ProjectPermissionSecretRotationActions,
ProjectPermissionSecretSyncActions,
ProjectPermissionSet,
ProjectPermissionSshHostActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
// Admins get full access to everything
[
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.Role,
ProjectPermissionSub.Integrations,
ProjectPermissionSub.Webhooks,
ProjectPermissionSub.ServiceTokens,
ProjectPermissionSub.Settings,
ProjectPermissionSub.Environments,
ProjectPermissionSub.Tags,
ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshCertificateTemplates,
ProjectPermissionSub.SshHostGroups
].forEach((el) => {
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
el
);
});
can(
[
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete,
ProjectPermissionCertificateActions.ReadPrivateKey
],
ProjectPermissionSub.Certificates
);
can(
[
ProjectPermissionSshHostActions.Edit,
ProjectPermissionSshHostActions.Read,
ProjectPermissionSshHostActions.Create,
ProjectPermissionSshHostActions.Delete,
ProjectPermissionSshHostActions.IssueHostCert
],
ProjectPermissionSub.SshHosts
);
can(
[
ProjectPermissionMemberActions.Create,
ProjectPermissionMemberActions.Edit,
ProjectPermissionMemberActions.Delete,
ProjectPermissionMemberActions.Read,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionMemberActions.AssumePrivileges
],
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,
ProjectPermissionIdentityActions.AssumePrivileges
],
ProjectPermissionSub.Identity
);
can(
[
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
ProjectPermissionDynamicSecretActions.EditRootCredential,
ProjectPermissionDynamicSecretActions.CreateRootCredential,
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
ProjectPermissionDynamicSecretActions.Lease
],
ProjectPermissionSub.DynamicSecrets
);
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
can(
[
ProjectPermissionCmekActions.Create,
ProjectPermissionCmekActions.Edit,
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
can(
[
ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSecretSyncActions.RemoveSecrets
],
ProjectPermissionSub.SecretSyncs
);
can(
[
ProjectPermissionKmipActions.CreateClients,
ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionKmipActions.ReadClients,
ProjectPermissionKmipActions.GenerateClientCertificates
],
ProjectPermissionSub.Kmip
);
can(
[
ProjectPermissionSecretRotationActions.Create,
ProjectPermissionSecretRotationActions.Edit,
ProjectPermissionSecretRotationActions.Delete,
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSecretRotationActions.ReadGeneratedCredentials,
ProjectPermissionSecretRotationActions.RotateSecrets
],
ProjectPermissionSub.SecretRotation
);
return rules;
};
const buildMemberPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(
[
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.SecretFolders
);
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
ProjectPermissionDynamicSecretActions.EditRootCredential,
ProjectPermissionDynamicSecretActions.CreateRootCredential,
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
ProjectPermissionDynamicSecretActions.Lease
],
ProjectPermissionSub.DynamicSecrets
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.SecretImports
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member);
can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Integrations
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Webhooks
);
can(
[
ProjectPermissionIdentityActions.Read,
ProjectPermissionIdentityActions.Edit,
ProjectPermissionIdentityActions.Create,
ProjectPermissionIdentityActions.Delete
],
ProjectPermissionSub.Identity
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.ServiceTokens
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Settings
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Environments
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Tags
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
// double check if all CRUD are needed for CA and Certificates
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
can(
[
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete
],
ProjectPermissionSub.Certificates
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
can(
[
ProjectPermissionCmekActions.Create,
ProjectPermissionCmekActions.Edit,
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
can(
[
ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSecretSyncActions.RemoveSecrets
],
ProjectPermissionSub.SecretSyncs
);
return rules;
};
const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
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(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
return rules;
};
const buildNoAccessProjectPermission = () => {
const { rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
return rules;
};
const buildSshHostBootstrapPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(
[ProjectPermissionSshHostActions.Create, ProjectPermissionSshHostActions.IssueHostCert],
ProjectPermissionSub.SshHosts
);
return rules;
};
const buildCryptographicOperatorPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(
[
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
return rules;
};
// General
export const projectAdminPermissions = buildAdminPermissionRules();
export const projectMemberPermissions = buildMemberPermissionRules();
export const projectViewerPermission = buildViewerPermissionRules();
export const projectNoAccessPermissions = buildNoAccessProjectPermission();
// SSH
export const sshHostBootstrapPermissions = buildSshHostBootstrapPermissionRules();
// KMS
export const cryptographicOperatorPermissions = buildCryptographicOperatorPermissionRules();

View File

@@ -12,6 +12,14 @@ import {
TIdentityProjectMemberships,
TProjectMemberships
} from "@app/db/schemas";
import {
cryptographicOperatorPermissions,
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
projectViewerPermission,
sshHostBootstrapPermissions
} from "@app/ee/services/permission/default-roles";
import { conditionsMatcher } from "@app/lib/casl";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { objectify } from "@app/lib/fn";
@@ -32,14 +40,7 @@ import {
TGetServiceTokenProjectPermissionArg,
TGetUserProjectPermissionArg
} from "./permission-service-types";
import {
buildServiceTokenProjectPermission,
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
ProjectPermissionSet,
projectViewerPermission
} from "./project-permission";
import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission";
type TPermissionServiceFactoryDep = {
orgRoleDAL: Pick<TOrgRoleDALFactory, "findOne">;
@@ -95,6 +96,10 @@ export const permissionServiceFactory = ({
return projectViewerPermission;
case ProjectMembershipRole.NoAccess:
return projectNoAccessPermissions;
case ProjectMembershipRole.SshHostBootstrapper:
return sshHostBootstrapPermissions;
case ProjectMembershipRole.KmsCryptographicOperator:
return cryptographicOperatorPermissions;
case ProjectMembershipRole.Custom: {
return unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]

View File

@@ -678,403 +678,6 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
export type TProjectPermissionV2Schema = z.infer<typeof ProjectPermissionV2Schema>;
const buildAdminPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
// Admins get full access to everything
[
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval,
ProjectPermissionSub.Role,
ProjectPermissionSub.Integrations,
ProjectPermissionSub.Webhooks,
ProjectPermissionSub.ServiceTokens,
ProjectPermissionSub.Settings,
ProjectPermissionSub.Environments,
ProjectPermissionSub.Tags,
ProjectPermissionSub.AuditLogs,
ProjectPermissionSub.IpAllowList,
ProjectPermissionSub.CertificateAuthorities,
ProjectPermissionSub.CertificateTemplates,
ProjectPermissionSub.PkiAlerts,
ProjectPermissionSub.PkiCollections,
ProjectPermissionSub.SshCertificateAuthorities,
ProjectPermissionSub.SshCertificates,
ProjectPermissionSub.SshCertificateTemplates,
ProjectPermissionSub.SshHostGroups
].forEach((el) => {
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
el
);
});
can(
[
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete,
ProjectPermissionCertificateActions.ReadPrivateKey
],
ProjectPermissionSub.Certificates
);
can(
[
ProjectPermissionSshHostActions.Edit,
ProjectPermissionSshHostActions.Read,
ProjectPermissionSshHostActions.Create,
ProjectPermissionSshHostActions.Delete,
ProjectPermissionSshHostActions.IssueHostCert
],
ProjectPermissionSub.SshHosts
);
can(
[
ProjectPermissionMemberActions.Create,
ProjectPermissionMemberActions.Edit,
ProjectPermissionMemberActions.Delete,
ProjectPermissionMemberActions.Read,
ProjectPermissionMemberActions.GrantPrivileges,
ProjectPermissionMemberActions.AssumePrivileges
],
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,
ProjectPermissionIdentityActions.AssumePrivileges
],
ProjectPermissionSub.Identity
);
can(
[
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
ProjectPermissionDynamicSecretActions.EditRootCredential,
ProjectPermissionDynamicSecretActions.CreateRootCredential,
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
ProjectPermissionDynamicSecretActions.Lease
],
ProjectPermissionSub.DynamicSecrets
);
can([ProjectPermissionActions.Edit, ProjectPermissionActions.Delete], ProjectPermissionSub.Project);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionActions.Edit], ProjectPermissionSub.Kms);
can(
[
ProjectPermissionCmekActions.Create,
ProjectPermissionCmekActions.Edit,
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
can(
[
ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSecretSyncActions.RemoveSecrets
],
ProjectPermissionSub.SecretSyncs
);
can(
[
ProjectPermissionKmipActions.CreateClients,
ProjectPermissionKmipActions.UpdateClients,
ProjectPermissionKmipActions.DeleteClients,
ProjectPermissionKmipActions.ReadClients,
ProjectPermissionKmipActions.GenerateClientCertificates
],
ProjectPermissionSub.Kmip
);
can(
[
ProjectPermissionSecretRotationActions.Create,
ProjectPermissionSecretRotationActions.Edit,
ProjectPermissionSecretRotationActions.Delete,
ProjectPermissionSecretRotationActions.Read,
ProjectPermissionSecretRotationActions.ReadGeneratedCredentials,
ProjectPermissionSecretRotationActions.RotateSecrets
],
ProjectPermissionSub.SecretRotation
);
return rules;
};
export const projectAdminPermissions = buildAdminPermissionRules();
const buildMemberPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(
[
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.SecretFolders
);
can(
[
ProjectPermissionDynamicSecretActions.ReadRootCredential,
ProjectPermissionDynamicSecretActions.EditRootCredential,
ProjectPermissionDynamicSecretActions.CreateRootCredential,
ProjectPermissionDynamicSecretActions.DeleteRootCredential,
ProjectPermissionDynamicSecretActions.Lease
],
ProjectPermissionSub.DynamicSecrets
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.SecretImports
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SecretApproval);
can([ProjectPermissionSecretRotationActions.Read], ProjectPermissionSub.SecretRotation);
can([ProjectPermissionActions.Read, ProjectPermissionActions.Create], ProjectPermissionSub.SecretRollback);
can([ProjectPermissionMemberActions.Read, ProjectPermissionMemberActions.Create], ProjectPermissionSub.Member);
can([ProjectPermissionGroupActions.Read], ProjectPermissionSub.Groups);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Integrations
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Webhooks
);
can(
[
ProjectPermissionIdentityActions.Read,
ProjectPermissionIdentityActions.Edit,
ProjectPermissionIdentityActions.Create,
ProjectPermissionIdentityActions.Delete
],
ProjectPermissionSub.Identity
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.ServiceTokens
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Settings
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Environments
);
can(
[
ProjectPermissionActions.Read,
ProjectPermissionActions.Edit,
ProjectPermissionActions.Create,
ProjectPermissionActions.Delete
],
ProjectPermissionSub.Tags
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
// double check if all CRUD are needed for CA and Certificates
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateAuthorities);
can(
[
ProjectPermissionCertificateActions.Read,
ProjectPermissionCertificateActions.Edit,
ProjectPermissionCertificateActions.Create,
ProjectPermissionCertificateActions.Delete
],
ProjectPermissionSub.Certificates
);
can([ProjectPermissionActions.Read], ProjectPermissionSub.CertificateTemplates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiAlerts);
can([ProjectPermissionActions.Read], ProjectPermissionSub.PkiCollections);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Create], ProjectPermissionSub.SshCertificates);
can([ProjectPermissionActions.Read], ProjectPermissionSub.SshCertificateTemplates);
can([ProjectPermissionSshHostActions.Read], ProjectPermissionSub.SshHosts);
can(
[
ProjectPermissionCmekActions.Create,
ProjectPermissionCmekActions.Edit,
ProjectPermissionCmekActions.Delete,
ProjectPermissionCmekActions.Read,
ProjectPermissionCmekActions.Encrypt,
ProjectPermissionCmekActions.Decrypt,
ProjectPermissionCmekActions.Sign,
ProjectPermissionCmekActions.Verify
],
ProjectPermissionSub.Cmek
);
can(
[
ProjectPermissionSecretSyncActions.Create,
ProjectPermissionSecretSyncActions.Edit,
ProjectPermissionSecretSyncActions.Delete,
ProjectPermissionSecretSyncActions.Read,
ProjectPermissionSecretSyncActions.SyncSecrets,
ProjectPermissionSecretSyncActions.ImportSecrets,
ProjectPermissionSecretSyncActions.RemoveSecrets
],
ProjectPermissionSub.SecretSyncs
);
return rules;
};
export const projectMemberPermissions = buildMemberPermissionRules();
const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionSecretRotationActions.Read, ProjectPermissionSub.SecretRotation);
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(ProjectPermissionIdentityActions.Read, ProjectPermissionSub.Identity);
can(ProjectPermissionActions.Read, ProjectPermissionSub.ServiceTokens);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
can(ProjectPermissionCmekActions.Read, ProjectPermissionSub.Cmek);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificates);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SshCertificateTemplates);
can(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs);
return rules;
};
export const projectViewerPermission = buildViewerPermissionRules();
const buildNoAccessProjectPermission = () => {
const { rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
return rules;
};
export const buildServiceTokenProjectPermission = (
scopes: Array<{ secretPath: string; environment: string }>,
permission: string[]
@@ -1116,8 +719,6 @@ export const buildServiceTokenProjectPermission = (
return build({ conditionsMatcher });
};
export const projectNoAccessPermissions = buildNoAccessProjectPermission();
/* eslint-disable */
/**

View File

@@ -1,22 +1,27 @@
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import { ProjectType } from "@app/db/schemas";
import {
InfisicalProjectTemplate,
TUnpackedPermission
} from "@app/ee/services/project-template/project-template-types";
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
export const getDefaultProjectTemplate = (orgId: string) => ({
import { ProjectTemplateDefaultEnvironments } from "./project-template-constants";
export const getDefaultProjectTemplate = (orgId: string, type: ProjectType) => ({
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
type,
name: InfisicalProjectTemplate.Default,
createdAt: new Date(),
updatedAt: new Date(),
description: "Infisical's default project template",
environments: ProjectTemplateDefaultEnvironments,
roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})),
description: `Infisical's ${type} default project template`,
environments: type === ProjectType.SecretManager ? ProjectTemplateDefaultEnvironments : null,
roles: [...getPredefinedRoles({ projectId: "project-template", projectType: type })].map(
({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})
),
orgId
});

View File

@@ -1,10 +1,11 @@
import { ForbiddenError } from "@casl/ability";
import { packRules } from "@casl/ability/extra";
import { TProjectTemplates } from "@app/db/schemas";
import { ProjectType, TProjectTemplates } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
import {
TCreateProjectTemplateDTO,
@@ -32,11 +33,13 @@ const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTempla
...rest,
environments: environments as TProjectTemplateEnvironment[],
roles: [
...getPredefinedRoles("project-template").map(({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})),
...getPredefinedRoles({ projectId: "project-template", projectType: rest.type as ProjectType }).map(
({ name, slug, permissions }) => ({
name,
slug,
permissions: permissions as TUnpackedPermission[]
})
),
...(roles as TProjectTemplateRole[]).map((role) => ({
...role,
permissions: unpackPermissions(role.permissions)
@@ -49,7 +52,7 @@ export const projectTemplateServiceFactory = ({
permissionService,
projectTemplateDAL
}: TProjectTemplatesServiceFactoryDep) => {
const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => {
const listProjectTemplatesByOrg = async (actor: OrgServiceActor, type?: ProjectType) => {
const plan = await licenseService.getPlan(actor.orgId);
if (!plan.projectTemplates)
@@ -68,11 +71,14 @@ export const projectTemplateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
const projectTemplates = await projectTemplateDAL.find({
orgId: actor.orgId
orgId: actor.orgId,
...(type ? { type } : {})
});
return [
getDefaultProjectTemplate(actor.orgId),
...(type
? [getDefaultProjectTemplate(actor.orgId, type)]
: Object.values(ProjectType).map((projectType) => getDefaultProjectTemplate(actor.orgId, projectType))),
...projectTemplates.map((template) => $unpackProjectTemplate(template))
];
};
@@ -134,7 +140,7 @@ export const projectTemplateServiceFactory = ({
};
const createProjectTemplate = async (
{ roles, environments, ...params }: TCreateProjectTemplateDTO,
{ roles, environments, type, ...params }: TCreateProjectTemplateDTO,
actor: OrgServiceActor
) => {
const plan = await licenseService.getPlan(actor.orgId);
@@ -154,6 +160,17 @@ export const projectTemplateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
if (environments && type !== ProjectType.SecretManager) {
throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" });
}
if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) {
throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
message: `Failed to create project template due to environment count exceeding your current limit of ${plan.environmentLimit}. Contact Infisical to increase limit.`
});
}
const isConflictingName = Boolean(
await projectTemplateDAL.findOne({
name: params.name,
@@ -169,8 +186,10 @@ export const projectTemplateServiceFactory = ({
const projectTemplate = await projectTemplateDAL.create({
...params,
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
environments: JSON.stringify(environments),
orgId: actor.orgId
environments:
type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null,
orgId: actor.orgId,
type
});
return $unpackProjectTemplate(projectTemplate);
@@ -202,6 +221,19 @@ export const projectTemplateServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
if (projectTemplate.type !== ProjectType.SecretManager && environments)
throw new BadRequestError({ message: "Cannot configure environments for non-SecretManager project templates" });
if (projectTemplate.type === ProjectType.SecretManager && environments === null)
throw new BadRequestError({ message: "Environments cannot be removed for SecretManager project templates" });
if (environments && plan.environmentLimit !== null && environments.length > plan.environmentLimit) {
throw new BadRequestError({
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
message: `Failed to update project template due to environment count exceeding your current limit of ${plan.environmentLimit}. Contact Infisical to increase limit.`
});
}
if (params.name && projectTemplate.name !== params.name) {
const isConflictingName = Boolean(
await projectTemplateDAL.findOne({

View File

@@ -1,6 +1,6 @@
import { z } from "zod";
import { TProjectEnvironments } from "@app/db/schemas";
import { ProjectType, TProjectEnvironments } from "@app/db/schemas";
import { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
@@ -15,8 +15,9 @@ export type TProjectTemplateRole = {
export type TCreateProjectTemplateDTO = {
name: string;
description?: string;
type: ProjectType;
roles: TProjectTemplateRole[];
environments: TProjectTemplateEnvironment[];
environments?: TProjectTemplateEnvironment[] | null;
};
export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>;

View File

@@ -1,6 +1,8 @@
import { Redis } from "ioredis";
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
import { applyJitter } from "@app/lib/dates";
import { delay as delayMs } from "@app/lib/delay";
import { Redlock, Settings } from "@app/lib/red-lock";
export const PgSqlLock = {
@@ -48,6 +50,13 @@ export const KeyStoreTtls = {
AccessTokenStatusUpdateInSeconds: 120
};
type TDeleteItems = {
pattern: string;
batchSize?: number;
delay?: number;
jitter?: number;
};
type TWaitTillReady = {
key: string;
waitingCb?: () => void;
@@ -75,6 +84,35 @@ export const keyStoreFactory = (redisUrl: string) => {
const deleteItem = async (key: string) => redis.del(key);
const deleteItems = async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }: TDeleteItems) => {
let cursor = "0";
let totalDeleted = 0;
do {
// Await in loop is needed so that Redis is not overwhelmed
// eslint-disable-next-line no-await-in-loop
const [nextCursor, keys] = await redis.scan(cursor, "MATCH", pattern, "COUNT", 1000); // Count should be 1000 - 5000 for prod loads
cursor = nextCursor;
for (let i = 0; i < keys.length; i += batchSize) {
const batch = keys.slice(i, i + batchSize);
const pipeline = redis.pipeline();
for (const key of batch) {
pipeline.unlink(key);
}
// eslint-disable-next-line no-await-in-loop
await pipeline.exec();
totalDeleted += batch.length;
console.log("BATCH DONE");
// eslint-disable-next-line no-await-in-loop
await delayMs(Math.max(0, applyJitter(delay, jitter)));
}
} while (cursor !== "0");
return totalDeleted;
};
const incrementBy = async (key: string, value: number) => redis.incrby(key, value);
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
@@ -94,7 +132,7 @@ export const keyStoreFactory = (redisUrl: string) => {
// eslint-disable-next-line
await new Promise((resolve) => {
waitingCb?.();
setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter)));
setTimeout(resolve, Math.max(0, applyJitter(delay, jitter)));
});
attempts += 1;
// eslint-disable-next-line
@@ -108,6 +146,7 @@ export const keyStoreFactory = (redisUrl: string) => {
setExpiry,
setItemWithExpiry,
deleteItem,
deleteItems,
incrementBy,
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
return redisLock.acquire(resources, duration, settings);

View File

@@ -1,3 +1,7 @@
import RE2 from "re2";
import { applyJitter } from "@app/lib/dates";
import { delay as delayMs } from "@app/lib/delay";
import { Lock } from "@app/lib/red-lock";
import { TKeyStoreFactory } from "./keystore";
@@ -19,6 +23,27 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => {
delete store[key];
return 1;
},
deleteItems: async ({ pattern, batchSize = 500, delay = 1500, jitter = 200 }) => {
const regex = new RE2(`^${pattern.replace(/[-[\]/{}()+?.\\^$|]/g, "\\$&").replace(/\*/g, ".*")}$`);
let totalDeleted = 0;
const keys = Object.keys(store);
for (let i = 0; i < keys.length; i += batchSize) {
const batch = keys.slice(i, i + batchSize);
for (const key of batch) {
if (regex.test(key)) {
delete store[key];
totalDeleted += 1;
}
}
// eslint-disable-next-line no-await-in-loop
await delayMs(Math.max(0, applyJitter(delay, jitter)));
}
return totalDeleted;
},
getItem: async (key) => {
const value = store[key];
if (typeof value === "string") {

View File

@@ -1866,8 +1866,12 @@ export const KMS = {
};
export const ProjectTemplates = {
LIST: {
type: "The type of project template to list."
},
CREATE: {
name: "The name of the project template to be created. Must be slug-friendly.",
type: "The type of project template to be created.",
description: "An optional description of the project template.",
roles: "The roles to be created when the template is applied to a project.",
environments: "The environments to be created when the template is applied to a project."

View File

@@ -0,0 +1,4 @@
export const delay = (ms: number) =>
new Promise<void>((resolve) => {
setTimeout(resolve, ms);
});

View File

@@ -25,6 +25,7 @@ import {
TQueueSecretSyncSyncSecretsByIdDTO,
TQueueSendSecretSyncActionFailedNotificationsDTO
} from "@app/services/secret-sync/secret-sync-types";
import { CacheType } from "@app/services/super-admin/super-admin-types";
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
export enum QueueName {
@@ -49,7 +50,8 @@ export enum QueueName {
AccessTokenStatusUpdate = "access-token-status-update",
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
AppConnectionSecretSync = "app-connection-secret-sync",
SecretRotationV2 = "secret-rotation-v2"
SecretRotationV2 = "secret-rotation-v2",
InvalidateCache = "invalidate-cache"
}
export enum QueueJobs {
@@ -81,7 +83,8 @@ export enum QueueJobs {
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification"
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
InvalidateCache = "invalidate-cache"
}
export type TQueueJobTypes = {
@@ -234,6 +237,14 @@ export type TQueueJobTypes = {
name: QueueJobs.SecretRotationV2SendNotification;
payload: TSecretRotationSendNotificationJobPayload;
};
[QueueName.InvalidateCache]: {
name: QueueJobs.InvalidateCache;
payload: {
data: {
type: CacheType;
};
};
};
};
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;

View File

@@ -100,3 +100,10 @@ export const publicSshCaLimit: RateLimitOptions = {
max: 30, // conservative default
keyGenerator: (req) => req.realIp
};
export const invalidateCacheLimit: RateLimitOptions = {
timeWindow: 60 * 1000,
hook: "preValidation",
max: 1,
keyGenerator: (req) => req.realIp
};

View File

@@ -244,6 +244,7 @@ import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
import { slackServiceFactory } from "@app/services/slack/slack-service";
import { TSmtpService } from "@app/services/smtp/smtp-service";
import { invalidateCacheQueueFactory } from "@app/services/super-admin/invalidate-cache-queue";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
@@ -614,6 +615,11 @@ export const registerRoutes = async (
queueService
});
const invalidateCacheQueue = invalidateCacheQueueFactory({
keyStore,
queueService
});
const userService = userServiceFactory({
userDAL,
userAliasDAL,
@@ -725,7 +731,8 @@ export const registerRoutes = async (
keyStore,
licenseService,
kmsService,
microsoftTeamsService
microsoftTeamsService,
invalidateCacheQueue
});
const orgAdminService = orgAdminServiceFactory({

View File

@@ -4,13 +4,14 @@ import { z } from "zod";
import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { invalidateCacheLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
import { LoginMethod } from "@app/services/super-admin/super-admin-types";
import { CacheType, LoginMethod } from "@app/services/super-admin/super-admin-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
export const registerAdminRouter = async (server: FastifyZodProvider) => {
@@ -548,4 +549,69 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
};
}
});
server.route({
method: "POST",
url: "/invalidate-cache",
config: {
rateLimit: invalidateCacheLimit
},
schema: {
body: z.object({
type: z.nativeEnum(CacheType)
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
await server.services.superAdmin.invalidateCache(req.body.type);
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.InvalidateCache,
distinctId: getTelemetryDistinctId(req),
properties: {
...req.auditLogInfo
}
});
return {
message: "Cache invalidation job started"
};
}
});
server.route({
method: "GET",
url: "/invalidating-cache-status",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
invalidating: z.boolean()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async () => {
const invalidating = await server.services.superAdmin.checkIfInvalidatingCache();
return {
invalidating
};
}
});
};

View File

@@ -170,7 +170,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.optional()
.default(InfisicalProjectTemplate.Default)
.describe(PROJECTS.CREATE.template),
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager)
type: z.nativeEnum(ProjectType).default(ProjectType.SecretManager),
shouldCreateDefaultEnvs: z.boolean().optional().default(true)
}),
response: {
200: z.object({
@@ -190,7 +191,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
slug: req.body.slug,
kmsKeyId: req.body.kmsKeyId,
template: req.body.template,
type: req.body.type
type: req.body.type,
createDefaultEnvs: req.body.shouldCreateDefaultEnvs
});
await server.services.telemetry.sendPostHogEvents({
@@ -272,7 +274,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
},
schema: {
params: z.object({
slug: slugSchema({ min: 5, max: 36 }).describe("The slug of the project to get.")
slug: slugSchema({ max: 36 }).describe("The slug of the project to get.")
}),
response: {
200: projectWithEnv

View File

@@ -1,15 +1,20 @@
import { ProjectMembershipRole } from "@app/db/schemas";
import { v4 as uuidv4 } from "uuid";
import { ProjectMembershipRole, ProjectType } from "@app/db/schemas";
import {
cryptographicOperatorPermissions,
projectAdminPermissions,
projectMemberPermissions,
projectNoAccessPermissions,
projectViewerPermission
} from "@app/ee/services/permission/project-permission";
projectViewerPermission,
sshHostBootstrapPermissions
} from "@app/ee/services/permission/default-roles";
import { TGetPredefinedRolesDTO } from "@app/services/project-role/project-role-types";
export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
export const getPredefinedRoles = ({ projectId, projectType, roleFilter }: TGetPredefinedRolesDTO) => {
return [
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
id: uuidv4(),
projectId,
name: "Admin",
slug: ProjectMembershipRole.Admin,
@@ -19,7 +24,7 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
id: uuidv4(),
projectId,
name: "Developer",
slug: ProjectMembershipRole.Member,
@@ -29,7 +34,29 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
id: uuidv4(),
projectId,
name: "SSH Host Bootstrapper",
slug: ProjectMembershipRole.SshHostBootstrapper,
permissions: sshHostBootstrapPermissions,
description: "Create and issue SSH Hosts in a project",
createdAt: new Date(),
updatedAt: new Date(),
type: ProjectType.SSH
},
{
id: uuidv4(),
projectId,
name: "Cryptographic Operator",
slug: ProjectMembershipRole.KmsCryptographicOperator,
permissions: cryptographicOperatorPermissions,
description: "Perform cryptographic operations, such as encryption and signing, in a project",
createdAt: new Date(),
updatedAt: new Date(),
type: ProjectType.KMS
},
{
id: uuidv4(),
projectId,
name: "Viewer",
slug: ProjectMembershipRole.Viewer,
@@ -39,7 +66,7 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember
updatedAt: new Date()
},
{
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
id: uuidv4(),
projectId,
name: "No Access",
slug: ProjectMembershipRole.NoAccess,
@@ -48,5 +75,5 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember
createdAt: new Date(),
updatedAt: new Date()
}
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
].filter(({ slug, type }) => (type ? type === projectType : true) && (!roleFilter || roleFilter === slug));
};

View File

@@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
import { requestContext } from "@fastify/request-context";
import { ActionProjectType, ProjectMembershipRole, TableName } from "@app/db/schemas";
import { ActionProjectType, ProjectMembershipRole, ProjectType, TableName, TProjects } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import {
ProjectPermissionActions,
@@ -34,7 +34,7 @@ type TProjectRoleServiceFactoryDep = {
projectRoleDAL: TProjectRoleDALFactory;
identityDAL: Pick<TIdentityDALFactory, "findById">;
userDAL: Pick<TUserDALFactory, "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findProjectById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
@@ -98,30 +98,37 @@ export const projectRoleServiceFactory = ({
roleSlug,
filter
}: TGetRoleDetailsDTO) => {
let projectId = "";
let project: TProjects;
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
const project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
if (!project) throw new NotFoundError({ message: "Project not found" });
projectId = project.id;
project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
} else {
projectId = filter.projectId;
project = await projectDAL.findProjectById(filter.projectId);
}
if (!project) throw new NotFoundError({ message: "Project not found" });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
projectId: project.id,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
if (roleSlug !== "custom" && Object.values(ProjectMembershipRole).includes(roleSlug as ProjectMembershipRole)) {
const predefinedRole = getPredefinedRoles(projectId, roleSlug as ProjectMembershipRole)[0];
const [predefinedRole] = getPredefinedRoles({
projectId: project.id,
projectType: project.type as ProjectType,
roleFilter: roleSlug as ProjectMembershipRole
});
if (!predefinedRole) throw new NotFoundError({ message: `Default role with slug '${roleSlug}' not found` });
return { ...predefinedRole, permissions: UnpackedPermissionSchema.array().parse(predefinedRole.permissions) };
}
const customRole = await projectRoleDAL.findOne({ slug: roleSlug, projectId });
const customRole = await projectRoleDAL.findOne({ slug: roleSlug, projectId: project.id });
if (!customRole) throw new NotFoundError({ message: `Project role with slug '${roleSlug}' not found` });
return { ...customRole, permissions: unpackPermissions(customRole.permissions) };
};
@@ -194,29 +201,32 @@ export const projectRoleServiceFactory = ({
};
const listRoles = async ({ actorOrgId, actorAuthMethod, actorId, actor, filter }: TListRolesDTO) => {
let projectId = "";
let project: TProjects;
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
const project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
if (!project) throw new BadRequestError({ message: "Project not found" });
projectId = project.id;
project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
} else {
projectId = filter.projectId;
project = await projectDAL.findProjectById(filter.projectId);
}
if (!project) throw new BadRequestError({ message: "Project not found" });
const { permission } = await permissionService.getProjectPermission({
actor,
actorId,
projectId,
projectId: project.id,
actorAuthMethod,
actorOrgId,
actionProjectType: ActionProjectType.Any
});
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
const customRoles = await projectRoleDAL.find(
{ projectId },
{ projectId: project.id },
{ sort: [[`${TableName.ProjectRoles}.slug` as "slug", "asc"]] }
);
const roles = [...getPredefinedRoles(projectId), ...(customRoles || [])];
const roles = [
...getPredefinedRoles({ projectId: project.id, projectType: project.type as ProjectType }),
...(customRoles || [])
];
return roles;
};

View File

@@ -1,4 +1,4 @@
import { TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas";
import { ProjectMembershipRole, ProjectType, TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas";
import { TProjectPermission } from "@app/lib/types";
export enum ProjectRoleServiceIdentifierType {
@@ -34,3 +34,9 @@ export type TListRolesDTO = {
| { type: ProjectRoleServiceIdentifierType.SLUG; projectSlug: string }
| { type: ProjectRoleServiceIdentifierType.ID; projectId: string };
} & Omit<TProjectPermission, "projectId">;
export type TGetPredefinedRolesDTO = {
projectId: string;
projectType: ProjectType;
roleFilter?: ProjectMembershipRole;
};

View File

@@ -329,14 +329,16 @@ export const projectServiceFactory = ({
// set default environments and root folder for provided environments
let envs: TProjectEnvironments[] = [];
if (projectTemplate) {
envs = await projectEnvDAL.insertMany(
projectTemplate.environments.map((env) => ({ ...env, projectId: project.id })),
tx
);
await folderDAL.insertMany(
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
tx
);
if (projectTemplate.environments) {
envs = await projectEnvDAL.insertMany(
projectTemplate.environments.map((env) => ({ ...env, projectId: project.id })),
tx
);
await folderDAL.insertMany(
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
tx
);
}
await projectRoleDAL.insertMany(
projectTemplate.packedRoles.map((role) => ({
...role,
@@ -592,7 +594,10 @@ export const projectServiceFactory = ({
workspaces.map(async (workspace) => {
return {
...workspace,
roles: [...(workspaceMappedToRoles[workspace.id] || []), ...getPredefinedRoles(workspace.id)]
roles: [
...(workspaceMappedToRoles[workspace.id] || []),
...getPredefinedRoles({ projectId: workspace.id, projectType: workspace.type as ProjectType })
]
};
})
);

View File

@@ -169,7 +169,7 @@ const getParameterStoreTagsRecord = async (
throw new SecretSyncError({
message:
"IAM role has inadequate permissions to manage resource tags. Ensure the following polices are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource",
"IAM role has inadequate permissions to manage resource tags. Ensure the following policies are present: ssm:ListTagsForResource, ssm:AddTagsToResource, and ssm:RemoveTagsFromResource",
shouldRetry: false
});
}

View File

@@ -0,0 +1,49 @@
import { TKeyStoreFactory } from "@app/keystore/keystore";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { CacheType } from "./super-admin-types";
export type TInvalidateCacheQueueFactoryDep = {
queueService: TQueueServiceFactory;
keyStore: Pick<TKeyStoreFactory, "deleteItems" | "setItemWithExpiry" | "deleteItem">;
};
export type TInvalidateCacheQueueFactory = ReturnType<typeof invalidateCacheQueueFactory>;
export const invalidateCacheQueueFactory = ({ queueService, keyStore }: TInvalidateCacheQueueFactoryDep) => {
const startInvalidate = async (dto: {
data: {
type: CacheType;
};
}) => {
await queueService.queue(QueueName.InvalidateCache, QueueJobs.InvalidateCache, dto, {
removeOnComplete: true,
removeOnFail: true,
jobId: `invalidate-cache-${dto.data.type}`
});
};
queueService.start(QueueName.InvalidateCache, async (job) => {
try {
const {
data: { type }
} = job.data;
await keyStore.setItemWithExpiry("invalidating-cache", 1800, "true"); // 30 minutes max (in case the job somehow silently fails)
if (type === CacheType.ALL || type === CacheType.SECRETS)
await keyStore.deleteItems({ pattern: "secret-manager:*" });
await keyStore.deleteItem("invalidating-cache");
} catch (err) {
logger.error(err, "Failed to invalidate cache");
await keyStore.deleteItem("invalidating-cache");
}
});
return {
startInvalidate
};
};

View File

@@ -25,8 +25,10 @@ import { TOrgServiceFactory } from "../org/org-service";
import { TUserDALFactory } from "../user/user-dal";
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
import { UserAliasType } from "../user-alias/user-alias-types";
import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue";
import { TSuperAdminDALFactory } from "./super-admin-dal";
import {
CacheType,
LoginMethod,
TAdminBootstrapInstanceDTO,
TAdminGetIdentitiesDTO,
@@ -46,9 +48,10 @@ type TSuperAdminServiceFactoryDep = {
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey" | "updateEncryptionStrategy">;
kmsRootConfigDAL: TKmsRootConfigDALFactory;
orgService: Pick<TOrgServiceFactory, "createOrganization">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem" | "deleteItems">;
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
invalidateCacheQueue: TInvalidateCacheQueueFactory;
};
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
@@ -64,7 +67,7 @@ export let getServerCfg: () => Promise<
const ADMIN_CONFIG_KEY = "infisical-admin-cfg";
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
export const ADMIN_CONFIG_DB_UUID = "00000000-0000-0000-0000-000000000000";
export const superAdminServiceFactory = ({
serverCfgDAL,
@@ -80,7 +83,8 @@ export const superAdminServiceFactory = ({
identityAccessTokenDAL,
identityTokenAuthDAL,
identityOrgMembershipDAL,
microsoftTeamsService
microsoftTeamsService,
invalidateCacheQueue
}: TSuperAdminServiceFactoryDep) => {
const initServerCfg = async () => {
// TODO(akhilmhdh): bad pattern time less change this later to me itself
@@ -631,6 +635,16 @@ export const superAdminServiceFactory = ({
await kmsService.updateEncryptionStrategy(strategy);
};
const invalidateCache = async (type: CacheType) => {
await invalidateCacheQueue.startInvalidate({
data: { type }
});
};
const checkIfInvalidatingCache = async () => {
return (await keyStore.getItem("invalidating-cache")) !== null;
};
return {
initServerCfg,
updateServerCfg,
@@ -644,6 +658,8 @@ export const superAdminServiceFactory = ({
getConfiguredEncryptionStrategies,
grantServerAdminAccessToUser,
deleteIdentitySuperAdminAccess,
deleteUserSuperAdminAccess
deleteUserSuperAdminAccess,
invalidateCache,
checkIfInvalidatingCache
};
};

View File

@@ -44,3 +44,8 @@ export enum LoginMethod {
LDAP = "ldap",
OIDC = "oidc"
}
export enum CacheType {
ALL = "all",
SECRETS = "secrets"
}

View File

@@ -21,7 +21,8 @@ export enum PostHogEventTypes {
IssueSshHostUserCert = "Issue SSH Host User Certificate",
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
SignCert = "Sign PKI Certificate",
IssueCert = "Issue PKI Certificate"
IssueCert = "Issue PKI Certificate",
InvalidateCache = "Invalidate Cache"
}
export type TSecretModifiedEvent = {
@@ -203,6 +204,13 @@ export type TIssueCertificateEvent = {
};
};
export type TInvalidateCacheEvent = {
event: PostHogEventTypes.InvalidateCache;
properties: {
userAgent?: string;
};
};
export type TPostHogEvent = { distinctId: string } & (
| TSecretModifiedEvent
| TAdminInitEvent
@@ -221,4 +229,5 @@ export type TPostHogEvent = { distinctId: string } & (
| TIssueSshHostHostCertEvent
| TSignCertificateEvent
| TIssueCertificateEvent
| TInvalidateCacheEvent
);

View File

@@ -1,6 +1,6 @@
---
title: "Get Certificate Bundle"
openapi: "GET /api/v2/workspace/{slug}/bundle"
openapi: "GET /api/v1/pki/certificates/{serialNumber}/bundle"
---
<Note>

View File

@@ -1,4 +1,4 @@
---
title: "Get Certificate Private Key"
openapi: "GET /api/v2/workspace/{slug}/private-key"
openapi: "GET /api/v1/pki/certificates/{serialNumber}/private-key"
---

View File

@@ -27,7 +27,7 @@ User identities can have metadata attributes assigned directly. These attributes
</Tabs>
#### Applying ABAC Policies with User Metadata
Attribute-based access controls are currently only available for polices defined on Secrets Manager projects.
Attribute-based access controls are currently only available for policies defined on Secrets Manager projects.
You can set ABAC permissions to dynamically set access to environments, folders, secrets, and secret tags.
<img src="/images/platform/access-controls/example-abac-1.png" />

View File

@@ -3,11 +3,6 @@ title: General
description: "Learn how to authenticate with Infisical using LDAP."
---
<Note>
LDAP is a paid feature. If you're using Infisical Cloud, then it is available under the Enterprise Tier. If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
</Note>
**LDAP Auth** is an LDAP based authentication method that allows you to authenticate with Infisical using a machine identity configured with an [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) directory.
## Guide
@@ -89,4 +84,4 @@ description: "Learn how to authenticate with Infisical using LDAP."
You can read more about the login API endpoint [here](/api-reference/endpoints/ldap-auth/login).
</Step>
</Step>
</Steps>
</Steps>

View File

@@ -3,11 +3,6 @@ title: JumpCloud
description: "Learn how to authenticate with Infisical using LDAP with JumpCloud."
---
<Note>
LDAP is a paid feature. If you're using Infisical Cloud, then it is available under the Enterprise Tier. If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it.
</Note>
**LDAP Auth** is an LDAP based authentication method that allows you to authenticate with Infisical using a machine identity configured with an [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) directory.
## Guide
@@ -99,4 +94,4 @@ description: "Learn how to authenticate with Infisical using LDAP with JumpCloud
You can read more about the login API endpoint [here](/api-reference/endpoints/ldap-auth/login).
</Step>
</Step>
</Steps>
</Steps>

View File

@@ -33,7 +33,7 @@ In the following steps, we'll explore how to set up a project template.
<Tab title="Infisical UI">
<Steps>
<Step title="Creating a Project Template">
Navigate to the Project Templates tab on the Organization Settings page and tap on the **Add Template** button.
Navigate to the **Project Templates** tab on the Feature Settings page for the project type you want to create a template for and tap on the **Add Template** button.
![project template add button](/images/platform/project-templates/project-template-add-button.png)
Specify your template details. Here's some guidance on each field:
@@ -67,6 +67,7 @@ In the following steps, we'll explore how to set up a project template.
--header 'Content-Type: application/json' \
--data '{
"name": "my-project-template",
"type": "secret-manager",
"description": "...",
"environments": "[...]",
"roles": "[...]",

View File

@@ -31,16 +31,9 @@ we will register a remote host with Infisical through a [machine identity](/docu
<Steps>
<Step title="Create an Infisical SSH project">
1.1. Start by creating a new Infisical SSH project in Infisical.
Start by creating a new Infisical SSH project in Infisical.
![ssh project create](/images/platform/ssh/v2/ssh-create-project.png)
1.2. Create a custom role in the project under Access Control > Project Roles to grant the machine identity that we will create in step 2 the ability to **Create** and **Issue Host Certificates** on the **SSH Host** resource; this will enable the linked machine identity to bootstrap a remote host with Infisical
and establish the necessary configuration on it.
![ssh custom role bootstrap 1](/images/platform/ssh/v2/ssh-add-bootstrap-role-1.png)
![ssh custom role bootstrap 2](/images/platform/ssh/v2/ssh-add-bootstrap-role-2.png)
</Step>
<Step title="Create a machine identity for bootstrapping Infisical SSH">
2.1. Follow the instructions [here](/documentation/platform/identities/universal-auth) to configure a [machine identity](/documentation/platform/identities/machine-identities) in Infisical with Universal Auth.
@@ -52,7 +45,14 @@ we will register a remote host with Infisical through a [machine identity](/docu
You may use other authentication methods as suitable (e.g. [AWS Auth](/documentation/platform/identities/aws-auth), [Azure Auth](/documentation/platform/identities/azure-auth), [GCP Auth](/documentation/platform/identities/gcp-auth), etc.) as part of the machine identity configuration but, to keep this example simple, we will be using Universal Auth.
</Note>
2.2. Add the machine identity to the Infisical SSH project you created in the previous step and assign it the custom role you created in step 1.2.
2.2. Add the machine identity to the Infisical SSH project you created in the previous step and assign it the **SSH Host Bootstrapper** role.
This role grants the ability to **Create** and **Issue Host Certificates** on the **SSH Host** resource; this will enable the linked machine identity to bootstrap a remote host with Infisical
and establish the necessary configuration on it.
<Tip>
If you plan to use a custom role to bootstrap SSH hosts, ensure the role has the **Create** and **Issue Host Certificates** on the **SSH Host** resource.
</Tip>
![ssh add identity to project](/images/platform/ssh/v2/ssh-add-identity-to-project.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 951 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 579 KiB

After

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 562 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1015 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 852 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 KiB

After

Width:  |  Height:  |  Size: 680 KiB

View File

@@ -165,7 +165,7 @@ spec:
</Accordion>
<Accordion title="managedSecretReference.creationPolicy">
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
#### Available options

View File

@@ -34,7 +34,7 @@ Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes sec
metadata:
name: infisical-push-secret-demo
spec:
resyncInterval: 1m
resyncInterval: 1m # Remove this field to disable automatic reconciliation of the InfisicalPushSecret CRD.
hostAPI: https://app.infisical.com/api
# Optional, defaults to no replacement.
@@ -124,7 +124,9 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
<Accordion title="resyncInterval">
The `resyncInterval` is a string-formatted duration that defines the time between each resync.
The `resyncInterval` is a string-formatted duration that defines the time between each resync. The field is optional, and will default to no automatic resync if not defined.
If you don't want to automatically reconcile the InfisicalPushSecret CRD on an interval, you can remove the `resyncInterval` field entirely from your InfisicalPushSecret CRD.
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
@@ -239,7 +241,21 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
DATABASE_URL: postgres://127.0.0.1:5432
ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab
```
</Accordion>
<Accordion title="generators[]">
The `generators[]` field is used to define the generators you want to use for your InfisicalPushSecret CRD.
You can follow the guide for [using generators to push secrets](#using-generators-to-push-secrets) for more information.
Example:
```yaml
push:
generators:
- destinationSecretName: password-generator-test
generatorRef:
kind: Password
name: password-generator
```
</Accordion>
</Accordion>
@@ -459,6 +475,148 @@ Using Go templates, you can format, combine, and create new key-value pairs of s
Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information.
</Accordion>
## Using generators to push secrets
Generators allow secrets to be dynamically generated during each reconciliation cycle and then pushed to Infisical. They are useful for use cases where a new secret value is needed on every sync, such as ephemeral credentials or one-time-use tokens.
A generator is defined as a custom resource (`ClusterGenerator`) within the cluster, which specifies the logic for generating secret values. Generators are stateless, each invocation triggers the creation of a new set of values, with no tracking or persistence of previously generated data.
Because of this behavior, you may want to disable automatic syncing for the `InfisicalPushSecret` resource to avoid continuous regeneration of secrets. This can be done by omitting the `resyncInterval` field from the InfisicalPushSecret CRD.
### Example usage
```yaml
push:
secret:
secretName: push-secret-source-secret
secretNamespace: dev
generators:
- destinationSecretName: password-generator # Name of the secret that will be created in Infisical
generatorRef:
kind: Password # Kind of the resource, must match the generator kind.
name: custom-generator # Name of the generator resource
```
To use a generator, you must specify at least one generator in the `push.generators[]` field.
<Accordion title="push.generators[]">
This field holds an array of the generators you want to use for your InfisicalPushSecret CRD.
</Accordion>
<Accordion title="push.generators[].destinationSecretName">
The name of the secret that will be created in Infisical.
</Accordion>
<Accordion title="push.generators[].generatorRef">
The reference to the generator resource.
Valid fields:
- `kind`: The kind of the generator resource, must match the generator kind.
- `name`: The name of the generator resource.
</Accordion>
<Accordion title="push.generators[].generatorRef.kind">
The kind of the generator resource, must match the generator kind.
Valid values:
- `Password`
- `UUID`
</Accordion>
<Accordion title="push.generators[].generatorRef.name">
The name of the generator resource.
</Accordion>
### Supported Generators
Below are the currently supported generators for the InfisicalPushSecret CRD. Each generator is a `ClusterGenerator` custom resource that can be used to customize the generated secret.
<Accordion title="Password Generator">
### Password Generator
The Password generator is a custom resource that is installed on the cluster that defines the logic for generating a password.
- `kind`: The kind of the generator resource, must match the generator kind. For the Password generator, the kind is `Password`.
- `generator.passwordSpec`: The spec of the password generator.
<Accordion title="generator.kind">
The `generator.kind` field must match the kind of the generator resource. For the Password generator, the kind should always be set to `Password`.
</Accordion>
<Accordion title="generator.passwordSpec">
- `length`: The length of the password.
- `digits`: The number of digits in the password.
- `symbols`: The number of symbols in the password.
- `symbolCharacters`: The characters to use for the symbols in the password.
- `noUpper`: Whether to include uppercase letters in the password.
- `allowRepeat`: Whether to allow repeating characters in the password.
</Accordion>
```yaml password-cluster-generator.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: ClusterGenerator
metadata:
name: password-generator
spec:
kind: Password
generator:
passwordSpec:
length: 10
digits: 5
symbols: 5
symbolCharacters: "-_$@"
noUpper: false
allowRepeat: true
```
Example InfisicalPushSecret CRD using the Password generator:
```yaml infisical-push-secret-crd.yaml
push:
generators:
- destinationSecretName: password-generator-test
generatorRef:
kind: Password
name: password-generator
```
</Accordion>
<Accordion title="UUID Generator">
### UUID Generator
The UUID generator is a custom resource that is installed on the cluster that defines the logic for generating a UUID.
- `kind`: The kind of the generator resource, must match the generator kind. For the UUID generator, the kind is `UUID`.
- `generator.uuidSpec`: The spec of the UUID generator. For UUID's, this can be left empty.
<Accordion title="generator.kind">
The `generator.kind` field must match the kind of the generator resource. For the UUID generator, the kind should always be set to `UUID`.
</Accordion>
<Accordion title="generator.uuidSpec">
The spec of the UUID generator. For UUID's, this can be left empty.
</Accordion>
```yaml uuid-cluster-generator.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: ClusterGenerator
metadata:
name: uuid-generator
spec:
kind: UUID
generator:
uuidSpec:
```
Example InfisicalPushSecret CRD using the UUID generator:
```yaml infisical-push-secret-crd.yaml
push:
generators:
- destinationSecretName: uuid-generator-test
generatorRef:
kind: UUID
name: uuid-generator
```
</Accordion>
## Applying the InfisicalPushSecret CRD to your cluster
Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster.

View File

@@ -832,7 +832,7 @@ The namespace of the managed Kubernetes secret to be created.
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
</Accordion>
<Accordion title="managedKubeSecretReferences[].creationPolicy">
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
#### Available options
@@ -940,7 +940,7 @@ The Infisical operator will automatically create the Kubernetes config map in th
The namespace of the managed Kubernetes config map that your Infisical data will be stored in.
</Accordion>
<Accordion title="managedKubeConfigMapReferences[].creationPolicy">
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes config map that is generated by the Infisical operator.
Creation policies allow you to control whether or not owner references should be added to the managed Kubernetes config map that is generated by the Infisical operator.
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
#### Available options

View File

@@ -58,3 +58,23 @@ We ask that researchers:
- Give us a reasonable window to investigate and patch before going public
Researchers can also spin up our [self-hosted version of Infisical](/self-hosting/overview) to test for vulnerabilities locally.
### Program Conduct and Enforcement
We value professional and collaborative interaction with security researchers. To maintain the integrity of our bug bounty program, we expect all participants to adhere to the following guidelines:
- Maintain professional communication in all interactions
- Do not threaten public disclosure of vulnerabilities before we've had reasonable time to investigate and address the issue
- Do not attempt to extort or coerce compensation through threats
- Follow the responsible disclosure process outlined in this document
- Do not use automated scanning tools without prior permission
Violations of these guidelines may result in:
1. **Warning**: For minor violations, we may issue a warning explaining the violation and requesting compliance with program guidelines.
2. **Temporary Ban**: Repeated minor violations or more serious violations may result in a temporary suspension from the program.
3. **Permanent Ban**: Severe violations such as threats, extortion attempts, or unauthorized public disclosure will result in permanent removal from the Infisical Bug Bounty Program.
We reserve the right to reject reports, withhold bounties, and remove participants from the program at our discretion for conduct that undermines the collaborative spirit of security research.
Infisical is committed to working respectfully with security researchers who follow these guidelines, and we strive to recognize and reward valuable contributions that help protect our platform and users.

View File

@@ -1484,6 +1484,8 @@
"api-reference/endpoints/certificates/revoke",
"api-reference/endpoints/certificates/delete",
"api-reference/endpoints/certificates/cert-body",
"api-reference/endpoints/certificates/bundle",
"api-reference/endpoints/certificates/private-key",
"api-reference/endpoints/certificates/issue-certificate",
"api-reference/endpoints/certificates/sign-certificate"
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -72,7 +72,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
OrgPermissionSubjects.ProjectTemplates
);
const { data: projectTemplates = [] } = useListProjectTemplates({
const { data: projectTemplates = [] } = useListProjectTemplates(projectType, {
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
});

View File

@@ -0,0 +1,30 @@
import { useState } from "react";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ProjectTemplatesTab } from "./components";
const tabs = [
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab }
];
export const ProjectSettings = () => {
const [selectedTab, setSelectedTab] = useState(tabs[0].key);
return (
<Tabs value={selectedTab} onValueChange={setSelectedTab}>
<TabList>
{tabs.map((tab) => (
<Tab value={tab.key} key={tab.key}>
{tab.name}
</Tab>
))}
</TabList>
{tabs.map(({ key, component: Component }) => (
<TabPanel value={key} key={`tab-panel-${key}`}>
<Component />
</TabPanel>
))}
</Tabs>
);
};

View File

@@ -7,6 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks";
import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { ProjectTemplateDetailsModal } from "../../ProjectTemplateDetailsModal";
import { ProjectTemplateEnvironmentsForm } from "./ProjectTemplateEnvironmentsForm";
@@ -24,7 +25,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
"editDetails"
] as const);
const { id: templateId, name, description } = projectTemplate;
const { id: templateId, name, description, type } = projectTemplate;
const deleteProjectTemplate = useDeleteProjectTemplate();
@@ -94,10 +95,12 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
</div>
)}
</div>
<ProjectTemplateEnvironmentsForm
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
/>
{type === ProjectType.SecretManager && (
<ProjectTemplateEnvironmentsForm
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}
/>
)}
<ProjectTemplateRolesSection
isInfisicalTemplate={isInfisicalTemplate}
projectTemplate={projectTemplate}

View File

@@ -6,15 +6,15 @@ import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, Input, Modal, ModalContent, ModalTrigger } from "@app/components/v2";
import { Button, FormControl, Input } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { isCustomProjectRole } from "@app/helpers/roles";
import { usePopUp } from "@app/hooks";
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
import { slugSchema } from "@app/lib/schemas";
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
import { NewPermissionRule } from "@app/pages/project/RoleDetailsBySlugPage/components/NewPermissionRule";
import { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
import {
formRolePermission2API,
PROJECT_PERMISSION_OBJECT,
@@ -44,7 +44,7 @@ export const ProjectTemplateEditRoleForm = ({
role,
isDisabled
}: Props) => {
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
const formMethods = useForm<TFormSchema>({
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
@@ -119,34 +119,29 @@ export const ProjectTemplateEditRoleForm = ({
<Button
variant="outline_bg"
type="submit"
className={twMerge("h-10 rounded-r-none", isDirty && "bg-primary text-black")}
className={twMerge(
"h-10 rounded-r-none border border-primary",
isDirty && "bg-primary text-black"
)}
isDisabled={isSubmitting || !isDirty || isDisabled}
isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />}
>
Save
</Button>
<Modal
isOpen={popUp.createPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
<Button
className="h-10 rounded-l-none"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={isDisabled}
onClick={() => handlePopUpToggle("addPolicy")}
>
<ModalTrigger asChild>
<Button
className="h-10 rounded-l-none"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={isDisabled}
>
New Policy
</Button>
</ModalTrigger>
<ModalContent
title="New Policy"
subTitle="Policies grant additional permissions."
>
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
</ModalContent>
</Modal>
Add Policies
</Button>
<PolicySelectionModal
isOpen={popUp.addPolicy.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
/>
</div>
</div>
)}

View File

@@ -19,7 +19,7 @@ import {
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
import { slugSchema } from "@app/lib/schemas";
@@ -35,6 +35,7 @@ const formSchema = z.object({
slug: slugSchema({ min: 1, max: 32 })
})
.array()
.nullish()
});
type TFormSchema = z.infer<typeof formSchema>;
@@ -55,6 +56,8 @@ export const ProjectTemplateEnvironmentsForm = ({
resolver: zodResolver(formSchema)
});
const { subscription } = useSubscription();
const {
fields: environments,
move,
@@ -67,7 +70,7 @@ export const ProjectTemplateEnvironmentsForm = ({
const onFormSubmit = async (form: TFormSchema) => {
try {
const { environments: updatedEnvs } = await updateProjectTemplate.mutateAsync({
environments: form.environments.map((env, index) => ({
environments: form.environments?.map((env, index) => ({
...env,
position: index + 1
})),
@@ -89,6 +92,9 @@ export const ProjectTemplateEnvironmentsForm = ({
}
};
const isEnvironmentLimitExceeded =
Boolean(subscription.environmentLimit) && environments.length >= subscription.environmentLimit;
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
@@ -138,6 +144,8 @@ export const ProjectTemplateEnvironmentsForm = ({
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.ProjectTemplates}
renderTooltip={isEnvironmentLimitExceeded ? true : undefined}
allowedLabel={`Plan environment limit of ${subscription.environmentLimit} exceeded. Contact Infisical to increase limit.`}
>
{(isAllowed) => (
<Button
@@ -147,7 +155,7 @@ export const ProjectTemplateEnvironmentsForm = ({
variant="solid"
size="xs"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
isDisabled={!isAllowed}
isDisabled={!isAllowed || isEnvironmentLimitExceeded}
>
Add Environment
</Button>

View File

@@ -12,6 +12,7 @@ import {
ModalContent,
TextArea
} from "@app/components/v2";
import { useGetProjectTypeFromRoute } from "@app/hooks";
import {
TProjectTemplate,
useCreateProjectTemplate,
@@ -41,6 +42,7 @@ type FormProps = {
const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
const createProjectTemplate = useCreateProjectTemplate();
const updateProjectTemplate = useUpdateProjectTemplate();
const projectType = useGetProjectTypeFromRoute();
const {
handleSubmit,
@@ -55,9 +57,17 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
});
const onFormSubmit = async (data: FormData) => {
if (!projectType) {
createNotification({
text: "Failed to determine project type",
type: "error"
});
return;
}
const mutation = projectTemplate
? updateProjectTemplate.mutateAsync({ templateId: projectTemplate.id, ...data })
: createProjectTemplate.mutateAsync(data);
: createProjectTemplate.mutateAsync({ ...data, type: projectType });
try {
const template = await mutation;
createNotification({

View File

@@ -50,7 +50,7 @@ export const ProjectTemplatesSection = () => {
className="absolute min-h-[10rem] w-full"
>
<div>
<p className="mb-6 text-bunker-300">
<p className="mb-6 font-inter text-bunker-300">
Create and configure templates with predefined roles and environments to streamline
project setup
</p>

View File

@@ -23,7 +23,7 @@ import {
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useGetProjectTypeFromRoute, usePopUp } from "@app/hooks";
import { TProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
import { DeleteProjectTemplateModal } from "./DeleteProjectTemplateModal";
@@ -35,8 +35,10 @@ type Props = {
export const ProjectTemplatesTable = ({ onEdit }: Props) => {
const { subscription } = useSubscription();
const { isPending, data: projectTemplates = [] } = useListProjectTemplates({
enabled: subscription?.projectTemplates
const projectType = useGetProjectTypeFromRoute();
const { isPending, data: projectTemplates = [] } = useListProjectTemplates(projectType, {
enabled: subscription?.projectTemplates && Boolean(projectType)
});
const [search, setSearch] = useState("");
@@ -50,6 +52,8 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
[search, projectTemplates]
);
const isSecretManagerTemplates = projectType === "secret-manager";
return (
<div>
<Input
@@ -64,7 +68,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
<Tr>
<Th>Name</Th>
<Th>Roles</Th>
<Th>Environments</Th>
{isSecretManagerTemplates && <Th>Environments</Th>}
<Th />
</Tr>
</THead>
@@ -77,7 +81,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
/>
)}
{filteredTemplates.map((template) => {
const { id, name, roles, environments, description } = template;
const { id, name, roles, environments = [], description } = template;
return (
<Tr
onClick={() => onEdit(template)}
@@ -116,28 +120,30 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
</Tooltip>
)}
</Td>
<Td className="pl-14">
{environments.length}
{environments.length > 0 && (
<Tooltip
content={
<ul className="ml-2 list-disc">
{environments
.sort((a, b) => (a.position > b.position ? 1 : -1))
.map((env) => (
<li key={env.slug}>{env.name}</li>
))}
</ul>
}
>
<FontAwesomeIcon
size="sm"
className="ml-2 text-mineshaft-400"
icon={faCircleInfo}
/>
</Tooltip>
)}
</Td>
{isSecretManagerTemplates && environments && (
<Td className="pl-14">
{environments.length}
{environments.length > 0 && (
<Tooltip
content={
<ul className="ml-2 list-disc">
{environments
.sort((a, b) => (a.position > b.position ? 1 : -1))
.map((env) => (
<li key={env.slug}>{env.name}</li>
))}
</ul>
}
>
<FontAwesomeIcon
size="sm"
className="ml-2 text-mineshaft-400"
icon={faCircleInfo}
/>
</Tooltip>
)}
</Td>
)}
<Td className="w-5">
{name !== "default" && (
<OrgPermissionCan

View File

@@ -0,0 +1 @@
export * from "./ProjectTemplatesTab";

View File

@@ -0,0 +1 @@
export * from "./ProjectSettings";

View File

@@ -6,21 +6,9 @@ export enum OrgMembershipRole {
NoAccess = "no-access"
}
enum ProjectMemberRole {
Admin = "admin",
Member = "member",
Viewer = "viewer",
NoAccess = "no-access"
}
export const isCustomOrgRole = (slug: string) =>
!Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole);
export const formatProjectRoleName = (name: string) => {
if (name === ProjectMemberRole.Member) return "developer";
return name;
};
export const isCustomProjectRole = (slug: string) =>
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
@@ -28,3 +16,24 @@ export const findOrgMembershipRole = (roles: TOrgRole[], roleIdOrSlug: string) =
isCustomOrgRole(roleIdOrSlug)
? roles.find((r) => r.id === roleIdOrSlug)
: roles.find((r) => r.slug === roleIdOrSlug);
export const formatProjectRoleName = (role: string, customRoleName?: string) => {
switch (role) {
case ProjectMembershipRole.Admin:
return "Admin";
case ProjectMembershipRole.Member:
return "Developer";
case ProjectMembershipRole.Viewer:
return "Viewer";
case ProjectMembershipRole.NoAccess:
return "No Access";
case ProjectMembershipRole.Custom:
return customRoleName ?? role;
case ProjectMembershipRole.SshHostBootstrapper:
return "SSH Host Bootstrapper";
case ProjectMembershipRole.KmsCryptographicOperator:
return "Cryptographic Operator";
default:
return role;
}
};

View File

@@ -3,6 +3,7 @@ export {
useAdminGrantServerAdminAccess,
useAdminRemoveIdentitySuperAdminAccess,
useCreateAdminUser,
useInvalidateCache,
useRemoveUserServerAdminAccess,
useUpdateServerConfig,
useUpdateServerEncryptionStrategy

View File

@@ -8,6 +8,7 @@ import { adminQueryKeys, adminStandaloneKeys } from "./queries";
import {
RootKeyEncryptionStrategy,
TCreateAdminUserDTO,
TInvalidateCacheDTO,
TServerConfig,
TUpdateServerConfigDTO
} from "./types";
@@ -126,3 +127,15 @@ export const useUpdateServerEncryptionStrategy = () => {
}
});
};
export const useInvalidateCache = () => {
const queryClient = useQueryClient();
return useMutation<void, object, TInvalidateCacheDTO>({
mutationFn: async (dto) => {
await apiRequest.post("/api/v1/admin/invalidate-cache", dto);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: adminQueryKeys.getInvalidateCache() });
}
});
};

View File

@@ -8,6 +8,7 @@ import {
AdminGetIdentitiesFilters,
AdminGetUsersFilters,
AdminIntegrationsConfig,
TGetInvalidatingCacheStatus,
TGetServerRootKmsEncryptionDetails,
TServerConfig
} from "./types";
@@ -22,8 +23,10 @@ export const adminQueryKeys = {
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
getIdentities: (filters: AdminGetIdentitiesFilters) =>
[adminStandaloneKeys.getIdentities, { filters }] as const,
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
getAdminSlackConfig: () => ["admin-slack-config"] as const,
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
getInvalidateCache: () => ["admin-invalidate-cache"] as const,
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const
};
export const fetchServerConfig = async () => {
@@ -118,3 +121,18 @@ export const useGetServerRootKmsEncryptionDetails = () => {
}
});
};
export const useGetInvalidatingCacheStatus = (enabled = true) => {
return useQuery({
queryKey: adminQueryKeys.getInvalidateCache(),
queryFn: async () => {
const { data } = await apiRequest.get<TGetInvalidatingCacheStatus>(
"/api/v1/admin/invalidating-cache-status"
);
return data.invalidating;
},
enabled,
refetchInterval: (data) => (data ? 3000 : false)
});
};

View File

@@ -24,6 +24,7 @@ export type TServerConfig = {
enabledLoginMethods: LoginMethod[];
authConsentContent?: string;
pageFrameContent?: string;
invalidatingCache: boolean;
};
export type TUpdateServerConfigDTO = {
@@ -84,3 +85,16 @@ export enum RootKeyEncryptionStrategy {
Software = "SOFTWARE",
HSM = "HSM"
}
export enum CacheType {
ALL = "all",
SECRETS = "secrets"
}
export type TInvalidateCacheDTO = {
type: CacheType;
};
export type TGetInvalidatingCacheStatus = {
invalidating: boolean;
};

View File

@@ -6,14 +6,17 @@ import {
TProjectTemplate,
TProjectTemplateResponse
} from "@app/hooks/api/projectTemplates/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const projectTemplateKeys = {
all: ["project-template"] as const,
list: () => [...projectTemplateKeys.all, "list"] as const,
list: (projectType?: ProjectType) =>
[...projectTemplateKeys.all, "list", ...(projectType ? [projectType] : [])] as const,
byId: (templateId: string) => [...projectTemplateKeys.all, templateId] as const
};
export const useListProjectTemplates = (
type?: ProjectType,
options?: Omit<
UseQueryOptions<
TProjectTemplate[],
@@ -25,9 +28,11 @@ export const useListProjectTemplates = (
>
) => {
return useQuery({
queryKey: projectTemplateKeys.list(),
queryKey: projectTemplateKeys.list(type),
queryFn: async () => {
const { data } = await apiRequest.get<TListProjectTemplates>("/api/v1/project-templates");
const { data } = await apiRequest.get<TListProjectTemplates>("/api/v1/project-templates", {
params: { type }
});
return data.projectTemplates;
},

View File

@@ -1,11 +1,13 @@
import { TProjectRole } from "@app/hooks/api/roles/types";
import { ProjectType } from "@app/hooks/api/workspace/types";
export type TProjectTemplate = {
id: string;
name: string;
type: ProjectType;
description?: string;
roles: Pick<TProjectRole, "slug" | "name" | "permissions">[];
environments: { name: string; slug: string; position: number }[];
environments?: { name: string; slug: string; position: number }[] | null;
createdAt: string;
updatedAt: string;
};
@@ -14,6 +16,7 @@ export type TListProjectTemplates = { projectTemplates: TProjectTemplate[] };
export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate };
export type TCreateProjectTemplateDTO = {
type: ProjectType;
name: string;
description?: string;
};

View File

@@ -3,7 +3,9 @@ export enum ProjectMembershipRole {
Member = "member",
Custom = "custom",
Viewer = "viewer",
NoAccess = "no-access"
NoAccess = "no-access",
SshHostBootstrapper = "ssh-host-bootstrapper",
KmsCryptographicOperator = "cryptographic-operator"
}
export type TGetProjectRolesDTO = {

View File

@@ -1,4 +1,5 @@
export { useDebounce } from "./useDebounce";
export * from "./useGetProjectTypeFromRoute";
export { usePagination } from "./usePagination";
export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp";

View File

@@ -0,0 +1,22 @@
import { useMemo } from "react";
import { useRouterState } from "@tanstack/react-router";
import { ProjectType } from "@app/hooks/api/workspace/types";
export const useGetProjectTypeFromRoute = () => {
const { location } = useRouterState();
return useMemo(() => {
const segments = location.pathname.split("/");
let type: ProjectType | undefined;
// location of project type can vary in router path, so we need to check all possible values
segments.forEach((segment) => {
if (Object.values(ProjectType).includes(segment as ProjectType))
type = segment as ProjectType;
});
return type;
}, [location]);
};

View File

@@ -11,11 +11,12 @@ import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2";
import { OrgPermissionSubjects, useOrgPermission, useServerConfig } from "@app/context";
import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types";
import { usePopUp } from "@app/hooks";
import { ProjectType } from "@app/hooks/api/workspace/types";
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
import { SidebarHeader } from "./components/SidebarHeader";
import { DefaultSideBar, SecretSharingSideBar } from "./ProductsSideBar";
import { DefaultSideBar, ProjectOverviewSideBar, SecretSharingSideBar } from "./ProductsSideBar";
export const OrganizationLayout = () => {
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
@@ -45,21 +46,38 @@ export const OrganizationLayout = () => {
] as string[]
).includes(location.pathname);
const isProjectOverviewOrSettingsPage = (
[
linkOptions({ to: "/organization/secret-manager/overview" }).to,
linkOptions({ to: "/organization/secret-manager/settings" }).to,
linkOptions({ to: "/organization/cert-manager/overview" }).to,
linkOptions({ to: "/organization/cert-manager/settings" }).to,
linkOptions({ to: "/organization/kms/overview" }).to,
linkOptions({ to: "/organization/kms/settings" }).to,
linkOptions({ to: "/organization/ssh/overview" }).to,
linkOptions({ to: "/organization/ssh/settings" }).to
] as string[]
).includes(location.pathname);
const shouldShowOrgSidebar =
location.pathname.startsWith("/organization") &&
(!isSecretSharingPage || shouldShowProductsSidebar) &&
!(
[
linkOptions({ to: "/organization/secret-manager/overview" }).to,
linkOptions({ to: "/organization/cert-manager/overview" }).to,
linkOptions({ to: "/organization/ssh/overview" }).to,
linkOptions({ to: "/organization/kms/overview" }).to,
linkOptions({ to: "/organization/secret-scanning" }).to
] as string[]
).includes(location.pathname);
!([linkOptions({ to: "/organization/secret-scanning" }).to] as string[]).includes(
location.pathname
);
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
let SideBarComponent = <DefaultSideBar />;
if (isSecretSharingPage) {
SideBarComponent = <SecretSharingSideBar />;
} else if (isProjectOverviewOrSettingsPage) {
SideBarComponent = (
<ProjectOverviewSideBar type={location.pathname.split("/")[2] as ProjectType} />
);
}
return (
<>
<Banner />
@@ -80,10 +98,12 @@ export const OrganizationLayout = () => {
className="dark w-60 overflow-hidden border-r border-mineshaft-600 bg-gradient-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
<div className="p-2 pt-3">
<SidebarHeader />
</div>
{isSecretSharingPage ? <SecretSharingSideBar /> : <DefaultSideBar />}
{!isProjectOverviewOrSettingsPage && (
<div className="p-2 pt-3">
<SidebarHeader />
</div>
)}
{SideBarComponent}
</nav>
</motion.div>
)}

View File

@@ -0,0 +1,94 @@
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useMatchRoute } from "@tanstack/react-router";
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { ProjectType } from "@app/hooks/api/workspace/types";
type TProjectOverviewSideBarProps = {
type: ProjectType;
};
export const ProjectOverviewSideBar = ({ type }: TProjectOverviewSideBarProps) => {
const matchRoute = useMatchRoute();
const isOverviewActive = !!matchRoute({
to: `/organization/${type}/overview`,
fuzzy: false
});
let label: string;
let icon: string;
let link: string;
switch (type) {
case ProjectType.CertificateManager:
label = "Cert Management";
icon = "note";
link = "https://infisical.com/docs/documentation/platform/pki/overview";
break;
case ProjectType.SecretManager:
label = "Secret Management";
icon = "sliding-carousel";
link = "https://infisical.com/docs/documentation/getting-started/introduction";
break;
case ProjectType.KMS:
label = "KMS";
icon = "unlock";
link = "https://infisical.com/docs/documentation/platform/kms/overview";
break;
case ProjectType.SSH:
label = "SSH";
icon = "verified";
link = "https://infisical.com/docs/documentation/platform/ssh/overview";
break;
default:
throw new Error("Unknown project type");
}
return (
<>
<div className="p-2 pt-3">
<a
target="_blank"
rel="noopener noreferrer"
href={link}
className="flex w-full items-center rounded-md border border-mineshaft-600 p-2 pl-3 transition-all duration-150 hover:bg-mineshaft-700"
>
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-mineshaft-500 p-4">
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="h-3.5 w-3.5 text-mineshaft-300"
/>
</div>
<div className="-mt-1 flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
Infisical Docs
</div>
<div className="text-xs leading-[10px] text-mineshaft-400">
{type === ProjectType.SecretManager ? "Get Started" : `${label} Overview`}
</div>
</div>
</a>
</div>
<Menu>
<MenuGroup title="Overview">
<Link to={`/organization/${type}/overview`}>
<MenuItem isSelected={isOverviewActive} icon={icon}>
{label}
</MenuItem>
</Link>
</MenuGroup>
<MenuGroup title="Other">
<Link to={`/organization/${type}/settings`}>
{({ isActive }) => (
<MenuItem isSelected={isActive} icon="toggle-settings">
Settings
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</>
);
};

View File

@@ -1,2 +1,3 @@
export * from "./DefaultSideBar";
export * from "./ProjectOverviewSideBar";
export * from "./SecretSharingSideBar";

View File

@@ -269,7 +269,9 @@ export const MinimizedOrgSidebar = () => {
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/${ProjectType.SecretManager}`)
window.location.pathname.startsWith(
`/organization/${ProjectType.SecretManager}`
)
}
icon="sliding-carousel"
>
@@ -282,7 +284,9 @@ export const MinimizedOrgSidebar = () => {
<MenuIconButton
isSelected={
isActive ||
window.location.pathname.startsWith(`/${ProjectType.CertificateManager}`)
window.location.pathname.startsWith(
`/organization/${ProjectType.CertificateManager}`
)
}
icon="note"
>
@@ -294,7 +298,8 @@ export const MinimizedOrgSidebar = () => {
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive || window.location.pathname.startsWith(`/${ProjectType.KMS}`)
isActive ||
window.location.pathname.startsWith(`/organization/${ProjectType.KMS}`)
}
icon="unlock"
>
@@ -306,7 +311,8 @@ export const MinimizedOrgSidebar = () => {
{({ isActive }) => (
<MenuIconButton
isSelected={
isActive || window.location.pathname.startsWith(`/${ProjectType.SSH}`)
isActive ||
window.location.pathname.startsWith(`/organization/${ProjectType.SSH}`)
}
icon="verified"
>

View File

@@ -31,6 +31,7 @@ import {
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
import { AuthPanel } from "./components/AuthPanel";
import { CachingPanel } from "./components/CachingPanel";
import { EncryptionPanel } from "./components/EncryptionPanel";
import { IntegrationPanel } from "./components/IntegrationPanel";
import { UserPanel } from "./components/UserPanel";
@@ -42,7 +43,8 @@ enum TabSections {
Integrations = "integrations",
Users = "users",
Identities = "identities",
Kmip = "kmip"
Kmip = "kmip",
Caching = "caching"
}
enum SignUpModes {
@@ -164,6 +166,7 @@ export const OverviewPage = () => {
<Tab value={TabSections.Integrations}>Integrations</Tab>
<Tab value={TabSections.Users}>User Identities</Tab>
<Tab value={TabSections.Identities}>Machine Identities</Tab>
<Tab value={TabSections.Caching}>Caching</Tab>
</div>
</TabList>
<TabPanel value={TabSections.Settings}>
@@ -408,6 +411,9 @@ export const OverviewPage = () => {
<TabPanel value={TabSections.Identities}>
<IdentityPanel />
</TabPanel>
<TabPanel value={TabSections.Caching}>
<CachingPanel />
</TabPanel>
</Tabs>
</div>
)}

View File

@@ -0,0 +1,101 @@
import { useEffect, useState } from "react";
import { faRotate } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Badge, Button, DeleteActionModal } from "@app/components/v2";
import { useUser } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useInvalidateCache } from "@app/hooks/api";
import { useGetInvalidatingCacheStatus } from "@app/hooks/api/admin/queries";
import { CacheType } from "@app/hooks/api/admin/types";
export const CachingPanel = () => {
const { mutateAsync: invalidateCache } = useInvalidateCache();
const { user } = useUser();
const [type, setType] = useState<CacheType | null>(null);
const [shouldPoll, setShouldPoll] = useState(false);
const {
data: invalidationStatus,
isFetching,
refetch
} = useGetInvalidatingCacheStatus(shouldPoll);
const isInvalidating = Boolean(shouldPoll && (isFetching || invalidationStatus));
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"invalidateCache"
] as const);
const handleInvalidateCacheSubmit = async () => {
if (!type || isInvalidating) return;
try {
await invalidateCache({ type });
createNotification({ text: `Began invalidating ${type} cache`, type: "success" });
setShouldPoll(true);
handlePopUpClose("invalidateCache");
} catch (err) {
console.error(err);
createNotification({ text: `Failed to invalidate ${type} cache`, type: "error" });
}
};
useEffect(() => {
if (isInvalidating) return;
if (shouldPoll) {
setShouldPoll(false);
createNotification({ text: "Successfully invalidated cache", type: "success" });
}
}, [isInvalidating, shouldPoll]);
useEffect(() => {
refetch().then((v) => setShouldPoll(v.data || false));
}, []);
return (
<>
<div className="mb-6 flex flex-wrap items-end justify-between gap-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex flex-col">
<div className="mb-2 flex items-center gap-3">
<span className="text-xl font-semibold text-mineshaft-100">Secrets Cache</span>
{isInvalidating && (
<Badge
variant="danger"
className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap"
>
<FontAwesomeIcon icon={faRotate} className="animate-spin" />
Invalidating Cache
</Badge>
)}
</div>
<span className="max-w-xl text-sm text-mineshaft-400">
The encrypted secrets cache encompasses all secrets stored within the system and
provides a temporary, secure storage location for frequently accessed credentials.
</span>
</div>
<Button
colorSchema="danger"
onClick={() => {
setType(CacheType.SECRETS);
handlePopUpOpen("invalidateCache");
}}
isDisabled={!user.superAdmin || isInvalidating}
>
Invalidate Secrets Cache
</Button>
</div>
<DeleteActionModal
isOpen={popUp.invalidateCache.isOpen}
title={`Are you sure you want to invalidate ${type} cache?`}
subTitle="This action is permanent and irreversible. The cache invalidation process may take several minutes to complete."
onChange={(isOpen) => handlePopUpToggle("invalidateCache", isOpen)}
deleteKey="confirm"
onDeleteApproved={handleInvalidateCacheSubmit}
/>
</>
);
};

View File

@@ -0,0 +1,20 @@
import { Helmet } from "react-helmet";
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
import { PageHeader } from "@app/components/v2";
export const CertManagerSettingsPage = () => {
return (
<>
<Helmet>
<title>Cert Management Settings</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="Cert Management Settings" />
<ProjectSettings />
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,26 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { CertManagerSettingsPage } from "./CertManagerSettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/cert-manager/settings"
)({
component: CertManagerSettingsPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Cert Management",
link: linkOptions({ to: "/organization/cert-manager/overview" })
},
{
label: "Settings"
}
]
})
});

View File

@@ -6,9 +6,9 @@ import { format } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2";
import { formatProjectRoleName } from "@app/helpers/roles";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { IdentityMembership } from "@app/hooks/api/identities/types";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
export enum TabSections {
@@ -26,15 +26,6 @@ type Props = {
) => void;
};
const formatRoleName = (role: string, customRoleName?: string) => {
if (role === ProjectMembershipRole.Custom) return customRoleName;
if (role === ProjectMembershipRole.Admin) return "Admin";
if (role === ProjectMembershipRole.Member) return "Developer";
if (role === ProjectMembershipRole.Viewer) return "Viewer";
if (role === ProjectMembershipRole.NoAccess) return "No Access";
return role;
};
export const IdentityProjectRow = ({
membership: { id, createdAt, identity, project, roles },
handlePopUpOpen
@@ -80,7 +71,7 @@ export const IdentityProjectRow = ({
<Td>
<Tag size="xs">{project.type}</Tag>
</Td>
<Td>{`${formatRoleName(roles[0].role, roles[0].customRoleName)}${
<Td>{`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${
roles.length > 1 ? ` (+${roles.length - 1})` : ""
}`}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>

View File

@@ -0,0 +1,20 @@
import { Helmet } from "react-helmet";
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
import { PageHeader } from "@app/components/v2";
export const KmsSettingsPage = () => {
return (
<>
<Helmet>
<title>KMS Settings</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="KMS Settings" />
<ProjectSettings />
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,26 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { KmsSettingsPage } from "./KmsSettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/kms/settings"
)({
component: KmsSettingsPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "KMS",
link: linkOptions({ to: "/organization/kms/overview" })
},
{
label: "Settings"
}
]
})
});

View File

@@ -117,12 +117,8 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
});
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text =
error?.response?.data?.message ??
`Failed to ${popUp?.role?.data ? "update" : "create"} role`;
} catch {
const text = `Failed to ${popUp?.role?.data ? "update" : "create"} role`;
createNotification({
text,

View File

@@ -27,6 +27,7 @@ export const ProjectListToggle = ({ value, onChange }: Props) => {
value={value}
onValueChange={onChange}
className="absolute left-0 top-0 h-full w-full cursor-pointer opacity-0"
dropdownContainerClassName="mt-10 ml-5"
>
<SelectItem value={ProjectListView.MyProjects}>My Projects</SelectItem>
<SelectItem value={ProjectListView.AllProjects}>All Projects</SelectItem>

View File

@@ -0,0 +1,20 @@
import { Helmet } from "react-helmet";
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
import { PageHeader } from "@app/components/v2";
export const SecretManagerSettingsPage = () => {
return (
<>
<Helmet>
<title>Secret Management Settings</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="Secret Management Settings" />
<ProjectSettings />
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,26 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SecretManagerSettingsPage } from "./SecretManagerSettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/secret-manager/settings"
)({
component: SecretManagerSettingsPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "Secret Management",
link: linkOptions({ to: "/organization/secret-manager/overview" })
},
{
label: "Settings"
}
]
})
});

View File

@@ -2,6 +2,7 @@ import { useState } from "react";
import { useSearch } from "@tanstack/react-router";
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
import { ROUTE_PATHS } from "@app/const/routes";
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
@@ -11,7 +12,6 @@ import { OrgAuthTab } from "../OrgAuthTab";
import { OrgEncryptionTab } from "../OrgEncryptionTab";
import { OrgGeneralTab } from "../OrgGeneralTab";
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab";
import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
export const OrgTabGroup = () => {
const search = useSearch({
@@ -28,7 +28,34 @@ export const OrgTabGroup = () => {
},
{ name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab },
{ name: "Import", key: "tab-import", component: ImportTab },
{ name: "Project Templates", key: "project-templates", component: ProjectTemplatesTab },
{
name: "Project Templates",
key: "project-templates",
// scott: temporary, remove once users have adjusted
// eslint-disable-next-line react/no-unstable-nested-components
component: () => (
<div>
<NoticeBannerV2
className="mx-auto mt-10 max-w-4xl"
title="Project Templates New Location"
>
<p className="text-sm text-bunker-200">
Project templates have been moved to the Feature Settings page, under the &#34;Project
Templates&#34; tab.
</p>
<p className="mt-2 text-sm text-bunker-200">
Project templates are now product-specific, and can be configured for each project
type.
</p>
<img
src="/images/project-templates/project-templates-new-location.png"
className="mt-4 w-full max-w-4xl rounded"
alt="Project Templates New Location"
/>
</NoticeBannerV2>
</div>
)
},
{ name: "KMIP", key: "kmip", component: KmipTab }
];

View File

@@ -0,0 +1,20 @@
import { Helmet } from "react-helmet";
import { ProjectSettings } from "@app/components/projects/ProjectSettings";
import { PageHeader } from "@app/components/v2";
export const SshSettingsPage = () => {
return (
<>
<Helmet>
<title>SSH Settings</title>
</Helmet>
<div className="flex w-full justify-center bg-bunker-800 text-white">
<div className="w-full max-w-7xl">
<PageHeader title="SSH Settings" />
<ProjectSettings />
</div>
</div>
</>
);
};

View File

@@ -0,0 +1,26 @@
import { faHome } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createFileRoute, linkOptions } from "@tanstack/react-router";
import { SshSettingsPage } from "./SshSettingsPage";
export const Route = createFileRoute(
"/_authenticate/_inject-org-details/_org-layout/organization/ssh/settings"
)({
component: SshSettingsPage,
context: () => ({
breadcrumbs: [
{
label: "Products",
icon: () => <FontAwesomeIcon icon={faHome} />
},
{
label: "SSH",
link: linkOptions({ to: "/organization/ssh/overview" })
},
{
label: "Settings"
}
]
})
});

View File

@@ -5,8 +5,8 @@ import { useNavigate } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2";
import { formatProjectRoleName } from "@app/helpers/roles";
import { useGetUserWorkspaces } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { TWorkspaceUser } from "@app/hooks/api/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { OrgAccessControlTabSections } from "@app/types/org";
@@ -19,15 +19,6 @@ type Props = {
) => void;
};
const formatRoleName = (role: string, customRoleName?: string) => {
if (role === ProjectMembershipRole.Custom) return customRoleName;
if (role === ProjectMembershipRole.Admin) return "Admin";
if (role === ProjectMembershipRole.Member) return "Developer";
if (role === ProjectMembershipRole.Viewer) return "Viewer";
if (role === ProjectMembershipRole.NoAccess) return "No Access";
return role;
};
export const UserProjectRow = ({
membership: { id, project, user, roles },
handlePopUpOpen
@@ -73,7 +64,7 @@ export const UserProjectRow = ({
<Td>
<Tag size="xs">{project.type}</Tag>
</Td>
<Td>{`${formatRoleName(roles[0].role, roles[0].customRoleName)}${
<Td>{`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${
roles.length > 1 ? ` (+${roles.length - 1})` : ""
}`}</Td>
<Td>

Some files were not shown because too many files have changed in this diff Show More