Merge branch 'main' into ENG-2636
@@ -171,6 +171,7 @@ ENV NODE_ENV production
|
|||||||
ENV STANDALONE_BUILD true
|
ENV STANDALONE_BUILD true
|
||||||
ENV STANDALONE_MODE true
|
ENV STANDALONE_MODE true
|
||||||
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
ENV ChrystokiConfigurationPath=/usr/safenet/lunaclient/
|
||||||
|
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||||
|
|
||||||
WORKDIR /backend
|
WORKDIR /backend
|
||||||
|
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ ENV HTTPS_ENABLED false
|
|||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
ENV STANDALONE_BUILD true
|
ENV STANDALONE_BUILD true
|
||||||
ENV STANDALONE_MODE true
|
ENV STANDALONE_MODE true
|
||||||
|
ENV NODE_OPTIONS="--max-old-space-size=1024"
|
||||||
|
|
||||||
WORKDIR /backend
|
WORKDIR /backend
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import RE2 from "re2";
|
||||||
|
|
||||||
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
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";
|
import { Lock } from "@app/lib/red-lock";
|
||||||
|
|
||||||
export const mockKeyStore = (): TKeyStoreFactory => {
|
export const mockKeyStore = (): TKeyStoreFactory => {
|
||||||
@@ -18,6 +22,27 @@ export const mockKeyStore = (): TKeyStoreFactory => {
|
|||||||
delete store[key];
|
delete store[key];
|
||||||
return 1;
|
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) => {
|
getItem: async (key) => {
|
||||||
const value = store[key];
|
const value = store[key];
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -185,11 +185,16 @@ export enum OrgMembershipStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum ProjectMembershipRole {
|
export enum ProjectMembershipRole {
|
||||||
|
// general
|
||||||
Admin = "admin",
|
Admin = "admin",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
Custom = "custom",
|
Custom = "custom",
|
||||||
Viewer = "viewer",
|
Viewer = "viewer",
|
||||||
NoAccess = "no-access"
|
NoAccess = "no-access",
|
||||||
|
// ssh
|
||||||
|
SshHostBootstrapper = "ssh-host-bootstrapper",
|
||||||
|
// kms
|
||||||
|
KmsCryptographicOperator = "cryptographic-operator"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SecretEncryptionAlgo {
|
export enum SecretEncryptionAlgo {
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ export const ProjectTemplatesSchema = z.object({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string().nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
roles: z.unknown(),
|
roles: z.unknown(),
|
||||||
environments: z.unknown(),
|
environments: z.unknown().nullable().optional(),
|
||||||
orgId: z.string().uuid(),
|
orgId: z.string().uuid(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date(),
|
||||||
|
type: z.string().default("secret-manager")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TProjectTemplates = z.infer<typeof ProjectTemplatesSchema>;
|
export type TProjectTemplates = z.infer<typeof ProjectTemplatesSchema>;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { z } from "zod";
|
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 { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
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 { isInfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
|
||||||
import { ApiDocsTags, ProjectTemplates } from "@app/lib/api-docs";
|
import { ApiDocsTags, ProjectTemplates } from "@app/lib/api-docs";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
@@ -35,6 +34,7 @@ const SanitizedProjectTemplateSchema = ProjectTemplatesSchema.extend({
|
|||||||
position: z.number().min(1)
|
position: z.number().min(1)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
|
.nullable()
|
||||||
});
|
});
|
||||||
|
|
||||||
const ProjectTemplateRolesSchema = z
|
const ProjectTemplateRolesSchema = z
|
||||||
@@ -104,6 +104,9 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
hide: false,
|
hide: false,
|
||||||
tags: [ApiDocsTags.ProjectTemplates],
|
tags: [ApiDocsTags.ProjectTemplates],
|
||||||
description: "List project templates for the current organization.",
|
description: "List project templates for the current organization.",
|
||||||
|
querystring: z.object({
|
||||||
|
type: z.nativeEnum(ProjectType).optional().describe(ProjectTemplates.LIST.type)
|
||||||
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
projectTemplates: SanitizedProjectTemplateSchema.array()
|
projectTemplates: SanitizedProjectTemplateSchema.array()
|
||||||
@@ -112,7 +115,8 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
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));
|
const auditTemplates = projectTemplates.filter((template) => !isInfisicalProjectTemplate(template.name));
|
||||||
|
|
||||||
@@ -184,6 +188,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
tags: [ApiDocsTags.ProjectTemplates],
|
tags: [ApiDocsTags.ProjectTemplates],
|
||||||
description: "Create a project template.",
|
description: "Create a project template.",
|
||||||
body: z.object({
|
body: z.object({
|
||||||
|
type: z.nativeEnum(ProjectType).describe(ProjectTemplates.CREATE.type),
|
||||||
name: slugSchema({ field: "name" })
|
name: slugSchema({ field: "name" })
|
||||||
.refine((val) => !isInfisicalProjectTemplate(val), {
|
.refine((val) => !isInfisicalProjectTemplate(val), {
|
||||||
message: `The requested project template name is reserved.`
|
message: `The requested project template name is reserved.`
|
||||||
@@ -191,9 +196,7 @@ export const registerProjectTemplateRouter = async (server: FastifyZodProvider)
|
|||||||
.describe(ProjectTemplates.CREATE.name),
|
.describe(ProjectTemplates.CREATE.name),
|
||||||
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
|
description: z.string().max(256).trim().optional().describe(ProjectTemplates.CREATE.description),
|
||||||
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
|
roles: ProjectTemplateRolesSchema.default([]).describe(ProjectTemplates.CREATE.roles),
|
||||||
environments: ProjectTemplateEnvironmentsSchema.default(ProjectTemplateDefaultEnvironments).describe(
|
environments: ProjectTemplateEnvironmentsSchema.describe(ProjectTemplates.CREATE.environments).optional()
|
||||||
ProjectTemplates.CREATE.environments
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -12,6 +12,14 @@ import {
|
|||||||
TIdentityProjectMemberships,
|
TIdentityProjectMemberships,
|
||||||
TProjectMemberships
|
TProjectMemberships
|
||||||
} from "@app/db/schemas";
|
} 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 { conditionsMatcher } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
import { objectify } from "@app/lib/fn";
|
import { objectify } from "@app/lib/fn";
|
||||||
@@ -32,14 +40,7 @@ import {
|
|||||||
TGetServiceTokenProjectPermissionArg,
|
TGetServiceTokenProjectPermissionArg,
|
||||||
TGetUserProjectPermissionArg
|
TGetUserProjectPermissionArg
|
||||||
} from "./permission-service-types";
|
} from "./permission-service-types";
|
||||||
import {
|
import { buildServiceTokenProjectPermission, ProjectPermissionSet } from "./project-permission";
|
||||||
buildServiceTokenProjectPermission,
|
|
||||||
projectAdminPermissions,
|
|
||||||
projectMemberPermissions,
|
|
||||||
projectNoAccessPermissions,
|
|
||||||
ProjectPermissionSet,
|
|
||||||
projectViewerPermission
|
|
||||||
} from "./project-permission";
|
|
||||||
|
|
||||||
type TPermissionServiceFactoryDep = {
|
type TPermissionServiceFactoryDep = {
|
||||||
orgRoleDAL: Pick<TOrgRoleDALFactory, "findOne">;
|
orgRoleDAL: Pick<TOrgRoleDALFactory, "findOne">;
|
||||||
@@ -95,6 +96,10 @@ export const permissionServiceFactory = ({
|
|||||||
return projectViewerPermission;
|
return projectViewerPermission;
|
||||||
case ProjectMembershipRole.NoAccess:
|
case ProjectMembershipRole.NoAccess:
|
||||||
return projectNoAccessPermissions;
|
return projectNoAccessPermissions;
|
||||||
|
case ProjectMembershipRole.SshHostBootstrapper:
|
||||||
|
return sshHostBootstrapPermissions;
|
||||||
|
case ProjectMembershipRole.KmsCryptographicOperator:
|
||||||
|
return cryptographicOperatorPermissions;
|
||||||
case ProjectMembershipRole.Custom: {
|
case ProjectMembershipRole.Custom: {
|
||||||
return unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
|
return unpackRules<RawRuleOf<MongoAbility<ProjectPermissionSet>>>(
|
||||||
permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]
|
permissions as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[]
|
||||||
|
|||||||
@@ -678,403 +678,6 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
|
|||||||
|
|
||||||
export type TProjectPermissionV2Schema = z.infer<typeof ProjectPermissionV2Schema>;
|
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 = (
|
export const buildServiceTokenProjectPermission = (
|
||||||
scopes: Array<{ secretPath: string; environment: string }>,
|
scopes: Array<{ secretPath: string; environment: string }>,
|
||||||
permission: string[]
|
permission: string[]
|
||||||
@@ -1116,8 +719,6 @@ export const buildServiceTokenProjectPermission = (
|
|||||||
return build({ conditionsMatcher });
|
return build({ conditionsMatcher });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const projectNoAccessPermissions = buildNoAccessProjectPermission();
|
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
|
import { ProjectType } from "@app/db/schemas";
|
||||||
import {
|
import {
|
||||||
InfisicalProjectTemplate,
|
InfisicalProjectTemplate,
|
||||||
TUnpackedPermission
|
TUnpackedPermission
|
||||||
} from "@app/ee/services/project-template/project-template-types";
|
} from "@app/ee/services/project-template/project-template-types";
|
||||||
import { getPredefinedRoles } from "@app/services/project-role/project-role-fns";
|
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
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // random ID to appease zod
|
||||||
|
type,
|
||||||
name: InfisicalProjectTemplate.Default,
|
name: InfisicalProjectTemplate.Default,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
description: "Infisical's default project template",
|
description: `Infisical's ${type} default project template`,
|
||||||
environments: ProjectTemplateDefaultEnvironments,
|
environments: type === ProjectType.SecretManager ? ProjectTemplateDefaultEnvironments : null,
|
||||||
roles: [...getPredefinedRoles("project-template")].map(({ name, slug, permissions }) => ({
|
roles: [...getPredefinedRoles({ projectId: "project-template", projectType: type })].map(
|
||||||
name,
|
({ name, slug, permissions }) => ({
|
||||||
slug,
|
name,
|
||||||
permissions: permissions as TUnpackedPermission[]
|
slug,
|
||||||
})),
|
permissions: permissions as TUnpackedPermission[]
|
||||||
|
})
|
||||||
|
),
|
||||||
orgId
|
orgId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import { packRules } from "@casl/ability/extra";
|
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 { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { ProjectTemplateDefaultEnvironments } from "@app/ee/services/project-template/project-template-constants";
|
||||||
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
|
import { getDefaultProjectTemplate } from "@app/ee/services/project-template/project-template-fns";
|
||||||
import {
|
import {
|
||||||
TCreateProjectTemplateDTO,
|
TCreateProjectTemplateDTO,
|
||||||
@@ -32,11 +33,13 @@ const $unpackProjectTemplate = ({ roles, environments, ...rest }: TProjectTempla
|
|||||||
...rest,
|
...rest,
|
||||||
environments: environments as TProjectTemplateEnvironment[],
|
environments: environments as TProjectTemplateEnvironment[],
|
||||||
roles: [
|
roles: [
|
||||||
...getPredefinedRoles("project-template").map(({ name, slug, permissions }) => ({
|
...getPredefinedRoles({ projectId: "project-template", projectType: rest.type as ProjectType }).map(
|
||||||
name,
|
({ name, slug, permissions }) => ({
|
||||||
slug,
|
name,
|
||||||
permissions: permissions as TUnpackedPermission[]
|
slug,
|
||||||
})),
|
permissions: permissions as TUnpackedPermission[]
|
||||||
|
})
|
||||||
|
),
|
||||||
...(roles as TProjectTemplateRole[]).map((role) => ({
|
...(roles as TProjectTemplateRole[]).map((role) => ({
|
||||||
...role,
|
...role,
|
||||||
permissions: unpackPermissions(role.permissions)
|
permissions: unpackPermissions(role.permissions)
|
||||||
@@ -49,7 +52,7 @@ export const projectTemplateServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
projectTemplateDAL
|
projectTemplateDAL
|
||||||
}: TProjectTemplatesServiceFactoryDep) => {
|
}: TProjectTemplatesServiceFactoryDep) => {
|
||||||
const listProjectTemplatesByOrg = async (actor: OrgServiceActor) => {
|
const listProjectTemplatesByOrg = async (actor: OrgServiceActor, type?: ProjectType) => {
|
||||||
const plan = await licenseService.getPlan(actor.orgId);
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
|
|
||||||
if (!plan.projectTemplates)
|
if (!plan.projectTemplates)
|
||||||
@@ -68,11 +71,14 @@ export const projectTemplateServiceFactory = ({
|
|||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||||
|
|
||||||
const projectTemplates = await projectTemplateDAL.find({
|
const projectTemplates = await projectTemplateDAL.find({
|
||||||
orgId: actor.orgId
|
orgId: actor.orgId,
|
||||||
|
...(type ? { type } : {})
|
||||||
});
|
});
|
||||||
|
|
||||||
return [
|
return [
|
||||||
getDefaultProjectTemplate(actor.orgId),
|
...(type
|
||||||
|
? [getDefaultProjectTemplate(actor.orgId, type)]
|
||||||
|
: Object.values(ProjectType).map((projectType) => getDefaultProjectTemplate(actor.orgId, projectType))),
|
||||||
...projectTemplates.map((template) => $unpackProjectTemplate(template))
|
...projectTemplates.map((template) => $unpackProjectTemplate(template))
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -134,7 +140,7 @@ export const projectTemplateServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createProjectTemplate = async (
|
const createProjectTemplate = async (
|
||||||
{ roles, environments, ...params }: TCreateProjectTemplateDTO,
|
{ roles, environments, type, ...params }: TCreateProjectTemplateDTO,
|
||||||
actor: OrgServiceActor
|
actor: OrgServiceActor
|
||||||
) => {
|
) => {
|
||||||
const plan = await licenseService.getPlan(actor.orgId);
|
const plan = await licenseService.getPlan(actor.orgId);
|
||||||
@@ -154,6 +160,17 @@ export const projectTemplateServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
|
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(
|
const isConflictingName = Boolean(
|
||||||
await projectTemplateDAL.findOne({
|
await projectTemplateDAL.findOne({
|
||||||
name: params.name,
|
name: params.name,
|
||||||
@@ -169,8 +186,10 @@ export const projectTemplateServiceFactory = ({
|
|||||||
const projectTemplate = await projectTemplateDAL.create({
|
const projectTemplate = await projectTemplateDAL.create({
|
||||||
...params,
|
...params,
|
||||||
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
|
roles: JSON.stringify(roles.map((role) => ({ ...role, permissions: packRules(role.permissions) }))),
|
||||||
environments: JSON.stringify(environments),
|
environments:
|
||||||
orgId: actor.orgId
|
type === ProjectType.SecretManager ? JSON.stringify(environments ?? ProjectTemplateDefaultEnvironments) : null,
|
||||||
|
orgId: actor.orgId,
|
||||||
|
type
|
||||||
});
|
});
|
||||||
|
|
||||||
return $unpackProjectTemplate(projectTemplate);
|
return $unpackProjectTemplate(projectTemplate);
|
||||||
@@ -202,6 +221,19 @@ export const projectTemplateServiceFactory = ({
|
|||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.ProjectTemplates);
|
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) {
|
if (params.name && projectTemplate.name !== params.name) {
|
||||||
const isConflictingName = Boolean(
|
const isConflictingName = Boolean(
|
||||||
await projectTemplateDAL.findOne({
|
await projectTemplateDAL.findOne({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
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 { TProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||||
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
import { UnpackedPermissionSchema } from "@app/server/routes/sanitizedSchema/permission";
|
||||||
|
|
||||||
@@ -15,8 +15,9 @@ export type TProjectTemplateRole = {
|
|||||||
export type TCreateProjectTemplateDTO = {
|
export type TCreateProjectTemplateDTO = {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
type: ProjectType;
|
||||||
roles: TProjectTemplateRole[];
|
roles: TProjectTemplateRole[];
|
||||||
environments: TProjectTemplateEnvironment[];
|
environments?: TProjectTemplateEnvironment[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>;
|
export type TUpdateProjectTemplateDTO = Partial<TCreateProjectTemplateDTO>;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
|
|
||||||
import { pgAdvisoryLockHashText } from "@app/lib/crypto/hashtext";
|
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";
|
import { Redlock, Settings } from "@app/lib/red-lock";
|
||||||
|
|
||||||
export const PgSqlLock = {
|
export const PgSqlLock = {
|
||||||
@@ -48,6 +50,13 @@ export const KeyStoreTtls = {
|
|||||||
AccessTokenStatusUpdateInSeconds: 120
|
AccessTokenStatusUpdateInSeconds: 120
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TDeleteItems = {
|
||||||
|
pattern: string;
|
||||||
|
batchSize?: number;
|
||||||
|
delay?: number;
|
||||||
|
jitter?: number;
|
||||||
|
};
|
||||||
|
|
||||||
type TWaitTillReady = {
|
type TWaitTillReady = {
|
||||||
key: string;
|
key: string;
|
||||||
waitingCb?: () => void;
|
waitingCb?: () => void;
|
||||||
@@ -75,6 +84,35 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
|
|
||||||
const deleteItem = async (key: string) => redis.del(key);
|
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 incrementBy = async (key: string, value: number) => redis.incrby(key, value);
|
||||||
|
|
||||||
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
|
const setExpiry = async (key: string, expiryInSeconds: number) => redis.expire(key, expiryInSeconds);
|
||||||
@@ -94,7 +132,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
waitingCb?.();
|
waitingCb?.();
|
||||||
setTimeout(resolve, Math.max(0, delay + Math.floor((Math.random() * 2 - 1) * jitter)));
|
setTimeout(resolve, Math.max(0, applyJitter(delay, jitter)));
|
||||||
});
|
});
|
||||||
attempts += 1;
|
attempts += 1;
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -108,6 +146,7 @@ export const keyStoreFactory = (redisUrl: string) => {
|
|||||||
setExpiry,
|
setExpiry,
|
||||||
setItemWithExpiry,
|
setItemWithExpiry,
|
||||||
deleteItem,
|
deleteItem,
|
||||||
|
deleteItems,
|
||||||
incrementBy,
|
incrementBy,
|
||||||
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
acquireLock(resources: string[], duration: number, settings?: Partial<Settings>) {
|
||||||
return redisLock.acquire(resources, duration, settings);
|
return redisLock.acquire(resources, duration, settings);
|
||||||
|
|||||||
@@ -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 { Lock } from "@app/lib/red-lock";
|
||||||
|
|
||||||
import { TKeyStoreFactory } from "./keystore";
|
import { TKeyStoreFactory } from "./keystore";
|
||||||
@@ -19,6 +23,27 @@ export const inMemoryKeyStore = (): TKeyStoreFactory => {
|
|||||||
delete store[key];
|
delete store[key];
|
||||||
return 1;
|
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) => {
|
getItem: async (key) => {
|
||||||
const value = store[key];
|
const value = store[key];
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
|
|||||||
@@ -1822,8 +1822,12 @@ export const KMS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectTemplates = {
|
export const ProjectTemplates = {
|
||||||
|
LIST: {
|
||||||
|
type: "The type of project template to list."
|
||||||
|
},
|
||||||
CREATE: {
|
CREATE: {
|
||||||
name: "The name of the project template to be created. Must be slug-friendly.",
|
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.",
|
description: "An optional description of the project template.",
|
||||||
roles: "The roles to be created when the template is applied to a project.",
|
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."
|
environments: "The environments to be created when the template is applied to a project."
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const delay = (ms: number) =>
|
||||||
|
new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
TQueueSecretSyncSyncSecretsByIdDTO,
|
TQueueSecretSyncSyncSecretsByIdDTO,
|
||||||
TQueueSendSecretSyncActionFailedNotificationsDTO
|
TQueueSendSecretSyncActionFailedNotificationsDTO
|
||||||
} from "@app/services/secret-sync/secret-sync-types";
|
} 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";
|
import { TWebhookPayloads } from "@app/services/webhook/webhook-types";
|
||||||
|
|
||||||
export enum QueueName {
|
export enum QueueName {
|
||||||
@@ -49,7 +50,8 @@ export enum QueueName {
|
|||||||
AccessTokenStatusUpdate = "access-token-status-update",
|
AccessTokenStatusUpdate = "access-token-status-update",
|
||||||
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
ImportSecretsFromExternalSource = "import-secrets-from-external-source",
|
||||||
AppConnectionSecretSync = "app-connection-secret-sync",
|
AppConnectionSecretSync = "app-connection-secret-sync",
|
||||||
SecretRotationV2 = "secret-rotation-v2"
|
SecretRotationV2 = "secret-rotation-v2",
|
||||||
|
InvalidateCache = "invalidate-cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum QueueJobs {
|
export enum QueueJobs {
|
||||||
@@ -81,7 +83,8 @@ export enum QueueJobs {
|
|||||||
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
SecretSyncSendActionFailedNotifications = "secret-sync-send-action-failed-notifications",
|
||||||
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
SecretRotationV2QueueRotations = "secret-rotation-v2-queue-rotations",
|
||||||
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
SecretRotationV2RotateSecrets = "secret-rotation-v2-rotate-secrets",
|
||||||
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification"
|
SecretRotationV2SendNotification = "secret-rotation-v2-send-notification",
|
||||||
|
InvalidateCache = "invalidate-cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TQueueJobTypes = {
|
export type TQueueJobTypes = {
|
||||||
@@ -234,6 +237,14 @@ export type TQueueJobTypes = {
|
|||||||
name: QueueJobs.SecretRotationV2SendNotification;
|
name: QueueJobs.SecretRotationV2SendNotification;
|
||||||
payload: TSecretRotationSendNotificationJobPayload;
|
payload: TSecretRotationSendNotificationJobPayload;
|
||||||
};
|
};
|
||||||
|
[QueueName.InvalidateCache]: {
|
||||||
|
name: QueueJobs.InvalidateCache;
|
||||||
|
payload: {
|
||||||
|
data: {
|
||||||
|
type: CacheType;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
export type TQueueServiceFactory = ReturnType<typeof queueServiceFactory>;
|
||||||
|
|||||||
@@ -100,3 +100,10 @@ export const publicSshCaLimit: RateLimitOptions = {
|
|||||||
max: 30, // conservative default
|
max: 30, // conservative default
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const invalidateCacheLimit: RateLimitOptions = {
|
||||||
|
timeWindow: 60 * 1000,
|
||||||
|
hook: "preValidation",
|
||||||
|
max: 1,
|
||||||
|
keyGenerator: (req) => req.realIp
|
||||||
|
};
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ import { projectSlackConfigDALFactory } from "@app/services/slack/project-slack-
|
|||||||
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
|
import { slackIntegrationDALFactory } from "@app/services/slack/slack-integration-dal";
|
||||||
import { slackServiceFactory } from "@app/services/slack/slack-service";
|
import { slackServiceFactory } from "@app/services/slack/slack-service";
|
||||||
import { TSmtpService } from "@app/services/smtp/smtp-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 { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
|
||||||
import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg, superAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||||
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
|
import { telemetryDALFactory } from "@app/services/telemetry/telemetry-dal";
|
||||||
@@ -611,6 +612,11 @@ export const registerRoutes = async (
|
|||||||
queueService
|
queueService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const invalidateCacheQueue = invalidateCacheQueueFactory({
|
||||||
|
keyStore,
|
||||||
|
queueService
|
||||||
|
});
|
||||||
|
|
||||||
const userService = userServiceFactory({
|
const userService = userServiceFactory({
|
||||||
userDAL,
|
userDAL,
|
||||||
userAliasDAL,
|
userAliasDAL,
|
||||||
@@ -722,7 +728,8 @@ export const registerRoutes = async (
|
|||||||
keyStore,
|
keyStore,
|
||||||
licenseService,
|
licenseService,
|
||||||
kmsService,
|
kmsService,
|
||||||
microsoftTeamsService
|
microsoftTeamsService,
|
||||||
|
invalidateCacheQueue
|
||||||
});
|
});
|
||||||
|
|
||||||
const orgAdminService = orgAdminServiceFactory({
|
const orgAdminService = orgAdminServiceFactory({
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { z } from "zod";
|
|||||||
import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
import { IdentitiesSchema, OrganizationsSchema, SuperAdminSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
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 { verifySuperAdmin } from "@app/server/plugins/auth/superAdmin";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
import { RootKeyEncryptionStrategy } from "@app/services/kms/kms-types";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
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";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
export const registerAdminRouter = async (server: FastifyZodProvider) => {
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -170,7 +170,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
.optional()
|
.optional()
|
||||||
.default(InfisicalProjectTemplate.Default)
|
.default(InfisicalProjectTemplate.Default)
|
||||||
.describe(PROJECTS.CREATE.template),
|
.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: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -190,7 +191,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
slug: req.body.slug,
|
slug: req.body.slug,
|
||||||
kmsKeyId: req.body.kmsKeyId,
|
kmsKeyId: req.body.kmsKeyId,
|
||||||
template: req.body.template,
|
template: req.body.template,
|
||||||
type: req.body.type
|
type: req.body.type,
|
||||||
|
createDefaultEnvs: req.body.shouldCreateDefaultEnvs
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.telemetry.sendPostHogEvents({
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
@@ -272,7 +274,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
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: {
|
response: {
|
||||||
200: projectWithEnv
|
200: projectWithEnv
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { ProjectMembershipRole } from "@app/db/schemas";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { ProjectMembershipRole, ProjectType } from "@app/db/schemas";
|
||||||
import {
|
import {
|
||||||
|
cryptographicOperatorPermissions,
|
||||||
projectAdminPermissions,
|
projectAdminPermissions,
|
||||||
projectMemberPermissions,
|
projectMemberPermissions,
|
||||||
projectNoAccessPermissions,
|
projectNoAccessPermissions,
|
||||||
projectViewerPermission
|
projectViewerPermission,
|
||||||
} from "@app/ee/services/permission/project-permission";
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
|
id: uuidv4(),
|
||||||
projectId,
|
projectId,
|
||||||
name: "Admin",
|
name: "Admin",
|
||||||
slug: ProjectMembershipRole.Admin,
|
slug: ProjectMembershipRole.Admin,
|
||||||
@@ -19,7 +24,7 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember
|
|||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
|
id: uuidv4(),
|
||||||
projectId,
|
projectId,
|
||||||
name: "Developer",
|
name: "Developer",
|
||||||
slug: ProjectMembershipRole.Member,
|
slug: ProjectMembershipRole.Member,
|
||||||
@@ -29,7 +34,29 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember
|
|||||||
updatedAt: new Date()
|
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,
|
projectId,
|
||||||
name: "Viewer",
|
name: "Viewer",
|
||||||
slug: ProjectMembershipRole.Viewer,
|
slug: ProjectMembershipRole.Viewer,
|
||||||
@@ -39,7 +66,7 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember
|
|||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
|
id: uuidv4(),
|
||||||
projectId,
|
projectId,
|
||||||
name: "No Access",
|
name: "No Access",
|
||||||
slug: ProjectMembershipRole.NoAccess,
|
slug: ProjectMembershipRole.NoAccess,
|
||||||
@@ -48,5 +75,5 @@ export const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMember
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
}
|
}
|
||||||
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
|
].filter(({ slug, type }) => (type ? type === projectType : true) && (!roleFilter || roleFilter === slug));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
|||||||
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||||
import { requestContext } from "@fastify/request-context";
|
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 { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
@@ -34,7 +34,7 @@ type TProjectRoleServiceFactoryDep = {
|
|||||||
projectRoleDAL: TProjectRoleDALFactory;
|
projectRoleDAL: TProjectRoleDALFactory;
|
||||||
identityDAL: Pick<TIdentityDALFactory, "findById">;
|
identityDAL: Pick<TIdentityDALFactory, "findById">;
|
||||||
userDAL: Pick<TUserDALFactory, "findById">;
|
userDAL: Pick<TUserDALFactory, "findById">;
|
||||||
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug" | "findProjectById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||||
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
|
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
|
||||||
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
|
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
|
||||||
@@ -98,30 +98,37 @@ export const projectRoleServiceFactory = ({
|
|||||||
roleSlug,
|
roleSlug,
|
||||||
filter
|
filter
|
||||||
}: TGetRoleDetailsDTO) => {
|
}: TGetRoleDetailsDTO) => {
|
||||||
let projectId = "";
|
let project: TProjects;
|
||||||
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
|
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
|
||||||
const project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
|
project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
|
||||||
if (!project) throw new NotFoundError({ message: "Project not found" });
|
|
||||||
projectId = project.id;
|
|
||||||
} else {
|
} else {
|
||||||
projectId = filter.projectId;
|
project = await projectDAL.findProjectById(filter.projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!project) throw new NotFoundError({ message: "Project not found" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId: project.id,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||||
if (roleSlug !== "custom" && Object.values(ProjectMembershipRole).includes(roleSlug as ProjectMembershipRole)) {
|
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) };
|
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` });
|
if (!customRole) throw new NotFoundError({ message: `Project role with slug '${roleSlug}' not found` });
|
||||||
return { ...customRole, permissions: unpackPermissions(customRole.permissions) };
|
return { ...customRole, permissions: unpackPermissions(customRole.permissions) };
|
||||||
};
|
};
|
||||||
@@ -194,29 +201,32 @@ export const projectRoleServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const listRoles = async ({ actorOrgId, actorAuthMethod, actorId, actor, filter }: TListRolesDTO) => {
|
const listRoles = async ({ actorOrgId, actorAuthMethod, actorId, actor, filter }: TListRolesDTO) => {
|
||||||
let projectId = "";
|
let project: TProjects;
|
||||||
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
|
if (filter.type === ProjectRoleServiceIdentifierType.SLUG) {
|
||||||
const project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
|
project = await projectDAL.findProjectBySlug(filter.projectSlug, actorOrgId);
|
||||||
if (!project) throw new BadRequestError({ message: "Project not found" });
|
|
||||||
projectId = project.id;
|
|
||||||
} else {
|
} else {
|
||||||
projectId = filter.projectId;
|
project = await projectDAL.findProjectById(filter.projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission({
|
const { permission } = await permissionService.getProjectPermission({
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
projectId,
|
projectId: project.id,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||||
const customRoles = await projectRoleDAL.find(
|
const customRoles = await projectRoleDAL.find(
|
||||||
{ projectId },
|
{ projectId: project.id },
|
||||||
{ sort: [[`${TableName.ProjectRoles}.slug` as "slug", "asc"]] }
|
{ 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;
|
return roles;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
export enum ProjectRoleServiceIdentifierType {
|
export enum ProjectRoleServiceIdentifierType {
|
||||||
@@ -34,3 +34,9 @@ export type TListRolesDTO = {
|
|||||||
| { type: ProjectRoleServiceIdentifierType.SLUG; projectSlug: string }
|
| { type: ProjectRoleServiceIdentifierType.SLUG; projectSlug: string }
|
||||||
| { type: ProjectRoleServiceIdentifierType.ID; projectId: string };
|
| { type: ProjectRoleServiceIdentifierType.ID; projectId: string };
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetPredefinedRolesDTO = {
|
||||||
|
projectId: string;
|
||||||
|
projectType: ProjectType;
|
||||||
|
roleFilter?: ProjectMembershipRole;
|
||||||
|
};
|
||||||
|
|||||||
@@ -329,14 +329,16 @@ export const projectServiceFactory = ({
|
|||||||
// set default environments and root folder for provided environments
|
// set default environments and root folder for provided environments
|
||||||
let envs: TProjectEnvironments[] = [];
|
let envs: TProjectEnvironments[] = [];
|
||||||
if (projectTemplate) {
|
if (projectTemplate) {
|
||||||
envs = await projectEnvDAL.insertMany(
|
if (projectTemplate.environments) {
|
||||||
projectTemplate.environments.map((env) => ({ ...env, projectId: project.id })),
|
envs = await projectEnvDAL.insertMany(
|
||||||
tx
|
projectTemplate.environments.map((env) => ({ ...env, projectId: project.id })),
|
||||||
);
|
tx
|
||||||
await folderDAL.insertMany(
|
);
|
||||||
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
await folderDAL.insertMany(
|
||||||
tx
|
envs.map(({ id }) => ({ name: ROOT_FOLDER_NAME, envId: id, version: 1 })),
|
||||||
);
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
await projectRoleDAL.insertMany(
|
await projectRoleDAL.insertMany(
|
||||||
projectTemplate.packedRoles.map((role) => ({
|
projectTemplate.packedRoles.map((role) => ({
|
||||||
...role,
|
...role,
|
||||||
@@ -592,7 +594,10 @@ export const projectServiceFactory = ({
|
|||||||
workspaces.map(async (workspace) => {
|
workspaces.map(async (workspace) => {
|
||||||
return {
|
return {
|
||||||
...workspace,
|
...workspace,
|
||||||
roles: [...(workspaceMappedToRoles[workspace.id] || []), ...getPredefinedRoles(workspace.id)]
|
roles: [
|
||||||
|
...(workspaceMappedToRoles[workspace.id] || []),
|
||||||
|
...getPredefinedRoles({ projectId: workspace.id, projectType: workspace.type as ProjectType })
|
||||||
|
]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ const getParameterStoreTagsRecord = async (
|
|||||||
|
|
||||||
throw new SecretSyncError({
|
throw new SecretSyncError({
|
||||||
message:
|
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
|
shouldRetry: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -25,8 +25,10 @@ import { TOrgServiceFactory } from "../org/org-service";
|
|||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
import { TUserAliasDALFactory } from "../user-alias/user-alias-dal";
|
||||||
import { UserAliasType } from "../user-alias/user-alias-types";
|
import { UserAliasType } from "../user-alias/user-alias-types";
|
||||||
|
import { TInvalidateCacheQueueFactory } from "./invalidate-cache-queue";
|
||||||
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
import { TSuperAdminDALFactory } from "./super-admin-dal";
|
||||||
import {
|
import {
|
||||||
|
CacheType,
|
||||||
LoginMethod,
|
LoginMethod,
|
||||||
TAdminBootstrapInstanceDTO,
|
TAdminBootstrapInstanceDTO,
|
||||||
TAdminGetIdentitiesDTO,
|
TAdminGetIdentitiesDTO,
|
||||||
@@ -46,9 +48,10 @@ type TSuperAdminServiceFactoryDep = {
|
|||||||
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey" | "updateEncryptionStrategy">;
|
kmsService: Pick<TKmsServiceFactory, "encryptWithRootKey" | "decryptWithRootKey" | "updateEncryptionStrategy">;
|
||||||
kmsRootConfigDAL: TKmsRootConfigDALFactory;
|
kmsRootConfigDAL: TKmsRootConfigDALFactory;
|
||||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||||
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem">;
|
keyStore: Pick<TKeyStoreFactory, "getItem" | "setItemWithExpiry" | "deleteItem" | "deleteItems">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
licenseService: Pick<TLicenseServiceFactory, "onPremFeatures">;
|
||||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
|
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "initializeTeamsBot">;
|
||||||
|
invalidateCacheQueue: TInvalidateCacheQueueFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSuperAdminServiceFactory = ReturnType<typeof superAdminServiceFactory>;
|
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 = "infisical-admin-cfg";
|
||||||
const ADMIN_CONFIG_KEY_EXP = 60; // 60s
|
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 = ({
|
export const superAdminServiceFactory = ({
|
||||||
serverCfgDAL,
|
serverCfgDAL,
|
||||||
@@ -80,7 +83,8 @@ export const superAdminServiceFactory = ({
|
|||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
identityTokenAuthDAL,
|
identityTokenAuthDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
microsoftTeamsService
|
microsoftTeamsService,
|
||||||
|
invalidateCacheQueue
|
||||||
}: TSuperAdminServiceFactoryDep) => {
|
}: TSuperAdminServiceFactoryDep) => {
|
||||||
const initServerCfg = async () => {
|
const initServerCfg = async () => {
|
||||||
// TODO(akhilmhdh): bad pattern time less change this later to me itself
|
// TODO(akhilmhdh): bad pattern time less change this later to me itself
|
||||||
@@ -631,6 +635,16 @@ export const superAdminServiceFactory = ({
|
|||||||
await kmsService.updateEncryptionStrategy(strategy);
|
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 {
|
return {
|
||||||
initServerCfg,
|
initServerCfg,
|
||||||
updateServerCfg,
|
updateServerCfg,
|
||||||
@@ -644,6 +658,8 @@ export const superAdminServiceFactory = ({
|
|||||||
getConfiguredEncryptionStrategies,
|
getConfiguredEncryptionStrategies,
|
||||||
grantServerAdminAccessToUser,
|
grantServerAdminAccessToUser,
|
||||||
deleteIdentitySuperAdminAccess,
|
deleteIdentitySuperAdminAccess,
|
||||||
deleteUserSuperAdminAccess
|
deleteUserSuperAdminAccess,
|
||||||
|
invalidateCache,
|
||||||
|
checkIfInvalidatingCache
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,3 +44,8 @@ export enum LoginMethod {
|
|||||||
LDAP = "ldap",
|
LDAP = "ldap",
|
||||||
OIDC = "oidc"
|
OIDC = "oidc"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CacheType {
|
||||||
|
ALL = "all",
|
||||||
|
SECRETS = "secrets"
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export enum PostHogEventTypes {
|
|||||||
IssueSshHostUserCert = "Issue SSH Host User Certificate",
|
IssueSshHostUserCert = "Issue SSH Host User Certificate",
|
||||||
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
||||||
SignCert = "Sign PKI Certificate",
|
SignCert = "Sign PKI Certificate",
|
||||||
IssueCert = "Issue PKI Certificate"
|
IssueCert = "Issue PKI Certificate",
|
||||||
|
InvalidateCache = "Invalidate Cache"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TSecretModifiedEvent = {
|
export type TSecretModifiedEvent = {
|
||||||
@@ -203,6 +204,13 @@ export type TIssueCertificateEvent = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TInvalidateCacheEvent = {
|
||||||
|
event: PostHogEventTypes.InvalidateCache;
|
||||||
|
properties: {
|
||||||
|
userAgent?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type TPostHogEvent = { distinctId: string } & (
|
export type TPostHogEvent = { distinctId: string } & (
|
||||||
| TSecretModifiedEvent
|
| TSecretModifiedEvent
|
||||||
| TAdminInitEvent
|
| TAdminInitEvent
|
||||||
@@ -221,4 +229,5 @@ export type TPostHogEvent = { distinctId: string } & (
|
|||||||
| TIssueSshHostHostCertEvent
|
| TIssueSshHostHostCertEvent
|
||||||
| TSignCertificateEvent
|
| TSignCertificateEvent
|
||||||
| TIssueCertificateEvent
|
| TIssueCertificateEvent
|
||||||
|
| TInvalidateCacheEvent
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "Get Certificate Bundle"
|
title: "Get Certificate Bundle"
|
||||||
openapi: "GET /api/v2/workspace/{slug}/bundle"
|
openapi: "GET /api/v1/pki/certificates/{serialNumber}/bundle"
|
||||||
---
|
---
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
---
|
---
|
||||||
title: "Get Certificate Private Key"
|
title: "Get Certificate Private Key"
|
||||||
openapi: "GET /api/v2/workspace/{slug}/private-key"
|
openapi: "GET /api/v1/pki/certificates/{serialNumber}/private-key"
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ User identities can have metadata attributes assigned directly. These attributes
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
#### Applying ABAC Policies with User Metadata
|
#### 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.
|
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" />
|
<img src="/images/platform/access-controls/example-abac-1.png" />
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ In the following steps, we'll explore how to set up a project template.
|
|||||||
<Tab title="Infisical UI">
|
<Tab title="Infisical UI">
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Creating a Project Template">
|
<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.
|
||||||

|

|
||||||
|
|
||||||
Specify your template details. Here's some guidance on each field:
|
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' \
|
--header 'Content-Type: application/json' \
|
||||||
--data '{
|
--data '{
|
||||||
"name": "my-project-template",
|
"name": "my-project-template",
|
||||||
|
"type": "secret-manager",
|
||||||
"description": "...",
|
"description": "...",
|
||||||
"environments": "[...]",
|
"environments": "[...]",
|
||||||
"roles": "[...]",
|
"roles": "[...]",
|
||||||
|
|||||||
@@ -31,16 +31,9 @@ we will register a remote host with Infisical through a [machine identity](/docu
|
|||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Create an Infisical SSH project">
|
<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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||

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

|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Create a machine identity for bootstrapping Infisical SSH">
|
<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.
|
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.
|
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>
|
</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>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 951 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 579 KiB After Width: | Height: | Size: 687 KiB |
|
Before Width: | Height: | Size: 562 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1015 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 852 KiB |
|
Before Width: | Height: | Size: 564 KiB After Width: | Height: | Size: 680 KiB |
@@ -165,7 +165,7 @@ spec:
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="managedSecretReference.creationPolicy">
|
<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.
|
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
|
||||||
|
|
||||||
#### Available options
|
#### Available options
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes sec
|
|||||||
metadata:
|
metadata:
|
||||||
name: infisical-push-secret-demo
|
name: infisical-push-secret-demo
|
||||||
spec:
|
spec:
|
||||||
resyncInterval: 1m
|
resyncInterval: 1m # Remove this field to disable automatic reconciliation of the InfisicalPushSecret CRD.
|
||||||
hostAPI: https://app.infisical.com/api
|
hostAPI: https://app.infisical.com/api
|
||||||
|
|
||||||
# Optional, defaults to no replacement.
|
# Optional, defaults to no replacement.
|
||||||
@@ -124,7 +124,9 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
|
|||||||
|
|
||||||
<Accordion title="resyncInterval">
|
<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.
|
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
|
DATABASE_URL: postgres://127.0.0.1:5432
|
||||||
ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab
|
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>
|
||||||
</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.
|
Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information.
|
||||||
</Accordion>
|
</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
|
## 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.
|
Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster.
|
||||||
|
|||||||
@@ -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.
|
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="managedKubeSecretReferences[].creationPolicy">
|
<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.
|
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
|
||||||
|
|
||||||
#### Available options
|
#### 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.
|
The namespace of the managed Kubernetes config map that your Infisical data will be stored in.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="managedKubeConfigMapReferences[].creationPolicy">
|
<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.
|
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
|
||||||
|
|
||||||
#### Available options
|
#### Available options
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.2 MiB |
@@ -72,7 +72,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
|||||||
OrgPermissionSubjects.ProjectTemplates
|
OrgPermissionSubjects.ProjectTemplates
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: projectTemplates = [] } = useListProjectTemplates({
|
const { data: projectTemplates = [] } = useListProjectTemplates(projectType, {
|
||||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import { Button, DeleteActionModal } from "@app/components/v2";
|
|||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates";
|
import { TProjectTemplate, useDeleteProjectTemplate } from "@app/hooks/api/projectTemplates";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { ProjectTemplateDetailsModal } from "../../ProjectTemplateDetailsModal";
|
import { ProjectTemplateDetailsModal } from "../../ProjectTemplateDetailsModal";
|
||||||
import { ProjectTemplateEnvironmentsForm } from "./ProjectTemplateEnvironmentsForm";
|
import { ProjectTemplateEnvironmentsForm } from "./ProjectTemplateEnvironmentsForm";
|
||||||
@@ -24,7 +25,7 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
|
|||||||
"editDetails"
|
"editDetails"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const { id: templateId, name, description } = projectTemplate;
|
const { id: templateId, name, description, type } = projectTemplate;
|
||||||
|
|
||||||
const deleteProjectTemplate = useDeleteProjectTemplate();
|
const deleteProjectTemplate = useDeleteProjectTemplate();
|
||||||
|
|
||||||
@@ -94,10 +95,12 @@ export const EditProjectTemplate = ({ isInfisicalTemplate, projectTemplate, onBa
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ProjectTemplateEnvironmentsForm
|
{type === ProjectType.SecretManager && (
|
||||||
isInfisicalTemplate={isInfisicalTemplate}
|
<ProjectTemplateEnvironmentsForm
|
||||||
projectTemplate={projectTemplate}
|
isInfisicalTemplate={isInfisicalTemplate}
|
||||||
/>
|
projectTemplate={projectTemplate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ProjectTemplateRolesSection
|
<ProjectTemplateRolesSection
|
||||||
isInfisicalTemplate={isInfisicalTemplate}
|
isInfisicalTemplate={isInfisicalTemplate}
|
||||||
projectTemplate={projectTemplate}
|
projectTemplate={projectTemplate}
|
||||||
@@ -6,15 +6,15 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
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 { ProjectPermissionSub } from "@app/context";
|
||||||
import { isCustomProjectRole } from "@app/helpers/roles";
|
import { isCustomProjectRole } from "@app/helpers/roles";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
|
import { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
import { GeneralPermissionPolicies } from "@app/pages/project/RoleDetailsBySlugPage/components/GeneralPermissionPolicies";
|
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 { PermissionEmptyState } from "@app/pages/project/RoleDetailsBySlugPage/components/PermissionEmptyState";
|
||||||
|
import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal";
|
||||||
import {
|
import {
|
||||||
formRolePermission2API,
|
formRolePermission2API,
|
||||||
PROJECT_PERMISSION_OBJECT,
|
PROJECT_PERMISSION_OBJECT,
|
||||||
@@ -44,7 +44,7 @@ export const ProjectTemplateEditRoleForm = ({
|
|||||||
role,
|
role,
|
||||||
isDisabled
|
isDisabled
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { popUp, handlePopUpToggle } = usePopUp(["createPolicy"] as const);
|
const { popUp, handlePopUpToggle } = usePopUp(["addPolicy"] as const);
|
||||||
|
|
||||||
const formMethods = useForm<TFormSchema>({
|
const formMethods = useForm<TFormSchema>({
|
||||||
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
|
||||||
@@ -119,34 +119,29 @@ export const ProjectTemplateEditRoleForm = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
type="submit"
|
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}
|
isDisabled={isSubmitting || !isDirty || isDisabled}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
leftIcon={<FontAwesomeIcon icon={faSave} />}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
<Modal
|
<Button
|
||||||
isOpen={popUp.createPolicy.isOpen}
|
className="h-10 rounded-l-none"
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("createPolicy", isOpen)}
|
variant="outline_bg"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onClick={() => handlePopUpToggle("addPolicy")}
|
||||||
>
|
>
|
||||||
<ModalTrigger asChild>
|
Add Policies
|
||||||
<Button
|
</Button>
|
||||||
className="h-10 rounded-l-none"
|
<PolicySelectionModal
|
||||||
variant="outline_bg"
|
isOpen={popUp.addPolicy.isOpen}
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
onOpenChange={(isOpen) => handlePopUpToggle("addPolicy", isOpen)}
|
||||||
isDisabled={isDisabled}
|
/>
|
||||||
>
|
|
||||||
New Policy
|
|
||||||
</Button>
|
|
||||||
</ModalTrigger>
|
|
||||||
<ModalContent
|
|
||||||
title="New Policy"
|
|
||||||
subTitle="Policies grant additional permissions."
|
|
||||||
>
|
|
||||||
<NewPermissionRule onClose={() => handlePopUpToggle("createPolicy")} />
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
THead,
|
THead,
|
||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} 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 { TProjectTemplate, useUpdateProjectTemplate } from "@app/hooks/api/projectTemplates";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
@@ -35,6 +35,7 @@ const formSchema = z.object({
|
|||||||
slug: slugSchema({ min: 1, max: 32 })
|
slug: slugSchema({ min: 1, max: 32 })
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
|
.nullish()
|
||||||
});
|
});
|
||||||
|
|
||||||
type TFormSchema = z.infer<typeof formSchema>;
|
type TFormSchema = z.infer<typeof formSchema>;
|
||||||
@@ -55,6 +56,8 @@ export const ProjectTemplateEnvironmentsForm = ({
|
|||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fields: environments,
|
fields: environments,
|
||||||
move,
|
move,
|
||||||
@@ -67,7 +70,7 @@ export const ProjectTemplateEnvironmentsForm = ({
|
|||||||
const onFormSubmit = async (form: TFormSchema) => {
|
const onFormSubmit = async (form: TFormSchema) => {
|
||||||
try {
|
try {
|
||||||
const { environments: updatedEnvs } = await updateProjectTemplate.mutateAsync({
|
const { environments: updatedEnvs } = await updateProjectTemplate.mutateAsync({
|
||||||
environments: form.environments.map((env, index) => ({
|
environments: form.environments?.map((env, index) => ({
|
||||||
...env,
|
...env,
|
||||||
position: index + 1
|
position: index + 1
|
||||||
})),
|
})),
|
||||||
@@ -89,6 +92,9 @@ export const ProjectTemplateEnvironmentsForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isEnvironmentLimitExceeded =
|
||||||
|
Boolean(subscription.environmentLimit) && environments.length >= subscription.environmentLimit;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
@@ -138,6 +144,8 @@ export const ProjectTemplateEnvironmentsForm = ({
|
|||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
I={OrgPermissionActions.Edit}
|
I={OrgPermissionActions.Edit}
|
||||||
a={OrgPermissionSubjects.ProjectTemplates}
|
a={OrgPermissionSubjects.ProjectTemplates}
|
||||||
|
renderTooltip={isEnvironmentLimitExceeded ? true : undefined}
|
||||||
|
allowedLabel={`Plan environment limit of ${subscription.environmentLimit} exceeded. Contact Infisical to increase limit.`}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
@@ -147,7 +155,7 @@ export const ProjectTemplateEnvironmentsForm = ({
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
size="xs"
|
size="xs"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed || isEnvironmentLimitExceeded}
|
||||||
>
|
>
|
||||||
Add Environment
|
Add Environment
|
||||||
</Button>
|
</Button>
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
TextArea
|
TextArea
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
|
import { useGetProjectTypeFromRoute } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
TProjectTemplate,
|
TProjectTemplate,
|
||||||
useCreateProjectTemplate,
|
useCreateProjectTemplate,
|
||||||
@@ -41,6 +42,7 @@ type FormProps = {
|
|||||||
const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
|
const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
|
||||||
const createProjectTemplate = useCreateProjectTemplate();
|
const createProjectTemplate = useCreateProjectTemplate();
|
||||||
const updateProjectTemplate = useUpdateProjectTemplate();
|
const updateProjectTemplate = useUpdateProjectTemplate();
|
||||||
|
const projectType = useGetProjectTypeFromRoute();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -55,9 +57,17 @@ const ProjectTemplateForm = ({ onComplete, projectTemplate }: FormProps) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = async (data: FormData) => {
|
const onFormSubmit = async (data: FormData) => {
|
||||||
|
if (!projectType) {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to determine project type",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const mutation = projectTemplate
|
const mutation = projectTemplate
|
||||||
? updateProjectTemplate.mutateAsync({ templateId: projectTemplate.id, ...data })
|
? updateProjectTemplate.mutateAsync({ templateId: projectTemplate.id, ...data })
|
||||||
: createProjectTemplate.mutateAsync(data);
|
: createProjectTemplate.mutateAsync({ ...data, type: projectType });
|
||||||
try {
|
try {
|
||||||
const template = await mutation;
|
const template = await mutation;
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -50,7 +50,7 @@ export const ProjectTemplatesSection = () => {
|
|||||||
className="absolute min-h-[10rem] w-full"
|
className="absolute min-h-[10rem] w-full"
|
||||||
>
|
>
|
||||||
<div>
|
<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
|
Create and configure templates with predefined roles and environments to streamline
|
||||||
project setup
|
project setup
|
||||||
</p>
|
</p>
|
||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
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 { TProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||||
|
|
||||||
import { DeleteProjectTemplateModal } from "./DeleteProjectTemplateModal";
|
import { DeleteProjectTemplateModal } from "./DeleteProjectTemplateModal";
|
||||||
@@ -35,8 +35,10 @@ type Props = {
|
|||||||
export const ProjectTemplatesTable = ({ onEdit }: Props) => {
|
export const ProjectTemplatesTable = ({ onEdit }: Props) => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const { isPending, data: projectTemplates = [] } = useListProjectTemplates({
|
const projectType = useGetProjectTypeFromRoute();
|
||||||
enabled: subscription?.projectTemplates
|
|
||||||
|
const { isPending, data: projectTemplates = [] } = useListProjectTemplates(projectType, {
|
||||||
|
enabled: subscription?.projectTemplates && Boolean(projectType)
|
||||||
});
|
});
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
@@ -50,6 +52,8 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
|
|||||||
[search, projectTemplates]
|
[search, projectTemplates]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isSecretManagerTemplates = projectType === "secret-manager";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
@@ -64,7 +68,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
|
|||||||
<Tr>
|
<Tr>
|
||||||
<Th>Name</Th>
|
<Th>Name</Th>
|
||||||
<Th>Roles</Th>
|
<Th>Roles</Th>
|
||||||
<Th>Environments</Th>
|
{isSecretManagerTemplates && <Th>Environments</Th>}
|
||||||
<Th />
|
<Th />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
@@ -77,7 +81,7 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{filteredTemplates.map((template) => {
|
{filteredTemplates.map((template) => {
|
||||||
const { id, name, roles, environments, description } = template;
|
const { id, name, roles, environments = [], description } = template;
|
||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
onClick={() => onEdit(template)}
|
onClick={() => onEdit(template)}
|
||||||
@@ -116,28 +120,30 @@ export const ProjectTemplatesTable = ({ onEdit }: Props) => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Td>
|
</Td>
|
||||||
<Td className="pl-14">
|
{isSecretManagerTemplates && environments && (
|
||||||
{environments.length}
|
<Td className="pl-14">
|
||||||
{environments.length > 0 && (
|
{environments.length}
|
||||||
<Tooltip
|
{environments.length > 0 && (
|
||||||
content={
|
<Tooltip
|
||||||
<ul className="ml-2 list-disc">
|
content={
|
||||||
{environments
|
<ul className="ml-2 list-disc">
|
||||||
.sort((a, b) => (a.position > b.position ? 1 : -1))
|
{environments
|
||||||
.map((env) => (
|
.sort((a, b) => (a.position > b.position ? 1 : -1))
|
||||||
<li key={env.slug}>{env.name}</li>
|
.map((env) => (
|
||||||
))}
|
<li key={env.slug}>{env.name}</li>
|
||||||
</ul>
|
))}
|
||||||
}
|
</ul>
|
||||||
>
|
}
|
||||||
<FontAwesomeIcon
|
>
|
||||||
size="sm"
|
<FontAwesomeIcon
|
||||||
className="ml-2 text-mineshaft-400"
|
size="sm"
|
||||||
icon={faCircleInfo}
|
className="ml-2 text-mineshaft-400"
|
||||||
/>
|
icon={faCircleInfo}
|
||||||
</Tooltip>
|
/>
|
||||||
)}
|
</Tooltip>
|
||||||
</Td>
|
)}
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
<Td className="w-5">
|
<Td className="w-5">
|
||||||
{name !== "default" && (
|
{name !== "default" && (
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ProjectTemplatesTab";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./ProjectSettings";
|
||||||
@@ -6,21 +6,9 @@ export enum OrgMembershipRole {
|
|||||||
NoAccess = "no-access"
|
NoAccess = "no-access"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ProjectMemberRole {
|
|
||||||
Admin = "admin",
|
|
||||||
Member = "member",
|
|
||||||
Viewer = "viewer",
|
|
||||||
NoAccess = "no-access"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const isCustomOrgRole = (slug: string) =>
|
export const isCustomOrgRole = (slug: string) =>
|
||||||
!Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole);
|
!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) =>
|
export const isCustomProjectRole = (slug: string) =>
|
||||||
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
!Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole);
|
||||||
|
|
||||||
@@ -28,3 +16,24 @@ export const findOrgMembershipRole = (roles: TOrgRole[], roleIdOrSlug: string) =
|
|||||||
isCustomOrgRole(roleIdOrSlug)
|
isCustomOrgRole(roleIdOrSlug)
|
||||||
? roles.find((r) => r.id === roleIdOrSlug)
|
? roles.find((r) => r.id === roleIdOrSlug)
|
||||||
: roles.find((r) => r.slug === 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export {
|
|||||||
useAdminGrantServerAdminAccess,
|
useAdminGrantServerAdminAccess,
|
||||||
useAdminRemoveIdentitySuperAdminAccess,
|
useAdminRemoveIdentitySuperAdminAccess,
|
||||||
useCreateAdminUser,
|
useCreateAdminUser,
|
||||||
|
useInvalidateCache,
|
||||||
useRemoveUserServerAdminAccess,
|
useRemoveUserServerAdminAccess,
|
||||||
useUpdateServerConfig,
|
useUpdateServerConfig,
|
||||||
useUpdateServerEncryptionStrategy
|
useUpdateServerEncryptionStrategy
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { adminQueryKeys, adminStandaloneKeys } from "./queries";
|
|||||||
import {
|
import {
|
||||||
RootKeyEncryptionStrategy,
|
RootKeyEncryptionStrategy,
|
||||||
TCreateAdminUserDTO,
|
TCreateAdminUserDTO,
|
||||||
|
TInvalidateCacheDTO,
|
||||||
TServerConfig,
|
TServerConfig,
|
||||||
TUpdateServerConfigDTO
|
TUpdateServerConfigDTO
|
||||||
} from "./types";
|
} 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() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
AdminGetIdentitiesFilters,
|
AdminGetIdentitiesFilters,
|
||||||
AdminGetUsersFilters,
|
AdminGetUsersFilters,
|
||||||
AdminIntegrationsConfig,
|
AdminIntegrationsConfig,
|
||||||
|
TGetInvalidatingCacheStatus,
|
||||||
TGetServerRootKmsEncryptionDetails,
|
TGetServerRootKmsEncryptionDetails,
|
||||||
TServerConfig
|
TServerConfig
|
||||||
} from "./types";
|
} from "./types";
|
||||||
@@ -22,8 +23,10 @@ export const adminQueryKeys = {
|
|||||||
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
|
||||||
getIdentities: (filters: AdminGetIdentitiesFilters) =>
|
getIdentities: (filters: AdminGetIdentitiesFilters) =>
|
||||||
[adminStandaloneKeys.getIdentities, { filters }] as const,
|
[adminStandaloneKeys.getIdentities, { filters }] as const,
|
||||||
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const,
|
getAdminSlackConfig: () => ["admin-slack-config"] as const,
|
||||||
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
|
getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const,
|
||||||
|
getInvalidateCache: () => ["admin-invalidate-cache"] as const,
|
||||||
|
getAdminIntegrationsConfig: () => ["admin-integrations-config"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchServerConfig = async () => {
|
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)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export type TServerConfig = {
|
|||||||
enabledLoginMethods: LoginMethod[];
|
enabledLoginMethods: LoginMethod[];
|
||||||
authConsentContent?: string;
|
authConsentContent?: string;
|
||||||
pageFrameContent?: string;
|
pageFrameContent?: string;
|
||||||
|
invalidatingCache: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateServerConfigDTO = {
|
export type TUpdateServerConfigDTO = {
|
||||||
@@ -84,3 +85,16 @@ export enum RootKeyEncryptionStrategy {
|
|||||||
Software = "SOFTWARE",
|
Software = "SOFTWARE",
|
||||||
HSM = "HSM"
|
HSM = "HSM"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum CacheType {
|
||||||
|
ALL = "all",
|
||||||
|
SECRETS = "secrets"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TInvalidateCacheDTO = {
|
||||||
|
type: CacheType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TGetInvalidatingCacheStatus = {
|
||||||
|
invalidating: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,14 +6,17 @@ import {
|
|||||||
TProjectTemplate,
|
TProjectTemplate,
|
||||||
TProjectTemplateResponse
|
TProjectTemplateResponse
|
||||||
} from "@app/hooks/api/projectTemplates/types";
|
} from "@app/hooks/api/projectTemplates/types";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
export const projectTemplateKeys = {
|
export const projectTemplateKeys = {
|
||||||
all: ["project-template"] as const,
|
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
|
byId: (templateId: string) => [...projectTemplateKeys.all, templateId] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useListProjectTemplates = (
|
export const useListProjectTemplates = (
|
||||||
|
type?: ProjectType,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<
|
UseQueryOptions<
|
||||||
TProjectTemplate[],
|
TProjectTemplate[],
|
||||||
@@ -25,9 +28,11 @@ export const useListProjectTemplates = (
|
|||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: projectTemplateKeys.list(),
|
queryKey: projectTemplateKeys.list(type),
|
||||||
queryFn: async () => {
|
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;
|
return data.projectTemplates;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
export type TProjectTemplate = {
|
export type TProjectTemplate = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
type: ProjectType;
|
||||||
description?: string;
|
description?: string;
|
||||||
roles: Pick<TProjectRole, "slug" | "name" | "permissions">[];
|
roles: Pick<TProjectRole, "slug" | "name" | "permissions">[];
|
||||||
environments: { name: string; slug: string; position: number }[];
|
environments?: { name: string; slug: string; position: number }[] | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
@@ -14,6 +16,7 @@ export type TListProjectTemplates = { projectTemplates: TProjectTemplate[] };
|
|||||||
export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate };
|
export type TProjectTemplateResponse = { projectTemplate: TProjectTemplate };
|
||||||
|
|
||||||
export type TCreateProjectTemplateDTO = {
|
export type TCreateProjectTemplateDTO = {
|
||||||
|
type: ProjectType;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ export enum ProjectMembershipRole {
|
|||||||
Member = "member",
|
Member = "member",
|
||||||
Custom = "custom",
|
Custom = "custom",
|
||||||
Viewer = "viewer",
|
Viewer = "viewer",
|
||||||
NoAccess = "no-access"
|
NoAccess = "no-access",
|
||||||
|
SshHostBootstrapper = "ssh-host-bootstrapper",
|
||||||
|
KmsCryptographicOperator = "cryptographic-operator"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TGetProjectRolesDTO = {
|
export type TGetProjectRolesDTO = {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export { useDebounce } from "./useDebounce";
|
export { useDebounce } from "./useDebounce";
|
||||||
|
export * from "./useGetProjectTypeFromRoute";
|
||||||
export { usePagination } from "./usePagination";
|
export { usePagination } from "./usePagination";
|
||||||
export { usePersistentState } from "./usePersistentState";
|
export { usePersistentState } from "./usePersistentState";
|
||||||
export { usePopUp } from "./usePopUp";
|
export { usePopUp } from "./usePopUp";
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
};
|
||||||
@@ -11,11 +11,12 @@ import { BreadcrumbContainer, TBreadcrumbFormat } from "@app/components/v2";
|
|||||||
import { OrgPermissionSubjects, useOrgPermission, useServerConfig } from "@app/context";
|
import { OrgPermissionSubjects, useOrgPermission, useServerConfig } from "@app/context";
|
||||||
import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types";
|
import { OrgPermissionSecretShareAction } from "@app/context/OrgPermissionContext/types";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
|
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
|
||||||
import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
|
import { MinimizedOrgSidebar } from "./components/MinimizedOrgSidebar";
|
||||||
import { SidebarHeader } from "./components/SidebarHeader";
|
import { SidebarHeader } from "./components/SidebarHeader";
|
||||||
import { DefaultSideBar, SecretSharingSideBar } from "./ProductsSideBar";
|
import { DefaultSideBar, ProjectOverviewSideBar, SecretSharingSideBar } from "./ProductsSideBar";
|
||||||
|
|
||||||
export const OrganizationLayout = () => {
|
export const OrganizationLayout = () => {
|
||||||
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
const matches = useRouterState({ select: (s) => s.matches.at(-1)?.context });
|
||||||
@@ -45,21 +46,38 @@ export const OrganizationLayout = () => {
|
|||||||
] as string[]
|
] as string[]
|
||||||
).includes(location.pathname);
|
).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 =
|
const shouldShowOrgSidebar =
|
||||||
location.pathname.startsWith("/organization") &&
|
location.pathname.startsWith("/organization") &&
|
||||||
(!isSecretSharingPage || shouldShowProductsSidebar) &&
|
(!isSecretSharingPage || shouldShowProductsSidebar) &&
|
||||||
!(
|
!([linkOptions({ to: "/organization/secret-scanning" }).to] as string[]).includes(
|
||||||
[
|
location.pathname
|
||||||
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);
|
|
||||||
|
|
||||||
const containerHeight = config.pageFrameContent ? "h-[94vh]" : "h-screen";
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Banner />
|
<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"
|
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]">
|
<nav className="items-between flex h-full flex-col overflow-y-auto dark:[color-scheme:dark]">
|
||||||
<div className="p-2 pt-3">
|
{!isProjectOverviewOrSettingsPage && (
|
||||||
<SidebarHeader />
|
<div className="p-2 pt-3">
|
||||||
</div>
|
<SidebarHeader />
|
||||||
{isSecretSharingPage ? <SecretSharingSideBar /> : <DefaultSideBar />}
|
</div>
|
||||||
|
)}
|
||||||
|
{SideBarComponent}
|
||||||
</nav>
|
</nav>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from "./DefaultSideBar";
|
export * from "./DefaultSideBar";
|
||||||
|
export * from "./ProjectOverviewSideBar";
|
||||||
export * from "./SecretSharingSideBar";
|
export * from "./SecretSharingSideBar";
|
||||||
|
|||||||
@@ -269,7 +269,9 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
<MenuIconButton
|
<MenuIconButton
|
||||||
isSelected={
|
isSelected={
|
||||||
isActive ||
|
isActive ||
|
||||||
window.location.pathname.startsWith(`/${ProjectType.SecretManager}`)
|
window.location.pathname.startsWith(
|
||||||
|
`/organization/${ProjectType.SecretManager}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
icon="sliding-carousel"
|
icon="sliding-carousel"
|
||||||
>
|
>
|
||||||
@@ -282,7 +284,9 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
<MenuIconButton
|
<MenuIconButton
|
||||||
isSelected={
|
isSelected={
|
||||||
isActive ||
|
isActive ||
|
||||||
window.location.pathname.startsWith(`/${ProjectType.CertificateManager}`)
|
window.location.pathname.startsWith(
|
||||||
|
`/organization/${ProjectType.CertificateManager}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
icon="note"
|
icon="note"
|
||||||
>
|
>
|
||||||
@@ -294,7 +298,8 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<MenuIconButton
|
<MenuIconButton
|
||||||
isSelected={
|
isSelected={
|
||||||
isActive || window.location.pathname.startsWith(`/${ProjectType.KMS}`)
|
isActive ||
|
||||||
|
window.location.pathname.startsWith(`/organization/${ProjectType.KMS}`)
|
||||||
}
|
}
|
||||||
icon="unlock"
|
icon="unlock"
|
||||||
>
|
>
|
||||||
@@ -306,7 +311,8 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<MenuIconButton
|
<MenuIconButton
|
||||||
isSelected={
|
isSelected={
|
||||||
isActive || window.location.pathname.startsWith(`/${ProjectType.SSH}`)
|
isActive ||
|
||||||
|
window.location.pathname.startsWith(`/organization/${ProjectType.SSH}`)
|
||||||
}
|
}
|
||||||
icon="verified"
|
icon="verified"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
|
import { IdentityPanel } from "@app/pages/admin/OverviewPage/components/IdentityPanel";
|
||||||
|
|
||||||
import { AuthPanel } from "./components/AuthPanel";
|
import { AuthPanel } from "./components/AuthPanel";
|
||||||
|
import { CachingPanel } from "./components/CachingPanel";
|
||||||
import { EncryptionPanel } from "./components/EncryptionPanel";
|
import { EncryptionPanel } from "./components/EncryptionPanel";
|
||||||
import { IntegrationPanel } from "./components/IntegrationPanel";
|
import { IntegrationPanel } from "./components/IntegrationPanel";
|
||||||
import { UserPanel } from "./components/UserPanel";
|
import { UserPanel } from "./components/UserPanel";
|
||||||
@@ -42,7 +43,8 @@ enum TabSections {
|
|||||||
Integrations = "integrations",
|
Integrations = "integrations",
|
||||||
Users = "users",
|
Users = "users",
|
||||||
Identities = "identities",
|
Identities = "identities",
|
||||||
Kmip = "kmip"
|
Kmip = "kmip",
|
||||||
|
Caching = "caching"
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SignUpModes {
|
enum SignUpModes {
|
||||||
@@ -164,6 +166,7 @@ export const OverviewPage = () => {
|
|||||||
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
<Tab value={TabSections.Integrations}>Integrations</Tab>
|
||||||
<Tab value={TabSections.Users}>User Identities</Tab>
|
<Tab value={TabSections.Users}>User Identities</Tab>
|
||||||
<Tab value={TabSections.Identities}>Machine Identities</Tab>
|
<Tab value={TabSections.Identities}>Machine Identities</Tab>
|
||||||
|
<Tab value={TabSections.Caching}>Caching</Tab>
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={TabSections.Settings}>
|
<TabPanel value={TabSections.Settings}>
|
||||||
@@ -408,6 +411,9 @@ export const OverviewPage = () => {
|
|||||||
<TabPanel value={TabSections.Identities}>
|
<TabPanel value={TabSections.Identities}>
|
||||||
<IdentityPanel />
|
<IdentityPanel />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Caching}>
|
||||||
|
<CachingPanel />
|
||||||
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -6,9 +6,9 @@ import { format } from "date-fns";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2";
|
import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2";
|
||||||
|
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||||
import { useGetUserWorkspaces } from "@app/hooks/api";
|
import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||||
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
import { IdentityMembership } from "@app/hooks/api/identities/types";
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
export enum TabSections {
|
export enum TabSections {
|
||||||
@@ -26,15 +26,6 @@ type Props = {
|
|||||||
) => void;
|
) => 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 = ({
|
export const IdentityProjectRow = ({
|
||||||
membership: { id, createdAt, identity, project, roles },
|
membership: { id, createdAt, identity, project, roles },
|
||||||
handlePopUpOpen
|
handlePopUpOpen
|
||||||
@@ -80,7 +71,7 @@ export const IdentityProjectRow = ({
|
|||||||
<Td>
|
<Td>
|
||||||
<Tag size="xs">{project.type}</Tag>
|
<Tag size="xs">{project.type}</Tag>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{`${formatRoleName(roles[0].role, roles[0].customRoleName)}${
|
<Td>{`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${
|
||||||
roles.length > 1 ? ` (+${roles.length - 1})` : ""
|
roles.length > 1 ? ` (+${roles.length - 1})` : ""
|
||||||
}`}</Td>
|
}`}</Td>
|
||||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -117,12 +117,8 @@ export const RoleModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.error(err);
|
const text = `Failed to ${popUp?.role?.data ? "update" : "create"} role`;
|
||||||
const error = err as any;
|
|
||||||
const text =
|
|
||||||
error?.response?.data?.message ??
|
|
||||||
`Failed to ${popUp?.role?.data ? "update" : "create"} role`;
|
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const ProjectListToggle = ({ value, onChange }: Props) => {
|
|||||||
value={value}
|
value={value}
|
||||||
onValueChange={onChange}
|
onValueChange={onChange}
|
||||||
className="absolute left-0 top-0 h-full w-full cursor-pointer opacity-0"
|
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.MyProjects}>My Projects</SelectItem>
|
||||||
<SelectItem value={ProjectListView.AllProjects}>All Projects</SelectItem>
|
<SelectItem value={ProjectListView.AllProjects}>All Projects</SelectItem>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|||||||
import { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
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 { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
|
||||||
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
import { AuditLogStreamsTab } from "../AuditLogStreamTab";
|
||||||
@@ -11,7 +12,6 @@ import { OrgAuthTab } from "../OrgAuthTab";
|
|||||||
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
import { OrgEncryptionTab } from "../OrgEncryptionTab";
|
||||||
import { OrgGeneralTab } from "../OrgGeneralTab";
|
import { OrgGeneralTab } from "../OrgGeneralTab";
|
||||||
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab";
|
import { OrgWorkflowIntegrationTab } from "../OrgWorkflowIntegrationTab/OrgWorkflowIntegrationTab";
|
||||||
import { ProjectTemplatesTab } from "../ProjectTemplatesTab";
|
|
||||||
|
|
||||||
export const OrgTabGroup = () => {
|
export const OrgTabGroup = () => {
|
||||||
const search = useSearch({
|
const search = useSearch({
|
||||||
@@ -28,7 +28,34 @@ export const OrgTabGroup = () => {
|
|||||||
},
|
},
|
||||||
{ name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab },
|
{ name: "Audit Log Streams", key: "tag-audit-log-streams", component: AuditLogStreamsTab },
|
||||||
{ name: "Import", key: "tab-import", component: ImportTab },
|
{ 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 "Project
|
||||||
|
Templates" 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 }
|
{ name: "KMIP", key: "kmip", component: KmipTab }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -5,8 +5,8 @@ import { useNavigate } from "@tanstack/react-router";
|
|||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2";
|
import { IconButton, Tag, Td, Tooltip, Tr } from "@app/components/v2";
|
||||||
|
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||||
import { useGetUserWorkspaces } from "@app/hooks/api";
|
import { useGetUserWorkspaces } from "@app/hooks/api";
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
|
||||||
import { TWorkspaceUser } from "@app/hooks/api/types";
|
import { TWorkspaceUser } from "@app/hooks/api/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
import { OrgAccessControlTabSections } from "@app/types/org";
|
import { OrgAccessControlTabSections } from "@app/types/org";
|
||||||
@@ -19,15 +19,6 @@ type Props = {
|
|||||||
) => void;
|
) => 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 = ({
|
export const UserProjectRow = ({
|
||||||
membership: { id, project, user, roles },
|
membership: { id, project, user, roles },
|
||||||
handlePopUpOpen
|
handlePopUpOpen
|
||||||
@@ -73,7 +64,7 @@ export const UserProjectRow = ({
|
|||||||
<Td>
|
<Td>
|
||||||
<Tag size="xs">{project.type}</Tag>
|
<Tag size="xs">{project.type}</Tag>
|
||||||
</Td>
|
</Td>
|
||||||
<Td>{`${formatRoleName(roles[0].role, roles[0].customRoleName)}${
|
<Td>{`${formatProjectRoleName(roles[0].role, roles[0].customRoleName)}${
|
||||||
roles.length > 1 ? ` (+${roles.length - 1})` : ""
|
roles.length > 1 ? ` (+${roles.length - 1})` : ""
|
||||||
}`}</Td>
|
}`}</Td>
|
||||||
<Td>
|
<Td>
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ import {
|
|||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useGetProjectRoles, useUpdateGroupWorkspaceRole } from "@app/hooks/api";
|
import { useGetProjectRoles, useUpdateGroupWorkspaceRole } from "@app/hooks/api";
|
||||||
import { TGroupMembership } from "@app/hooks/api/groups/types";
|
import { TGroupMembership } from "@app/hooks/api/groups/types";
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
|
||||||
import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
|
import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types";
|
||||||
import { groupBy } from "@app/lib/fn/array";
|
import { groupBy } from "@app/lib/fn/array";
|
||||||
|
|
||||||
@@ -260,12 +260,6 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatRoleName = (role: string, customRoleName?: string) => {
|
|
||||||
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
|
||||||
if (role === ProjectMembershipRole.Member) return "Developer";
|
|
||||||
return role;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{roles
|
{roles
|
||||||
@@ -275,7 +269,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
|
|||||||
return (
|
return (
|
||||||
<Tag key={id} className="capitalize">
|
<Tag key={id} className="capitalize">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div>{formatRoleName(role, customRoleName)}</div>
|
<div>{formatProjectRoleName(role, customRoleName)}</div>
|
||||||
{isTemporary && (
|
{isTemporary && (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content={isExpired ? "Expired Temporary Access" : "Temporary Access"}>
|
<Tooltip content={isExpired ? "Expired Temporary Access" : "Temporary Access"}>
|
||||||
@@ -303,7 +297,7 @@ export const GroupRoles = ({ roles = [], disableEdit = false, groupId }: TMember
|
|||||||
return (
|
return (
|
||||||
<Tag key={id} className="capitalize">
|
<Tag key={id} className="capitalize">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div>{formatRoleName(role, customRoleName)}</div>
|
<div>{formatProjectRoleName(role, customRoleName)}</div>
|
||||||
{isTemporary && (
|
{isTemporary && (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ import {
|
|||||||
Tr
|
Tr
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
import { withProjectPermission } from "@app/hoc";
|
||||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||||
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
|
import { useDeleteIdentityFromWorkspace, useGetWorkspaceIdentityMemberships } from "@app/hooks/api";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
|
||||||
import { ProjectIdentityOrderBy } from "@app/hooks/api/workspace/types";
|
import { ProjectIdentityOrderBy } from "@app/hooks/api/workspace/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -53,12 +53,6 @@ import { IdentityModal } from "./components/IdentityModal";
|
|||||||
|
|
||||||
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
||||||
|
|
||||||
const formatRoleName = (role: string, customRoleName?: string) => {
|
|
||||||
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
|
||||||
if (role === ProjectMembershipRole.Member) return "Developer";
|
|
||||||
if (role === ProjectMembershipRole.NoAccess) return "No access";
|
|
||||||
return role;
|
|
||||||
};
|
|
||||||
export const IdentityTab = withProjectPermission(
|
export const IdentityTab = withProjectPermission(
|
||||||
() => {
|
() => {
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
@@ -286,7 +280,7 @@ export const IdentityTab = withProjectPermission(
|
|||||||
<Tag key={roleId}>
|
<Tag key={roleId}>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="capitalize">
|
<div className="capitalize">
|
||||||
{formatRoleName(role, customRoleName)}
|
{formatProjectRoleName(role, customRoleName)}
|
||||||
</div>
|
</div>
|
||||||
{isTemporary && (
|
{isTemporary && (
|
||||||
<div>
|
<div>
|
||||||
@@ -331,7 +325,9 @@ export const IdentityTab = withProjectPermission(
|
|||||||
return (
|
return (
|
||||||
<Tag key={roleId} className="capitalize">
|
<Tag key={roleId} className="capitalize">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div>{formatRoleName(role, customRoleName)}</div>
|
<div>
|
||||||
|
{formatProjectRoleName(role, customRoleName)}
|
||||||
|
</div>
|
||||||
{isTemporary && (
|
{isTemporary && (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -50,19 +50,13 @@ import {
|
|||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
|
import { formatProjectRoleName } from "@app/helpers/roles";
|
||||||
import { usePagination, useResetPageHelper } from "@app/hooks";
|
import { usePagination, useResetPageHelper } from "@app/hooks";
|
||||||
import { useGetProjectRoles, useGetWorkspaceUsers } from "@app/hooks/api";
|
import { useGetProjectRoles, useGetWorkspaceUsers } from "@app/hooks/api";
|
||||||
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
|
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
||||||
const formatRoleName = (role: string, customRoleName?: string) => {
|
|
||||||
if (role === ProjectMembershipRole.Custom) return customRoleName;
|
|
||||||
if (role === ProjectMembershipRole.Member) return "Developer";
|
|
||||||
if (role === ProjectMembershipRole.NoAccess) return "No access";
|
|
||||||
return role;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
@@ -343,7 +337,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
<Tag key={id}>
|
<Tag key={id}>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="capitalize">
|
<div className="capitalize">
|
||||||
{formatRoleName(role, customRoleName)}
|
{formatProjectRoleName(role, customRoleName)}
|
||||||
</div>
|
</div>
|
||||||
{isTemporary && (
|
{isTemporary && (
|
||||||
<div>
|
<div>
|
||||||
@@ -386,7 +380,7 @@ export const MembersTable = ({ handlePopUpOpen }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Tag key={id} className="capitalize">
|
<Tag key={id} className="capitalize">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div>{formatRoleName(role, customRoleName)}</div>
|
<div>{formatProjectRoleName(role, customRoleName)}</div>
|
||||||
{isTemporary && (
|
{isTemporary && (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
|
import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
|
||||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
import { ProjectMembershipRole, TProjectRole } from "@app/hooks/api/roles/types";
|
||||||
import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal";
|
import { RoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/RoleModal";
|
||||||
|
|
||||||
export const ProjectRoleList = () => {
|
export const ProjectRoleList = () => {
|
||||||
@@ -86,7 +86,9 @@ export const ProjectRoleList = () => {
|
|||||||
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
{isRolesLoading && <TableSkeleton columns={4} innerKey="org-roles" />}
|
||||||
{roles?.map((role) => {
|
{roles?.map((role) => {
|
||||||
const { id, name, slug } = role;
|
const { id, name, slug } = role;
|
||||||
const isNonMutatable = ["admin", "member", "viewer", "no-access"].includes(slug);
|
const isNonMutatable = Object.values(ProjectMembershipRole).includes(
|
||||||
|
slug as ProjectMembershipRole
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tr
|
<Tr
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import { AxiosError } from "axios";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
|
||||||
decryptAssymmetric,
|
|
||||||
encryptSymmetric
|
|
||||||
} from "@app/components/utilities/cryptography/crypto";
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@@ -28,7 +24,7 @@ import {
|
|||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useWorkspace } from "@app/context";
|
import { useWorkspace } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import { useCreateServiceToken, useGetUserWsKey } from "@app/hooks/api";
|
import { useCreateServiceToken } from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const apiTokenExpiry = [
|
const apiTokenExpiry = [
|
||||||
@@ -97,7 +93,6 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const [newToken, setToken] = useState("");
|
const [newToken, setToken] = useState("");
|
||||||
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
|
const [isTokenCopied, setIsTokenCopied] = useToggle(false);
|
||||||
|
|
||||||
const { data: latestFileKey } = useGetUserWsKey(currentWorkspace?.id ?? "");
|
|
||||||
const createServiceToken = useCreateServiceToken();
|
const createServiceToken = useCreateServiceToken();
|
||||||
const hasServiceToken = Boolean(newToken);
|
const hasServiceToken = Boolean(newToken);
|
||||||
|
|
||||||
@@ -118,26 +113,13 @@ export const AddServiceTokenModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => {
|
const onFormSubmit = async ({ name, scopes, expiresIn, permissions }: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!currentWorkspace?.id) return;
|
if (!currentWorkspace?.id) return;
|
||||||
if (!latestFileKey) return;
|
|
||||||
|
|
||||||
const key = decryptAssymmetric({
|
|
||||||
ciphertext: latestFileKey.encryptedKey,
|
|
||||||
nonce: latestFileKey.nonce,
|
|
||||||
publicKey: latestFileKey.sender.publicKey,
|
|
||||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
|
||||||
});
|
|
||||||
|
|
||||||
const randomBytes = crypto.randomBytes(16).toString("hex");
|
const randomBytes = crypto.randomBytes(16).toString("hex");
|
||||||
|
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
|
||||||
plaintext: key,
|
|
||||||
key: randomBytes
|
|
||||||
});
|
|
||||||
|
|
||||||
const { serviceToken } = await createServiceToken.mutateAsync({
|
const { serviceToken } = await createServiceToken.mutateAsync({
|
||||||
encryptedKey: ciphertext,
|
encryptedKey: "",
|
||||||
iv,
|
iv: "",
|
||||||
tag,
|
tag: "",
|
||||||
scopes,
|
scopes,
|
||||||
expiresIn: Number(expiresIn),
|
expiresIn: Number(expiresIn),
|
||||||
name,
|
name,
|
||||||
|
|||||||