diff --git a/.gitignore b/.gitignore index af3d457e05..4a12c15f24 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,8 @@ yarn-error.log* # Infisical init .infisical.json +.infisicalignore + # Editor specific .vscode/* diff --git a/.infisicalignore b/.infisicalignore index b8fafe6db7..348f9e3277 100644 --- a/.infisicalignore +++ b/.infisicalignore @@ -1 +1,5 @@ .github/resources/docker-compose.be-test.yml:generic-api-key:16 +frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx:generic-api-key:206 +frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:304 +frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx:generic-api-key:206 +frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx:generic-api-key:292 \ No newline at end of file diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 3c22cd3c63..a4c3eea7bc 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -6,9 +6,11 @@ import { TCreateAuditLogDTO } from "@app/ee/services/audit-log/audit-log-types"; import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; +import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TLdapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { TProjectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { TSamlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { TScimServiceFactory } from "@app/ee/services/scim/scim-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; @@ -125,6 +127,8 @@ declare module "fastify" { telemetry: TTelemetryServiceFactory; dynamicSecret: TDynamicSecretServiceFactory; dynamicSecretLease: TDynamicSecretLeaseServiceFactory; + projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory; + identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 99ac78932f..2c8b8be5a5 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -47,6 +47,9 @@ import { TIdentityOrgMemberships, TIdentityOrgMembershipsInsert, TIdentityOrgMembershipsUpdate, + TIdentityProjectAdditionalPrivilege, + TIdentityProjectAdditionalPrivilegeInsert, + TIdentityProjectAdditionalPrivilegeUpdate, TIdentityProjectMembershipRole, TIdentityProjectMembershipRoleInsert, TIdentityProjectMembershipRoleUpdate, @@ -101,6 +104,9 @@ import { TProjects, TProjectsInsert, TProjectsUpdate, + TProjectUserAdditionalPrivilege, + TProjectUserAdditionalPrivilegeInsert, + TProjectUserAdditionalPrivilegeUpdate, TProjectUserMembershipRoles, TProjectUserMembershipRolesInsert, TProjectUserMembershipRolesUpdate, @@ -267,6 +273,11 @@ declare module "knex/types/tables" { TProjectUserMembershipRolesUpdate >; [TableName.ProjectRoles]: Knex.CompositeTableType; + [TableName.ProjectUserAdditionalPrivilege]: Knex.CompositeTableType< + TProjectUserAdditionalPrivilege, + TProjectUserAdditionalPrivilegeInsert, + TProjectUserAdditionalPrivilegeUpdate + >; [TableName.ProjectKeys]: Knex.CompositeTableType; [TableName.Secret]: Knex.CompositeTableType; [TableName.SecretBlindIndex]: Knex.CompositeTableType< @@ -322,6 +333,11 @@ declare module "knex/types/tables" { TIdentityProjectMembershipRoleInsert, TIdentityProjectMembershipRoleUpdate >; + [TableName.IdentityProjectAdditionalPrivilege]: Knex.CompositeTableType< + TIdentityProjectAdditionalPrivilege, + TIdentityProjectAdditionalPrivilegeInsert, + TIdentityProjectAdditionalPrivilegeUpdate + >; [TableName.ScimToken]: Knex.CompositeTableType; [TableName.SecretApprovalPolicy]: Knex.CompositeTableType< TSecretApprovalPolicies, diff --git a/backend/src/db/migrations/20240326172010_project-user-additional-privilege.ts b/backend/src/db/migrations/20240326172010_project-user-additional-privilege.ts new file mode 100644 index 0000000000..0366ba507b --- /dev/null +++ b/backend/src/db/migrations/20240326172010_project-user-additional-privilege.ts @@ -0,0 +1,29 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.ProjectUserAdditionalPrivilege))) { + await knex.schema.createTable(TableName.ProjectUserAdditionalPrivilege, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("slug", 60).notNullable(); + t.uuid("projectMembershipId").notNullable(); + t.foreign("projectMembershipId").references("id").inTable(TableName.ProjectMembership).onDelete("CASCADE"); + t.boolean("isTemporary").notNullable().defaultTo(false); + t.string("temporaryMode"); + t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc + t.datetime("temporaryAccessStartTime"); + t.datetime("temporaryAccessEndTime"); + t.jsonb("permissions").notNullable(); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.ProjectUserAdditionalPrivilege); +} + +export async function down(knex: Knex): Promise { + await dropOnUpdateTrigger(knex, TableName.ProjectUserAdditionalPrivilege); + await knex.schema.dropTableIfExists(TableName.ProjectUserAdditionalPrivilege); +} diff --git a/backend/src/db/migrations/20240326172011_machine-identity-additional-privilege.ts b/backend/src/db/migrations/20240326172011_machine-identity-additional-privilege.ts new file mode 100644 index 0000000000..c59fc685a4 --- /dev/null +++ b/backend/src/db/migrations/20240326172011_machine-identity-additional-privilege.ts @@ -0,0 +1,32 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.IdentityProjectAdditionalPrivilege))) { + await knex.schema.createTable(TableName.IdentityProjectAdditionalPrivilege, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + t.string("slug", 60).notNullable(); + t.uuid("projectMembershipId").notNullable(); + t.foreign("projectMembershipId") + .references("id") + .inTable(TableName.IdentityProjectMembership) + .onDelete("CASCADE"); + t.boolean("isTemporary").notNullable().defaultTo(false); + t.string("temporaryMode"); + t.string("temporaryRange"); // could be cron or relative time like 1H or 1minute etc + t.datetime("temporaryAccessStartTime"); + t.datetime("temporaryAccessEndTime"); + t.jsonb("permissions").notNullable(); + t.timestamps(true, true, true); + }); + } + + await createOnUpdateTrigger(knex, TableName.IdentityProjectAdditionalPrivilege); +} + +export async function down(knex: Knex): Promise { + await dropOnUpdateTrigger(knex, TableName.IdentityProjectAdditionalPrivilege); + await knex.schema.dropTableIfExists(TableName.IdentityProjectAdditionalPrivilege); +} diff --git a/backend/src/db/schemas/identity-project-additional-privilege.ts b/backend/src/db/schemas/identity-project-additional-privilege.ts new file mode 100644 index 0000000000..7a9dbe19eb --- /dev/null +++ b/backend/src/db/schemas/identity-project-additional-privilege.ts @@ -0,0 +1,31 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const IdentityProjectAdditionalPrivilegeSchema = z.object({ + id: z.string().uuid(), + slug: z.string(), + projectMembershipId: z.string().uuid(), + isTemporary: z.boolean().default(false), + temporaryMode: z.string().nullable().optional(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + permissions: z.unknown(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TIdentityProjectAdditionalPrivilege = z.infer; +export type TIdentityProjectAdditionalPrivilegeInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TIdentityProjectAdditionalPrivilegeUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index c179740911..b9dab06bae 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -13,6 +13,7 @@ export * from "./groups"; export * from "./identities"; export * from "./identity-access-tokens"; export * from "./identity-org-memberships"; +export * from "./identity-project-additional-privilege"; export * from "./identity-project-membership-role"; export * from "./identity-project-memberships"; export * from "./identity-ua-client-secrets"; @@ -31,6 +32,7 @@ export * from "./project-environments"; export * from "./project-keys"; export * from "./project-memberships"; export * from "./project-roles"; +export * from "./project-user-additional-privilege"; export * from "./project-user-membership-roles"; export * from "./projects"; export * from "./saml-configs"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index ace4d5edbe..d5cf1b8867 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -24,6 +24,7 @@ export enum TableName { Environment = "project_environments", ProjectMembership = "project_memberships", ProjectRoles = "project_roles", + ProjectUserAdditionalPrivilege = "project_user_additional_privilege", ProjectUserMembershipRole = "project_user_membership_roles", ProjectKeys = "project_keys", Secret = "secrets", @@ -47,6 +48,7 @@ export enum TableName { IdentityOrgMembership = "identity_org_memberships", IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembershipRole = "identity_project_membership_role", + IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", ScimToken = "scim_tokens", SecretApprovalPolicy = "secret_approval_policies", SecretApprovalPolicyApprover = "secret_approval_policies_approvers", diff --git a/backend/src/db/schemas/project-user-additional-privilege.ts b/backend/src/db/schemas/project-user-additional-privilege.ts new file mode 100644 index 0000000000..0fd0e5faad --- /dev/null +++ b/backend/src/db/schemas/project-user-additional-privilege.ts @@ -0,0 +1,31 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ProjectUserAdditionalPrivilegeSchema = z.object({ + id: z.string().uuid(), + slug: z.string(), + projectMembershipId: z.string().uuid(), + isTemporary: z.boolean().default(false), + temporaryMode: z.string().nullable().optional(), + temporaryRange: z.string().nullable().optional(), + temporaryAccessStartTime: z.date().nullable().optional(), + temporaryAccessEndTime: z.date().nullable().optional(), + permissions: z.unknown(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TProjectUserAdditionalPrivilege = z.infer; +export type TProjectUserAdditionalPrivilegeInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TProjectUserAdditionalPrivilegeUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts new file mode 100644 index 0000000000..f08dfde1e9 --- /dev/null +++ b/backend/src/ee/routes/v1/identity-project-additional-privilege-router.ts @@ -0,0 +1,308 @@ +import { MongoAbility, RawRuleOf } from "@casl/ability"; +import { PackRule, packRules, unpackRules } from "@casl/ability/extra"; +import slugify from "@sindresorhus/slugify"; +import ms from "ms"; +import { z } from "zod"; + +import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas"; +import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types"; +import { ProjectPermissionSet } from "@app/ee/services/permission/project-permission"; +import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/permanent", + method: "POST", + schema: { + description: "Create a permanent or a non expiry specific privilege for identity.", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug), + slug: z + .string() + .min(1) + .max(60) + .trim() + .default(slugify(alphaNumericNanoId(12))) + .refine((val) => val.toLowerCase() === val, "Must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), + permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions) + }), + response: { + 200: z.object({ + privilege: IdentityProjectAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilege.create({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + ...req.body, + isTemporary: false, + permissions: JSON.stringify(packRules(req.body.permissions)) + }); + return { privilege }; + } + }); + + server.route({ + url: "/temporary", + method: "POST", + schema: { + description: "Create a temporary or a expiring specific privilege for identity.", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.projectSlug), + slug: z + .string() + .min(1) + .max(60) + .trim() + .default(slugify(alphaNumericNanoId(12))) + .refine((val) => val.toLowerCase() === val, "Must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug), + permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions), + temporaryMode: z + .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime) + }), + response: { + 200: z.object({ + privilege: IdentityProjectAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilege.create({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + ...req.body, + isTemporary: true, + permissions: JSON.stringify(packRules(req.body.permissions)) + }); + return { privilege }; + } + }); + + server.route({ + url: "/", + method: "PATCH", + schema: { + description: "Update a specific privilege of an identity.", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + // disallow empty string + privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.slug), + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.projectSlug), + privilegeDetails: z + .object({ + slug: z + .string() + .min(1) + .max(60) + .trim() + .refine((val) => val.toLowerCase() === val, "Must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug), + permissions: z.any().array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions), + isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), + temporaryMode: z + .nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode) + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime) + }) + .partial() + }), + response: { + 200: z.object({ + privilege: IdentityProjectAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const updatedInfo = req.body.privilegeDetails; + const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + slug: req.body.privilegeSlug, + identityId: req.body.identityId, + projectSlug: req.body.projectSlug, + data: { + ...updatedInfo, + permissions: updatedInfo?.permissions ? JSON.stringify(packRules(updatedInfo.permissions)) : undefined + } + }); + return { privilege }; + } + }); + + server.route({ + url: "/", + method: "DELETE", + schema: { + description: "Delete a specific privilege of an identity.", + security: [ + { + bearerAuth: [] + } + ], + body: z.object({ + privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.slug), + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.DELETE.projectSlug) + }), + response: { + 200: z.object({ + privilege: IdentityProjectAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilege.deleteBySlug({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + slug: req.body.privilegeSlug, + identityId: req.body.identityId, + projectSlug: req.body.projectSlug + }); + return { privilege }; + } + }); + + server.route({ + url: "/:privilegeSlug", + method: "GET", + schema: { + description: "Retrieve details of a specific privilege by privilege slug.", + security: [ + { + bearerAuth: [] + } + ], + params: z.object({ + privilegeSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.slug) + }), + querystring: z.object({ + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.GET_BY_SLUG.projectSlug) + }), + response: { + 200: z.object({ + privilege: IdentityProjectAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privilege = await server.services.identityProjectAdditionalPrivilege.getPrivilegeDetailsBySlug({ + actorId: req.permission.id, + actorAuthMethod: req.permission.authMethod, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + slug: req.params.privilegeSlug, + ...req.query + }); + return { privilege }; + } + }); + + server.route({ + url: "/", + method: "GET", + schema: { + description: "List of a specific privilege of an identity in a project.", + security: [ + { + bearerAuth: [] + } + ], + querystring: z.object({ + identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.identityId), + projectSlug: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.projectSlug), + unpacked: z + .enum(["false", "true"]) + .transform((el) => el === "true") + .default("true") + .describe(IDENTITY_ADDITIONAL_PRIVILEGE.LIST.unpacked) + }), + response: { + 200: z.object({ + privileges: IdentityProjectAdditionalPrivilegeSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + handler: async (req) => { + const privileges = await server.services.identityProjectAdditionalPrivilege.listIdentityProjectPrivileges({ + actorId: req.permission.id, + actor: req.permission.type, + actorAuthMethod: req.permission.authMethod, + actorOrgId: req.permission.orgId, + ...req.query + }); + if (req.query.unpacked) { + return { + privileges: privileges.map(({ permissions, ...el }) => ({ + ...el, + permissions: unpackRules(permissions as PackRule>>[]) + })) + }; + } + return { privileges }; + } + }); +}; diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 95ae587310..6860098fd9 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,6 +1,7 @@ import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerGroupRouter } from "./group-router"; +import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; import { registerLdapRouter } from "./ldap-router"; import { registerLicenseRouter } from "./license-router"; import { registerOrgRoleRouter } from "./org-role-router"; @@ -16,6 +17,7 @@ import { registerSecretScanningRouter } from "./secret-scanning-router"; import { registerSecretVersionRouter } from "./secret-version-router"; import { registerSnapshotRouter } from "./snapshot-router"; import { registerTrustedIpRouter } from "./trusted-ip-router"; +import { registerUserAdditionalPrivilegeRouter } from "./user-additional-privilege-router"; export const registerV1EERoutes = async (server: FastifyZodProvider) => { // org role starts with organization @@ -53,4 +55,11 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => { await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" }); await server.register(registerSecretVersionRouter, { prefix: "/secret" }); await server.register(registerGroupRouter, { prefix: "/groups" }); + await server.register( + async (privilegeRouter) => { + await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" }); + await privilegeRouter.register(registerIdentityProjectAdditionalPrivilegeRouter, { prefix: "/identity" }); + }, + { prefix: "/additional-privilege" } + ); }; diff --git a/backend/src/ee/routes/v1/user-additional-privilege-router.ts b/backend/src/ee/routes/v1/user-additional-privilege-router.ts new file mode 100644 index 0000000000..9b6bfb6fb4 --- /dev/null +++ b/backend/src/ee/routes/v1/user-additional-privilege-router.ts @@ -0,0 +1,235 @@ +import slugify from "@sindresorhus/slugify"; +import ms from "ms"; +import { z } from "zod"; + +import { ProjectUserAdditionalPrivilegeSchema } from "@app/db/schemas"; +import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; +import { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs"; +import { alphaNumericNanoId } from "@app/lib/nanoid"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => { + server.route({ + url: "/permanent", + method: "POST", + schema: { + body: z.object({ + projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), + slug: z + .string() + .min(1) + .max(60) + .trim() + .default(slugify(alphaNumericNanoId(12))) + .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), + permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions) + }), + response: { + 200: z.object({ + privilege: ProjectUserAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const privilege = await server.services.projectUserAdditionalPrivilege.create({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + ...req.body, + isTemporary: false, + permissions: JSON.stringify(req.body.permissions) + }); + return { privilege }; + } + }); + + server.route({ + url: "/temporary", + method: "POST", + schema: { + body: z.object({ + projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), + slug: z + .string() + .min(1) + .max(60) + .trim() + .default(`privilege-${slugify(alphaNumericNanoId(12))}`) + .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), + permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), + temporaryMode: z + .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryRange), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.temporaryAccessStartTime) + }), + response: { + 200: z.object({ + privilege: ProjectUserAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const privilege = await server.services.projectUserAdditionalPrivilege.create({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + ...req.body, + isTemporary: true, + permissions: JSON.stringify(req.body.permissions) + }); + return { privilege }; + } + }); + + server.route({ + url: "/:privilegeId", + method: "PATCH", + schema: { + params: z.object({ + privilegeId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.privilegeId) + }), + body: z + .object({ + slug: z + .string() + .max(60) + .trim() + .refine((v) => v.toLowerCase() === v, "Slug must be lowercase") + .refine((v) => slugify(v) === v, { + message: "Slug must be a valid slug" + }) + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), + permissions: z.any().array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), + isTemporary: z.boolean().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary), + temporaryMode: z + .nativeEnum(ProjectUserAdditionalPrivilegeTemporaryMode) + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode), + temporaryRange: z + .string() + .refine((val) => ms(val) > 0, "Temporary range must be a positive number") + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange), + temporaryAccessStartTime: z + .string() + .datetime() + .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.temporaryAccessStartTime) + }) + .partial(), + response: { + 200: z.object({ + privilege: ProjectUserAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const privilege = await server.services.projectUserAdditionalPrivilege.updateById({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + ...req.body, + permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined, + privilegeId: req.params.privilegeId + }); + return { privilege }; + } + }); + + server.route({ + url: "/:privilegeId", + method: "DELETE", + schema: { + params: z.object({ + privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.DELETE.privilegeId) + }), + response: { + 200: z.object({ + privilege: ProjectUserAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const privilege = await server.services.projectUserAdditionalPrivilege.deleteById({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + privilegeId: req.params.privilegeId + }); + return { privilege }; + } + }); + + server.route({ + url: "/", + method: "GET", + schema: { + querystring: z.object({ + projectMembershipId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.LIST.projectMembershipId) + }), + response: { + 200: z.object({ + privileges: ProjectUserAdditionalPrivilegeSchema.array() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const privileges = await server.services.projectUserAdditionalPrivilege.listPrivileges({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + projectMembershipId: req.query.projectMembershipId + }); + return { privileges }; + } + }); + + server.route({ + url: "/:privilegeId", + method: "GET", + schema: { + params: z.object({ + privilegeId: z.string().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.GET_BY_PRIVILEGEID.privilegeId) + }), + response: { + 200: z.object({ + privilege: ProjectUserAdditionalPrivilegeSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const privilege = await server.services.projectUserAdditionalPrivilege.getPrivilegeDetailsById({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + privilegeId: req.params.privilegeId + }); + return { privilege }; + } + }); +}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts new file mode 100644 index 0000000000..26252f2d18 --- /dev/null +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal.ts @@ -0,0 +1,12 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TIdentityProjectAdditionalPrivilegeDALFactory = ReturnType< + typeof identityProjectAdditionalPrivilegeDALFactory +>; + +export const identityProjectAdditionalPrivilegeDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.IdentityProjectAdditionalPrivilege); + return orm; +}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts new file mode 100644 index 0000000000..81dc11a007 --- /dev/null +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service.ts @@ -0,0 +1,297 @@ +import { ForbiddenError } from "@casl/ability"; +import ms from "ms"; + +import { isAtLeastAsPrivileged } from "@app/lib/casl"; +import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; +import { ActorType } from "@app/services/auth/auth-type"; +import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal"; +import { TProjectDALFactory } from "@app/services/project/project-dal"; + +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TIdentityProjectAdditionalPrivilegeDALFactory } from "./identity-project-additional-privilege-dal"; +import { + IdentityProjectAdditionalPrivilegeTemporaryMode, + TCreateIdentityPrivilegeDTO, + TDeleteIdentityPrivilegeDTO, + TGetIdentityPrivilegeDetailsDTO, + TListIdentityPrivilegesDTO, + TUpdateIdentityPrivilegeDTO +} from "./identity-project-additional-privilege-types"; + +type TIdentityProjectAdditionalPrivilegeServiceFactoryDep = { + identityProjectAdditionalPrivilegeDAL: TIdentityProjectAdditionalPrivilegeDALFactory; + identityProjectDAL: Pick; + projectDAL: Pick; + permissionService: Pick; +}; + +export type TIdentityProjectAdditionalPrivilegeServiceFactory = ReturnType< + typeof identityProjectAdditionalPrivilegeServiceFactory +>; + +export const identityProjectAdditionalPrivilegeServiceFactory = ({ + identityProjectAdditionalPrivilegeDAL, + identityProjectDAL, + permissionService, + projectDAL +}: TIdentityProjectAdditionalPrivilegeServiceFactoryDep) => { + const create = async ({ + slug, + actor, + actorId, + identityId, + projectSlug, + permissions: customPermission, + actorOrgId, + actorAuthMethod, + ...dto + }: TCreateIdentityPrivilegeDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( + ActorType.IDENTITY, + identityId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + if (!hasRequiredPriviledges) + throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + + const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug, + projectMembershipId: identityProjectMembership.id + }); + if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + + if (!dto.isTemporary) { + const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ + projectMembershipId: identityProjectMembership.id, + slug, + permissions: customPermission + }); + return additionalPrivilege; + } + + const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); + const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.create({ + projectMembershipId: identityProjectMembership.id, + slug, + permissions: customPermission, + isTemporary: true, + temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: dto.temporaryRange, + temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), + temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) + }); + return additionalPrivilege; + }; + + const updateBySlug = async ({ + projectSlug, + slug, + identityId, + data, + actorOrgId, + actor, + actorId, + actorAuthMethod + }: TUpdateIdentityPrivilegeDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( + ActorType.IDENTITY, + identityProjectMembership.identityId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + if (!hasRequiredPriviledges) + throw new ForbiddenRequestError({ message: "Failed to update more privileged identity" }); + + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug, + projectMembershipId: identityProjectMembership.id + }); + if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); + if (data?.slug) { + const existingSlug = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug: data.slug, + projectMembershipId: identityProjectMembership.id + }); + if (existingSlug && existingSlug.id !== identityPrivilege.id) + throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + } + + const isTemporary = typeof data?.isTemporary !== "undefined" ? data.isTemporary : identityPrivilege.isTemporary; + if (isTemporary) { + const temporaryAccessStartTime = data?.temporaryAccessStartTime || identityPrivilege?.temporaryAccessStartTime; + const temporaryRange = data?.temporaryRange || identityPrivilege?.temporaryRange; + const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { + ...data, + temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), + temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) + }); + return additionalPrivilege; + } + + const additionalPrivilege = await identityProjectAdditionalPrivilegeDAL.updateById(identityPrivilege.id, { + ...data, + isTemporary: false, + temporaryAccessStartTime: null, + temporaryAccessEndTime: null, + temporaryRange: null, + temporaryMode: null + }); + return additionalPrivilege; + }; + + const deleteBySlug = async ({ + actorId, + slug, + identityId, + projectSlug, + actor, + actorOrgId, + actorAuthMethod + }: TDeleteIdentityPrivilegeDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + const { permission: identityRolePermission } = await permissionService.getProjectPermission( + ActorType.IDENTITY, + identityProjectMembership.identityId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + const hasRequiredPriviledges = isAtLeastAsPrivileged(permission, identityRolePermission); + if (!hasRequiredPriviledges) + throw new ForbiddenRequestError({ message: "Failed to edit more privileged identity" }); + + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug, + projectMembershipId: identityProjectMembership.id + }); + if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); + + const deletedPrivilege = await identityProjectAdditionalPrivilegeDAL.deleteById(identityPrivilege.id); + return deletedPrivilege; + }; + + const getPrivilegeDetailsBySlug = async ({ + projectSlug, + identityId, + slug, + actorOrgId, + actor, + actorId, + actorAuthMethod + }: TGetIdentityPrivilegeDetailsDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + + const identityPrivilege = await identityProjectAdditionalPrivilegeDAL.findOne({ + slug, + projectMembershipId: identityProjectMembership.id + }); + if (!identityPrivilege) throw new BadRequestError({ message: "Identity additional privilege not found" }); + + return identityPrivilege; + }; + + const listIdentityProjectPrivileges = async ({ + identityId, + actorOrgId, + actor, + actorId, + actorAuthMethod, + projectSlug + }: TListIdentityPrivilegesDTO) => { + const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId); + if (!project) throw new BadRequestError({ message: "Project not found" }); + const projectId = project.id; + + const identityProjectMembership = await identityProjectDAL.findOne({ identityId, projectId }); + if (!identityProjectMembership) + throw new BadRequestError({ message: `Failed to find identity with id ${identityId}` }); + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + identityProjectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Identity); + + const identityPrivileges = await identityProjectAdditionalPrivilegeDAL.find({ + projectMembershipId: identityProjectMembership.id + }); + return identityPrivileges; + }; + + return { + create, + updateBySlug, + deleteBySlug, + getPrivilegeDetailsBySlug, + listIdentityProjectPrivileges + }; +}; diff --git a/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts new file mode 100644 index 0000000000..88ff01d7da --- /dev/null +++ b/backend/src/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types.ts @@ -0,0 +1,54 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum IdentityProjectAdditionalPrivilegeTemporaryMode { + Relative = "relative" +} + +export type TCreateIdentityPrivilegeDTO = { + permissions: unknown; + identityId: string; + projectSlug: string; + slug: string; +} & ( + | { + isTemporary: false; + } + | { + isTemporary: true; + temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + } +) & + Omit; + +export type TUpdateIdentityPrivilegeDTO = { slug: string; identityId: string; projectSlug: string } & Omit< + TProjectPermission, + "projectId" +> & { + data: Partial<{ + permissions: unknown; + slug: string; + isTemporary: boolean; + temporaryMode: IdentityProjectAdditionalPrivilegeTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + }>; + }; + +export type TDeleteIdentityPrivilegeDTO = Omit & { + slug: string; + identityId: string; + projectSlug: string; +}; + +export type TGetIdentityPrivilegeDetailsDTO = Omit & { + slug: string; + identityId: string; + projectSlug: string; +}; + +export type TListIdentityPrivilegesDTO = Omit & { + identityId: string; + projectSlug: string; +}; diff --git a/backend/src/ee/services/permission/permission-dal.ts b/backend/src/ee/services/permission/permission-dal.ts index c7692e5656..5d1cb72793 100644 --- a/backend/src/ee/services/permission/permission-dal.ts +++ b/backend/src/ee/services/permission/permission-dal.ts @@ -93,6 +93,11 @@ export const permissionDALFactory = (db: TDbClient) => { `${TableName.ProjectUserMembershipRole}.customRoleId`, `${TableName.ProjectRoles}.id` ) + .leftJoin( + TableName.ProjectUserAdditionalPrivilege, + `${TableName.ProjectUserAdditionalPrivilege}.projectMembershipId`, + `${TableName.ProjectMembership}.id` + ) .join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`) .join(TableName.Organization, `${TableName.Project}.orgId`, `${TableName.Organization}.id`) .where("userId", userId) @@ -106,9 +111,22 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("projectId").withSchema(TableName.ProjectMembership), db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"), db.ref("orgId").withSchema(TableName.Project), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug") - ) - .select("permissions"); + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), + db.ref("permissions").withSchema(TableName.ProjectRoles), + db.ref("id").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApId"), + db.ref("permissions").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApPermissions"), + db.ref("temporaryMode").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApIsTemporary"), + db.ref("temporaryRange").withSchema(TableName.ProjectUserAdditionalPrivilege).as("userApTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userApTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.ProjectUserAdditionalPrivilege) + .as("userApTemporaryAccessEndTime") + ); const permission = sqlNestRelationships({ data: docs.concat(groupDocs), @@ -132,17 +150,44 @@ export const permissionDALFactory = (db: TDbClient) => { permissions: z.unknown(), customRoleSlug: z.string().optional().nullable() }).parse(data) + }, + { + key: "userApId", + label: "additionalPrivileges" as const, + mapper: ({ + userApId, + userApPermissions, + userApIsTemporary, + userApTemporaryMode, + userApTemporaryRange, + userApTemporaryAccessEndTime, + userApTemporaryAccessStartTime + }) => ({ + id: userApId, + permissions: userApPermissions, + temporaryRange: userApTemporaryRange, + temporaryMode: userApTemporaryMode, + temporaryAccessEndTime: userApTemporaryAccessEndTime, + temporaryAccessStartTime: userApTemporaryAccessStartTime, + isTemporary: userApIsTemporary + }) } ] }); + if (!permission?.[0]) return undefined; // when introducting cron mode change it here - const activeRoles = permission?.[0]?.roles.filter( + const activeRoles = permission?.[0]?.roles?.filter( ({ isTemporary, temporaryAccessEndTime }) => !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) ); - return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined; + const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ); + + return { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges }; } catch (error) { throw new DatabaseError({ error, name: "GetProjectPermission" }); } @@ -161,6 +206,11 @@ export const permissionDALFactory = (db: TDbClient) => { `${TableName.IdentityProjectMembershipRole}.customRoleId`, `${TableName.ProjectRoles}.id` ) + .leftJoin( + TableName.IdentityProjectAdditionalPrivilege, + `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId`, + `${TableName.IdentityProjectMembership}.id` + ) .join( // Join the Project table to later select orgId TableName.Project, @@ -176,9 +226,28 @@ export const permissionDALFactory = (db: TDbClient) => { db.ref("role").withSchema(TableName.IdentityProjectMembership).as("oldRoleField"), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership).as("membershipCreatedAt"), db.ref("updatedAt").withSchema(TableName.IdentityProjectMembership).as("membershipUpdatedAt"), - db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug") - ) - .select("permissions"); + db.ref("slug").withSchema(TableName.ProjectRoles).as("customRoleSlug"), + db.ref("permissions").withSchema(TableName.ProjectRoles), + db.ref("id").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApId"), + db.ref("permissions").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApPermissions"), + db + .ref("temporaryMode") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryMode"), + db.ref("isTemporary").withSchema(TableName.IdentityProjectAdditionalPrivilege).as("identityApIsTemporary"), + db + .ref("temporaryRange") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryRange"), + db + .ref("temporaryAccessStartTime") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryAccessStartTime"), + db + .ref("temporaryAccessEndTime") + .withSchema(TableName.IdentityProjectAdditionalPrivilege) + .as("identityApTemporaryAccessEndTime") + ); const permission = sqlNestRelationships({ data: docs, @@ -203,16 +272,44 @@ export const permissionDALFactory = (db: TDbClient) => { permissions: z.unknown(), customRoleSlug: z.string().optional().nullable() }).parse(data) + }, + { + key: "identityApId", + label: "additionalPrivileges" as const, + mapper: ({ + identityApId, + identityApPermissions, + identityApIsTemporary, + identityApTemporaryMode, + identityApTemporaryRange, + identityApTemporaryAccessEndTime, + identityApTemporaryAccessStartTime + }) => ({ + id: identityApId, + permissions: identityApPermissions, + temporaryRange: identityApTemporaryRange, + temporaryMode: identityApTemporaryMode, + temporaryAccessEndTime: identityApTemporaryAccessEndTime, + temporaryAccessStartTime: identityApTemporaryAccessStartTime, + isTemporary: identityApIsTemporary + }) } ] }); + if (!permission?.[0]) return undefined; + // when introducting cron mode change it here const activeRoles = permission?.[0]?.roles.filter( ({ isTemporary, temporaryAccessEndTime }) => !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) ); - return permission?.[0] ? { ...permission[0], roles: activeRoles } : undefined; + const activeAdditionalPrivileges = permission?.[0]?.additionalPrivileges?.filter( + ({ isTemporary, temporaryAccessEndTime }) => + !isTemporary || (isTemporary && temporaryAccessEndTime && new Date() < temporaryAccessEndTime) + ); + + return { ...permission[0], roles: activeRoles, additionalPrivileges: activeAdditionalPrivileges }; } catch (error) { throw new DatabaseError({ error, name: "GetProjectIdentityPermission" }); } diff --git a/backend/src/ee/services/permission/permission-fns.ts b/backend/src/ee/services/permission/permission-fns.ts index 78b8ad8f24..eda19c2150 100644 --- a/backend/src/ee/services/permission/permission-fns.ts +++ b/backend/src/ee/services/permission/permission-fns.ts @@ -5,9 +5,13 @@ import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { if (!actorAuthMethod) return false; - return [AuthMethod.AZURE_SAML, AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.GOOGLE_SAML].includes( - actorAuthMethod - ); + return [ + AuthMethod.AZURE_SAML, + AuthMethod.OKTA_SAML, + AuthMethod.JUMPCLOUD_SAML, + AuthMethod.GOOGLE_SAML, + AuthMethod.KEYCLOAK_SAML + ].includes(actorAuthMethod); } function validateOrgSAML(actorAuthMethod: ActorAuthMethod, isSamlEnforced: TOrganizations["authEnforced"]) { diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index d632be3dfc..f4e4237975 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -180,10 +180,12 @@ export const permissionServiceFactory = ({ authMethod: ActorAuthMethod, userOrgId?: string ): Promise> => { - const membership = await permissionDAL.getProjectPermission(userId, projectId); - if (!membership) throw new UnauthorizedError({ name: "User not in project" }); + const userProjectPermission = await permissionDAL.getProjectPermission(userId, projectId); + if (!userProjectPermission) throw new UnauthorizedError({ name: "User not in project" }); - if (membership.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions)) { + if ( + userProjectPermission.roles.some(({ role, permissions }) => role === ProjectMembershipRole.Custom && !permissions) + ) { throw new BadRequestError({ name: "Custom permission not found" }); } @@ -192,17 +194,27 @@ export const permissionServiceFactory = ({ // Extra: This means that when users are using API keys to make requests, they can't use slug-based routes. // Slug-based routes depend on the organization ID being present on the request, since project slugs aren't globally unique, and we need a way to filter by organization. - if (userOrgId !== "API_KEY" && membership.orgId !== userOrgId) { + if (userOrgId !== "API_KEY" && userProjectPermission.orgId !== userOrgId) { throw new UnauthorizedError({ name: "You are not logged into this organization" }); } - validateOrgSAML(authMethod, membership.orgAuthEnforced); + validateOrgSAML(authMethod, userProjectPermission.orgAuthEnforced); + + // join two permissions and pass to build the final permission set + const rolePermissions = userProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; + const additionalPrivileges = + userProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ + role: ProjectMembershipRole.Custom, + permissions + })) || []; return { - permission: buildProjectPermission(membership.roles), - membership, + permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)), + membership: userProjectPermission, hasRole: (role: string) => - membership.roles.findIndex(({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug) !== -1 + userProjectPermission.roles.findIndex( + ({ role: slug, customRoleSlug }) => role === slug || slug === customRoleSlug + ) !== -1 }; }; @@ -226,8 +238,16 @@ export const permissionServiceFactory = ({ throw new UnauthorizedError({ name: "You are not a member of this organization" }); } + const rolePermissions = + identityProjectPermission.roles?.map(({ role, permissions }) => ({ role, permissions })) || []; + const additionalPrivileges = + identityProjectPermission.additionalPrivileges?.map(({ permissions }) => ({ + role: ProjectMembershipRole.Custom, + permissions + })) || []; + return { - permission: buildProjectPermission(identityProjectPermission.roles), + permission: buildProjectPermission(rolePermissions.concat(additionalPrivileges)), membership: identityProjectPermission, hasRole: (role: string) => identityProjectPermission.roles.findIndex( diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts new file mode 100644 index 0000000000..6c15d2d5dd --- /dev/null +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal.ts @@ -0,0 +1,10 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { ormify } from "@app/lib/knex"; + +export type TProjectUserAdditionalPrivilegeDALFactory = ReturnType; + +export const projectUserAdditionalPrivilegeDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ProjectUserAdditionalPrivilege); + return orm; +}; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts new file mode 100644 index 0000000000..c9ff2c7e07 --- /dev/null +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-service.ts @@ -0,0 +1,212 @@ +import { ForbiddenError } from "@casl/ability"; +import ms from "ms"; + +import { BadRequestError } from "@app/lib/errors"; +import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal"; + +import { TPermissionServiceFactory } from "../permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; +import { TProjectUserAdditionalPrivilegeDALFactory } from "./project-user-additional-privilege-dal"; +import { + ProjectUserAdditionalPrivilegeTemporaryMode, + TCreateUserPrivilegeDTO, + TDeleteUserPrivilegeDTO, + TGetUserPrivilegeDetailsDTO, + TListUserPrivilegesDTO, + TUpdateUserPrivilegeDTO +} from "./project-user-additional-privilege-types"; + +type TProjectUserAdditionalPrivilegeServiceFactoryDep = { + projectUserAdditionalPrivilegeDAL: TProjectUserAdditionalPrivilegeDALFactory; + projectMembershipDAL: Pick; + permissionService: Pick; +}; + +export type TProjectUserAdditionalPrivilegeServiceFactory = ReturnType< + typeof projectUserAdditionalPrivilegeServiceFactory +>; + +export const projectUserAdditionalPrivilegeServiceFactory = ({ + projectUserAdditionalPrivilegeDAL, + projectMembershipDAL, + permissionService +}: TProjectUserAdditionalPrivilegeServiceFactoryDep) => { + const create = async ({ + slug, + actor, + actorId, + permissions: customPermission, + actorOrgId, + actorAuthMethod, + projectMembershipId, + ...dto + }: TCreateUserPrivilegeDTO) => { + const projectMembership = await projectMembershipDAL.findById(projectMembershipId); + if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + + const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ slug, projectMembershipId }); + if (existingSlug) throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + + if (!dto.isTemporary) { + const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ + projectMembershipId, + slug, + permissions: customPermission + }); + return additionalPrivilege; + } + + const relativeTempAllocatedTimeInMs = ms(dto.temporaryRange); + const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.create({ + projectMembershipId, + slug, + permissions: customPermission, + isTemporary: true, + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative, + temporaryRange: dto.temporaryRange, + temporaryAccessStartTime: new Date(dto.temporaryAccessStartTime), + temporaryAccessEndTime: new Date(new Date(dto.temporaryAccessStartTime).getTime() + relativeTempAllocatedTimeInMs) + }); + return additionalPrivilege; + }; + + const updateById = async ({ + privilegeId, + actorOrgId, + actor, + actorId, + actorAuthMethod, + ...dto + }: TUpdateUserPrivilegeDTO) => { + const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); + if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" }); + + const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId); + if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + + if (dto?.slug) { + const existingSlug = await projectUserAdditionalPrivilegeDAL.findOne({ + slug: dto.slug, + projectMembershipId: projectMembership.id + }); + if (existingSlug && existingSlug.id !== userPrivilege.id) + throw new BadRequestError({ message: "Additional privilege of provided slug exist" }); + } + + const isTemporary = typeof dto?.isTemporary !== "undefined" ? dto.isTemporary : userPrivilege.isTemporary; + if (isTemporary) { + const temporaryAccessStartTime = dto?.temporaryAccessStartTime || userPrivilege?.temporaryAccessStartTime; + const temporaryRange = dto?.temporaryRange || userPrivilege?.temporaryRange; + const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { + ...dto, + temporaryAccessStartTime: new Date(temporaryAccessStartTime || ""), + temporaryAccessEndTime: new Date(new Date(temporaryAccessStartTime || "").getTime() + ms(temporaryRange || "")) + }); + return additionalPrivilege; + } + + const additionalPrivilege = await projectUserAdditionalPrivilegeDAL.updateById(userPrivilege.id, { + ...dto, + isTemporary: false, + temporaryAccessStartTime: null, + temporaryAccessEndTime: null, + temporaryRange: null, + temporaryMode: null + }); + return additionalPrivilege; + }; + + const deleteById = async ({ actorId, actor, actorOrgId, actorAuthMethod, privilegeId }: TDeleteUserPrivilegeDTO) => { + const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); + if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" }); + + const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId); + if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Member); + + const deletedPrivilege = await projectUserAdditionalPrivilegeDAL.deleteById(userPrivilege.id); + return deletedPrivilege; + }; + + const getPrivilegeDetailsById = async ({ + privilegeId, + actorOrgId, + actor, + actorId, + actorAuthMethod + }: TGetUserPrivilegeDetailsDTO) => { + const userPrivilege = await projectUserAdditionalPrivilegeDAL.findById(privilegeId); + if (!userPrivilege) throw new BadRequestError({ message: "User additional privilege not found" }); + + const projectMembership = await projectMembershipDAL.findById(userPrivilege.projectMembershipId); + if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + + return userPrivilege; + }; + + const listPrivileges = async ({ + projectMembershipId, + actorOrgId, + actor, + actorId, + actorAuthMethod + }: TListUserPrivilegesDTO) => { + const projectMembership = await projectMembershipDAL.findById(projectMembershipId); + if (!projectMembership) throw new BadRequestError({ message: "Project membership not found" }); + + const { permission } = await permissionService.getProjectPermission( + actor, + actorId, + projectMembership.projectId, + actorAuthMethod, + actorOrgId + ); + ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Member); + + const userPrivileges = await projectUserAdditionalPrivilegeDAL.find({ projectMembershipId }); + return userPrivileges; + }; + + return { + create, + updateById, + deleteById, + getPrivilegeDetailsById, + listPrivileges + }; +}; diff --git a/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts new file mode 100644 index 0000000000..572474270c --- /dev/null +++ b/backend/src/ee/services/project-user-additional-privilege/project-user-additional-privilege-types.ts @@ -0,0 +1,40 @@ +import { TProjectPermission } from "@app/lib/types"; + +export enum ProjectUserAdditionalPrivilegeTemporaryMode { + Relative = "relative" +} + +export type TCreateUserPrivilegeDTO = ( + | { + permissions: unknown; + projectMembershipId: string; + slug: string; + isTemporary: false; + } + | { + permissions: unknown; + projectMembershipId: string; + slug: string; + isTemporary: true; + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + } +) & + Omit; + +export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit & + Partial<{ + permissions: unknown; + slug: string; + isTemporary: boolean; + temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative; + temporaryRange: string; + temporaryAccessStartTime: string; + }>; + +export type TDeleteUserPrivilegeDTO = Omit & { privilegeId: string }; + +export type TGetUserPrivilegeDetailsDTO = Omit & { privilegeId: string }; + +export type TListUserPrivilegesDTO = Omit & { projectMembershipId: string }; diff --git a/backend/src/ee/services/saml-config/saml-config-service.ts b/backend/src/ee/services/saml-config/saml-config-service.ts index dc97289573..f88182e614 100644 --- a/backend/src/ee/services/saml-config/saml-config-service.ts +++ b/backend/src/ee/services/saml-config/saml-config-service.ts @@ -319,6 +319,11 @@ export const samlConfigServiceFactory = ({ const organization = await orgDAL.findOrgById(orgId); if (!organization) throw new BadRequestError({ message: "Org not found" }); + // TODO(dangtony98): remove this after aliases update + if (authProvider === AuthMethod.KEYCLOAK_SAML && appCfg.LICENSE_SERVER_KEY) { + throw new BadRequestError({ message: "Keycloak SAML is not yet available on Infisical Cloud" }); + } + if (user) { await userDAL.transaction(async (tx) => { const [orgMembership] = await orgDAL.findMembership( diff --git a/backend/src/ee/services/saml-config/saml-config-types.ts b/backend/src/ee/services/saml-config/saml-config-types.ts index 9aedf5d193..df76949203 100644 --- a/backend/src/ee/services/saml-config/saml-config-types.ts +++ b/backend/src/ee/services/saml-config/saml-config-types.ts @@ -5,7 +5,8 @@ export enum SamlProviders { OKTA_SAML = "okta-saml", AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", - GOOGLE_SAML = "google-saml" + GOOGLE_SAML = "google-saml", + KEYCLOAK_SAML = "keycloak-saml" } export type TCreateSamlCfgDTO = { diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index e892233ad4..cbb217bbf2 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -263,6 +263,7 @@ export const SECRETS = { export const RAW_SECRETS = { LIST: { + recursive: "Whether or not to fetch all secrets from the specified base path, and all of its subdirectories.", workspaceId: "The ID of the project to list secrets from.", workspaceSlug: "The slug of the project to list secrets from. This parameter is only usable by machine identities.", environment: "The slug of the environment to list secrets from.", @@ -445,3 +446,158 @@ export const SECRET_TAGS = { projectId: "The ID of the project to delete the tag from." } } as const; + +export const IDENTITY_ADDITIONAL_PRIVILEGE = { + CREATE: { + projectSlug: "The slug of the project of the identity in.", + identityId: "The ID of the identity to delete.", + slug: "The slug of the privilege to create.", + permissions: `The permission object for the privilege. +1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]] +2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]] +2. [["read", "secrets", {environment: "dev"}]] +`, + isPackPermission: "Whether the server should pack(compact) the permission object.", + isTemporary: "Whether the privilege is temporary.", + temporaryMode: "Type of temporary access given. Types: relative", + temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", + temporaryAccessStartTime: "ISO time for which temporary access should begin." + }, + UPDATE: { + projectSlug: "The slug of the project of the identity in.", + identityId: "The ID of the identity to update.", + slug: "The slug of the privilege to update.", + newSlug: "The new slug of the privilege to update.", + permissions: `The permission object for the privilege. +1. [["read", "secrets", {environment: "dev", secretPath: {$glob: "/"}}]] +2. [["read", "secrets", {environment: "dev"}], ["create", "secrets", {environment: "dev"}]] +2. [["read", "secrets", {environment: "dev"}]] +`, + isPackPermission: "Whether the server should pack(compact) the permission object.", + isTemporary: "Whether the privilege is temporary.", + temporaryMode: "Type of temporary access given. Types: relative", + temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", + temporaryAccessStartTime: "ISO time for which temporary access should begin." + }, + DELETE: { + projectSlug: "The slug of the project of the identity in.", + identityId: "The ID of the identity to delete.", + slug: "The slug of the privilege to delete." + }, + GET_BY_SLUG: { + projectSlug: "The slug of the project of the identity in.", + identityId: "The ID of the identity to list.", + slug: "The slug of the privilege." + }, + LIST: { + projectSlug: "The slug of the project of the identity in.", + identityId: "The ID of the identity to list.", + unpacked: "Whether the system should send the permissions as unpacked" + } +}; + +export const PROJECT_USER_ADDITIONAL_PRIVILEGE = { + CREATE: { + projectMembershipId: "Project membership id of user", + slug: "The slug of the privilege to create.", + permissions: + "The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape", + isPackPermission: "Whether the server should pack(compact) the permission object.", + isTemporary: "Whether the privilege is temporary.", + temporaryMode: "Type of temporary access given. Types: relative", + temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", + temporaryAccessStartTime: "ISO time for which temporary access should begin." + }, + UPDATE: { + privilegeId: "The id of privilege object", + slug: "The slug of the privilege to create.", + newSlug: "The new slug of the privilege to create.", + permissions: + "The permission object for the privilege. Refer https://casl.js.org/v6/en/guide/define-rules#the-shape-of-raw-rule to understand the shape", + isPackPermission: "Whether the server should pack(compact) the permission object.", + isTemporary: "Whether the privilege is temporary.", + temporaryMode: "Type of temporary access given. Types: relative", + temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d", + temporaryAccessStartTime: "ISO time for which temporary access should begin." + }, + DELETE: { + privilegeId: "The id of privilege object" + }, + GET_BY_PRIVILEGEID: { + privilegeId: "The id of privilege object" + }, + LIST: { + projectMembershipId: "Project membership id of user" + } +}; + +export const INTEGRATION_AUTH = { + GET: { + integrationAuthId: "The id of integration authentication object." + }, + DELETE: { + integration: "The slug of the integration to be unauthorized.", + projectId: "The ID of the project to delete the integration auth from." + }, + DELETE_BY_ID: { + integrationAuthId: "The id of integration authentication object to delete." + }, + CREATE_ACCESS_TOKEN: { + workspaceId: "The ID of the project to create the integration auth for.", + integration: "The slug of integration for the auth object.", + accessId: "The unique authorized access id of the external integration provider.", + accessToken: "The unique authorized access token of the external integration provider.", + url: "", + namespace: "", + refreshToken: "The refresh token for integration authorization." + }, + LIST_AUTHORIZATION: { + workspaceId: "The ID of the project to list integration auths for." + } +}; + +export const INTEGRATION = { + CREATE: { + integrationAuthId: "The ID of the integration auth object to link with integration.", + app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", + isActive: "Whether the integration should be active or disabled.", + appId: + "The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", + secretPath: "The path of the secrets to sync secrets from.", + sourceEnvironment: "The environment to sync secret from.", + targetEnvironment: + "The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.", + targetEnvironmentId: + "The target environment id of the integration provider. Used in cloudflare pages, teamcity, gitlab integrations.", + targetService: + "The service based grouping identifier of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank", + targetServiceId: + "The service based grouping identifier ID of the external provider. Used in Terraform cloud, Checkly, Railway and NorthFlank", + owner: "External integration providers service entity owner. Used in Github.", + path: "Path to save the synced secrets. Used by Gitlab, AWS Parameter Store, Vault", + region: "AWS region to sync secrets to.", + scope: "Scope of the provider. Used by Github, Qovery", + metadata: { + secretPrefix: "The prefix for the saved secret. Used by GCP", + secretSuffix: "The suffix for the saved secret. Used by GCP", + initialSyncBehavoir: "Type of syncing behavoir with the integration", + shouldAutoRedeploy: "Used by Render to trigger auto deploy", + secretGCPLabel: "The label for the GCP secrets" + } + }, + UPDATE: { + integrationId: "The ID of the integration object.", + app: "The name of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", + appId: + "The ID of the external integration providers app entity that you want to sync secrets with. Used in Netlify, GitHub, Vercel integrations.", + isActive: "Whether the integration should be active or disabled.", + secretPath: "The path of the secrets to sync secrets from.", + owner: "External integration providers service entity owner. Used in Github.", + targetEnvironment: + "The target environment of the integration provider. Used in cloudflare pages, TeamCity, Gitlab integrations.", + environment: "The environment to sync secrets from." + }, + DELETE: { + integrationId: "The ID of the integration object." + } +}; diff --git a/backend/src/server/config/rateLimiter.ts b/backend/src/server/config/rateLimiter.ts index 444158cbf4..76429b0a5f 100644 --- a/backend/src/server/config/rateLimiter.ts +++ b/backend/src/server/config/rateLimiter.ts @@ -20,7 +20,13 @@ export const globalRateLimiterCfg = (): RateLimitPluginOptions => { export const authRateLimit: RateLimitOptions = { timeWindow: 60 * 1000, - max: 600, + max: 60, + keyGenerator: (req) => req.realIp +}; + +export const inviteUserRateLimit: RateLimitOptions = { + timeWindow: 60 * 1000, + max: 10, keyGenerator: (req) => req.realIp }; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 7f7ddff8a0..465f32f83b 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -14,12 +14,16 @@ import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secre import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; +import { identityProjectAdditionalPrivilegeDALFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-dal"; +import { identityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { ldapConfigDALFactory } from "@app/ee/services/ldap-config/ldap-config-dal"; import { ldapConfigServiceFactory } from "@app/ee/services/ldap-config/ldap-config-service"; import { licenseDALFactory } from "@app/ee/services/license/license-dal"; import { licenseServiceFactory } from "@app/ee/services/license/license-service"; import { permissionDALFactory } from "@app/ee/services/permission/permission-dal"; import { permissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { projectUserAdditionalPrivilegeDALFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-dal"; +import { projectUserAdditionalPrivilegeServiceFactory } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-service"; import { samlConfigDALFactory } from "@app/ee/services/saml-config/saml-config-dal"; import { samlConfigServiceFactory } from "@app/ee/services/saml-config/saml-config-service"; import { scimDALFactory } from "@app/ee/services/scim/scim-dal"; @@ -155,6 +159,7 @@ export const registerRoutes = async ( const projectDAL = projectDALFactory(db); const projectMembershipDAL = projectMembershipDALFactory(db); + const projectUserAdditionalPrivilegeDAL = projectUserAdditionalPrivilegeDALFactory(db); const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(db); const projectRoleDAL = projectRoleDALFactory(db); const projectEnvDAL = projectEnvDALFactory(db); @@ -180,6 +185,7 @@ export const registerRoutes = async ( const identityOrgMembershipDAL = identityOrgDALFactory(db); const identityProjectDAL = identityProjectDALFactory(db); const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db); + const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db); const identityUaDAL = identityUaDALFactory(db); const identityUaClientSecretDAL = identityUaClientSecretDALFactory(db); @@ -381,6 +387,11 @@ export const registerRoutes = async ( projectRoleDAL, licenseService }); + const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({ + permissionService, + projectMembershipDAL, + projectUserAdditionalPrivilegeDAL + }); const projectKeyService = projectKeyServiceFactory({ permissionService, projectKeyDAL, @@ -515,6 +526,7 @@ export const registerRoutes = async ( snapshotService, secretQueueService, secretImportDAL, + projectEnvDAL, projectBotService }); const sarService = secretApprovalRequestServiceFactory({ @@ -584,6 +596,12 @@ export const registerRoutes = async ( identityProjectMembershipRoleDAL, projectRoleDAL }); + const identityProjectAdditionalPrivilegeService = identityProjectAdditionalPrivilegeServiceFactory({ + projectDAL, + identityProjectAdditionalPrivilegeDAL, + permissionService, + identityProjectDAL + }); const identityUaService = identityUaServiceFactory({ identityOrgMembershipDAL, permissionService, @@ -676,7 +694,9 @@ export const registerRoutes = async ( trustedIp: trustedIpService, scim: scimService, secretBlindIndex: secretBlindIndexService, - telemetry: telemetryService + telemetry: telemetryService, + projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService, + identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService }); server.decorate("store", { diff --git a/backend/src/server/routes/v1/integration-auth-router.ts b/backend/src/server/routes/v1/integration-auth-router.ts index 004ca298d0..a68479074f 100644 --- a/backend/src/server/routes/v1/integration-auth-router.ts +++ b/backend/src/server/routes/v1/integration-auth-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { INTEGRATION_AUTH } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -10,8 +11,14 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) server.route({ url: "/integration-options", method: "GET", - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + description: "List of integrations available.", + security: [ + { + bearerAuth: [] + } + ], response: { 200: z.object({ integrationOptions: z @@ -38,10 +45,16 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) server.route({ url: "/:integrationAuthId", method: "GET", - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + description: "Get details of an integration authorization by auth object id.", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - integrationAuthId: z.string().trim() + integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.GET.integrationAuthId) }), response: { 200: z.object({ @@ -64,11 +77,17 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) server.route({ url: "/", method: "DELETE", - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + description: "Remove all integration's auth object from the project.", + security: [ + { + bearerAuth: [] + } + ], querystring: z.object({ - integration: z.string().trim(), - projectId: z.string().trim() + integration: z.string().trim().describe(INTEGRATION_AUTH.DELETE.integration), + projectId: z.string().trim().describe(INTEGRATION_AUTH.DELETE.projectId) }), response: { 200: z.object({ @@ -104,10 +123,16 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) server.route({ url: "/:integrationAuthId", method: "DELETE", - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + description: "Remove an integration auth object by object id.", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - integrationAuthId: z.string().trim() + integrationAuthId: z.string().trim().describe(INTEGRATION_AUTH.DELETE_BY_ID.integrationAuthId) }), response: { 200: z.object({ @@ -183,16 +208,22 @@ export const registerIntegrationAuthRouter = async (server: FastifyZodProvider) server.route({ url: "/access-token", method: "POST", - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), schema: { + description: "Create the integration authentication object required for syncing secrets.", + security: [ + { + bearerAuth: [] + } + ], body: z.object({ - workspaceId: z.string().trim(), - integration: z.string().trim(), - accessId: z.string().trim().optional(), - accessToken: z.string().trim().optional(), - url: z.string().url().trim().optional(), - namespace: z.string().trim().optional(), - refreshToken: z.string().trim().optional() + workspaceId: z.string().trim().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.workspaceId), + integration: z.string().trim().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.integration), + accessId: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessId), + accessToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.accessToken), + url: z.string().url().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.url), + namespace: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.namespace), + refreshToken: z.string().trim().optional().describe(INTEGRATION_AUTH.CREATE_ACCESS_TOKEN.refreshToken) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/integration-router.ts b/backend/src/server/routes/v1/integration-router.ts index 4859ca5843..0bc434dbe2 100644 --- a/backend/src/server/routes/v1/integration-router.ts +++ b/backend/src/server/routes/v1/integration-router.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { IntegrationsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { INTEGRATION } from "@app/lib/api-docs"; import { removeTrailingSlash, shake } from "@app/lib/fn"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; @@ -13,33 +14,45 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { url: "/", method: "POST", schema: { + description: "Create an integration to sync secrets.", + security: [ + { + bearerAuth: [] + } + ], body: z.object({ - integrationAuthId: z.string().trim(), - app: z.string().trim().optional(), - isActive: z.boolean(), - appId: z.string().trim().optional(), - secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - sourceEnvironment: z.string().trim(), - targetEnvironment: z.string().trim().optional(), - targetEnvironmentId: z.string().trim().optional(), - targetService: z.string().trim().optional(), - targetServiceId: z.string().trim().optional(), - owner: z.string().trim().optional(), - path: z.string().trim().optional(), - region: z.string().trim().optional(), - scope: z.string().trim().optional(), + integrationAuthId: z.string().trim().describe(INTEGRATION.CREATE.integrationAuthId), + app: z.string().trim().optional().describe(INTEGRATION.CREATE.app), + isActive: z.boolean().describe(INTEGRATION.CREATE.isActive).default(true), + appId: z.string().trim().optional().describe(INTEGRATION.CREATE.appId), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(INTEGRATION.CREATE.secretPath), + sourceEnvironment: z.string().trim().describe(INTEGRATION.CREATE.sourceEnvironment), + targetEnvironment: z.string().trim().optional().describe(INTEGRATION.CREATE.targetEnvironment), + targetEnvironmentId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetEnvironmentId), + targetService: z.string().trim().optional().describe(INTEGRATION.CREATE.targetService), + targetServiceId: z.string().trim().optional().describe(INTEGRATION.CREATE.targetServiceId), + owner: z.string().trim().optional().describe(INTEGRATION.CREATE.owner), + path: z.string().trim().optional().describe(INTEGRATION.CREATE.path), + region: z.string().trim().optional().describe(INTEGRATION.CREATE.region), + scope: z.string().trim().optional().describe(INTEGRATION.CREATE.scope), metadata: z .object({ - secretPrefix: z.string().optional(), - secretSuffix: z.string().optional(), - initialSyncBehavior: z.string().optional(), - shouldAutoRedeploy: z.boolean().optional(), + secretPrefix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretPrefix), + secretSuffix: z.string().optional().describe(INTEGRATION.CREATE.metadata.secretSuffix), + initialSyncBehavior: z.string().optional().describe(INTEGRATION.CREATE.metadata.initialSyncBehavoir), + shouldAutoRedeploy: z.boolean().optional().describe(INTEGRATION.CREATE.metadata.shouldAutoRedeploy), secretGCPLabel: z .object({ labelName: z.string(), labelValue: z.string() }) .optional() + .describe(INTEGRATION.CREATE.metadata.secretGCPLabel) }) .optional() }), @@ -49,7 +62,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const { integration, integrationAuth } = await server.services.integration.createIntegration({ actorId: req.permission.id, @@ -102,17 +115,28 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { url: "/:integrationId", method: "PATCH", schema: { + description: "Update an integration by integration id", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - integrationId: z.string().trim() + integrationId: z.string().trim().describe(INTEGRATION.UPDATE.integrationId) }), body: z.object({ - app: z.string().trim(), - appId: z.string().trim(), - isActive: z.boolean(), - secretPath: z.string().trim().default("/").transform(removeTrailingSlash), - targetEnvironment: z.string().trim(), - owner: z.string().trim(), - environment: z.string().trim() + app: z.string().trim().describe(INTEGRATION.UPDATE.app), + appId: z.string().trim().describe(INTEGRATION.UPDATE.appId), + isActive: z.boolean().describe(INTEGRATION.UPDATE.isActive), + secretPath: z + .string() + .trim() + .default("/") + .transform(removeTrailingSlash) + .describe(INTEGRATION.UPDATE.secretPath), + targetEnvironment: z.string().trim().describe(INTEGRATION.UPDATE.targetEnvironment), + owner: z.string().trim().describe(INTEGRATION.UPDATE.owner), + environment: z.string().trim().describe(INTEGRATION.UPDATE.environment) }), response: { 200: z.object({ @@ -120,7 +144,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const integration = await server.services.integration.updateIntegration({ actorId: req.permission.id, @@ -138,8 +162,14 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { url: "/:integrationId", method: "DELETE", schema: { + description: "Remove an integration using the integration object ID", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - integrationId: z.string().trim() + integrationId: z.string().trim().describe(INTEGRATION.DELETE.integrationId) }), response: { 200: z.object({ @@ -147,7 +177,7 @@ export const registerIntegrationRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const integration = await server.services.integration.deleteIntegration({ actorId: req.permission.id, diff --git a/backend/src/server/routes/v1/invite-org-router.ts b/backend/src/server/routes/v1/invite-org-router.ts index 212020034d..89fc561e69 100644 --- a/backend/src/server/routes/v1/invite-org-router.ts +++ b/backend/src/server/routes/v1/invite-org-router.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { UsersSchema } from "@app/db/schemas"; +import { inviteUserRateLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { ActorType, AuthMode } from "@app/services/auth/auth-type"; @@ -9,6 +10,9 @@ import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; export const registerInviteOrgRouter = async (server: FastifyZodProvider) => { server.route({ url: "/signup", + config: { + rateLimit: inviteUserRateLimit + }, method: "POST", schema: { body: z.object({ diff --git a/backend/src/server/routes/v1/project-membership-router.ts b/backend/src/server/routes/v1/project-membership-router.ts index 9e2f5bd22c..f277e556ca 100644 --- a/backend/src/server/routes/v1/project-membership-router.ts +++ b/backend/src/server/routes/v1/project-membership-router.ts @@ -158,7 +158,7 @@ export const registerProjectMembershipRouter = async (server: FastifyZodProvider ]) ) .min(1) - .refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least long lived role is required") + .refine((data) => data.some(({ isTemporary }) => !isTemporary), "At least one long lived role is required") .describe(PROJECTS.UPDATE_USER_MEMBERSHIP.roles) }), response: { diff --git a/backend/src/server/routes/v1/project-router.ts b/backend/src/server/routes/v1/project-router.ts index 55f1fdda9e..ca01820a9e 100644 --- a/backend/src/server/routes/v1/project-router.ts +++ b/backend/src/server/routes/v1/project-router.ts @@ -7,7 +7,7 @@ import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; -import { PROJECTS } from "@app/lib/api-docs"; +import { INTEGRATION_AUTH, PROJECTS } from "@app/lib/api-docs"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { ProjectFilterType } from "@app/services/project/project-types"; @@ -332,8 +332,14 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { url: "/:workspaceId/authorizations", method: "GET", schema: { + description: "List integration auth objects for a workspace.", + security: [ + { + bearerAuth: [] + } + ], params: z.object({ - workspaceId: z.string().trim() + workspaceId: z.string().trim().describe(INTEGRATION_AUTH.LIST_AUTHORIZATION.workspaceId) }), response: { 200: z.object({ @@ -341,7 +347,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { }) } }, - onRequest: verifyAuth([AuthMode.JWT]), + onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { const authorizations = await server.services.integrationAuth.listIntegrationAuthByProjectId({ actorId: req.permission.id, diff --git a/backend/src/server/routes/v2/project-router.ts b/backend/src/server/routes/v2/project-router.ts index 3258be50af..19ffc57ede 100644 --- a/backend/src/server/routes/v2/project-router.ts +++ b/backend/src/server/routes/v2/project-router.ts @@ -4,7 +4,6 @@ import { z } from "zod"; import { ProjectKeysSchema, ProjectsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PROJECTS } from "@app/lib/api-docs"; -import { authRateLimit } from "@app/server/config/rateLimiter"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; @@ -136,9 +135,6 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => { server.route({ method: "POST", url: "/", - config: { - rateLimit: authRateLimit - }, schema: { body: z.object({ projectName: z.string().trim().describe(PROJECTS.CREATE.projectName), diff --git a/backend/src/server/routes/v3/secret-router.ts b/backend/src/server/routes/v3/secret-router.ts index f694663281..5344be43d8 100644 --- a/backend/src/server/routes/v3/secret-router.ts +++ b/backend/src/server/routes/v3/secret-router.ts @@ -157,6 +157,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), + recursive: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true") + .describe(RAW_SECRETS.LIST.recursive), include_imports: z .enum(["true", "false"]) .default("false") @@ -165,7 +170,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - secrets: secretRawSchema.array(), + secrets: secretRawSchema + .extend({ + secretPath: z.string().optional() + }) + .array(), imports: z .object({ secretPath: z.string(), @@ -218,7 +227,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { actorAuthMethod: req.permission.authMethod, projectId: workspaceId, path: secretPath, - includeImports: req.query.include_imports + includeImports: req.query.include_imports, + recursive: req.query.recursive }); await server.services.auditLog.createAuditLog({ @@ -596,6 +606,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { workspaceId: z.string().trim(), environment: z.string().trim(), secretPath: z.string().trim().default("/").transform(removeTrailingSlash), + recursive: z + .enum(["true", "false"]) + .default("false") + .transform((value) => value === "true"), include_imports: z .enum(["true", "false"]) .default("false") @@ -604,19 +618,18 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { response: { 200: z.object({ secrets: SecretsSchema.omit({ secretBlindIndex: true }) - .merge( - z.object({ - _id: z.string(), - workspace: z.string(), - environment: z.string(), - tags: SecretTagsSchema.pick({ - id: true, - slug: true, - name: true, - color: true - }).array() - }) - ) + .extend({ + _id: z.string(), + workspace: z.string(), + environment: z.string(), + secretPath: z.string().optional(), + tags: SecretTagsSchema.pick({ + id: true, + slug: true, + name: true, + color: true + }).array() + }) .array(), imports: z .object({ @@ -648,7 +661,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => { environment: req.query.environment, projectId: req.query.workspaceId, path: req.query.secretPath, - includeImports: req.query.include_imports + includeImports: req.query.include_imports, + recursive: req.query.recursive }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index a3c53658c2..8e7b922539 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -7,6 +7,7 @@ export enum AuthMethod { AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", GOOGLE_SAML = "google-saml", + KEYCLOAK_SAML = "keycloak-saml", LDAP = "ldap" } diff --git a/backend/src/services/identity-project/identity-project-dal.ts b/backend/src/services/identity-project/identity-project-dal.ts index dd3ba04f48..e932d20682 100644 --- a/backend/src/services/identity-project/identity-project-dal.ts +++ b/backend/src/services/identity-project/identity-project-dal.ts @@ -25,6 +25,11 @@ export const identityProjectDALFactory = (db: TDbClient) => { `${TableName.IdentityProjectMembershipRole}.customRoleId`, `${TableName.ProjectRoles}.id` ) + .leftJoin( + TableName.IdentityProjectAdditionalPrivilege, + `${TableName.IdentityProjectMembership}.id`, + `${TableName.IdentityProjectAdditionalPrivilege}.projectMembershipId` + ) .select( db.ref("id").withSchema(TableName.IdentityProjectMembership), db.ref("createdAt").withSchema(TableName.IdentityProjectMembership), diff --git a/backend/src/services/secret-import/secret-import-dal.ts b/backend/src/services/secret-import/secret-import-dal.ts index f9c6f1be77..cbc8936e11 100644 --- a/backend/src/services/secret-import/secret-import-dal.ts +++ b/backend/src/services/secret-import/secret-import-dal.ts @@ -70,9 +70,31 @@ export const secretImportDALFactory = (db: TDbClient) => { } }; + const findByFolderIds = async (folderIds: string[], tx?: Knex) => { + try { + const docs = await (tx || db)(TableName.SecretImport) + .whereIn("folderId", folderIds) + .join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`) + .select( + db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports, + db.ref("slug").withSchema(TableName.Environment), + db.ref("name").withSchema(TableName.Environment), + db.ref("id").withSchema(TableName.Environment).as("envId") + ) + .orderBy("position", "asc"); + return docs.map(({ envId, slug, name, ...el }) => ({ + ...el, + importEnv: { id: envId, slug, name } + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find secret imports" }); + } + }; + return { ...secretImportOrm, find, + findByFolderIds, findLastImportPosition, updateAllPosition }; diff --git a/backend/src/services/secret/secret-dal.ts b/backend/src/services/secret/secret-dal.ts index 5041747658..8a5970b835 100644 --- a/backend/src/services/secret/secret-dal.ts +++ b/backend/src/services/secret/secret-dal.ts @@ -171,6 +171,50 @@ export const secretDALFactory = (db: TDbClient) => { } }; + const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { + try { + // check if not uui then userId id is null (corner case because service token's ID is not UUI in effort to keep backwards compatibility from mongo) + if (userId && !uuidValidate(userId)) { + // eslint-disable-next-line no-param-reassign + userId = undefined; + } + + const secs = await (tx || db)(TableName.Secret) + .whereIn("folderId", folderIds) + .where((bd) => { + void bd.whereNull("userId").orWhere({ userId: userId || null }); + }) + .leftJoin(TableName.JnSecretTag, `${TableName.Secret}.id`, `${TableName.JnSecretTag}.${TableName.Secret}Id`) + .leftJoin(TableName.SecretTag, `${TableName.JnSecretTag}.${TableName.SecretTag}Id`, `${TableName.SecretTag}.id`) + .select(selectAllTableCols(TableName.Secret)) + .select(db.ref("id").withSchema(TableName.SecretTag).as("tagId")) + .select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor")) + .select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")) + .select(db.ref("name").withSchema(TableName.SecretTag).as("tagName")) + .orderBy("id", "asc"); + const data = sqlNestRelationships({ + data: secs, + key: "id", + parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }), + childrenMapper: [ + { + key: "tagId", + label: "tags" as const, + mapper: ({ tagId: id, tagColor: color, tagSlug: slug, tagName: name }) => ({ + id, + color, + slug, + name + }) + } + ] + }); + return data; + } catch (error) { + throw new DatabaseError({ error, name: "get all secret" }); + } + }; + const findByBlindIndexes = async ( folderId: string, blindIndexes: Array<{ blindIndex: string; type: SecretType }>, @@ -207,6 +251,7 @@ export const secretDALFactory = (db: TDbClient) => { bulkUpdateNoVersionIncrement, getSecretTags, findByFolderId, + findByFolderIds, findByBlindIndexes }; }; diff --git a/backend/src/services/secret/secret-fns.ts b/backend/src/services/secret/secret-fns.ts index 212bb01f07..24c2bd811c 100644 --- a/backend/src/services/secret/secret-fns.ts +++ b/backend/src/services/secret/secret-fns.ts @@ -1,4 +1,5 @@ /* eslint-disable no-await-in-loop */ +import { subject } from "@casl/ability"; import path from "path"; import { @@ -7,8 +8,11 @@ import { SecretType, TableName, TSecretBlindIndexes, + TSecretFolders, TSecrets } from "@app/db/schemas"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { getConfig } from "@app/lib/config/env"; import { buildSecretBlindIndexFromName, @@ -18,7 +22,9 @@ import { import { BadRequestError } from "@app/lib/errors"; import { groupBy, unique } from "@app/lib/fn"; +import { ActorAuthMethod, ActorType } from "../auth/auth-type"; import { getBotKeyFnFactory } from "../project-bot/project-bot-fns"; +import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretDALFactory } from "./secret-dal"; import { @@ -45,6 +51,133 @@ export const generateSecretBlindIndexBySalt = async (secretName: string, secretB return secretBlindIndex; }; +type TRecursivelyFetchSecretsFromFoldersArg = { + permissionService: Pick; + folderDAL: Pick; + projectEnvDAL: Pick; +}; + +type TGetPathsDTO = { + projectId: string; + environment: string; + currentPath: string; + + auth: { + actor: ActorType; + actorId: string; + actorAuthMethod: ActorAuthMethod; + actorOrgId: string | undefined; + }; +}; + +// Introduce a new interface for mapping parent IDs to their children +interface FolderMap { + [parentId: string]: TSecretFolders[]; +} +const buildHierarchy = (folders: TSecretFolders[]): FolderMap => { + const map: FolderMap = {}; + map.null = []; // Initialize mapping for root directory + + folders.forEach((folder) => { + const parentId = folder.parentId || "null"; + if (!map[parentId]) { + map[parentId] = []; + } + map[parentId].push(folder); + }); + + return map; +}; + +const generatePaths = ( + map: FolderMap, + parentId: string = "null", + basePath: string = "" +): { path: string; folderId: string }[] => { + const children = map[parentId || "null"] || []; + let paths: { path: string; folderId: string }[] = []; + + children.forEach((child) => { + // Determine if this is the root folder of the environment. If no parentId is present and the name is root, it's the root folder + const isRootFolder = child.name === "root" && !child.parentId; + + // Form the current path based on the base path and the current child + // eslint-disable-next-line no-nested-ternary + const currPath = basePath === "" ? (isRootFolder ? "/" : `/${child.name}`) : `${basePath}/${child.name}`; + + paths.push({ + path: currPath, + folderId: child.id + }); // Add the current path + + // Recursively generate paths for children, passing down the formatted pathh + const childPaths = generatePaths(map, child.id, currPath); + paths = paths.concat( + childPaths.map((p) => ({ + path: p.path, + folderId: p.folderId + })) + ); + }); + + return paths; +}; + +export const recursivelyGetSecretPaths = ({ + folderDAL, + projectEnvDAL, + permissionService +}: TRecursivelyFetchSecretsFromFoldersArg) => { + const getPaths = async ({ projectId, environment, currentPath, auth }: TGetPathsDTO) => { + const env = await projectEnvDAL.findOne({ + projectId, + slug: environment + }); + + if (!env) { + throw new Error(`'${environment}' environment not found in project with ID ${projectId}`); + } + + // Fetch all folders in env once with a single query + const folders = await folderDAL.find({ + envId: env.id + }); + + // Build the folder hierarchy map + const folderMap = buildHierarchy(folders); + + // Generate the paths paths and normalize the root path to / + const paths = generatePaths(folderMap).map((p) => ({ + path: p.path === "/" ? p.path : p.path.substring(1), + folderId: p.folderId + })); + + const { permission } = await permissionService.getProjectPermission( + auth.actor, + auth.actorId, + projectId, + auth.actorAuthMethod, + auth.actorOrgId + ); + + // Filter out paths that the user does not have permission to access, and paths that are not in the current path + const allowedPaths = paths.filter( + (folder) => + permission.can( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { + environment, + secretPath: folder.path + }) + ) && folder.path.startsWith(currentPath === "/" ? "" : currentPath) + ); + + return allowedPaths; + }; + + return getPaths; +}; + type TInterpolateSecretArg = { projectId: string; secretEncKey: string; @@ -202,9 +335,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD ); // eslint-disable-next-line - secrets[key].value = secrets[key].skipMultilineEncoding - ? expandedVal - : formatMultiValueEnv(expandedVal); + secrets[key].value = secrets[key].skipMultilineEncoding ? expandedVal : formatMultiValueEnv(expandedVal); } return secrets; @@ -212,7 +343,10 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD return expandSecrets; }; -export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environment: string }, key: string) => { +export const decryptSecretRaw = ( + secret: TSecrets & { workspace: string; environment: string; secretPath?: string }, + key: string +) => { const secretKey = decryptSymmetric128BitHexKeyUTF8({ ciphertext: secret.secretKeyCiphertext, iv: secret.secretKeyIV, @@ -240,6 +374,7 @@ export const decryptSecretRaw = (secret: TSecrets & { workspace: string; environ return { secretKey, + secretPath: secret.secretPath, workspace: secret.workspace, environment: secret.environment, secretValue, diff --git a/backend/src/services/secret/secret-queue.ts b/backend/src/services/secret/secret-queue.ts index 2e5ea7f93f..d815875dfe 100644 --- a/backend/src/services/secret/secret-queue.ts +++ b/backend/src/services/secret/secret-queue.ts @@ -229,7 +229,6 @@ export const secretQueueFactory = ({ const getIntegrationSecrets = async (dto: TGetSecrets & { folderId: string }, key: string) => { const secrets = await secretDAL.findByFolderId(dto.folderId); - if (!secrets.length) return {}; // get imported secrets const secretImport = await secretImportDAL.find({ folderId: dto.folderId }); @@ -238,6 +237,9 @@ export const secretQueueFactory = ({ secretDAL, folderDAL }); + + if (!secrets.length && !importedSecrets.length) return {}; + const content: Record = {}; importedSecrets.forEach(({ secrets: secs }) => { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index f8fed95bb8..3b504fbf4b 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1,3 +1,5 @@ +/* eslint-disable no-unreachable-loop */ +/* eslint-disable no-await-in-loop */ import { ForbiddenError, subject } from "@casl/ability"; import { SecretEncryptionAlgo, SecretKeyEncoding, SecretsSchema, SecretType } from "@app/db/schemas"; @@ -13,13 +15,20 @@ import { logger } from "@app/lib/logger"; import { ActorType } from "../auth/auth-type"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; +import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretBlindIndexDALFactory } from "../secret-blind-index/secret-blind-index-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "../secret-import/secret-import-dal"; import { fnSecretsFromImports } from "../secret-import/secret-import-fns"; import { TSecretTagDALFactory } from "../secret-tag/secret-tag-dal"; import { TSecretDALFactory } from "./secret-dal"; -import { decryptSecretRaw, fnSecretBlindIndexCheck, fnSecretBulkInsert, fnSecretBulkUpdate } from "./secret-fns"; +import { + decryptSecretRaw, + fnSecretBlindIndexCheck, + fnSecretBulkInsert, + fnSecretBulkUpdate, + recursivelyGetSecretPaths +} from "./secret-fns"; import { TSecretQueueFactory } from "./secret-queue"; import { TAttachSecretTagsDTO, @@ -47,20 +56,25 @@ type TSecretServiceFactoryDep = { secretDAL: TSecretDALFactory; secretTagDAL: TSecretTagDALFactory; secretVersionDAL: TSecretVersionDALFactory; - folderDAL: Pick; projectDAL: Pick; + projectEnvDAL: Pick; + folderDAL: Pick< + TSecretFolderDALFactory, + "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find" + >; secretBlindIndexDAL: TSecretBlindIndexDALFactory; permissionService: Pick; snapshotService: Pick; secretQueueService: Pick; projectBotService: Pick; - secretImportDAL: Pick; + secretImportDAL: Pick; secretVersionTagDAL: Pick; }; export type TSecretServiceFactory = ReturnType; export const secretServiceFactory = ({ secretDAL, + projectEnvDAL, secretTagDAL, secretVersionDAL, folderDAL, @@ -425,7 +439,8 @@ export const secretServiceFactory = ({ actor, actorOrgId, actorAuthMethod, - includeImports + includeImports, + recursive }: TGetSecretsDTO) => { const { permission } = await permissionService.getProjectPermission( actor, @@ -434,19 +449,52 @@ export const secretServiceFactory = ({ actorAuthMethod, actorOrgId ); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Read, - subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + + let paths: { folderId: string; path: string }[] = []; + + if (recursive) { + const getPaths = recursivelyGetSecretPaths({ + permissionService, + folderDAL, + projectEnvDAL + }); + + const deepPaths = await getPaths({ + projectId, + environment, + currentPath: path, + auth: { + actor, + actorId, + actorAuthMethod, + actorOrgId + } + }); + + if (!deepPaths) return { secrets: [], imports: [] }; + + paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); + } else { + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionActions.Read, + subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) + ); + + const folder = await folderDAL.findBySecretPath(projectId, environment, path); + if (!folder) return { secrets: [], imports: [] }; + + paths = [{ folderId: folder.id, path }]; + } + + const groupedPaths = groupBy(paths, (p) => p.folderId); + + const secrets = await secretDAL.findByFolderIds( + paths.map((p) => p.folderId), + actorId ); - const folder = await folderDAL.findBySecretPath(projectId, environment, path); - if (!folder) return { secrets: [], imports: [] }; - const folderId = folder.id; - - const secrets = await secretDAL.findByFolderId(folderId, actorId); - if (includeImports) { - const secretImports = await secretImportDAL.find({ folderId }); + const secretImports = await secretImportDAL.findByFolderIds(paths.map((p) => p.folderId)); const allowedImports = secretImports.filter(({ importEnv, importPath }) => // if its service token allow full access over imported one actor === ActorType.SERVICE @@ -464,12 +512,26 @@ export const secretServiceFactory = ({ secretDAL, folderDAL }); + return { - secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })), + secrets: secrets.map((secret) => ({ + ...secret, + workspace: projectId, + environment, + secretPath: groupedPaths[secret.folderId][0].path + })), imports: importedSecrets }; } - return { secrets: secrets.map((el) => ({ ...el, workspace: projectId, environment })) }; + + return { + secrets: secrets.map((secret) => ({ + ...secret, + workspace: projectId, + environment, + secretPath: groupedPaths[secret.folderId][0].path + })) + }; }; const getSecretByName = async ({ @@ -655,7 +717,7 @@ export const secretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -741,7 +803,7 @@ export const secretServiceFactory = ({ actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionActions.Create, + ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); @@ -789,7 +851,8 @@ export const secretServiceFactory = ({ actorOrgId, actorAuthMethod, environment, - includeImports + includeImports, + recursive }: TGetSecretsRawDTO) => { const botKey = await projectBotService.getBotKey(projectId); if (!botKey) throw new BadRequestError({ message: "Project bot not found", name: "bot_not_found_error" }); @@ -802,7 +865,8 @@ export const secretServiceFactory = ({ actorOrgId, actorAuthMethod, path, - includeImports + includeImports, + recursive }); return { @@ -810,7 +874,10 @@ export const secretServiceFactory = ({ imports: (imports || [])?.map(({ secrets: importedSecrets, ...el }) => ({ ...el, secrets: importedSecrets.map((sec) => - decryptSecretRaw({ ...sec, environment: el.environment, workspace: projectId }, botKey) + decryptSecretRaw( + { ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath }, + botKey + ) ) })) }; diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index efd4f0f8bb..22347de4e8 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -74,6 +74,7 @@ export type TGetSecretsDTO = { path: string; environment: string; includeImports?: boolean; + recursive?: boolean; } & TProjectPermission; export type TGetASecretDTO = { @@ -140,6 +141,7 @@ export type TGetSecretsRawDTO = { path: string; environment: string; includeImports?: boolean; + recursive?: boolean; } & TProjectPermission; export type TGetASecretRawDTO = { diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 38d82a0a57..d45a42db4e 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -277,6 +277,10 @@ func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Req SetQueryParam("environment", request.Environment). SetQueryParam("workspaceId", request.WorkspaceId) + if request.Recursive { + httpRequest.SetQueryParam("recursive", "true") + } + if request.IncludeImport { httpRequest.SetQueryParam("include_imports", "true") } diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index b49cb15810..ed9a1d629f 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -291,6 +291,7 @@ type GetEncryptedSecretsV3Request struct { WorkspaceId string `json:"workspaceId"` SecretPath string `json:"secretPath"` IncludeImport bool `json:"include_imports"` + Recursive bool `json:"recursive"` } type GetFoldersV1Request struct { @@ -510,7 +511,7 @@ type CreateDynamicSecretLeaseV1Request struct { type CreateDynamicSecretLeaseV1Response struct { Lease struct { - Id string `json:"id"` + Id string `json:"id"` ExpireAt time.Time `json:"expireAt"` } `json:"lease"` DynamicSecret struct { diff --git a/cli/packages/cmd/agent.go b/cli/packages/cmd/agent.go index ce3f7a8b02..80f3213962 100644 --- a/cli/packages/cmd/agent.go +++ b/cli/packages/cmd/agent.go @@ -332,7 +332,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) { func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string) ([]models.SingleEnvironmentVariable, error) { return func(projectID, envSlug, secretPath string) ([]models.SingleEnvironmentVariable, error) { - res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false) + res, err := util.GetPlainTextSecretsViaMachineIdentity(accessToken, projectID, envSlug, secretPath, false, false) if err != nil { return nil, err } diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index d008af1ecc..cb44e3d7e4 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -98,7 +98,12 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports}, projectConfigDir) + recursive, err := cmd.Flags().GetBool("recursive") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, projectConfigDir) if err != nil { util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") @@ -202,6 +207,7 @@ func init() { runCmd.Flags().StringP("env", "e", "dev", "Set the environment (dev, prod, etc.) from which your secrets should be pulled from") runCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") runCmd.Flags().Bool("include-imports", true, "Import linked secrets ") + runCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index cf9c89b476..4e1b09c9e8 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -63,6 +63,11 @@ var secretsCmd = &cobra.Command{ util.HandleError(err) } + recursive, err := cmd.Flags().GetBool("recursive") + if err != nil { + util.HandleError(err) + } + tagSlugs, err := cmd.Flags().GetString("tags") if err != nil { util.HandleError(err, "Unable to parse flag") @@ -73,7 +78,7 @@ var secretsCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports}, "") + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: includeImports, Recursive: recursive}, "") if err != nil { util.HandleError(err) } @@ -413,12 +418,17 @@ func getSecretsByNames(cmd *cobra.Command, args []string) { util.HandleError(err, "Unable to parse path flag") } + recursive, err := cmd.Flags().GetBool("recursive") + if err != nil { + util.HandleError(err, "Unable to parse recursive flag") + } + showOnlyValue, err := cmd.Flags().GetBool("raw-value") if err != nil { util.HandleError(err, "Unable to parse path flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true}, "") + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath, IncludeImport: true, Recursive: recursive}, "") if err != nil { util.HandleError(err, "To fetch all secrets") } @@ -683,6 +693,7 @@ func init() { secretsCmd.AddCommand(secretsGetCmd) secretsGetCmd.Flags().String("path", "/", "get secrets within a folder path") secretsGetCmd.Flags().Bool("raw-value", false, "Returns only the value of secret, only works with one secret") + secretsGetCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") secretsCmd.AddCommand(secretsSetCmd) @@ -727,6 +738,7 @@ func init() { secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.Flags().Bool("include-imports", true, "Imported linked secrets ") + secretsCmd.Flags().Bool("recursive", false, "Fetch secrets from all sub-folders") secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") secretsCmd.Flags().String("path", "/", "get secrets within a folder path") rootCmd.AddCommand(secretsCmd) diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index 4a7dc782a8..e197218e0b 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -93,6 +93,7 @@ type GetAllSecretsParameters struct { WorkspaceId string SecretsPath string IncludeImport bool + Recursive bool } type GetAllFoldersParameters struct { diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index 6f56bc84a3..23e95db648 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -17,7 +17,7 @@ import ( "github.com/rs/zerolog/log" ) -func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) { +func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment string, secretPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, api.GetServiceTokenDetailsResponse, error) { serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4) if len(serviceTokenParts) < 4 { return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again") @@ -49,6 +49,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str Environment: environment, SecretPath: secretPath, IncludeImport: includeImports, + Recursive: recursive, }) if err != nil { @@ -80,7 +81,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string, environment str return plainTextSecrets, serviceTokenDetails, nil } -func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool) ([]models.SingleEnvironmentVariable, error) { +func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string, includeImports bool, recursive bool) ([]models.SingleEnvironmentVariable, error) { httpClient := resty.New() httpClient.SetAuthToken(JTWToken). SetHeader("Accept", "application/json") @@ -125,6 +126,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work WorkspaceId: workspaceId, Environment: environmentName, IncludeImport: includeImports, + Recursive: recursive, // TagSlugs: tagSlugs, } @@ -152,7 +154,7 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work return plainTextSecrets, nil } -func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool) (models.PlaintextSecretResult, error) { +func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId string, environmentName string, secretsPath string, includeImports bool, recursive bool) (models.PlaintextSecretResult, error) { httpClient := resty.New() httpClient.SetAuthToken(accessToken). SetHeader("Accept", "application/json") @@ -161,6 +163,7 @@ func GetPlainTextSecretsViaMachineIdentity(accessToken string, workspaceId strin WorkspaceId: workspaceId, Environment: environmentName, IncludeImport: includeImports, + Recursive: recursive, // TagSlugs: tagSlugs, } @@ -329,7 +332,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo } secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, infisicalDotJson.WorkspaceId, - params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport) + params.Environment, params.TagSlugs, params.SecretsPath, params.IncludeImport, params.Recursive) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] @@ -350,10 +353,10 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters, projectCo } else { if params.InfisicalToken != "" { log.Debug().Msg("Trying to fetch secrets using service token") - secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport) + secretsToReturn, _, errorToReturn = GetPlainTextSecretsViaServiceToken(params.InfisicalToken, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) } else if params.UniversalAuthAccessToken != "" { log.Debug().Msg("Trying to fetch secrets using universal auth") - res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport) + res, err := GetPlainTextSecretsViaMachineIdentity(params.UniversalAuthAccessToken, params.WorkspaceId, params.Environment, params.SecretsPath, params.IncludeImport, params.Recursive) errorToReturn = err secretsToReturn = res.Secrets diff --git a/docs/api-reference/endpoints/identity-specific-privilege/create-permanent.mdx b/docs/api-reference/endpoints/identity-specific-privilege/create-permanent.mdx new file mode 100644 index 0000000000..8e02c28a30 --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/create-permanent.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Permanent" +openapi: "POST /api/v1/additional-privilege/identity/permanent" +--- diff --git a/docs/api-reference/endpoints/identity-specific-privilege/create-temporary.mdx b/docs/api-reference/endpoints/identity-specific-privilege/create-temporary.mdx new file mode 100644 index 0000000000..808f278598 --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/create-temporary.mdx @@ -0,0 +1,4 @@ +--- +title: "Create Temporary" +openapi: "POST /api/v1/additional-privilege/identity/temporary" +--- diff --git a/docs/api-reference/endpoints/identity-specific-privilege/delete.mdx b/docs/api-reference/endpoints/identity-specific-privilege/delete.mdx new file mode 100644 index 0000000000..4302827896 --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/additional-privilege/identity" +--- diff --git a/docs/api-reference/endpoints/identity-specific-privilege/find-by-slug.mdx b/docs/api-reference/endpoints/identity-specific-privilege/find-by-slug.mdx new file mode 100644 index 0000000000..a6ec272173 --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/find-by-slug.mdx @@ -0,0 +1,4 @@ +--- +title: "Find By Privilege Slug" +openapi: "GET /api/v1/additional-privilege/identity/{privilegeSlug}" +--- diff --git a/docs/api-reference/endpoints/identity-specific-privilege/list.mdx b/docs/api-reference/endpoints/identity-specific-privilege/list.mdx new file mode 100644 index 0000000000..4698ed838e --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/list.mdx @@ -0,0 +1,4 @@ +--- +title: "List" +openapi: "GET /api/v1/additional-privilege/identity" +--- diff --git a/docs/api-reference/endpoints/identity-specific-privilege/update.mdx b/docs/api-reference/endpoints/identity-specific-privilege/update.mdx new file mode 100644 index 0000000000..987d6ac8c8 --- /dev/null +++ b/docs/api-reference/endpoints/identity-specific-privilege/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/additional-privilege/identity" +--- diff --git a/docs/api-reference/endpoints/integrations/create-auth.mdx b/docs/api-reference/endpoints/integrations/create-auth.mdx new file mode 100644 index 0000000000..5af7a0f9c6 --- /dev/null +++ b/docs/api-reference/endpoints/integrations/create-auth.mdx @@ -0,0 +1,32 @@ +--- +title: "Create Auth" +openapi: "POST /api/v1/integration-auth/access-token" +--- + +## Integration Authentication Parameters + +The integration authentication endpoint is generic and can be used for all native integrations. +For specific integration parameters for a given service, please review the respective documentation below. + + + + + This value must be **aws-secret-manager**. + + + Infisical project id for the integration. + + + The AWS IAM User Access ID. + + + The AWS IAM User Access Secret Key. + + + + Coming Soon + + + Coming Soon + + diff --git a/docs/api-reference/endpoints/integrations/create.mdx b/docs/api-reference/endpoints/integrations/create.mdx new file mode 100644 index 0000000000..0992e91b9a --- /dev/null +++ b/docs/api-reference/endpoints/integrations/create.mdx @@ -0,0 +1,40 @@ +--- +title: "Create" +openapi: "POST /api/v1/integration" +--- + +## Integration Parameters + +The integration creation endpoint is generic and can be used for all native integrations. +For specific integration parameters for a given service, please review the respective documentation below. + + + + + The ID of the integration auth object for authentication with AWS. + Refer [Create Integration Auth](./create-auth) for more info + + + Whether the integration should be active or inactive + + + The secret name used when saving secret in AWS SSM. Used for naming and can be arbitrary. + + + The AWS region of the SSM. Example: `us-east-1` + + + The Infisical environment slug from where secrets will be synced from. Example: `dev` + + + The Infisical folder path from where secrets will be synced from. Example: `/some/path`. The root of the environment is `/`. + + + + Coming Soon + + + Coming Soon + + + diff --git a/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx b/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx new file mode 100644 index 0000000000..5884363fc5 --- /dev/null +++ b/docs/api-reference/endpoints/integrations/delete-auth-by-id.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete Auth By ID" +openapi: "DELETE /api/v1/integration-auth/{integrationAuthId}" +--- diff --git a/docs/api-reference/endpoints/integrations/delete-auth.mdx b/docs/api-reference/endpoints/integrations/delete-auth.mdx new file mode 100644 index 0000000000..93d9579034 --- /dev/null +++ b/docs/api-reference/endpoints/integrations/delete-auth.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete Auth" +openapi: "DELETE /api/v1/integration-auth" +--- diff --git a/docs/api-reference/endpoints/integrations/delete.mdx b/docs/api-reference/endpoints/integrations/delete.mdx new file mode 100644 index 0000000000..51df56de70 --- /dev/null +++ b/docs/api-reference/endpoints/integrations/delete.mdx @@ -0,0 +1,4 @@ +--- +title: "Delete" +openapi: "DELETE /api/v1/integration/{integrationId}" +--- diff --git a/docs/api-reference/endpoints/integrations/find-auth.mdx b/docs/api-reference/endpoints/integrations/find-auth.mdx new file mode 100644 index 0000000000..439b829355 --- /dev/null +++ b/docs/api-reference/endpoints/integrations/find-auth.mdx @@ -0,0 +1,4 @@ +--- +title: "Get Auth By ID" +openapi: "GET /api/v1/integration-auth/{integrationAuthId}" +--- diff --git a/docs/api-reference/endpoints/integrations/list-auth.mdx b/docs/api-reference/endpoints/integrations/list-auth.mdx new file mode 100644 index 0000000000..3ca961d986 --- /dev/null +++ b/docs/api-reference/endpoints/integrations/list-auth.mdx @@ -0,0 +1,4 @@ +--- +title: "List Auth" +openapi: "GET /api/v1/workspace/{workspaceId}/authorizations" +--- diff --git a/docs/api-reference/endpoints/integrations/update.mdx b/docs/api-reference/endpoints/integrations/update.mdx new file mode 100644 index 0000000000..8567c46aea --- /dev/null +++ b/docs/api-reference/endpoints/integrations/update.mdx @@ -0,0 +1,4 @@ +--- +title: "Update" +openapi: "PATCH /api/v1/integration/{integrationId}" +--- diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx index dcf9719ea6..f2224577eb 100644 --- a/docs/api-reference/overview/authentication.mdx +++ b/docs/api-reference/overview/authentication.mdx @@ -1,9 +1,9 @@ --- title: "Authentication" -description: "How to authenticate with the Infisical Public API" +description: "Learn how to authenticate with the Infisical Public API." --- -You can authenticate with the Infisical API using [Identities](/documentation/platform/identities/overview) paired with authentication modes such as [Universal Auth](/documentation/platform/identities/universal-auth). +You can authenticate with the Infisical API using [Identities](/documentation/platform/identities/machine-identities) paired with authentication modes such as [Universal Auth](/documentation/platform/identities/universal-auth). To interact with the Infisical API, you will need to obtain an access token. Follow the step by [step guide](/documentation/platform/identities/universal-auth) to get an access token via Universal Auth. diff --git a/docs/api-reference/overview/introduction.mdx b/docs/api-reference/overview/introduction.mdx index 06ee491b5d..6d577e15b8 100644 --- a/docs/api-reference/overview/introduction.mdx +++ b/docs/api-reference/overview/introduction.mdx @@ -1,5 +1,6 @@ --- -title: "Introduction" +title: "API Reference" +sidebarTitle: "Introduction" --- Infisical's Public (REST) API provides users an alternative way to programmatically access and manage diff --git a/docs/documentation/getting-started/introduction-new.mdx b/docs/documentation/getting-started/introduction-new.mdx new file mode 100644 index 0000000000..c8eee87390 --- /dev/null +++ b/docs/documentation/getting-started/introduction-new.mdx @@ -0,0 +1,107 @@ +--- +mode: 'custom' +--- + +export function openSearch() { + document.getElementById('search-bar-entry').click(); +} + +
+
+ +
+
+
+ Infisical Documentation +
+

+ What can we help you build? +

+ +
+
+ +
+ +
+ Choose a topic below or simply{' '} + get started +
+ + + + Practical guides and best practices to get you up and running quickly. + + + Comprehensive details about the Infisical API. + + + Learn more about Infisical's architecture and underlying security. + + + Read self-hosting instruction for Infisical. + + + Infisical's growing number of third-party integrations. + + + News about features and changes in Pinecone and related tools. + + + +
\ No newline at end of file diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx index 4c96b5a783..bd9354aa11 100644 --- a/docs/documentation/getting-started/introduction.mdx +++ b/docs/documentation/getting-started/introduction.mdx @@ -1,107 +1,97 @@ --- -title: "Introduction" +title: "What is Infisical?" +sidebarTitle: "What is Infisical?" +description: "An Introduction to the Infisical secret management platform." --- -Infisical is an [open-source](https://opensource.com/resources/what-open-source), [end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) secrets management platform for storing, managing, and syncing -application configuration and secrets like API keys, database credentials, and environment variables across applications and infrastructure. +Infisical is an [open-source](https://github.com/infisical/infisical) secret management platform for developers. +It provides capabilities for storing, managing, and syncing application configuration and secrets like API keys, database +credentials, and certificates across infrastructure. In addition, Infisical prevents secrets leaks to git and enables secure +sharing of secrets among engineers. -Start syncing environment variables with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself. - -## Learn about Infisical - - - Store secrets like API keys, database credentials, environment variables with Infisical - - -## Access secrets +Start managing secrets securely with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself. - - Inject secrets into any application process/environment + + Get started with Infisical Cloud in just a few minutes. + + + Self-host Infisical on your own infrastructure. + + + +## Why Infisical? + +Infisical helps developers achieve secure centralized secret management and provides all the tools to easily manage secrets in various environments and infrastructure components. In particular, here are some of the most common points that developers mention after adopting Infisical: +- Streamlined **local development** processes (switching .env files to [Infisical CLI](/cli/commands/run) and removing secrets from developer machines). +- **Best-in-class developer experience** with an easy-to-use [Web Dashboard](/documentation/platform/project). +- Simple secret management inside **[CI/CD pipelines](/integrations/cicd/githubactions)** and staging environments. +- Secure and compliant secret management practices in **[production environments](/sdks/overview)**. +- **Facilitated workflows** around [secret change management](/documentation/platform/pr-workflows), [access requests](/documentation/platform/access-controls/access-requests), [temporary access provisioning](/documentation/platform/access-controls/temporary-access), and more. +- **Improved security posture** thanks to [secret scanning](/cli/scanning-overview), [granular access control policies](/documentation/platform/access-controls/overview), [automated secret rotation](http://localhost:3000/documentation/platform/secret-rotation/overview), and [dynamic secrets](/documentation/platform/dynamic-secrets/overview) capabilities. + +## How does Infisical work? + +To make secret management effortless and secure, Infisical follows a certain structure for enabling secret management workflows as defined below. + +**Identities** in Infisical are users or machine which have a certain set of roles and permissions assigned to them. Such identities are able to manage secrets in various **Clients** throughout the entire infrastructure. To do that, identities have to verify themselves through one of the available **Authentication Methods**. + +As a result, the 3 main concepts that are important to understand are: +- **[Identities](/documentation/platform/identities/overview)**: users or machines with a set permissions assigned to them. +- **[Clients](/integrations/platforms/kubernetes)**: Infisical-developed tools for managing secrets in various infrastructure components (e.g., [Kubernetes Operator](/integrations/platforms/kubernetes), [Infisical Agent](/integrations/platforms/infisical-agent), [CLI](/cli/usage), [SDKs](/sdks/overview), [API](/api-reference/overview/introduction), [Web Dashboard](/documentation/platform/organization)). +- **[Authentication Methods](/documentation/platform/identities/universal-auth)**: ways for Identities to authenticate inside different clients (e.g., SAML SSO for Web Dashboard, Universal Auth for Infisical Agent, etc.). + +## How to get started with Infisical? + +Depending on your use case, it might be helpful to look into some of the resources and guides provided below. + + + + Inject secrets into any application process/environment. - Fetch secrets with any programming language on demand + Fetch secrets with any programming language on demand. - - Inject secrets into Docker containers + + Inject secrets into Docker containers. - Fetch and save secrets as native Kubernetes secrets + Fetch and save secrets as native Kubernetes secrets. - Fetch secrets via HTTP request - - - -## Resources - - - - Learn how to configure and deploy Infisical - - - Explore guides for every language and stack + Fetch secrets via HTTP request. - Explore integrations for GitHub, Vercel, Netlify, and more - - - Explore integrations for Next.js, Express, Django, and more - - - Scan and prevent 140+ secret type leaks in your codebase - - - Questions? Need help setting up? Book a 1x1 meeting with us + Explore integrations for GitHub, Vercel, AWS, and more. diff --git a/docs/documentation/getting-started/platform.mdx b/docs/documentation/getting-started/platform.mdx index 4295241612..1a1164a40a 100644 --- a/docs/documentation/getting-started/platform.mdx +++ b/docs/documentation/getting-started/platform.mdx @@ -21,7 +21,7 @@ Here, you can also create a new project. The **Members** page lets you add or remove external members to your organization. Note that you can configure your organization in Infisical to have members authenticate with the platform via protocols like SAML 2.0. -![organization members](../../images/organization-members.png) +![organization members](../../images/organization/platform/organization-members.png) ## Managing your Projects diff --git a/docs/documentation/guides/local-development.mdx b/docs/documentation/guides/local-development.mdx new file mode 100644 index 0000000000..c2651cb589 --- /dev/null +++ b/docs/documentation/guides/local-development.mdx @@ -0,0 +1,34 @@ +--- +title: "Secret Management in Development Environments" +sidebarTitle: "Local Development" +description: "Learn how to manage secrets in local development environments." +--- + +## Problem at hand + +There is a number of issues that arise with secret management in local development environment: +1. **Getting secrets onto local machines**. When new developers join or a new project is created, the process of getting the development set of secrets onto local machines is often unclear. As a result, developers end up spending a lot of time onboarding and risk potentially following insecure practices when sharing secrets from one developer to another. +2. **Syncing secrets with teammates**. One of the problems with .env files is that they become unsynced when one of the developers updates a secret or configuration. Even if the rest of the team is notified, developers don't make all the right changes immediately, and later on end up spending a lot of time debugging an issue due to missing environment variables. This leads to a lot of inefficiencies and lost time. +3. **Accidentally leaking secrets**. When developing locally, it's common for developers to accidentally leak a hardcoded as part of a commit. As soon as the secret is part of the git history, it becomes hard to get it removed and create a security vulnerability. + +## Solution + +One of the main benefits of Infisical is the facilitation of secret management workflows in local development use cases. In particular, Infisical heavily follows the "Security Shift Left" principle to enable developers to effotlessly follow secure practices when coding. + +### CLI + +[Infisical CLI](/cli/overview) is the most frequently used Infisical tool for secret management in local development environments. It makes it easy to inject secrets right into the local application environments based on the permissions given to corresponsing developers. + +### Dashboard + +On top of that, Infisical provides a great [Web Dashboard](https://app.infisical.com/signup) that can be used to making quick secret updates. + +![project dashboard](../../images/dashboard.png) + +### Personal Overrides + +By default, all the secrets in the Infisical environments are shared among project members who have the permission to access those environment. At the same time, when doing local development, it is often desirable to change the value of a certain secret only for a particular self. For such use cases, Infisical supports the functionality of **Personal Overrides** – which allow developers to override values of any secrets without affecting the workflows of the rest of the team. Personal Overrides can be created both in the dashboard or via [Infisical CLI](/cli/overview). + +### Secret Scanning + +In addition, Infisical also provides a set of tools to automatically prevent secret leaks to git history. This functionlality can be set up on the level of [Infisical CLI using pre-commit hooks](/cli/scanning-overview#automatically-scan-changes-before-you-commit) or through a direct integration with platforms like GitHub. \ No newline at end of file diff --git a/docs/documentation/guides/nextjs-vercel.mdx b/docs/documentation/guides/nextjs-vercel.mdx index 2e8805cd0f..5aeadc752b 100644 --- a/docs/documentation/guides/nextjs-vercel.mdx +++ b/docs/documentation/guides/nextjs-vercel.mdx @@ -193,7 +193,7 @@ Next, navigate to your project's integrations tab in Infisical and press on the ![integrations](../../images/integrations.png) -![integrations vercel authorization](../../images/integrations-vercel-auth.png) +![integrations vercel authorization](../../images/integrations/vercel/integrations-vercel-auth.png) Opting in for the Infisical-Vercel integration will break end-to-end encryption since Infisical will be able to read @@ -205,8 +205,8 @@ Next, navigate to your project's integrations tab in Infisical and press on the Now select **Production** for (the source) **Environment** and sync it to the **Production Environment** of the (target) application in Vercel. Lastly, press create integration to start syncing secrets to Vercel. -![integrations vercel](../../images/integrations-vercel-create.png) -![integrations vercel](../../images/integrations-vercel.png) +![integrations vercel](../../images/integrations/vercel/integrations-vercel-create.png) +![integrations vercel](../../images/integrations/vercel/integrations-vercel.png) You should now see your secret from Infisical appear as production environment variables in your Vercel project. diff --git a/docs/documentation/platform/access-controls/access-requests.mdx b/docs/documentation/platform/access-controls/access-requests.mdx new file mode 100644 index 0000000000..e8a52b0e4e --- /dev/null +++ b/docs/documentation/platform/access-controls/access-requests.mdx @@ -0,0 +1,13 @@ +--- +title: "Access Requests" +description: "Learn how to request access to sensitive resources in Infisical." +--- + +In certain situations, developers need to expand their access to certain new project or a sensitive environment. For those use cases, it is helpful to utilize Infisical's **Access Requests** functionality. + +This functionality works in the following way: +1. A project administrator sets up a policy that assigns access managers to a certain sensitive folder or environment. +2. When a developer requests access to one of such sensitive resources, corresponding access managers get an email notification about it. +3. An access manager can approve or deny the access request as well as specify the duration of access in the case of approval. +4. As soon as the request is approved, developer is able to access the sought resources. + diff --git a/docs/documentation/platform/access-controls/additional-privileges.mdx b/docs/documentation/platform/access-controls/additional-privileges.mdx new file mode 100644 index 0000000000..8f29d1d6e1 --- /dev/null +++ b/docs/documentation/platform/access-controls/additional-privileges.mdx @@ -0,0 +1,22 @@ +--- +title: "Additional Privileges" +description: "Learn how to add specific privileges on top of predefined roles." +--- + +Even though Infisical supports full-fledged [role-base access controls](./role-based-access-controls) with ability to set predefined permissions for user and machine identities, it is sometimes desired to set additional privileges for specific user or machine identities on top of their roles. + +Infisical **Additional Privileges** functionality enables specific permissions with access to sensitive secrets/folders by identities within certain projects. It is possible to set up additional privileges through Web UI or API. + +To provision specific privileges through Web UI: +1. Click on the `Edit` button next to the set of roles for user or identities. +![Edit User Role](/images/platform/access-controls/edit-role.png) + +2. Click `Add Additional Privileges` in the corresponding section of the permission management modal. +![Add Specific Privilege](/images/platform/access-controls/add-additional-privileges.png) + +3. Fill out the necessary parameters in the privilege entry that appears. It is possible to specify the `Environment` and `Secret Path` to which you want to enable access. +It is also possible to define the range of permissions (`View`, `Create`, `Modify`, `Delete`) as well as how long the access should last (e.g., permanent or timed). +![Additional privileges](/images/platform/access-controls/additional-privileges.png) + +4. Click the `Save` button to enable the additional privilege. +![Confirm Specific Privilege](/images/platform/access-controls/confirm-additional-privileges.png) \ No newline at end of file diff --git a/docs/documentation/platform/access-controls/overview.mdx b/docs/documentation/platform/access-controls/overview.mdx new file mode 100644 index 0000000000..54fc8ff25b --- /dev/null +++ b/docs/documentation/platform/access-controls/overview.mdx @@ -0,0 +1,58 @@ +--- +title: "Access Controls" +sidebarTitle: "Overview" +description: "Learn about Infisical's access control toolset." +--- + +To make sure that users and machine identities are only accessing the resources and performing actions they are authorized to, Infisical supports a wide range of access control tools. + + + + Manage user and machine identitity permissions through predefined roles. + + + Add specific privileges to users and machines on top of their roles. + + + Grant timed access to roles and specific privileges. + + + Enable users to request (temporary) access to sensitive resources. + + + Set up review policies for secret changes in sensitive environments. + + + Track every action performed by user and machine identities in Infisical. + + diff --git a/docs/documentation/platform/access-controls/role-based-access-controls.mdx b/docs/documentation/platform/access-controls/role-based-access-controls.mdx new file mode 100644 index 0000000000..98a2e4659e --- /dev/null +++ b/docs/documentation/platform/access-controls/role-based-access-controls.mdx @@ -0,0 +1,44 @@ +--- +title: "Role-based Access Controls" +description: "Learn how to use RBAC to manage user permissions." +--- + +Infisical's Role-based Access Controls (RBAC) enable the usage of predefined and custom roles that imply a set of permissions for user and machine identities. Such roles male it possible to restrict access to resources and the range of actions that can be performed. + +In general, access controls can be split up across [projects](/documentation/platform/project) and [organizations](/documentation/platform/organization). + +## Organization-level access controls + +By default, every user and machine identity in a organization is either an **admin** or a **member**. + +**Admins** are able to perform every action with the organization, including adding and removing organization members, managing access controls, setting up security settings, and creating new projects. + +**Members**, on the other hand, are restricted from removing organization members, modifying billing information, updating access controls, and performing a number of other actions. + +Overall, organization-level access controls are significantly of administrative nature. Access to projects, secrets and other sensitive data is specified on the project level. + +![Org member role](/images/platform/rbac/org-member-role.png) + +## Project-level access controls + +By default, every user in a project is either a **viewer**, **developer**, or an **admin**. Each of these roles comes with a varying access to different features and resources inside projects. + +As such: +- **Admin**: This role enables identities to have access to all environments, folders, secrets, and actions within the project. +- **Developers**: This role restricts identities from performing project control actions, updating Approval Workflow policies, managing roles/members, and more. +- **Viewer**: The most limiting bulit-in role on the project level – it forbids user and machine identities to perform any action and rather shows them in the read-only mode. + +![Project member role](/images/platform/access-controls/rbac.png) + +## Creating custom roles + +By creating custom roles, you are able to adjust permissions to the needs of your organization. This can be useful for: +- Creating superadmin roles, roles specific to SRE engineers, etc. +- Restricting access of users to specific secrets, folders, and environments. +- Embedding these specific roles into [Approval Workflow policies](/documentation/platform/pr-workflows). + + +It is worth noting that users are able to assume multiple built-in and custom roles. A user will gain access to all actions within the roles assigned to them, not just the actions those roles share in common. + + +![project member custom role](/images/platform/rbac/project-member-custom-role.png) diff --git a/docs/documentation/platform/access-controls/temporary-access.mdx b/docs/documentation/platform/access-controls/temporary-access.mdx new file mode 100644 index 0000000000..c914c96f5c --- /dev/null +++ b/docs/documentation/platform/access-controls/temporary-access.mdx @@ -0,0 +1,26 @@ +--- +title: "Temporary Access" +description: "Learn how to set up timed access to sensitive resources for user and machine identities." +--- + +Certain environments and secrets are so sensitive that it is recommended to not give any user permanent access to those. For such use cases, Infisical supports the functionality of **Temporary Access** provisioning. + + +To provision temporary access through Web UI: +1. Click on the `Edit` button next to the set of roles for user or identities. +![Edit User Role](/images/platform/access-controls/edit-role.png) + +2. Click `Permanent` next to the role or specific privilege that you want to make temporary. + +3. Specify the duration of remporary access (e.g., `1m`, `2h`, `3d`). +![Configure temp access](/images/platform/access-controls/configure-temporary-access.png) + +4. Click `Grant`. + +5. Click the corresponding `Save` button to enable remporary access. +![Temporary Access](/images/platform/access-controls/temporary-access.png) + + +Every user and machine identity should always have at least one permanent role attached to it. + + diff --git a/docs/documentation/platform/audit-logs.mdx b/docs/documentation/platform/audit-logs.mdx index b5a47df507..be2381da2d 100644 --- a/docs/documentation/platform/audit-logs.mdx +++ b/docs/documentation/platform/audit-logs.mdx @@ -1,27 +1,28 @@ --- title: "Audit Logs" -description: "See which events are triggered within your Infisical project." +description: "Track evert event action performed within Infisical projects." --- Note that Audit Logs is a paid feature. - If you're using Infisical Cloud, then it is available under the **Team Tier**, **Pro Tier**, + If you're using Infisical Cloud, then it is available under the **Pro**, and **Enterprise Tier** with varying retention periods. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. Infisical provides audit logs for security and compliance teams to monitor information access. -With this feature, teams can track 25+ different events; -filter audit logs by event, actor, source, date or any combination of these filters; -and inspect extensive metadata in the event of any suspicious activity or incident review. +With the Audit Log functionality, teams can: +- **Track** 40+ different events; +- **Filter** audit logs by event, actor, source, date or any combination of these filters; +- **Inspect** extensive metadata in the event of any suspicious activity or incident review. ![Audit logs](../../images/platform/audit-logs/audit-logs-table.png) Each log contains the following data: -- Event: The underlying action such as create, list, read, update, or delete secret(s). -- Actor: The entity responsible for performing or causing the event; this can be a user or service. -- Timestamp: The date and time at which point the event occurred. -- Source (User agent + IP): The software (user agent) and network address (IP) from which the event was initiated. -- Metadata: Additional data to provide context for each event. For example, this could be the path at which a secret was fetched from etc. +- **Event**: The underlying action such as create, list, read, update, or delete secret(s). +- **Actor**: The entity responsible for performing or causing the event; this can be a user or service. +- **Timestamp**: The date and time at which point the event occurred. +- **Source** (User agent + IP): The software (user agent) and network address (IP) from which the event was initiated. +- **Metadata**: Additional data to provide context for each event. For example, this could be the path at which a secret was fetched from etc. diff --git a/docs/documentation/platform/auth-methods/email-password.mdx b/docs/documentation/platform/auth-methods/email-password.mdx new file mode 100644 index 0000000000..82eae16429 --- /dev/null +++ b/docs/documentation/platform/auth-methods/email-password.mdx @@ -0,0 +1,14 @@ +--- +title: "Email and Pasword" +description: "Learn how to authenticate into Infisical with email and password." +--- + +**Email and Password** is the most common authentication method that can be used by user identities for authentication into Web Dashboard and Infisical CLI. It is recommended to utilize [Multi-factor Authentication](/documentation/platform/mfa) in addition to it. + +It is currently possible to use the **Email and Password** auth method to authenticate into the Web Dashboard and Infisical CLI. + +Every **Email and Password** is accompanied by an emergency kit given to users during signup. If the password is lost or forgotten, emergency kit is only way to retrieve the access to your account. It is possible to generate a new emergency kit with the following steps: +1. Open the `Personal Settings` menu. +![open personal settings](../../images/auth-methods/access-personal-settings.png) +2. Scroll down to the `Emergency Kit` section. +3. Enter your current password and click `Save`. \ No newline at end of file diff --git a/docs/documentation/platform/dynamic-secrets/postgresql.mdx b/docs/documentation/platform/dynamic-secrets/postgresql.mdx index 2c623a477f..b7728b4842 100644 --- a/docs/documentation/platform/dynamic-secrets/postgresql.mdx +++ b/docs/documentation/platform/dynamic-secrets/postgresql.mdx @@ -3,7 +3,7 @@ title: "PostgreSQL" description: "Learn how to dynamically generate PostgreSQL Database user passwords." --- -The Infisical MySQL secret rotation allows you to automatically rotate your MySQL database user's password at a predefined interval. +The Infisical PostgreSQL secret rotation allows you to automatically rotate your PostgreSQL database user's password at a predefined interval. ## Prerequisite diff --git a/docs/documentation/platform/folder.mdx b/docs/documentation/platform/folder.mdx index 162cabfb9c..a3636a2eaa 100644 --- a/docs/documentation/platform/folder.mdx +++ b/docs/documentation/platform/folder.mdx @@ -1,11 +1,12 @@ --- title: "Folders" -description: "Organize your secrets with folders" +description: "Learn how to organize secrets with folders." --- -Infisical's folder feature lets you store secrets at a specific folder; we also call this **path-based secret storage**. -This is great for organizing secrets around hierarchies when multiple services, types of secrets, etc. are involved at great quantities. -With folders that can go infinitely deep, you can mirror your application architecture (be it microservices or monorepos) +Infisical Folders enable users to organize secrets using custom structures dependent on the intended use case (also known as **path-based secret storage**). + +It is great for organizing secrets around hierarchies with multiple services or types of secrets involved at large quantities. +Infisical Folders can be infinitely nested to mirror your application architecture – whether it's microservices, monorepos, or any logical grouping that best suits your needs. Consider the following structure for a microservice architecture: @@ -25,9 +26,7 @@ In this example, we store environment variables for each microservice under each We also store user-specific secrets for micro-service 1 under `/service1/users`. With this folder structure in place, your applications only need to specify a path like `/microservice1/envars` to fetch secrets from there. By extending this example, you can see how path-based secret storage provides a versatile approach to manage secrets for any architecture. -## Folders - -### Managing folders +## Managing folders To add a folder, press the downward chevron to the right of the **Add Secret** button; then press on the **Add Folder** button. diff --git a/docs/documentation/platform/identities/machine-identities.mdx b/docs/documentation/platform/identities/machine-identities.mdx new file mode 100644 index 0000000000..77999effe4 --- /dev/null +++ b/docs/documentation/platform/identities/machine-identities.mdx @@ -0,0 +1,59 @@ +--- +title: Machine Identities +description: "Learn how to use Machine Identities to programmatically interact with Infisical." +--- + +## Concept + +An Infisical machine identity is an entity that represents a workload or application that require access to various resources in Infisical. This is conceptually similar to an IAM user in AWS or service account in Google Cloud Platform (GCP). + +Each identity must authenticate with the API using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth) to get back a short-lived access token to be used in subsequent requests. + +![organization identities](/images/platform/organization/organization-machine-identities.png) + +Key Features: + +- Role Assignment: Identities must be assigned [roles](/documentation/platform/role-based-access-controls). These roles determine the scope of access to resources, either at the organization level or project level. +- Auth/Token Configuration: Identities must be configured with corresponding authentication methods and access token properties to securely interact with the Infisical API. + +## Workflow + +A typical workflow for using identities consists of four steps: + +1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities. +This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth). +2. Adding the identity to the project(s) you want it to have access to. +3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back. +4. Authenticating subsequent requests with the Infisical API using the short-lived access token. + + + + Currently, identities can only be used to make authenticated requests to the Infisical API, SDKs, Terraform, Kubernetes Operator, and Infisical Agent. They do not work with clients such as CLI, Ansible look up plugin, etc. + + Machine Identity support for the rest of the clients is planned to be released in the current quarter. + + + +## Authentication Methods + +To interact with various resources in Infisical, Machine Identities are able to authenticate using: + +- [Universal Auth](/documentation/platform/identities/universal-auth): the most versatile authentication method that can be configured on an identity from any platform/environment to access Infisical. + +## FAQ + + + + A service token is a project-level authentication method that is being phased out in favor of identities. + + Amongst many differences, identities provide broader access over the Infisical API, utilizes the same + permission system as user identities, and come with a significantly larger number of configurable authentication and security features. + + + There are a few reasons for why this might happen: + + - You have insufficient organization permissions to create, read, update, delete identities. + - The identity you are trying to read, update, or delete is more privileged than yourself. + - The role you are trying to create an identity for or update an identity to is more privileged than yours. + + diff --git a/docs/documentation/platform/identities/overview.mdx b/docs/documentation/platform/identities/overview.mdx index 4aae6fd083..18c173766f 100644 --- a/docs/documentation/platform/identities/overview.mdx +++ b/docs/documentation/platform/identities/overview.mdx @@ -1,53 +1,26 @@ --- -title: Identities -description: "Programmatically interact with Infisical" +title: "User and Machine Identities" +sidebarTitle: "Overview" +description: "Learn more about identities to interact with resources in Infisical." --- - - Currently, identities can only be used to make authenticated requests to the Infisical API, SDKs, Terraform, K8s Operator, and Agent. They do not work with clients such as CLI, Ansible look up plugin, etc. +To interact with secrets and resource with Infisical, it is important to undrestand the concept of identities. +Identities can be of two types: +- **People** (e.g., developers, platform engineers, administrators) +- **Machines** (e.g., machine entities for managing secrets in CI/CD pipelines, production applications, and more) - We will be releasing compatibility with it across clients in the coming quarter. - +Both people and machines are able to utilize corresponding clients (e.g., Dashboard UI, CLI, SDKs, API, Kubernetes Operator) together with allowed authentication methods (e.g., email & password, SAML SSO, LDAP, OIDC, Universal Auth). -## Concept - -A (machine) identity is an entity that you can create in an Infisical organization to represent a workload or application that requires access to the Infisical API. This is conceptually similar to an IAM user in AWS or service account in Google Cloud Platform (GCP). - -Each identity must authenticate with the API using a supported authentication method like [Universal Auth](/documentation/platform/identities/universal-auth) to get back a short-lived access token to be used in subsequent requests. - -Key Features: - -- Role Assignment: Identities must be assigned [roles](/documentation/platform/role-based-access-controls). These roles determine the scope of access to resources, either at the organization level or project level. -- Auth/Token Configuration: Identities must be configured with auth methods and access token properties to securely interact with the Infisical API. - -## Workflow - -A typical workflow for using identities consists of four steps: - -1. Creating the identity with a name and [role](/documentation/platform/role-based-access-controls) in Organization Access Control > Machine Identities. -This step also involves configuring an authentication method for it such as [Universal Auth](/documentation/platform/identities/universal-auth). -2. Adding the identity to the project(s) you want it to have access to. -3. Authenticating the identity with the Infisical API based on the configured authentication method on it and receiving a short-lived access token back. -4. Authenticating subsequent requests with the Infisical API using the short-lived access token. - -Check out the following authentication method-specific guides for step-by-step instruction on how to use identities to access Infisical: - -- [Universal Auth](/documentation/platform/identities/universal-auth) - -**FAQ** - - - - A service token is a project-level authentication method that is being phased out in favor of identities. - - Amongst many differences, identities provide broader access over the Infisical API, utilizes the same role-based - permission system used by users, and comes with ample more configurable authentication and security features. - - - There are a few reasons for why this might happen: - - - You have insufficient organization permissions to create, read, update, delete identities. - - The identity you are trying to read, update, or delete is more privileged than yourself. - - The role you are trying to create an identity for or update an identity to is more privileged than yours. - - + + + Learn more about the concept on user identities in Infisical. + + + Understand the concept of machine identities in Infisical. + + diff --git a/docs/documentation/platform/identities/universal-auth.mdx b/docs/documentation/platform/identities/universal-auth.mdx index e60ae1e33b..a9f4dffae3 100644 --- a/docs/documentation/platform/identities/universal-auth.mdx +++ b/docs/documentation/platform/identities/universal-auth.mdx @@ -1,9 +1,9 @@ --- title: Universal Auth -description: "Authenticate with Infisical from any platform/environment" +description: "Learn how to authenticate to Infisical from any platform or environment." --- -**Universal Auth** is the most versatile authentication method that can be configured on an identity from any platform/environment to access Infisical. +**Universal Auth** is the most versatile authentication method that can be configured for a [machine identity](/documentation/platform/identities/machine-identities) to access Infisical from any platform or environment. In this method, each identity is given a **Client ID** for which you can generate one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for an access token to authenticate with the Infisical API. @@ -50,7 +50,7 @@ using the Universal Auth authentication method. Restricting **Client Secret** and access token usage to specific trusted IPs is a paid feature. - If you’re using Infisical Cloud, then it is available under the Pro Tier. If you’re self-hosting Infisical, then you should contact team@infisical.com to purchase an enterprise license to use it. + If you’re using Infisical Cloud, then it is available under the Pro Tier. If you’re self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. diff --git a/docs/documentation/platform/identities/user-identities.mdx b/docs/documentation/platform/identities/user-identities.mdx new file mode 100644 index 0000000000..e88752d841 --- /dev/null +++ b/docs/documentation/platform/identities/user-identities.mdx @@ -0,0 +1,23 @@ +--- +title: User Identities +description: "Read more about the concept of user identities in Infisical." +--- + +## Concept + +A **user identity** (also known as **user**) represents a developer, admin, or any other human entity interacting with resources in Infisical. + +Users can be added manually (through Web UI) or programmatically (e.g., API) to [organizations](../organization) and [projects](../projects). + +Upon being added to an organizaztion and projects, users assume a certain set of roles and permissions that represents their identity. + +![organization members](../../images/platform/organization/organization-members.png) + +## Authentication methods + +To interact with various resources in Infisical, users are able to utilize a number of authentication methods: +- **Email & Password**: the most common authentication method that is used for authentication into Web Dashboard and Infisical CLI. It is recommended to utilize [Multi-factor Authentication](/documentation/platform/mfa) in addition to it. +- **Service Tokens**: Service tokens allow users authenticate into CLI and other clients under their own identity. For the majority of use cases, it is not a recommended approach. Instead, it is often a good idea to utilize [Machine Identities](./machine-identities) with [Universal Authentication](/documentation/platform/identities/universal-auth). +- **SSO**: Infisical natively integrates with a number of SSO identity providers like [Google](/documentation/platform/sso/google), [GitHub](/documentation/platform/sso/github), and [GitLab](/documentation/platform/sso/gitlab). +- **SAML SSO**: It is also possible to set up SAML SSO integration with identity providers like [Okta](/documentation/platform/sso/okta), [Microsoft Entra ID](/documentation/platform/sso/azure) (formerly known as Azure AD), [JumpCloud](/documentation/platform/sso/jumpcloud), [Google](/documentation/platform/sso/google-saml), and more. +- **LDAP**: For organizations with more advanced needs, Infisical also provides user authentication with [LDAP](/documentation/platform/ldap/overview) that includes a number of LDAP providers. diff --git a/docs/documentation/platform/ip-allowlisting.mdx b/docs/documentation/platform/ip-allowlisting.mdx index f0844e685f..7f787fff8c 100644 --- a/docs/documentation/platform/ip-allowlisting.mdx +++ b/docs/documentation/platform/ip-allowlisting.mdx @@ -14,7 +14,7 @@ description: "Restrict access to your secrets in Infisical using trusted IPs" Note that IP Allowlisting is a paid feature. If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. Projects in Infisical can be configured to restrict client access to specific IP addresses or CIDR ranges. This applies to any client using service tokens and diff --git a/docs/documentation/platform/ldap.mdx b/docs/documentation/platform/ldap.mdx index 01237e1c9a..ba01aa7433 100644 --- a/docs/documentation/platform/ldap.mdx +++ b/docs/documentation/platform/ldap.mdx @@ -7,7 +7,7 @@ description: "Log in to Infisical with LDAP" LDAP is a paid feature. If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol). diff --git a/docs/documentation/platform/ldap/general.mdx b/docs/documentation/platform/ldap/general.mdx index 137538cffc..5e50b736b2 100644 --- a/docs/documentation/platform/ldap/general.mdx +++ b/docs/documentation/platform/ldap/general.mdx @@ -1,12 +1,12 @@ --- title: "General LDAP" -description: "Log in to Infisical with LDAP" +description: "Learn how to log in to Infisical with LDAP." --- LDAP is a paid feature. If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) diff --git a/docs/documentation/platform/ldap/jumpcloud.mdx b/docs/documentation/platform/ldap/jumpcloud.mdx index 5e7e42ca85..454a4d5223 100644 --- a/docs/documentation/platform/ldap/jumpcloud.mdx +++ b/docs/documentation/platform/ldap/jumpcloud.mdx @@ -1,12 +1,12 @@ --- title: "JumpCloud LDAP" -description: "Configure JumpCloud LDAP for Logging into Infisical" +description: "Learn how to configure JumpCloud LDAP for authenticating into Infisical." --- LDAP is a paid feature. If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. diff --git a/docs/documentation/platform/ldap/overview.mdx b/docs/documentation/platform/ldap/overview.mdx index d19095b7ca..2423be8c06 100644 --- a/docs/documentation/platform/ldap/overview.mdx +++ b/docs/documentation/platform/ldap/overview.mdx @@ -1,6 +1,7 @@ --- title: "LDAP Overview" -description: "Log in to Infisical with LDAP" +sidebarTitle: "Overview" +description: "Learn how to authenticate into Infisical with LDAP." --- LDAP is a paid feature. @@ -9,9 +10,9 @@ description: "Log in to Infisical with LDAP" then you should contact sales@infisical.com to purchase an enterprise license to use it. -You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol) +You can configure your organization in Infisical to have members authenticate with the platform via [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol). -To note, configuring LDAP retains the end-to-end encrypted architecture of Infisical because we decouple the authentication and decryption steps; the LDAP server cannot and will not have access to the decryption key needed to decrypt your secrets. +To note, configuring LDAP retains the end-to-end encrypted nature of authentication in Infisical because we decouple the authentication and decryption steps; the LDAP server cannot and will not have access to the decryption key needed to decrypt your secrets. LDAP providers: @@ -20,4 +21,7 @@ LDAP providers: - AWS Directory Service - Foxpass -Check out the general instructions for configuring LDAP [here](/documentation/platform/ldap/general). +Read the general instructions for configuring LDAP [here](/documentation/platform/ldap/general). + +If the documentation for your required identity provider is not shown in the list above, please reach out to [team@infisical.com](mailto:team@infisical.com) for assistance. + diff --git a/docs/documentation/platform/mfa.mdx b/docs/documentation/platform/mfa.mdx index 7629401eb7..3ca9c5dff7 100644 --- a/docs/documentation/platform/mfa.mdx +++ b/docs/documentation/platform/mfa.mdx @@ -1,6 +1,7 @@ --- -title: "MFA" -description: "Secure your Infisical account with MFA" +title: "Multi-factor Authentication" +sidebarTitle: "MFA" +description: "Learn how to secure your Infisical account with MFA." --- MFA requires users to provide multiple forms of identification to access their account. Currently, this means logging in with your password and a 6-digit code sent to your email. diff --git a/docs/documentation/platform/organization.mdx b/docs/documentation/platform/organization.mdx index 82a4058a53..d45bb6d4f8 100644 --- a/docs/documentation/platform/organization.mdx +++ b/docs/documentation/platform/organization.mdx @@ -1,9 +1,9 @@ --- -title: "Organization" -description: "How Infisical structures its organizations." +title: "Organizations" +description: "Learn more and understand the concept of Infisical organizations." --- -An organization houses projects and members. +An Infisical organization is a set of [projects](./project) that use the same billing. Organizations allow one or more users to control billing and project permissions for all of the projects belonging to the organization. Each project belongs to an organization. ## Projects @@ -18,21 +18,23 @@ The **Settings** page lets you manage information about your organization includ - Name: The name of your organization. - Incident contacts: Emails that should be alerted if anything abnormal is detected within the organization. -- SAML Authentication: The SAML SSO configuration of the organization (if applicable); Infisical currently -supports Okta, Azure, and JumpCloud identity providers. ![organization settings general](../../images/platform/organization/organization-settings-general.png) + + +- Security and Authentication: A set of setting to enforce or manage [SAML](/documentation/platform/sso/overview), [SCIM](/documentation/platform/scim/overview), [LDAP](/documentation/platform/ldap/overview), and other authentication configurations. + ![organization settings auth](../../images/platform/organization/organization-settings-auth.png) -## Members +## Access Control -The **Members** page is where you can manage members and their permissions within the organization. -In the **Members** tab, you can add external members to your organization or remove them; you can also -change their role. +The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization. +You can add or remove additional members as well as modify their permissions. -![organization members](../../images/organization-members.png) +![organization members](../../images/platform/organization/organization-members.png) +![organization identities](../../images/platform/organization/organization-machine-identities.png) -In the **Roles** tab, you can manage roles for members within the organization. +In the **Organization Roles** tab, you can edit current or create new custom roles for members within the organization. Note that Role-Based Access Management (RBAC) is partly a paid feature. @@ -41,13 +43,13 @@ In the **Roles** tab, you can manage roles for members within the organization. at the organization and project level for free. If you're using Infisical Cloud, the ability to create custom roles is available under the **Pro Tier**. - If you're self-hosting Infisical, then you should contact team@infisical.com to purchase an enterprise license to use it. + If you're self-hosting Infisical, then you should contact sales@infisical.com to purchase an enterprise license to use it. ![organization roles](../../images/platform/organization/organization-members-roles.png) -As you can see next, Infisical supports granular permissions that you can tailor to each role. So, -if you need certain members to only be able to access billing details, for example, then you can +As you can see next, Infisical supports granular permissions that you can tailor to each role. +If you need certain members to only be able to access billing details, for example, then you can assign them that permission only. ![organization role permissions](../../images/platform/organization/organization-members-roles-add-perm.png) diff --git a/docs/documentation/platform/pit-recovery.mdx b/docs/documentation/platform/pit-recovery.mdx index 3cad5eff24..448faddbb7 100644 --- a/docs/documentation/platform/pit-recovery.mdx +++ b/docs/documentation/platform/pit-recovery.mdx @@ -1,21 +1,21 @@ --- title: "Point-in-Time Recovery" -description: "How to rollback secrets and configs to any commit with Infisical." +description: "Learn how to rollback secrets and configurations to any snapshot with Infisical." --- Point-in-Time Recovery is a paid feature. - If you're using Infisical Cloud, then it is available under the **Team Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, + then you should contact sales@infisical.com to purchase an enterprise license to use it. -Infisical's point-in-time recovery feature allows secrets to be rolled back to any point in time for any given [folder](./folder). -Under the hood, snapshots, capturing the state of the folder, get taken after any mutation an item within that folder. +Infisical's point-in-time recovery functionality allows secrets to be rolled back to any point in time for any given [folder](./folder) or [environment](/documentation/platform/project#project-environments). +Every time a secret is updated, a new snapshot is taken – capturing the state of the folder and environment at that point of time. ## Snapshots -Similar to Git, a commit (aka snapshot) in Infisical is the state of your project's secrets at a specific point in time scoped to +Similar to Git, a commit (also known as snapshot) in Infisical is the state of your project's secrets at a specific point in time scoped to an environment and [folder](./folder) within it. To view a list of snapshots for the current folder, press the **Commits** button. @@ -28,12 +28,14 @@ This opens up a sidebar from which you can select to view a particular snapshot: ## Rolling back -After pressing on a snapshot from the sidebar, you can view it and even roll back the state +After pressing on a snapshot from the sidebar, you can view it and roll back the state of the folder to that point in time by pressing the **Rollback** button. ![PIT snapshot](../../images/platform/pit-recovery/pit-recovery-rollback.png) Rolling back secrets to a past snapshot creates a creates a snapshot at the top of the stack and updates secret versions. -Note that rollbacks are localized to not affect other folders within the same environment. This means each [folder](./folder) maintains its own independent history of changes, offering precise and isolated control over rollback actions. + +Rollbacks are localized to not affect other folders within the same environment. This means each [folder](./folder) maintains its own independent history of changes, offering precise and isolated control over rollback actions. Put differently, every [folder](./folder) possesses a distinct and separate timeline, providing granular control when managing your secrets. + \ No newline at end of file diff --git a/docs/documentation/platform/pr-workflows.mdx b/docs/documentation/platform/pr-workflows.mdx index 1c7bb97877..9df1236127 100644 --- a/docs/documentation/platform/pr-workflows.mdx +++ b/docs/documentation/platform/pr-workflows.mdx @@ -1,6 +1,6 @@ --- -title: "PR Workflows" -description: "Infisical PR Workflows allows you to create a set of policies to control secret operations." +title: "Approval Workflows" +description: "Learn how to enable a set of policies to manage changes to sensitive secrets and environments." --- ## Problem at hand @@ -14,15 +14,15 @@ Updating secrets in high-stakes environments (e.g., production) can have a numbe As a wide-spread software engineering practice, developers have to submit their code as a PR that needs to be approved before the code is merged into the main branch. -In a similar way, to solve the above-mentioned issues, Infisical provides a feature called `PR Workflows` for secret management. This is a set of policies and workflows that help advance access controls, compliance procedures, and stability of a particular environment. In other words, **PR Workflows** help you secure, stabilize, and streamline the change of secrets in high-stakes environments. +In a similar way, to solve the above-mentioned issues, Infisical provides a feature called `Approval Workflows` for secret management. This is a set of policies and workflows that help advance access controls, compliance procedures, and stability of a particular environment. In other words, **Approval Workflows** help you secure, stabilize, and streamline the change of secrets in high-stakes environments. ### Setting a policy -First, you would need to create a set of policies for a certain environment. In the example below you can see a generic policy for a production environment. In this case, any user who submits a change to `prod` would first have to get an approval by a predefined user (or multiple users). +First, you would need to create a set of policies for a certain environment. In the example below, a generic policy for a production environment is shown. In this case, any user who submits a change to `prod` would first have to get an approval by a predefined approver (or multiple approvers). ![create secret update policy](../../images/platform/pr-workflows/secret-update-policy.png) -### Example of updating secrets with PR workflows +### Example of updating secrets with Approval workflows When a user submits a change to an enviropnment that is under a particular policy, a corresponsing change request will go to a predefined approver (or multiple approvers). diff --git a/docs/documentation/platform/project.mdx b/docs/documentation/platform/project.mdx index 06ad3eaa77..bd80d8ae58 100644 --- a/docs/documentation/platform/project.mdx +++ b/docs/documentation/platform/project.mdx @@ -1,13 +1,21 @@ --- -title: "Project" -description: "How Infisical organizes secrets into projects." +title: "Projects" +description: "Learn more and understand the concept of Infisical projects." --- -A project houses application configuration and secrets for an application. +A project in Infisical belongs to an [organization](./organization) and contains a number of environments, folders, and secrets. +Only users and machine identities who belong to a project can access resources inside of it according to predefined permissions. + +## Project environments + +For both visual and organizational structure, Infisical allows splitting up secrets into environments (e.g., development, staging, production). In project settings, such environments can be +customized depending on the intended use case. + +![project secrets overview](../../images/platform/project/project-environments.png) ## Secrets Overview -The **Secrets Overview** page captures a birds-eye-view of secrets and folders across environments like development, staging, or production. +The **Secrets Overview** page captures a birds-eye-view of secrets and [folders](./folder) across environments. This is useful for comparing secrets, identifying if anything is missing, and making quick changes. ![project secrets overview](../../images/platform/project/project-secrets-overview-open.png) diff --git a/docs/documentation/platform/role-based-access-controls.mdx b/docs/documentation/platform/role-based-access-controls.mdx deleted file mode 100644 index ba774f0d14..0000000000 --- a/docs/documentation/platform/role-based-access-controls.mdx +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: "Role-based Access Controls" -description: "Infisical's Role-based Access Controls enable creating permissions for user and machine identities to restrict access to resources and the range of actions that can be performed." ---- - -### General access controls - -Access Control Policies provide a highly granular declarative way to grant or forbid access to certain resources and operations in Infisical. In general, access controls can be split up across projects and organizations. - -### Organization-level access controls - -By default, every user in a organization is either an **admin** or a **member**. - -Admins are able to perform every action with the organization, including adding and removing organization members, managing access controls, setting up security settings, and creating new projects. Members, on the other hand, are restricted from removing organization members, modifying billing information, updating access controls, and performing a number of other actions. - -Overall, organization-level access controls are significantly of administrative nature. Access to projects, secrets and other sensitive data is specified on the project level. - -![Org member role](../../images/platform/rbac/org-member-role.png) - -### Project-level access controls - -By default, every user in a project is either a **viewer**, **developer**, or an **admin**. Each of these roles comes with a varying access to different features and resources inside projects. As such, **admins** by default have access to all environments, folders, secrets, and actions within the project. At the same time, **developers** are restricted from performing project control actions, updating PR Workflow policies, managing roles/members, and more. Lastly, **viewer** is the most limiting default role on the project level – it forbids developers to perform any action and rather shows them in the read-only mode. - -### Creating custom roles - -By creating custom roles, you are able to adjust permissions to the needs of your organization. This can be useful for: -- Creating superadmin roles, roles specific to SRE engineers, etc. -- Restricting access of users to specific secrets, folders, and environments. -- Embedding these specific roles into [PR Workflow policies](https://infisical.com/docs/documentation/platform/pr-workflows) - -![project member custom role](../../images/platform/rbac/project-member-custom-role.png) diff --git a/docs/documentation/platform/scim/azure.mdx b/docs/documentation/platform/scim/azure.mdx index 45f95e1350..ff46fe4e7e 100644 --- a/docs/documentation/platform/scim/azure.mdx +++ b/docs/documentation/platform/scim/azure.mdx @@ -1,13 +1,13 @@ --- title: "Azure SCIM" -description: "Configure SCIM provisioning with Azure for Infisical" +description: "Learn how to configure SCIM provisioning with Azure for Infisical." --- Azure SCIM provisioning is a paid feature. If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. Prerequisites: diff --git a/docs/documentation/platform/scim/jumpcloud.mdx b/docs/documentation/platform/scim/jumpcloud.mdx index 68bb9b66f1..cf8d1aa881 100644 --- a/docs/documentation/platform/scim/jumpcloud.mdx +++ b/docs/documentation/platform/scim/jumpcloud.mdx @@ -1,13 +1,13 @@ --- title: "JumpCloud SCIM" -description: "Configure SCIM provisioning with JumpCloud for Infisical" +description: "Learn how to configure SCIM provisioning with JumpCloud for Infisical." --- JumpCloud SCIM provisioning is a paid feature. If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. Prerequisites: diff --git a/docs/documentation/platform/scim/okta.mdx b/docs/documentation/platform/scim/okta.mdx index 4baa198157..e5d5bb48b6 100644 --- a/docs/documentation/platform/scim/okta.mdx +++ b/docs/documentation/platform/scim/okta.mdx @@ -1,13 +1,13 @@ --- title: "Okta SCIM" -description: "Configure SCIM provisioning with Okta for Infisical" +description: "Learn how to configure SCIM provisioning with Okta for Infisical." --- Okta SCIM provisioning is a paid feature. If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. Prerequisites: diff --git a/docs/documentation/platform/scim/overview.mdx b/docs/documentation/platform/scim/overview.mdx index deec8b6304..2bc73d810d 100644 --- a/docs/documentation/platform/scim/overview.mdx +++ b/docs/documentation/platform/scim/overview.mdx @@ -1,13 +1,13 @@ --- title: "SCIM Overview" -description: "Provision users for Infisical via SCIM" +description: "Learn how to provision users for Infisical via SCIM." --- SCIM provisioning is a paid feature. If you're using Infisical Cloud, then it is available under the **Enterprise Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. You can configure your organization in Infisical to have members be provisioned/deprovisioned using [SCIM](https://scim.cloud/#Implementations2) via providers like Okta, Azure, JumpCloud, etc. diff --git a/docs/documentation/platform/secret-reference.mdx b/docs/documentation/platform/secret-reference.mdx index 66961db99d..042f7525b8 100644 --- a/docs/documentation/platform/secret-reference.mdx +++ b/docs/documentation/platform/secret-reference.mdx @@ -1,11 +1,12 @@ --- -title: "Secret Referencing / Importing" -description: "How to use reference secrets in Infisical" +title: "Secret Referencing and Importing" +sidebarTitle: "Referencing and Importing" +description: "Learn the fundamentals of secret referencing and importing in Infisical." --- ## Secret Referencing -Infisical's secret referencing feature lets you reference the value of a "base" secret when defining the value of another secret. +Infisical's secret referencing functionality makes it possible to reference the value of a "base" secret when defining the value of another secret. This means that updating the value of a base secret propagates directly to other secrets whose values depend on the base secret. @@ -43,7 +44,7 @@ Here are a few more helpful examples for how to reference secrets in different c ## Secret Imports -Infisical's secret imports feature lets you import the items of another environment or folder into the current folder context. +Infisical's Secret Imports functionality makes it possible to import the secrets from another environment or folder into the current folder context. This can be useful if you have common secrets that need to be available across multiple environments/folders. To add a secret import, press the downward chevron to the right of the **Add Secret** button; then press on the **Add Import** button. diff --git a/docs/documentation/platform/secret-rotation/aws-iam.mdx b/docs/documentation/platform/secret-rotation/aws-iam.mdx index b4247af80f..c524abfbcf 100644 --- a/docs/documentation/platform/secret-rotation/aws-iam.mdx +++ b/docs/documentation/platform/secret-rotation/aws-iam.mdx @@ -1,6 +1,6 @@ --- title: "AWS IAM User" -description: "Rotated access key id and secret key of AWS IAM Users" +description: "Learn how to automatically rotate Access Key Id and Secret Key of AWS IAM Users." --- Infisical's AWS IAM User secret rotation capability lets you update the **Access key** and **Secret access key** credentials of a target IAM user from within Infisical diff --git a/docs/documentation/platform/secret-rotation/mysql.mdx b/docs/documentation/platform/secret-rotation/mysql.mdx index 5bd658a0d1..02356de483 100644 --- a/docs/documentation/platform/secret-rotation/mysql.mdx +++ b/docs/documentation/platform/secret-rotation/mysql.mdx @@ -1,6 +1,6 @@ --- title: "MySQL/MariaDB" -description: "How to rotate MySQL/MariaDB database user passwords" +description: "Learn how to automatically rotate MySQL/MariaDB user passwords." --- The Infisical MySQL secret rotation allows you to automatically rotate your MySQL database user's password at a predefined interval. diff --git a/docs/documentation/platform/secret-rotation/overview.mdx b/docs/documentation/platform/secret-rotation/overview.mdx index 284375401c..57ad17e097 100644 --- a/docs/documentation/platform/secret-rotation/overview.mdx +++ b/docs/documentation/platform/secret-rotation/overview.mdx @@ -1,4 +1,8 @@ -# Secret Rotation Overview +--- +title: "Secret Rotation" +sidebarTitle: "Overview" +description: "Learn how to set up automated secret rotation in Infisical." +--- ## Introduction @@ -7,8 +11,8 @@ Rotating secrets helps prevent unauthorized access to systems and sensitive data Rotated secrets may include, but are not limited to: -1. API keys for external services -2. Database credentials for various platforms +1. API keys for external services; +2. Database credentials for various platforms. ## Rotation Process @@ -42,3 +46,4 @@ Finally, the system promotes the future active (pending) secret to be the new cu 1. [SendGrid Integration](./sendgrid) 2. [PostgreSQL/CockroachDB Implementation](./postgres) 3. [MySQL/MariaDB Configuration](./mysql) +4. [AWS IAM User](./aws-iam) diff --git a/docs/documentation/platform/secret-rotation/postgres.mdx b/docs/documentation/platform/secret-rotation/postgres.mdx index 1ddc7d558f..0a6339e4ea 100644 --- a/docs/documentation/platform/secret-rotation/postgres.mdx +++ b/docs/documentation/platform/secret-rotation/postgres.mdx @@ -1,6 +1,6 @@ --- title: "PostgreSQL/CockroachDB" -description: "How to rotate postgreSQL/cockroach database user passwords" +description: "Learn how to automatically rotate PostgreSQL/CockroachDB user passwords." --- The Infisical Postgres secret rotation allows you to automatically rotate your Postgres database user's password at a predefined interval. diff --git a/docs/documentation/platform/secret-rotation/sendgrid.mdx b/docs/documentation/platform/secret-rotation/sendgrid.mdx index 2a7b91a153..4f27057ad3 100644 --- a/docs/documentation/platform/secret-rotation/sendgrid.mdx +++ b/docs/documentation/platform/secret-rotation/sendgrid.mdx @@ -1,6 +1,6 @@ --- title: "Twilio SendGrid" -description: "How to rotate Twilio SendGrid API keys" +description: "Find out how to rotate Twilio SendGrid API keys." --- Eliminate the use of long lived secrets by rotating Twilio SendGrid API keys with Infisical. @@ -9,7 +9,7 @@ Eliminate the use of long lived secrets by rotating Twilio SendGrid API keys wit You will need a valid SendGrid admin key with the necessary scope to create additional API keys. -Follow the [SendGrid Docs to create an admin api key](https://docs.sendgrid.com/ui/account-and-settings/api-keys) +Follow the [SendGrid Docs to create an admin api key](https://docs.sendgrid.com/ui/account-and-settings/api-keys). ## How it works diff --git a/docs/documentation/platform/secret-versioning.mdx b/docs/documentation/platform/secret-versioning.mdx index 04c3086cd4..741bbc3089 100644 --- a/docs/documentation/platform/secret-versioning.mdx +++ b/docs/documentation/platform/secret-versioning.mdx @@ -1,15 +1,20 @@ --- title: "Secret Versioning" -description: "Version secrets and configurations with Infisical" +description: "Learn how secret versioning works in Infisical." --- -Secret versioning records changes made to every secret. +Every time a secret change is persformed, a new version of the same secret is created. -![secret versioning](../../images/secret-versioning.png) +Such versions can be accessed visually by opening up the [secret sidebar](/documentation/platform/project#drawer) (as seen below) or [retrived via API](/api-reference/endpoints/secrets/read) +by specifying the `version` query parameter. + +![secret versioning](../../images/platform/secret-versioning.png) + +The secret versioning functionality is heavily connected to [Point-in-time Recovery](/documentation/platform/pit-recovery) of secrets in Infisical. You can copy and paste a secret version value to the "Value" input field "roll back" to that secret version. This creates a new secret version at the top of - the stack. We're releasing the ability to press and automatically roll back to + the stack. We're releasing the ability to automatically roll back to a secret version soon. diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index 30622e6e82..cbd5a7d0e6 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -1,13 +1,13 @@ --- -title: "Azure SAML" -description: "Configure Azure SAML for Infisical SSO" +title: "Entra ID / Azure AD SAML" +description: "Learn how to configure Microsoft Entra ID for Infisical SSO." --- Azure SAML SSO is a paid feature. If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. diff --git a/docs/documentation/platform/sso/github.mdx b/docs/documentation/platform/sso/github.mdx index 87d1b3cf7f..53a9b9156c 100644 --- a/docs/documentation/platform/sso/github.mdx +++ b/docs/documentation/platform/sso/github.mdx @@ -1,6 +1,6 @@ --- title: "GitHub SSO" -description: "Configure GitHub SSO for Infisical" +description: "Learn how to configure GitHub SSO for Infisical." --- Using GitHub SSO on a self-hosted instance of Infisical requires configuring an OAuth2 application in GitHub and registering your instance with it. diff --git a/docs/documentation/platform/sso/gitlab.mdx b/docs/documentation/platform/sso/gitlab.mdx index 446758ae01..d2a537bfaa 100644 --- a/docs/documentation/platform/sso/gitlab.mdx +++ b/docs/documentation/platform/sso/gitlab.mdx @@ -1,6 +1,6 @@ --- title: "GitLab SSO" -description: "Configure GitLab SSO for Infisical" +description: "Learn how to configure GitLab SSO for Infisical." --- Using GitLab SSO on a self-hosted instance of Infisical requires configuring an OAuth application in GitLab and registering your instance with it. diff --git a/docs/documentation/platform/sso/google-saml.mdx b/docs/documentation/platform/sso/google-saml.mdx index 743c4e3ff8..1897a651a7 100644 --- a/docs/documentation/platform/sso/google-saml.mdx +++ b/docs/documentation/platform/sso/google-saml.mdx @@ -1,13 +1,13 @@ --- title: "Google SAML" -description: "Configure Google SAML for Infisical SSO" +description: "Learn how to configure Google SAML for Infisical SSO." --- Google SAML SSO feature is a paid feature. If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. diff --git a/docs/documentation/platform/sso/google.mdx b/docs/documentation/platform/sso/google.mdx index cf35dcb687..36ee511d12 100644 --- a/docs/documentation/platform/sso/google.mdx +++ b/docs/documentation/platform/sso/google.mdx @@ -1,6 +1,6 @@ --- title: "Google SSO" -description: "Configure Google SSO for Infisical" +description: "Learn how to configure Google SSO for Infisical." --- Using Google SSO on a self-hosted instance of Infisical requires configuring an OAuth2 application in GCP and registering your instance with it. diff --git a/docs/documentation/platform/sso/jumpcloud.mdx b/docs/documentation/platform/sso/jumpcloud.mdx index 8b64c8643c..781f5224a7 100644 --- a/docs/documentation/platform/sso/jumpcloud.mdx +++ b/docs/documentation/platform/sso/jumpcloud.mdx @@ -1,13 +1,13 @@ --- title: "JumpCloud SAML" -description: "Configure JumpCloud SAML for Infisical SSO" +description: "Learn how to configure JumpCloud SAML for Infisical SSO." --- JumpCloud SAML SSO is a paid feature. If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. diff --git a/docs/documentation/platform/sso/keycloak-saml.mdx b/docs/documentation/platform/sso/keycloak-saml.mdx new file mode 100644 index 0000000000..9817397117 --- /dev/null +++ b/docs/documentation/platform/sso/keycloak-saml.mdx @@ -0,0 +1,139 @@ +--- +title: "Keycloak SAML" +description: "Learn how to configure Keycloak SAML for Infisical SSO." +--- + + + Keycloak SAML SSO is a paid feature. + + If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, + then you should contact sales@infisical.com to purchase an enterprise license to use it. + + + + + In Infisical, head to your Organization Settings > Authentication > SAML SSO Configuration and select **Manage**. + + ![Keycloak SAML organization security section](../../../images/sso/keycloak/org-security-section.png) + + Next, copy the **Valid redirect URI** and **SP Entity ID** to use when configuring the Keycloak SAML application. + + ![Keycloak SAML initial configuration](../../../images/sso/keycloak/init-config.png) + + + 2.1. In your realm, navigate to the **Clients** tab and click **Create client** to create a new client application. + + ![SAML keycloak list of clients](../../../images/sso/keycloak/clients-list.png) + + + You don’t typically need to make a realm dedicated to Infisical. We recommend adding Infisical as a client to your primary realm. + + + In the General Settings step, set **Client type** to **SAML**, the **Client ID** field to `https://app.infisical.com`, and the **Name** field to a friendly name like **Infisical**. + + ![SAML keycloak create client general settings](../../../images/sso/keycloak/create-client-general-settings.png) + + + If you’re self-hosting Infisical, then you will want to replace https://app.infisical.com with your own domain. + + + Next, in the Login Settings step, set both the **Home URL** field and **Valid redirect URIs** field to the **Valid redirect URI** from step 1 and press **Save**. + + ![SAML keycloak create client login settings](../../../images/sso/keycloak/create-client-login-settings.png) + + 2.2. Once you've created the client, under its **Settings** tab, make sure to set the following values: + + - Under **SAML Capabilities**: + - Name ID format: email (or username). + - Force name ID format: On. + - Force POST binding: On. + - Include AuthnStatement: On. + - Under **Signature and Encryption**: + - Sign documents: On. + - Sign assertions: On. + - Signature algorithm: RSA_SHA256. + + ![SAML keycloak client SAML capabilities](../../../images/sso/keycloak/client-saml-capabilities.png) + + ![SAML keycloak client signature encryption](../../../images/sso/keycloak/client-signature-encryption.png) + + 2.3. Next, navigate to the **Client scopes** tab select the client's dedicated scope. + + ![SAML keycloak client scopes list](../../../images/sso/keycloak/client-scopes-list.png) + + Next click **Add predefined mapper**. + + ![SAML keycloak client mappers empty](../../../images/sso/keycloak/client-mappers-empty.png) + + Select the **X500 email**, **X500 givenName**, and **X500 surname** attributes and click **Add**. + + ![SAML keycloak client mappers predefined](../../../images/sso/keycloak/client-mappers-predefined.png) + + Now click on the **X500 email** mapper and set the **SAML Attribute Name** field to **email**. + + ![SAML keycloak client mappers email](../../../images/sso/keycloak/client-mappers-email.png) + + Repeat the same for **X500 givenName** and **X500 surname** mappers, setting the **SAML Attribute Name** field to **firstName** and **lastName** respectively. + + Next, back in the client scope's **Mappers**, click **Add mapper** and select **by configuration**. + + ![SAML keycloak client mappers by configuration](../../../images/sso/keycloak/client-mappers-by-configuration.png) + + Select **User Property**. + + ![SAML keycloak client mappers user property](../../../images/sso/keycloak/client-mappers-user-property.png) + + Set the the **Name** field to **Username**, the **Property** field to **username**, and the **SAML Attribtue Name** to **username**. + + ![SAML keycloak client mappers username](../../../images/sso/keycloak/client-mappers-username.png) + + Repeat the same for the `id` attribute, setting the **Name** field to **ID**, the **Property** field to **id**, and the **SAML Attribute Name** to **id**. + + ![SAML keycloak client mappers id](../../../images/sso/keycloak/client-mappers-id.png) + + Once you've completed the above steps, the list of mappers should look like this: + + ![SAML keycloak client mappers completed](../../../images/sso/keycloak/client-mappers-completed.png) + + + Back in Keycloak, navigate to Configure > Realm settings > General tab > Endpoints > SAML 2.0 Identity Provider Metadata and copy the IDP URL. This should appear in various places and take the form: `https://keycloak-mysite.com/realms/myrealm/protocol/saml`. + + ![SAML keycloak realm SAML metadata](../../../images/sso/keycloak/realm-saml-metadata.png) + + Also, in the **Keys** tab, locate the RS256 key and copy the certificate to use when finishing configuring Keycloak SAML in Infisical. + + ![SAML keycloak realm settings keys](../../../images/sso/keycloak/realm-settings-keys.png) + + + Back in Infisical, set **IDP URL** and **Certificate** to the items from step 3. Also, set the **Client ID** to the `https://app.infisical.com`. + + Once you've done that, press **Update** to complete the required configuration. + + ![SAML Okta paste values into Infisical](../../../images/sso/keycloak/idp-values.png) + + + Enabling SAML SSO allows members in your organization to log into Infisical via Keycloak. + + ![SAML keycloak enable SAML](../../../images/sso/keycloak/enable-saml.png) + + + Enforcing SAML SSO ensures that members in your organization can only access Infisical + by logging into the organization via Keycloak. + + To enforce SAML SSO, you're required to test out the SAML connection by successfully authenticating at least one Keycloak user with Infisical; + Once you've completed this requirement, you can toggle the **Enforce SAML SSO** button to enforce SAML SSO. + + + We recommend ensuring that your account is provisioned the application in Keycloak + prior to enforcing SAML SSO to prevent any unintended issues. + + + + + + If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to + set the `AUTH_SECRET` and `SITE_URL` environment variable for it to work: + + - `AUTH_SECRET`: A secret key used for signing and verifying JWT. This can be a random 32-byte base64 string generated with `openssl rand -base64 32`. + - `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com) + \ No newline at end of file diff --git a/docs/documentation/platform/sso/okta.mdx b/docs/documentation/platform/sso/okta.mdx index f182039639..c81141c92b 100644 --- a/docs/documentation/platform/sso/okta.mdx +++ b/docs/documentation/platform/sso/okta.mdx @@ -1,13 +1,13 @@ --- title: "Okta SAML" -description: "Configure Okta SAML 2.0 for Infisical SSO" +description: "Learn how to configure Okta SAML 2.0 for Infisical SSO." --- Okta SAML SSO is a paid feature. If you're using Infisical Cloud, then it is available under the **Pro Tier**. If you're self-hosting Infisical, - then you should contact team@infisical.com to purchase an enterprise license to use it. + then you should contact sales@infisical.com to purchase an enterprise license to use it. diff --git a/docs/documentation/platform/sso/overview.mdx b/docs/documentation/platform/sso/overview.mdx index e1fd259573..6064f26e8a 100644 --- a/docs/documentation/platform/sso/overview.mdx +++ b/docs/documentation/platform/sso/overview.mdx @@ -1,6 +1,7 @@ --- title: "SSO Overview" -description: "Log in to Infisical via SSO protocols" +sidebarTitle: "Overview" +description: "Learn how to log in to Infisical via SSO protocols." --- @@ -13,8 +14,12 @@ description: "Log in to Infisical via SSO protocols" You can configure your organization in Infisical to have members authenticate with the platform via protocols like [SAML 2.0](https://en.wikipedia.org/wiki/SAML_2.0). -To note, configuring SSO retains the end-to-end encrypted architecture of Infisical because we decouple the **authentication** and **decryption** steps. In all login with SSO implementations, -your IdP cannot and will not have access to the decryption key needed to decrypt your secrets. +To note, Infisical's SSO implementation decouples the **authentication** and **decryption** steps – which implies that no +Identity Provider can have access to the decryption key needed to decrypt your secrets (this also implies that Infisical requires entering the user's Master Password on top of authenticating with SSO). + +## Identity providers + +Infisical supports these and many other identity providers: - [Google SSO](/documentation/platform/sso/google) - [GitHub SSO](/documentation/platform/sso/github) @@ -22,4 +27,7 @@ your IdP cannot and will not have access to the decryption key needed to decrypt - [Okta SAML](/documentation/platform/sso/okta) - [Azure SAML](/documentation/platform/sso/azure) - [JumpCloud SAML](/documentation/platform/sso/jumpcloud) +- [Keycloak SAML](/documentation/platform/sso/keycloak-saml) - [Google SAML](/documentation/platform/sso/google-saml) + +If your required identity provider is not shown in the list above, please reach out to [team@infisical.com](mailto:team@infisical.com) for assistance. diff --git a/docs/documentation/platform/token.mdx b/docs/documentation/platform/token.mdx index 9e304de3d1..efb1e05d4b 100644 --- a/docs/documentation/platform/token.mdx +++ b/docs/documentation/platform/token.mdx @@ -1,6 +1,6 @@ --- -title: "Service token" -description: "Infisical service tokens allows you to programmatically interact with Infisical" +title: "Service Token" +description: "Infisical service tokens allow users to programmatically interact with Infisical." --- Service tokens are authentication credentials that services can use to access designated endpoints in the Infisical API to manage project resources like secrets. @@ -43,6 +43,10 @@ Also, note that Infisical supports [glob patterns](https://www.malikbrowne.com/b In the above screenshot, you can see that we are creating a token token with `read` access to all subfolders at any depth of the `/common` path within the development environment of the project; the token expires in 6 months and can be used from any IP address. + +For a deeper understanding of service tokens, it is recommended to read [this guide](http://localhost:3000/internals/service-tokens). + + **FAQ** diff --git a/docs/documentation/platform/webhooks.mdx b/docs/documentation/platform/webhooks.mdx index e0de7be059..22277dd8c7 100644 --- a/docs/documentation/platform/webhooks.mdx +++ b/docs/documentation/platform/webhooks.mdx @@ -1,6 +1,6 @@ --- title: "Webhooks" -description: "How Infisical webhooks works?" +description: "Learn the fundamentals of Infisical webhooks." --- Webhooks can be used to trigger changes to your integrations when secrets are modified, providing smooth integration with other third-party applications. diff --git a/docs/images/auth-methods/access-personal-settings.png b/docs/images/auth-methods/access-personal-settings.png new file mode 100644 index 0000000000..a5e1989c1e Binary files /dev/null and b/docs/images/auth-methods/access-personal-settings.png differ diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png b/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png index 4dcaa04dda..cc17097e16 100644 Binary files a/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png and b/docs/images/integrations/aws/integrations-aws-secret-manager-auth.png differ diff --git a/docs/images/integrations/aws/integrations-aws-secret-manager-create.png b/docs/images/integrations/aws/integrations-aws-secret-manager-create.png index 703fb61011..619ec0a8e5 100644 Binary files a/docs/images/integrations/aws/integrations-aws-secret-manager-create.png and b/docs/images/integrations/aws/integrations-aws-secret-manager-create.png differ diff --git a/docs/images/organization-members.png b/docs/images/organization-members.png deleted file mode 100644 index 70190df0c8..0000000000 Binary files a/docs/images/organization-members.png and /dev/null differ diff --git a/docs/images/platform/access-controls/add-additional-privileges.png b/docs/images/platform/access-controls/add-additional-privileges.png new file mode 100644 index 0000000000..28848075a6 Binary files /dev/null and b/docs/images/platform/access-controls/add-additional-privileges.png differ diff --git a/docs/images/platform/access-controls/additional-privileges.png b/docs/images/platform/access-controls/additional-privileges.png new file mode 100644 index 0000000000..4561021baa Binary files /dev/null and b/docs/images/platform/access-controls/additional-privileges.png differ diff --git a/docs/images/platform/access-controls/configure-temporary-access.png b/docs/images/platform/access-controls/configure-temporary-access.png new file mode 100644 index 0000000000..0c16bbc35e Binary files /dev/null and b/docs/images/platform/access-controls/configure-temporary-access.png differ diff --git a/docs/images/platform/access-controls/confirm-additional-privileges.png b/docs/images/platform/access-controls/confirm-additional-privileges.png new file mode 100644 index 0000000000..b6fdbf518b Binary files /dev/null and b/docs/images/platform/access-controls/confirm-additional-privileges.png differ diff --git a/docs/images/platform/access-controls/edit-role.png b/docs/images/platform/access-controls/edit-role.png new file mode 100644 index 0000000000..598f585b8c Binary files /dev/null and b/docs/images/platform/access-controls/edit-role.png differ diff --git a/docs/images/platform/access-controls/rbac.png b/docs/images/platform/access-controls/rbac.png new file mode 100644 index 0000000000..22380c8051 Binary files /dev/null and b/docs/images/platform/access-controls/rbac.png differ diff --git a/docs/images/platform/access-controls/temporary-access.png b/docs/images/platform/access-controls/temporary-access.png new file mode 100644 index 0000000000..24be8a5844 Binary files /dev/null and b/docs/images/platform/access-controls/temporary-access.png differ diff --git a/docs/images/platform/organization/organization-machine-identities.png b/docs/images/platform/organization/organization-machine-identities.png new file mode 100644 index 0000000000..17bea6e9bf Binary files /dev/null and b/docs/images/platform/organization/organization-machine-identities.png differ diff --git a/docs/images/platform/organization/organization-members-roles.png b/docs/images/platform/organization/organization-members-roles.png index 454af0809f..08c2d1e905 100644 Binary files a/docs/images/platform/organization/organization-members-roles.png and b/docs/images/platform/organization/organization-members-roles.png differ diff --git a/docs/images/platform/organization/organization-members.png b/docs/images/platform/organization/organization-members.png new file mode 100644 index 0000000000..a79d3bbe09 Binary files /dev/null and b/docs/images/platform/organization/organization-members.png differ diff --git a/docs/images/platform/organization/organization-settings-auth.png b/docs/images/platform/organization/organization-settings-auth.png index 8643c44da1..ca2340e9f2 100644 Binary files a/docs/images/platform/organization/organization-settings-auth.png and b/docs/images/platform/organization/organization-settings-auth.png differ diff --git a/docs/images/platform/project/project-environments.png b/docs/images/platform/project/project-environments.png new file mode 100644 index 0000000000..e468f2b82b Binary files /dev/null and b/docs/images/platform/project/project-environments.png differ diff --git a/docs/images/platform/secret-versioning.png b/docs/images/platform/secret-versioning.png new file mode 100644 index 0000000000..593e8c96f4 Binary files /dev/null and b/docs/images/platform/secret-versioning.png differ diff --git a/docs/images/sso/keycloak/client-mappers-by-configuration.png b/docs/images/sso/keycloak/client-mappers-by-configuration.png new file mode 100644 index 0000000000..9bebb422ee Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-by-configuration.png differ diff --git a/docs/images/sso/keycloak/client-mappers-completed.png b/docs/images/sso/keycloak/client-mappers-completed.png new file mode 100644 index 0000000000..38fb820061 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-completed.png differ diff --git a/docs/images/sso/keycloak/client-mappers-email.png b/docs/images/sso/keycloak/client-mappers-email.png new file mode 100644 index 0000000000..e1a369bab8 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-email.png differ diff --git a/docs/images/sso/keycloak/client-mappers-empty.png b/docs/images/sso/keycloak/client-mappers-empty.png new file mode 100644 index 0000000000..01ec1d3e60 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-empty.png differ diff --git a/docs/images/sso/keycloak/client-mappers-id.png b/docs/images/sso/keycloak/client-mappers-id.png new file mode 100644 index 0000000000..a45638b873 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-id.png differ diff --git a/docs/images/sso/keycloak/client-mappers-predefined.png b/docs/images/sso/keycloak/client-mappers-predefined.png new file mode 100644 index 0000000000..750d600b75 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-predefined.png differ diff --git a/docs/images/sso/keycloak/client-mappers-user-property.png b/docs/images/sso/keycloak/client-mappers-user-property.png new file mode 100644 index 0000000000..c854f95215 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-user-property.png differ diff --git a/docs/images/sso/keycloak/client-mappers-username.png b/docs/images/sso/keycloak/client-mappers-username.png new file mode 100644 index 0000000000..ff2a8fc394 Binary files /dev/null and b/docs/images/sso/keycloak/client-mappers-username.png differ diff --git a/docs/images/sso/keycloak/client-saml-capabilities.png b/docs/images/sso/keycloak/client-saml-capabilities.png new file mode 100644 index 0000000000..a4383628a5 Binary files /dev/null and b/docs/images/sso/keycloak/client-saml-capabilities.png differ diff --git a/docs/images/sso/keycloak/client-scopes-list.png b/docs/images/sso/keycloak/client-scopes-list.png new file mode 100644 index 0000000000..16f908af47 Binary files /dev/null and b/docs/images/sso/keycloak/client-scopes-list.png differ diff --git a/docs/images/sso/keycloak/client-signature-encryption.png b/docs/images/sso/keycloak/client-signature-encryption.png new file mode 100644 index 0000000000..b03b07a75f Binary files /dev/null and b/docs/images/sso/keycloak/client-signature-encryption.png differ diff --git a/docs/images/sso/keycloak/clients-list.png b/docs/images/sso/keycloak/clients-list.png new file mode 100644 index 0000000000..ad05b20048 Binary files /dev/null and b/docs/images/sso/keycloak/clients-list.png differ diff --git a/docs/images/sso/keycloak/create-client-general-settings.png b/docs/images/sso/keycloak/create-client-general-settings.png new file mode 100644 index 0000000000..866a920706 Binary files /dev/null and b/docs/images/sso/keycloak/create-client-general-settings.png differ diff --git a/docs/images/sso/keycloak/create-client-login-settings.png b/docs/images/sso/keycloak/create-client-login-settings.png new file mode 100644 index 0000000000..6fa8b4ce48 Binary files /dev/null and b/docs/images/sso/keycloak/create-client-login-settings.png differ diff --git a/docs/images/sso/keycloak/enable-saml.png b/docs/images/sso/keycloak/enable-saml.png new file mode 100644 index 0000000000..f66af968a1 Binary files /dev/null and b/docs/images/sso/keycloak/enable-saml.png differ diff --git a/docs/images/sso/keycloak/idp-values.png b/docs/images/sso/keycloak/idp-values.png new file mode 100644 index 0000000000..9de14f23b1 Binary files /dev/null and b/docs/images/sso/keycloak/idp-values.png differ diff --git a/docs/images/sso/keycloak/init-config.png b/docs/images/sso/keycloak/init-config.png new file mode 100644 index 0000000000..d500bb86e6 Binary files /dev/null and b/docs/images/sso/keycloak/init-config.png differ diff --git a/docs/images/sso/keycloak/org-security-section.png b/docs/images/sso/keycloak/org-security-section.png new file mode 100644 index 0000000000..bbbfb2d42f Binary files /dev/null and b/docs/images/sso/keycloak/org-security-section.png differ diff --git a/docs/images/sso/keycloak/realm-saml-metadata.png b/docs/images/sso/keycloak/realm-saml-metadata.png new file mode 100644 index 0000000000..c5ea5d497d Binary files /dev/null and b/docs/images/sso/keycloak/realm-saml-metadata.png differ diff --git a/docs/images/sso/keycloak/realm-settings-keys.png b/docs/images/sso/keycloak/realm-settings-keys.png new file mode 100644 index 0000000000..3add94290d Binary files /dev/null and b/docs/images/sso/keycloak/realm-settings-keys.png differ diff --git a/docs/integrations/cloud/aws-parameter-store.mdx b/docs/integrations/cloud/aws-parameter-store.mdx index 6fd3410188..d54458db9d 100644 --- a/docs/integrations/cloud/aws-parameter-store.mdx +++ b/docs/integrations/cloud/aws-parameter-store.mdx @@ -1,6 +1,6 @@ --- title: "AWS Parameter Store" -description: "How to sync secrets from Infisical to AWS Parameter Store" +description: "Learn how to sync secrets from Infisical to AWS Parameter Store." --- Prerequisites: diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index f761d5164d..0eab1a5617 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -1,6 +1,6 @@ --- title: "AWS Secrets Manager" -description: "How to sync secrets from Infisical to AWS Secrets Manager" +description: "Learn how to sync secrets from Infisical to AWS Secrets Manager." --- Prerequisites: @@ -59,7 +59,22 @@ Prerequisites: - Select which Infisical environment secrets you want to sync to which AWS Secrets Manager region and under which secret name. Then, press create integration to start syncing secrets to AWS Secrets Manager. + Select how you want to integration to work by specifying a number of parameters: + + + The environment in Infisical from which you want to sync secrets to AWS Secrets Manager. + + + The path within the preselected environment form which you want to sync secrets to AWS Secrets Manager. + + + The region that you want to integrate with in AWS Secrets Manager. + + + The secret name/path in AWS into which you want to sync the secrets from Infisical. + + + Then, press `Create Integration` to start syncing secrets to AWS Secrets Manager. ![integration create](../../images/integrations/aws/integrations-aws-secret-manager-create.png) @@ -69,5 +84,8 @@ Prerequisites: group environment variable key-pairs under multiple secrets for greater control. + + Please note that upon deleting secrets in Infisical, AWS Secrets Manager immediately makes the secrets inaccessible but only schedules them for deletion after at least 7 days. + - \ No newline at end of file + diff --git a/docs/integrations/frameworks/terraform.mdx b/docs/integrations/frameworks/terraform.mdx index 2643ca5af5..7d30ec0d9e 100644 --- a/docs/integrations/frameworks/terraform.mdx +++ b/docs/integrations/frameworks/terraform.mdx @@ -1,6 +1,6 @@ --- title: "Terraform" -description: "Fetch Secrets From Infisical With Terraform" +description: "Learn how to fetch Secrets From Infisical With Terraform." --- This guide provides step-by-step guidance on how to fetch secrets from Infisical using Terraform. diff --git a/docs/integrations/platforms/ansible.mdx b/docs/integrations/platforms/ansible.mdx index 2d524d55c4..ad95d0d5dc 100644 --- a/docs/integrations/platforms/ansible.mdx +++ b/docs/integrations/platforms/ansible.mdx @@ -1,6 +1,6 @@ --- title: "Ansible" -description: "How to use Infisical for secret management in Ansible" +description: "Learn how to use Infisical for secret management in Ansible." --- The documentation for using Infisical to manage secrets in Ansible is currently available [here](https://galaxy.ansible.com/ui/repo/published/infisical/vault/). diff --git a/docs/integrations/platforms/docker-compose.mdx b/docs/integrations/platforms/docker-compose.mdx index 238e3dd71c..1c061e04e0 100644 --- a/docs/integrations/platforms/docker-compose.mdx +++ b/docs/integrations/platforms/docker-compose.mdx @@ -1,6 +1,6 @@ --- title: "Docker Compose" -description: "How to use Infisical to inject environment variables into services defined in your Docker Compose file." +description: "Find out how to use Infisical to inject environment variables into services defined in your Docker Compose file." --- Prerequisites: diff --git a/docs/integrations/platforms/docker-intro.mdx b/docs/integrations/platforms/docker-intro.mdx index 2823fc8ca6..bec0f42138 100644 --- a/docs/integrations/platforms/docker-intro.mdx +++ b/docs/integrations/platforms/docker-intro.mdx @@ -1,6 +1,6 @@ --- title: "Docker" -description: "Learn how to feed secrets from Infisical into your Docker application" +description: "Learn how to feed secrets from Infisical into your Docker application." --- There are many methods to inject Infisical secrets into Docker-based applications. Regardless of the method you choose, they all inject secrets from Infisical as environment variables into your Docker container. diff --git a/docs/integrations/platforms/docker-pass-envs.mdx b/docs/integrations/platforms/docker-pass-envs.mdx index d6451de714..04cc36d6b5 100644 --- a/docs/integrations/platforms/docker-pass-envs.mdx +++ b/docs/integrations/platforms/docker-pass-envs.mdx @@ -1,6 +1,6 @@ --- title: "Docker Run" -description: "Pass secrets to your docker container at run time" +description: "Learn how to pass secrets to your docker container at run time." --- This method allows you to feed secrets from Infisical into your container using the `--env-file` flag of `docker run` command. diff --git a/docs/infisical-agent/guides/docker-swarm-with-agent.mdx b/docs/integrations/platforms/docker-swarm-with-agent.mdx similarity index 98% rename from docs/infisical-agent/guides/docker-swarm-with-agent.mdx rename to docs/integrations/platforms/docker-swarm-with-agent.mdx index 8ab4ca9625..30118a8f01 100644 --- a/docs/infisical-agent/guides/docker-swarm-with-agent.mdx +++ b/docs/integrations/platforms/docker-swarm-with-agent.mdx @@ -1,6 +1,6 @@ --- title: 'Docker Swarm' -description: "How to manage secrets in Docker Swarm services" +description: "Learn how to manage secrets in Docker Swarm services." --- In this guide, we'll demonstrate how to use Infisical for managing secrets within Docker Swarm. diff --git a/docs/integrations/platforms/docker.mdx b/docs/integrations/platforms/docker.mdx index 8fbf288adb..92bd57943c 100644 --- a/docs/integrations/platforms/docker.mdx +++ b/docs/integrations/platforms/docker.mdx @@ -1,6 +1,6 @@ --- title: "Docker Entrypoint" -description: "How to use Infisical to inject environment variables into a Docker container." +description: "Learn how to use Infisical to inject environment variables into a Docker container." --- This approach allows you to inject secrets from Infisical directly into your application. diff --git a/docs/integrations/platforms/ecs-with-agent.mdx b/docs/integrations/platforms/ecs-with-agent.mdx index bbb88ea645..43760d2bb8 100644 --- a/docs/integrations/platforms/ecs-with-agent.mdx +++ b/docs/integrations/platforms/ecs-with-agent.mdx @@ -1,6 +1,6 @@ --- title: 'Amazon ECS' -description: "How to deliver secrets to Amazon Elastic Container Service" +description: "Learn how to deliver secrets to Amazon Elastic Container Service." --- ![ecs diagram](/images/guides/agent-with-ecs/ecs-diagram.png) diff --git a/docs/infisical-agent/overview.mdx b/docs/integrations/platforms/infisical-agent.mdx similarity index 98% rename from docs/infisical-agent/overview.mdx rename to docs/integrations/platforms/infisical-agent.mdx index 9265d9bfe3..3b0bd0bb13 100644 --- a/docs/infisical-agent/overview.mdx +++ b/docs/integrations/platforms/infisical-agent.mdx @@ -1,12 +1,12 @@ --- -title: "Overview" +title: "Infisical Agent" description: "This page describes how to manage secrets using Infisical Agent." --- Infisical Agent is a client daemon that simplifies the adoption of Infisical by providing a more scalable and user-friendly approach for applications to interact with Infisical. It eliminates the need to modify application logic by enabling clients to decide how they want their secrets rendered through the use of templates. - +![agent diagram](/images/agent/infisical-agent-diagram.png) ### Key features: - Token renewal: Automatically authenticates with Infisical and deposits renewed access tokens at specified path for applications to consume diff --git a/docs/internals/components.mdx b/docs/internals/components.mdx index 29522b0bba..65506a500e 100644 --- a/docs/internals/components.mdx +++ b/docs/internals/components.mdx @@ -1,6 +1,6 @@ --- title: "Components" -description: "Infisical's components span multiple clients, an API, and a storage backend" +description: "Infisical's components span multiple clients, an API, and a storage backend." --- ## Infisical API diff --git a/docs/internals/flows.mdx b/docs/internals/flows.mdx index e18a37a31a..0da671d643 100644 --- a/docs/internals/flows.mdx +++ b/docs/internals/flows.mdx @@ -1,6 +1,6 @@ --- title: "Flows" -description: "Infisical's core flows have strong cryptographic underpinnings" +description: "Infisical's core flows have strong cryptographic underpinnings." --- ## Signup diff --git a/docs/internals/overview.mdx b/docs/internals/overview.mdx index e5c47682db..e64327a033 100644 --- a/docs/internals/overview.mdx +++ b/docs/internals/overview.mdx @@ -1,6 +1,6 @@ --- title: "Overview" -description: "How Infisical works under the hood" +description: "Read how Infisical works under the hood." --- This section covers the internals of Infisical including its technical underpinnings, architecture, and security properties. @@ -12,26 +12,26 @@ This section covers the internals of Infisical including its technical underpinn ## Learn More - - Learn about the fundamental parts of Infisical + + Learn about the fundamental parts of Infisical. - - Find out more about the structure of core user flows in Infisical + + Find out more about the structure of core user flows in Infisical. - Read about most common security-related topics and questions + Read about most common security-related topics and questions. - Learn best practices for utilizing Infisical service tokens + Learn best practices for utilizing Infisical service tokens. diff --git a/docs/internals/security.mdx b/docs/internals/security.mdx index b47f38251b..1b0fb9f325 100644 --- a/docs/internals/security.mdx +++ b/docs/internals/security.mdx @@ -1,6 +1,6 @@ --- title: "Security" -description: "Infisical's security model includes many considerations and initiatives" +description: "Infisical's security model includes many considerations and initiatives." --- Given that Infisical is a secret management platform that manages sensitive data, the Infisical security model is very important. diff --git a/docs/internals/service-tokens.mdx b/docs/internals/service-tokens.mdx index a04eccee70..3222b1aa8f 100644 --- a/docs/internals/service-tokens.mdx +++ b/docs/internals/service-tokens.mdx @@ -1,6 +1,6 @@ --- title: "Service tokens" -description: "Understanding service tokens and their best practices" +description: "Understanding service tokens and their best practices." --- ​ Many clients use service tokens to authenticate and read/write secrets from/to Infisical; they can be created in your project settings. diff --git a/docs/mint.json b/docs/mint.json index e0250136e4..03605cba20 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -8,26 +8,34 @@ }, "favicon": "/favicon.png", "colors": { - "primary": "#A1B659", - "light": "#E1EB55", + "primary": "#26272b", + "light": "#97b31d", "dark": "#A1B659", - "ultraLight": "#EFF4DD", + "ultraLight": "#E7F256", "ultraDark": "#8D9F4C", "background": { + "light": "#ffffff", "dark": "#0D1117" }, "anchors": { - "from": "#A1B659", - "to": "#F8B7BD" + "from": "#000000", + "to": "#707174" } }, + "modeToggle": { + "default": "light", + "isHidden": true + }, "feedback": { "suggestEdit": true, "raiseIssue": true, "thumbsRating": true }, "api": { - "baseUrl": ["https://app.infisical.com", "http://localhost:8080"] + "baseUrl": [ + "https://app.infisical.com", + "http://localhost:8080" + ] }, "topbarLinks": [ { @@ -41,8 +49,12 @@ }, "tabs": [ { - "name": "Changelog", - "url": "changelog" + "name": "Integrations", + "url": "integrations" + }, + { + "name": "CLI", + "url": "cli" }, { "name": "API Reference", @@ -53,52 +65,21 @@ "url": "sdks" }, { - "name": "Contributing", - "url": "contributing" - } - ], - "anchors": [ - - { - "name": "Contributing", - "icon": "code", - "url": "contributing" - }, - - { - "name": "Blog", - "icon": "newspaper", - "url": "https://infisical.com/blog" - }, - { - "name": "Slack", - "icon": "slack", - "url": "https://infisical.com/slack" - }, - { - "name": "GitHub", - "icon": "github", - "url": "https://github.com/Infisical/infisical" - }, - { - "name": "Internals", - "icon": "sitemap", - "url": "internals" + "name": "Changelog", + "url": "changelog" } ], "navigation": [ { - "group": "Overview", + "group": "Getting Started", "pages": [ + "documentation/getting-started/introduction", { - "group": "Getting Started", + "group": "Quickstart", "pages": [ - "documentation/getting-started/introduction", - "documentation/getting-started/platform", - "documentation/getting-started/sdks", - "integrations/platforms/kubernetes", - "integrations/platforms/docker-intro", - "documentation/getting-started/api" + "documentation/guides/local-development", + "documentation/guides/staging", + "documentation/guides/production" ] }, { @@ -118,21 +99,35 @@ "documentation/platform/organization", "documentation/platform/project", "documentation/platform/folder", - "documentation/platform/secret-reference", - "documentation/platform/webhooks", - "documentation/platform/pit-recovery", - "documentation/platform/audit-logs", + { + "group": "Secrets", + "pages": [ + "documentation/platform/secret-versioning", + "documentation/platform/pit-recovery", + "documentation/platform/secret-reference", + "documentation/platform/webhooks" + ] + }, { "group": "Identities", "pages": [ "documentation/platform/identities/overview", - "documentation/platform/identities/universal-auth" + "documentation/platform/identities/user-identities", + "documentation/platform/identities/machine-identities" + ] + }, + { + "group": "Access Control", + "pages": [ + "documentation/platform/access-controls/overview", + "documentation/platform/access-controls/role-based-access-controls", + "documentation/platform/access-controls/additional-privileges", + "documentation/platform/access-controls/temporary-access", + "documentation/platform/access-controls/access-requests", + "documentation/platform/pr-workflows", + "documentation/platform/audit-logs" ] }, - "documentation/platform/token", - "documentation/platform/mfa", - "documentation/platform/pr-workflows", - "documentation/platform/role-based-access-controls", { "group": "Secret Rotation", "pages": [ @@ -150,7 +145,16 @@ "documentation/platform/dynamic-secrets/postgresql" ] }, - "documentation/platform/groups", + "documentation/platform/groups" + ] + }, + { + "group": "Authentication Methods", + "pages": [ + "documentation/platform/auth-methods/email-password", + "documentation/platform/token", + "documentation/platform/identities/universal-auth", + "documentation/platform/mfa", { "group": "SSO", "pages": [ @@ -161,6 +165,7 @@ "documentation/platform/sso/okta", "documentation/platform/sso/azure", "documentation/platform/sso/jumpcloud", + "documentation/platform/sso/keycloak-saml", "documentation/platform/sso/google-saml" ] }, @@ -240,19 +245,6 @@ "cli/faq" ] }, - { - "group": "Agent", - "pages": [ - "infisical-agent/overview", - { - "group": "Use cases", - "pages": [ - "infisical-agent/guides/docker-swarm-with-agent", - "integrations/platforms/ecs-with-agent" - ] - } - ] - }, { "group": "Infrastructure Integrations", "pages": [ @@ -260,10 +252,11 @@ "group": "Container orchestrators", "pages": [ "integrations/platforms/kubernetes", - "infisical-agent/guides/docker-swarm-with-agent", + "integrations/platforms/docker-swarm-with-agent", "integrations/platforms/ecs-with-agent" ] }, + "integrations/platforms/infisical-agent", { "group": "Docker", "pages": [ @@ -287,19 +280,23 @@ "integrations/cloud/aws-secret-manager" ] }, - { - "group": "Digital Ocean", - "pages": ["integrations/cloud/digital-ocean-app-platform"] - }, "integrations/cloud/vercel", "integrations/cloud/azure-key-vault", "integrations/cloud/gcp-secret-manager", + { + "group": "Cloudflare", + "pages": [ + "integrations/cloud/cloudflare-pages", + "integrations/cloud/cloudflare-workers" + ] + }, + "integrations/cloud/heroku", + "integrations/cloud/render", { "group": "View more", "pages": [ - "integrations/cloud/heroku", + "integrations/cloud/digital-ocean-app-platform", "integrations/cloud/netlify", - "integrations/cloud/render", "integrations/cloud/railway", "integrations/cloud/flyio", "integrations/cloud/laravel-forge", @@ -307,8 +304,6 @@ "integrations/cloud/northflank", "integrations/cloud/hasura-cloud", "integrations/cloud/terraform-cloud", - "integrations/cloud/cloudflare-pages", - "integrations/cloud/cloudflare-workers", "integrations/cloud/qovery", "integrations/cloud/hashicorp-vault", "integrations/cloud/cloud-66", @@ -320,17 +315,17 @@ { "group": "CI/CD Integrations", "pages": [ - "integrations/cloud/teamcity", + "integrations/cicd/jenkins", "integrations/cicd/githubactions", "integrations/cicd/gitlab", + "integrations/cicd/bitbucket", + "integrations/cloud/teamcity", { "group": "View more", "pages": [ "integrations/cicd/circleci", "integrations/cicd/travisci", - "integrations/cicd/bitbucket", "integrations/cicd/codefresh", - "integrations/cicd/jenkins", "integrations/cloud/checkly" ] } @@ -366,14 +361,16 @@ }, { "group": "Build Tool Integrations", - "pages": ["integrations/build-tools/gradle"] + "pages": [ + "integrations/build-tools/gradle" + ] }, { - "group": "Overview", + "group": "", "pages": [ "sdks/overview" ] - }, + }, { "group": "SDK's", "pages": [ @@ -504,13 +501,41 @@ "api-reference/endpoints/secret-imports/delete" ] }, + { + "group": "Identity Specific Privilege", + "pages": [ + "api-reference/endpoints/identity-specific-privilege/create-permanent", + "api-reference/endpoints/identity-specific-privilege/create-temporary", + "api-reference/endpoints/identity-specific-privilege/update", + "api-reference/endpoints/identity-specific-privilege/delete", + "api-reference/endpoints/identity-specific-privilege/find-by-slug", + "api-reference/endpoints/identity-specific-privilege/list" + ] + }, + { + "group": "Integrations", + "pages": [ + "api-reference/endpoints/integrations/create-auth", + "api-reference/endpoints/integrations/list-auth", + "api-reference/endpoints/integrations/find-auth", + "api-reference/endpoints/integrations/delete-auth", + "api-reference/endpoints/integrations/delete-auth-by-id", + "api-reference/endpoints/integrations/create", + "api-reference/endpoints/integrations/update", + "api-reference/endpoints/integrations/delete" + ] + }, { "group": "Service Tokens", - "pages": ["api-reference/endpoints/service-tokens/get"] + "pages": [ + "api-reference/endpoints/service-tokens/get" + ] }, { "group": "Audit Logs", - "pages": ["api-reference/endpoints/audit-logs/export-audit-log"] + "pages": [ + "api-reference/endpoints/audit-logs/export-audit-log" + ] } ] }, @@ -524,23 +549,24 @@ "internals/service-tokens" ] }, - { - "group": "Overview", - "pages": ["changelog/overview"] - }, { "group": "", "pages": [ - { - "group": "Getting Started", - "pages": [ - "contributing/getting-started/overview", - "contributing/getting-started/code-of-conduct", - "contributing/getting-started/pull-requests", - "contributing/getting-started/faq" - - ] - }, + "changelog/overview" + ] + }, + { + "group": "Contributing", + "pages": [ + { + "group": "Getting Started", + "pages": [ + "contributing/getting-started/overview", + "contributing/getting-started/code-of-conduct", + "contributing/getting-started/pull-requests", + "contributing/getting-started/faq" + ] + }, { "group": "Contributing to platform", "pages": [ @@ -550,11 +576,11 @@ ] }, { - "group": "Contributing to SDK", - "pages": [ - "contributing/sdk/developing" - ] - } + "group": "Contributing to SDK", + "pages": [ + "contributing/sdk/developing" + ] + } ] } ], diff --git a/docs/sdks/overview.mdx b/docs/sdks/overview.mdx index d032311f2c..578e8ad0f3 100644 --- a/docs/sdks/overview.mdx +++ b/docs/sdks/overview.mdx @@ -1,5 +1,6 @@ --- -title: "Introduction" +title: "SDKs" +sidebarTitle: "Introduction" --- From local development to production, Infisical SDKs provide the easiest way for your app to fetch back secrets from Infisical on demand. diff --git a/docs/self-hosting/configuration/envars.mdx b/docs/self-hosting/configuration/envars.mdx index 4bb56ebcd2..dae2e80445 100644 --- a/docs/self-hosting/configuration/envars.mdx +++ b/docs/self-hosting/configuration/envars.mdx @@ -1,6 +1,6 @@ --- title: "Configurations" -description: "Configure environment variables for self-hosted Infisical" +description: "Read how to configure environment variables for self-hosted Infisical." --- diff --git a/docs/self-hosting/configuration/requirements.mdx b/docs/self-hosting/configuration/requirements.mdx index 262c7fb7cb..2e31ac8537 100644 --- a/docs/self-hosting/configuration/requirements.mdx +++ b/docs/self-hosting/configuration/requirements.mdx @@ -1,6 +1,6 @@ --- title: "Requirements" -description: "" +description: "Find out the minimal requirements for operating Infisical." --- This page details the minimum requirements necessary for installing and using Infisical. @@ -47,7 +47,7 @@ Recommended minimum memory hardware for different sizes of deployments: PostgreSQL is the only database supported by Infisical. Infisical has been extensively tested with Postgres version 16. We recommend using versions 14 and up for optimal compatibility. Recommended resource allocation based on deployment size: -- **small:** 1 vCPU / 2 GB RAM / 10 GB Disk +- **small:** 2 vCPU / 8 GB RAM / 20 GB Disk - **large:** 4vCPU / 16 GB RAM / 100 GB Disk ### Redis @@ -58,7 +58,7 @@ Redis requirements: - Use Redis versions 6.x or 7.x. We advise upgrading to at least Redis 6.2. - Redis Cluster mode is currently not supported; use Redis Standalone, with or without High Availability (HA). -- Redis storage needs are minimal: a setup with 1 vCPU, 1 GB RAM, and 1GB SSD will be sufficient for small deployments. +- Redis storage needs are minimal: a setup with 2 vCPU, 4 GB RAM, and 30GB SSD will be sufficient for small deployments. ## Supported Web Browsers diff --git a/docs/self-hosting/configuration/schema-migrations.mdx b/docs/self-hosting/configuration/schema-migrations.mdx index 6a94af751f..d0608c9c25 100644 --- a/docs/self-hosting/configuration/schema-migrations.mdx +++ b/docs/self-hosting/configuration/schema-migrations.mdx @@ -1,6 +1,6 @@ --- title: "Schema migration" -description: "Run Postgres schema migrations" +description: "Learn how to run Postgres schema migrations." --- Running schema migrations is a requirement before deploying Infisical. diff --git a/docs/self-hosting/deployment-options/docker-compose.mdx b/docs/self-hosting/deployment-options/docker-compose.mdx index 583fe16745..291a91370f 100644 --- a/docs/self-hosting/deployment-options/docker-compose.mdx +++ b/docs/self-hosting/deployment-options/docker-compose.mdx @@ -1,6 +1,6 @@ --- title: "Docker Compose" -description: "Run Infisical with Docker Compose template" +description: "Read how to run Infisical with Docker Compose template." --- Install Infisical using Docker compose. This self hosting method contains all of the required components needed to run a functional instance of Infisical. @@ -80,4 +80,4 @@ docker-compose -f docker-compose.prod.yml up Your Infisical instance should now be running on port `80`. To access your instance, visit `http://localhost:80`. -![self host sign up](images/self-hosting/applicable-to-all/selfhost-signup.png) \ No newline at end of file +![self host sign up](/images/self-hosting/applicable-to-all/selfhost-signup.png) \ No newline at end of file diff --git a/docs/self-hosting/deployment-options/kubernetes-helm.mdx b/docs/self-hosting/deployment-options/kubernetes-helm.mdx index 99d2e1d792..b95a3fd4d3 100644 --- a/docs/self-hosting/deployment-options/kubernetes-helm.mdx +++ b/docs/self-hosting/deployment-options/kubernetes-helm.mdx @@ -1,6 +1,6 @@ --- title: "Kubernetes via Helm Chart" -description: "Use Helm chart to install Infisical on your Kubernetes cluster" +description: "Learn how to use Helm chart to install Infisical on your Kubernetes cluster." --- **Prerequisites** - You have extensive understanding of [Kubernetes](https://kubernetes.io/) diff --git a/docs/self-hosting/deployment-options/standalone-infisical.mdx b/docs/self-hosting/deployment-options/standalone-infisical.mdx index 7407925780..ab15126127 100644 --- a/docs/self-hosting/deployment-options/standalone-infisical.mdx +++ b/docs/self-hosting/deployment-options/standalone-infisical.mdx @@ -1,6 +1,6 @@ --- title: "Docker" -description: "Run Infisical with Docker" +description: "Learn how to run Infisical with Docker." --- Prerequisites: @@ -52,7 +52,7 @@ The following guide provides a detailed step-by-step walkthrough on how you can Once the container is running, verify the installation by opening your web browser and navigating to `http://localhost:80`. - ![self host sign up](images/self-hosting/applicable-to-all/selfhost-signup.png) + ![self host sign up](/images/self-hosting/applicable-to-all/selfhost-signup.png) diff --git a/docs/self-hosting/ee.mdx b/docs/self-hosting/ee.mdx index 3ff9a794aa..a72bad908f 100644 --- a/docs/self-hosting/ee.mdx +++ b/docs/self-hosting/ee.mdx @@ -1,6 +1,6 @@ --- -title: "Infisical enterprise" -description: "How to activate Infisical Enterprise Edition (EE) features" +title: "Infisical Enterprise" +description: "Find out how to activate Infisical Enterprise edition (EE) features." --- While most features in Infisical are free to use, others are paid and require purchasing an enterprise license to use them. diff --git a/docs/self-hosting/faq.mdx b/docs/self-hosting/faq.mdx index 598d408ae2..db98a23dc6 100644 --- a/docs/self-hosting/faq.mdx +++ b/docs/self-hosting/faq.mdx @@ -1,10 +1,10 @@ --- title: "FAQ" -description: "Frequently Asked Questions about Infisical self hosting" +description: "Frequently Asked Questions about self-hosting Infisical." --- Frequently asked questions about self hosted instance of Infisical can be found on this page. -If you can't find the answer you are looking for, please create an issue on our GitHub repository or join our Slack channel for additional support. +If you can't find the answer you are looking for, please create an issue on our [GitHub repository](https://github.com/Infisical/infisical) or join our [Slack community](https://infisical.com/slack) for additional support. This issue is typically seen when you haven't set up SSL for your self hosted instance of Infisical. When SSL is not enabled, you can't receive secure cookies, preventing the session data to not be saved. diff --git a/docs/self-hosting/guides/mongo-to-postgres.mdx b/docs/self-hosting/guides/mongo-to-postgres.mdx index f8a6cca0fe..b8781a19db 100644 --- a/docs/self-hosting/guides/mongo-to-postgres.mdx +++ b/docs/self-hosting/guides/mongo-to-postgres.mdx @@ -1,6 +1,6 @@ --- title: "Migrate Mongo to Postgres" -description: "How to migrate from MongoDB to PostgreSQL for Infisical" +description: "Learn how to migrate Infisical from MongoDB to PostgreSQL." --- This guide will provide step by step instructions on migrating your Infisical instance running on MongoDB to the newly released PostgreSQL version of Infisical. diff --git a/docs/self-hosting/overview.mdx b/docs/self-hosting/overview.mdx index f8089719d6..ccc4ae912f 100644 --- a/docs/self-hosting/overview.mdx +++ b/docs/self-hosting/overview.mdx @@ -1,31 +1,35 @@ --- -title: "Introduction" -description: "Self-host Infisical on your own infrastructure" +title: "" +sidebarTitle: "Introduction" +description: "Learn how to self-host Infisical on your own infrastructure." --- Self-hosting Infisical lets you retain data on your own infrastructure and network. -Choose from a variety of deployment options listed below to get started. +Choose from a number of deployment options listed below to get started. - Use the fully packaged docker image to deploy Infisical anywhere + Use the fully packaged docker image to deploy Infisical anywhere. - Install Infisical using our Docker Compose template + Install Infisical using our Docker Compose template. - Use our Helm chart to Install Infisical on your Kubernetes cluster + Use our Helm chart to Install Infisical on your Kubernetes cluster. diff --git a/docs/style.css b/docs/style.css new file mode 100644 index 0000000000..6674ce2c67 --- /dev/null +++ b/docs/style.css @@ -0,0 +1,114 @@ +#navbar .max-w-8xl { + max-width: 100%; + border-bottom: 1px solid #ebebeb; + background-color: #fcfcfc; +} + +.max-w-8xl { + /* background-color: #f5f5f5; */ +} + +#sidebar { + left: 0; + padding-left: 48px; + padding-right: 30px; + border-right: 1px; + border-color: #cdd64b; + background-color: #fcfcfc; + border-right: 1px solid #ebebeb; +} + +#sidebar .relative .sticky { + opacity: 0; +} + +#sidebar li > div.mt-2 { + border-radius: 0; + padding: 5px; +} + +#sidebar li > a.mt-2 { + border-radius: 0; + padding: 5px; +} + +#sidebar li > a.leading-6 { + border-radius: 0; + padding: 0px; +} + +/* #sidebar ul > div.mt-12 { + padding-top: 30px; + position: relative; +} + +#sidebar ul > div.mt-12 h5 { + position: absolute; + left: -12px; + top: -0px; +} */ + +#header { + border-left: 1px solid #26272b; + padding-left: 16px; + padding-right: 16px; + background-color: #f5f5f5; + padding-bottom: 10px; + padding-top: 10px; +} + +#content-area .mt-8 .block{ + border-radius: 0; + border-width: 1px; + border-color: #ebebeb; +} + +#content-area div.my-4{ + border-radius: 0; + border-width: 1px; +} + +#content-area div.flex-1 { + /* text-transform: uppercase; */ + opacity: 0.8; + font-weight: 400; +} + +#content-area button { + border-radius: 0; +} + +#content-area .not-prose { + border-radius: 0; +} + +/* .eyebrow { + text-transform: uppercase; + font-weight: 400; + color: red; +} */ + +#content-container { + /* background-color: #f5f5f5; */ + margin-top: 2rem; +} + +#topbar-cta-button .group .absolute { + background-color: black; + border-radius: 0px; +} + +/* #topbar-cta-button .group .absolute:hover { + background-color: white; + border-radius: 0px; +} */ + +#topbar-cta-button .group .flex { + margin-top: 5px; + margin-bottom: 5px; + font-size: medium; +} + +.flex-1 .flex .items-center { + /* background-color: #f5f5f5; */ +} \ No newline at end of file diff --git a/frontend/src/components/v2/Tag/Tag.tsx b/frontend/src/components/v2/Tag/Tag.tsx index 5f3771f621..7210e388b6 100644 --- a/frontend/src/components/v2/Tag/Tag.tsx +++ b/frontend/src/components/v2/Tag/Tag.tsx @@ -20,6 +20,7 @@ const tagVariants = cva( green: "bg-primary-800 text-white" }, size: { + xs: "text-xs px-1 py-0.5", sm: "px-2 py-0.5" } } diff --git a/frontend/src/hooks/api/identities/types.ts b/frontend/src/hooks/api/identities/types.ts index fd3e886140..4ac19c3511 100644 --- a/frontend/src/hooks/api/identities/types.ts +++ b/frontend/src/hooks/api/identities/types.ts @@ -29,18 +29,30 @@ export type IdentityMembershipOrg = { export type IdentityMembership = { id: string; identity: Identity; - roles: { - id: string; - role: "owner" | "admin" | "member" | "no-access" | "custom"; - customRoleId: string; - customRoleName: string; - customRoleSlug: string; - isTemporary: boolean; - temporaryMode: string | null; - temporaryRange: string | null; - temporaryAccessStartTime: string | null; - temporaryAccessEndTime: string | null; - }[]; + roles: Array< + { + id: string; + role: "owner" | "admin" | "member" | "no-access" | "custom"; + customRoleId: string; + customRoleName: string; + customRoleSlug: string; + } & ( + | { + isTemporary: false; + temporaryRange: null; + temporaryMode: null; + temporaryAccessEndTime: null; + temporaryAccessStartTime: null; + } + | { + isTemporary: true; + temporaryRange: string; + temporaryMode: string; + temporaryAccessEndTime: string; + temporaryAccessStartTime: string; + } + ) + >; createdAt: string; updatedAt: string; }; diff --git a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/index.tsx b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/index.tsx new file mode 100644 index 0000000000..401a06a72b --- /dev/null +++ b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/index.tsx @@ -0,0 +1,7 @@ +export { + useCreateIdentityProjectAdditionalPrivilege, + useDeleteIdentityProjectAdditionalPrivilege, + useUpdateIdentityProjectAdditionalPrivilege +} from "./mutation"; +export { useGetIdentityProjectPrivilegeDetails } from "./queries"; +export type { TIdentityProjectPrivilege } from "./types"; diff --git a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/mutation.tsx b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/mutation.tsx new file mode 100644 index 0000000000..bb3f6ca88b --- /dev/null +++ b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/mutation.tsx @@ -0,0 +1,73 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { identitiyProjectPrivilegeKeys } from "./queries"; +import { + TCreateIdentityProjectPrivilegeDTO, + TDeleteIdentityProjectPrivilegeDTO, + TIdentityProjectPrivilege, + TUpdateIdentityProjectPrivlegeDTO +} from "./types"; + +export const useCreateIdentityProjectAdditionalPrivilege = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post( + "/api/v1/additional-privilege/identity/permanent", + dto + ); + return data.privilege; + }, + onSuccess: (_, { projectSlug, identityId }) => { + queryClient.invalidateQueries( + identitiyProjectPrivilegeKeys.list({ projectSlug, identityId }) + ); + } + }); +}; + +export const useUpdateIdentityProjectAdditionalPrivilege = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ privilegeSlug, projectSlug, identityId, privilegeDetails }) => { + const { data: res } = await apiRequest.patch("/api/v1/additional-privilege/identity", { + privilegeSlug, + projectSlug, + identityId, + privilegeDetails + }); + return res.privilege; + }, + onSuccess: (_, { projectSlug, identityId }) => { + queryClient.invalidateQueries( + identitiyProjectPrivilegeKeys.list({ projectSlug, identityId }) + ); + } + }); +}; + +export const useDeleteIdentityProjectAdditionalPrivilege = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ identityId, projectSlug, privilegeSlug }) => { + const { data } = await apiRequest.delete("/api/v1/additional-privilege/identity", { + data: { + identityId, + projectSlug, + privilegeSlug + } + }); + return data.privilege; + }, + onSuccess: (_, { projectSlug, identityId }) => { + queryClient.invalidateQueries( + identitiyProjectPrivilegeKeys.list({ projectSlug, identityId }) + ); + } + }); +}; diff --git a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/queries.tsx b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/queries.tsx new file mode 100644 index 0000000000..72534c1584 --- /dev/null +++ b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/queries.tsx @@ -0,0 +1,77 @@ +import { PackRule, unpackRules } from "@casl/ability/extra"; +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TProjectPermission } from "../roles/types"; +import { + TGetIdentityProejctPrivilegeDetails as TGetIdentityProjectPrivilegeDetails, + TIdentityProjectPrivilege, + TListIdentityUserPrivileges as TListIdentityProjectPrivileges +} from "./types"; + +export const identitiyProjectPrivilegeKeys = { + details: ({ identityId, privilegeSlug, projectSlug }: TGetIdentityProjectPrivilegeDetails) => + [ + "identity-user-privilege", + { + identityId, + projectSlug, + privilegeSlug + } + ] as const, + list: ({ projectSlug, identityId }: TListIdentityProjectPrivileges) => + ["identity-user-privileges", { identityId, projectSlug }] as const +}; + +export const useGetIdentityProjectPrivilegeDetails = ({ + projectSlug, + identityId, + privilegeSlug +}: TGetIdentityProjectPrivilegeDetails) => { + return useQuery({ + enabled: Boolean(projectSlug && identityId && privilegeSlug), + queryKey: identitiyProjectPrivilegeKeys.details({ projectSlug, privilegeSlug, identityId }), + queryFn: async () => { + const { + data: { privilege } + } = await apiRequest.get<{ + privilege: Omit & { permissions: unknown }; + }>(`/api/v1/additional-privilege/identity/${privilegeSlug}`, { + params: { + identityId, + projectSlug + } + }); + return { + ...privilege, + permissions: unpackRules(privilege.permissions as PackRule[]) + }; + } + }); +}; + +export const useListIdentityProjectPrivileges = ({ + projectSlug, + identityId +}: TListIdentityProjectPrivileges) => { + return useQuery({ + enabled: Boolean(projectSlug && identityId), + queryKey: identitiyProjectPrivilegeKeys.list({ projectSlug, identityId }), + queryFn: async () => { + const { + data: { privileges } + } = await apiRequest.get<{ + privileges: Array< + Omit & { permissions: unknown } + >; + }>("/api/v1/additional-privilege/identity", { + params: { identityId, projectSlug, unpacked: false } + }); + return privileges.map((el) => ({ + ...el, + permissions: unpackRules(el.permissions as PackRule[]) + })); + } + }); +}; diff --git a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx new file mode 100644 index 0000000000..fad549e38d --- /dev/null +++ b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx @@ -0,0 +1,64 @@ +import { TProjectPermission } from "../roles/types"; + +export enum IdentityProjectAdditionalPrivilegeTemporaryMode { + Relative = "relative" +} + +export type TIdentityProjectPrivilege = { + projectMembershipId: string; + slug: string; + id: string; + createdAt: Date; + updatedAt: Date; + permissions?: TProjectPermission[]; +} & ( + | { + isTemporary: true; + temporaryMode: string; + temporaryRange: string; + temporaryAccessStartTime: string; + temporaryAccessEndTime?: string; + } + | { + isTemporary: false; + temporaryMode?: null; + temporaryRange?: null; + temporaryAccessStartTime?: null; + temporaryAccessEndTime?: null; + } + ); + +export type TCreateIdentityProjectPrivilegeDTO = { + identityId: string; + projectSlug: string; + slug?: string; + isTemporary?: boolean; + temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode; + temporaryRange?: string; + temporaryAccessStartTime?: string; + permissions: TProjectPermission[]; +}; + +export type TUpdateIdentityProjectPrivlegeDTO = { + projectSlug: string; + identityId: string; + privilegeSlug: string; + privilegeDetails: Partial>; +}; + +export type TDeleteIdentityProjectPrivilegeDTO = { + projectSlug: string; + identityId: string; + privilegeSlug: string; +}; + +export type TListIdentityUserPrivileges = { + projectSlug: string; + identityId: string; +}; + +export type TGetIdentityProejctPrivilegeDetails = { + projectSlug: string; + identityId: string; + privilegeSlug: string; +}; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 23c4b89db9..b2df27f2da 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -7,12 +7,14 @@ export * from "./dynamicSecret"; export * from "./dynamicSecretLease"; export * from "./groups"; export * from "./identities"; +export * from "./identityProjectAdditionalPrivilege"; export * from "./incidentContacts"; export * from "./integrationAuth"; export * from "./integrations"; export * from "./keys"; export * from "./ldapConfig"; export * from "./organization"; +export * from "./projectUserAdditionalPrivilege"; export * from "./roles"; export * from "./scim"; export * from "./secretApproval"; diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/index.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/index.tsx new file mode 100644 index 0000000000..14a2c89805 --- /dev/null +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/index.tsx @@ -0,0 +1,7 @@ +export { + useCreateProjectUserAdditionalPrivilege, + useDeleteProjectUserAdditionalPrivilege, + useUpdateProjectUserAdditionalPrivilege +} from "./mutation"; +export { useGetProjectUserPrivilegeDetails, useListProjectUserPrivileges } from "./queries"; +export type { TProjectUserPrivilege } from "./types"; diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx new file mode 100644 index 0000000000..fb21a425e0 --- /dev/null +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/mutation.tsx @@ -0,0 +1,62 @@ +import { packRules } from "@casl/ability/extra"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { projectUserPrivilegeKeys } from "./queries"; +import { + TCreateProjectUserPrivilegeDTO, + TDeleteProjectUserPrivilegeDTO, + TProjectUserPrivilege, + TUpdateProjectUserPrivlegeDTO +} from "./types"; + +export const useCreateProjectUserAdditionalPrivilege = () => { + const queryClient = useQueryClient(); + + return useMutation<{ privilege: TProjectUserPrivilege }, {}, TCreateProjectUserPrivilegeDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.post("/api/v1/additional-privilege/users/permanent", { + ...dto, + permissions: packRules(dto.permissions) + }); + return data.privilege; + }, + onSuccess: (_, { projectMembershipId }) => { + queryClient.invalidateQueries(projectUserPrivilegeKeys.list(projectMembershipId)); + } + }); +}; + +export const useUpdateProjectUserAdditionalPrivilege = () => { + const queryClient = useQueryClient(); + + return useMutation<{ privilege: TProjectUserPrivilege }, {}, TUpdateProjectUserPrivlegeDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.patch( + `/api/v1/additional-privilege/users/${dto.privilegeId}`, + { ...dto, permissions: dto.permissions ? packRules(dto.permissions) : undefined } + ); + return data.privilege; + }, + onSuccess: (_, { projectMembershipId }) => { + queryClient.invalidateQueries(projectUserPrivilegeKeys.list(projectMembershipId)); + } + }); +}; + +export const useDeleteProjectUserAdditionalPrivilege = () => { + const queryClient = useQueryClient(); + + return useMutation<{ privilege: TProjectUserPrivilege }, {}, TDeleteProjectUserPrivilegeDTO>({ + mutationFn: async (dto) => { + const { data } = await apiRequest.delete( + `/api/v1/additional-privilege/users/${dto.privilegeId}` + ); + return data.privilege; + }, + onSuccess: (_, { projectMembershipId }) => { + queryClient.invalidateQueries(projectUserPrivilegeKeys.list(projectMembershipId)); + } + }); +}; diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx new file mode 100644 index 0000000000..41c9a0dcc2 --- /dev/null +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/queries.tsx @@ -0,0 +1,51 @@ +import { PackRule, unpackRules } from "@casl/ability/extra"; +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TProjectPermission } from "../roles/types"; +import { TProjectUserPrivilege } from "./types"; + +export const projectUserPrivilegeKeys = { + details: (privilegeId: string) => ["project-user-privilege", { privilegeId }] as const, + list: (projectMembershipId: string) => + ["project-user-privileges", { projectMembershipId }] as const +}; + +const fetchProjectUserPrivilegeDetails = async (privilegeId: string) => { + const { + data: { privilege } + } = await apiRequest.get<{ + privilege: Omit & { permissions: unknown }; + }>(`/api/v1/additional-privilege/users/${privilegeId}`); + return { + ...privilege, + permissions: unpackRules(privilege.permissions as PackRule[]) + }; +}; + +export const useGetProjectUserPrivilegeDetails = (privilegeId: string) => { + return useQuery({ + enabled: Boolean(privilegeId), + queryKey: projectUserPrivilegeKeys.details(privilegeId), + queryFn: () => fetchProjectUserPrivilegeDetails(privilegeId) + }); +}; + +export const useListProjectUserPrivileges = (projectMembershipId: string) => { + return useQuery({ + enabled: Boolean(projectMembershipId), + queryKey: projectUserPrivilegeKeys.list(projectMembershipId), + queryFn: async () => { + const { + data: { privileges } + } = await apiRequest.get<{ + privileges: Array & { permissions: unknown }>; + }>("/api/v1/additional-privilege/users", { params: { projectMembershipId } }); + return privileges.map((el) => ({ + ...el, + permissions: unpackRules(el.permissions as PackRule[]) + })); + } + }); +}; diff --git a/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx b/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx new file mode 100644 index 0000000000..b757a07ab4 --- /dev/null +++ b/frontend/src/hooks/api/projectUserAdditionalPrivilege/types.tsx @@ -0,0 +1,57 @@ +import { TProjectPermission } from "../roles/types"; + +export enum ProjectUserAdditionalPrivilegeTemporaryMode { + Relative = "relative" +} + +export type TProjectUserPrivilege = { + projectMembershipId: string; + slug: string; + id: string; + createdAt: Date; + updatedAt: Date; + permissions?: TProjectPermission[]; +} & ( + | { + isTemporary: true; + temporaryMode: string; + temporaryRange: string; + temporaryAccessStartTime: string; + temporaryAccessEndTime?: string; + } + | { + isTemporary: false; + temporaryMode?: null; + temporaryRange?: null; + temporaryAccessStartTime?: null; + temporaryAccessEndTime?: null; + } + ); + +export type TCreateProjectUserPrivilegeDTO = { + projectMembershipId: string; + slug?: string; + isTemporary?: boolean; + temporaryMode?: ProjectUserAdditionalPrivilegeTemporaryMode; + temporaryRange?: string; + temporaryAccessStartTime?: string; + permissions: TProjectPermission[]; +}; + +export type TUpdateProjectUserPrivlegeDTO = { + privilegeId: string; + projectMembershipId: string; +} & Partial>; + +export type TDeleteProjectUserPrivilegeDTO = { + privilegeId: string; + projectMembershipId: string; +}; + +export type TGetProjectUserPrivilegeDetails = { + privilegeId: string; +}; + +export type TListProjectUserPrivileges = { + projectMembershipId: string; +}; diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index edc3bdf9e1..845030e9fa 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -8,6 +8,7 @@ export enum AuthMethod { OKTA_SAML = "okta-saml", AZURE_SAML = "azure-saml", JUMPCLOUD_SAML = "jumpcloud-saml", + KEYCLOAK_SAML = "keycloak-saml", LDAP = "ldap" } @@ -76,18 +77,32 @@ export type TWorkspaceUser = { }; inviteEmail: string; organization: string; - roles: { - id: string; - role: "owner" | "admin" | "member" | "no-access" | "custom"; - customRoleId: string; - customRoleName: string; - customRoleSlug: string; - isTemporary: boolean; - temporaryMode: string | null; - temporaryRange: string | null; - temporaryAccessStartTime: string | null; - temporaryAccessEndTime: string | null; - }[]; + roles: ( + | { + id: string; + role: "owner" | "admin" | "member" | "no-access" | "custom"; + customRoleId: string; + customRoleName: string; + customRoleSlug: string; + isTemporary: false; + temporaryRange: null; + temporaryMode: null; + temporaryAccessEndTime: null; + temporaryAccessStartTime: null; + } + | { + id: string; + role: "owner" | "admin" | "member" | "no-access" | "custom"; + customRoleId: string; + customRoleName: string; + customRoleSlug: string; + isTemporary: true; + temporaryRange: string; + temporaryMode: string; + temporaryAccessEndTime: string; + temporaryAccessStartTime: string; + } + )[]; status: "invited" | "accepted" | "verified" | "completed"; deniedPermissions: any[]; }; diff --git a/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx b/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx index ae94837deb..b3eb5fa881 100644 --- a/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx +++ b/frontend/src/pages/integrations/aws-parameter-store/authorize.tsx @@ -109,6 +109,8 @@ export default function AWSParameterStoreAuthorizeIntegrationPage() { setAccessSecretKey(e.target.value)} /> diff --git a/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx b/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx index 975b98b0a8..c901d0a569 100644 --- a/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx +++ b/frontend/src/pages/integrations/aws-secret-manager/authorize.tsx @@ -108,6 +108,8 @@ export default function AWSSecretManagerCreateIntegrationPage() { > setAccessSecretKey(e.target.value)} /> diff --git a/frontend/src/pages/integrations/checkly/authorize.tsx b/frontend/src/pages/integrations/checkly/authorize.tsx index c15516200b..27951993d1 100644 --- a/frontend/src/pages/integrations/checkly/authorize.tsx +++ b/frontend/src/pages/integrations/checkly/authorize.tsx @@ -86,6 +86,8 @@ export default function ChecklyCreateIntegrationPage() { setAccessToken(e.target.value)} /> diff --git a/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx b/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx index 6495f00bdd..7f006ef30e 100644 --- a/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx +++ b/frontend/src/pages/integrations/cloudflare-pages/authorize.tsx @@ -59,7 +59,13 @@ export default function CloudflarePagesIntegrationPage() { isError={accessKeyErrorText !== "" ?? false} className="mx-6" > - setAccessKey(e.target.value)} /> + setAccessKey(e.target.value)} + /> - setAccessKey(e.target.value)} /> + setAccessKey(e.target.value)} + /> - + )} /> diff --git a/frontend/src/pages/integrations/qovery/authorize.tsx b/frontend/src/pages/integrations/qovery/authorize.tsx index 8927491326..1be9d17416 100644 --- a/frontend/src/pages/integrations/qovery/authorize.tsx +++ b/frontend/src/pages/integrations/qovery/authorize.tsx @@ -86,6 +86,8 @@ export default function QoveryCreateIntegrationPage() { setAccessToken(e.target.value)} /> diff --git a/frontend/src/pages/integrations/render/authorize.tsx b/frontend/src/pages/integrations/render/authorize.tsx index 6337b0d48c..4226d58993 100644 --- a/frontend/src/pages/integrations/render/authorize.tsx +++ b/frontend/src/pages/integrations/render/authorize.tsx @@ -83,7 +83,13 @@ export default function RenderCreateIntegrationPage() { isError={apiKeyErrorText !== "" ?? false} className="px-6" > - setApiKey(e.target.value)} /> + setApiKey(e.target.value)} + /> + )} + + + + + + + + + + + + + {isLoading && } + {!isLoading && + data && + data.length > 0 && + data.map((identityMember, index) => { + const { + identity: { id, name }, + roles, + createdAt + } = identityMember; + return ( + + + + + + + + ); + })} + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
NameRoleAdded on +
{name} +
+ {roles + .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE) + .map( + ({ + role, + customRoleName, + id: roleId, + isTemporary, + temporaryAccessEndTime + }) => { + const isExpired = + new Date() > new Date(temporaryAccessEndTime || ("" as string)); + return ( + +
+
+ {formatRoleName(role, customRoleName)} +
+ {isTemporary && ( +
+ + + +
+ )} +
+
+ ); + } + )} + {roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && ( + + + +{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE} + + + {roles + .slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE) + .map( + ({ + role, + customRoleName, + id: roleId, + isTemporary, + temporaryAccessEndTime + }) => { + const isExpired = + new Date() > + new Date(temporaryAccessEndTime || ("" as string)); + return ( + +
+
{formatRoleName(role, customRoleName)}
+ {isTemporary && ( +
+ + + new Date( + temporaryAccessEndTime as string + ) && "text-red-600" + )} + /> + +
+ )} +
+
+ ); + } + )} +
+
+ )} + + + handlePopUpOpen("updateRole", { ...identityMember, index }) + } + > + + + +
+
{format(new Date(createdAt), "yyyy-MM-dd")} + + {(isAllowed) => ( + { + handlePopUpOpen("deleteIdentity", { + identityId: id, + name + }); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + className="ml-4" + isDisabled={!isAllowed} + > + + + )} + +
+ +
+
+ handlePopUpToggle("updateRole", state)} + > + + + handlePopUpOpen("upgradePlan", { description }) + } + identityProjectMember={ + data?.[ + (popUp.updateRole?.data as IdentityMembership & { index: number })?.index + ] as IdentityMembership + } + /> + + + + handlePopUpToggle("deleteIdentity", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemoveIdentitySubmit( + (popUp?.deleteIdentity?.data as { identityId: string })?.identityId + ) + } + /> + + + ); + }, + { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity } +); diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityModal.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx similarity index 100% rename from frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityModal.tsx rename to frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityModal.tsx diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx new file mode 100644 index 0000000000..ca7e314643 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRbacSection.tsx @@ -0,0 +1,354 @@ +/* eslint-disable no-nested-ternary */ +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faCaretDown, faClock, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format, formatDistance } from "date-fns"; +import ms from "ms"; +import { twMerge } from "tailwind-merge"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + FormControl, + IconButton, + Input, + Popover, + PopoverContent, + PopoverTrigger, + Select, + SelectItem, + Spinner, + Tag, + Tooltip +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useSubscription, + useWorkspace +} from "@app/context"; +import { useGetProjectRoles, useUpdateIdentityWorkspaceRole } from "@app/hooks/api"; +import { IdentityMembership } from "@app/hooks/api/identities/types"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types"; + +const roleFormSchema = z.object({ + roles: z + .object({ + slug: z.string(), + temporaryAccess: z.discriminatedUnion("isTemporary", [ + z.object({ + isTemporary: z.literal(true), + temporaryRange: z.string().min(1), + temporaryAccessStartTime: z.string().datetime(), + temporaryAccessEndTime: z.string().datetime().nullable().optional() + }), + z.object({ + isTemporary: z.literal(false) + }) + ]) + }) + .array() +}); +type TRoleForm = z.infer; + +type Props = { + identityProjectMember: IdentityMembership; + onOpenUpgradeModal: (title: string) => void; +}; +export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal }: Props) => { + const { subscription } = useSubscription(); + const { currentWorkspace } = useWorkspace(); + const workspaceId = currentWorkspace?.id || ""; + const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); + const { permission } = useProjectPermission(); + const isMemberEditDisabled = permission.cannot( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Identity + ); + + const roleForm = useForm({ + resolver: zodResolver(roleFormSchema), + values: { + roles: identityProjectMember?.roles?.map(({ customRoleSlug, role, ...dto }) => ({ + slug: customRoleSlug || role, + temporaryAccess: dto.isTemporary + ? { + isTemporary: true, + temporaryRange: dto.temporaryRange, + temporaryAccessEndTime: dto.temporaryAccessEndTime, + temporaryAccessStartTime: dto.temporaryAccessStartTime + } + : { + isTemporary: dto.isTemporary + } + })) + } + }); + const selectedRoleList = useFieldArray({ + name: "roles", + control: roleForm.control + }); + + const formRoleField = roleForm.watch("roles"); + + const updateMembershipRole = useUpdateIdentityWorkspaceRole(); + + const handleRoleUpdate = async (data: TRoleForm) => { + if (updateMembershipRole.isLoading) return; + + const sanitizedRoles = data.roles.map((el) => { + const { isTemporary } = el.temporaryAccess; + if (!isTemporary) { + return { role: el.slug, isTemporary: false as const }; + } + return { + role: el.slug, + isTemporary: true as const, + temporaryMode: ProjectUserMembershipTemporaryMode.Relative, + temporaryRange: el.temporaryAccess.temporaryRange, + temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime + }; + }); + + const hasCustomRoleSelected = sanitizedRoles.some( + (el) => !Object.values(ProjectMembershipRole).includes(el.role as ProjectMembershipRole) + ); + + if (hasCustomRoleSelected && subscription && !subscription?.rbac) { + onOpenUpgradeModal( + "You can assign custom roles to members if you upgrade your Infisical plan." + ); + return; + } + + try { + await updateMembershipRole.mutateAsync({ + workspaceId, + identityId: identityProjectMember.identity.id, + roles: sanitizedRoles + }); + createNotification({ text: "Successfully updated roles", type: "success" }); + roleForm.reset(undefined, { keepValues: true }); + } catch (err) { + createNotification({ text: "Failed to update role", type: "error" }); + } + }; + + if (isRolesLoading) + return ( +
+ +
+ ); + + return ( +
+
Roles
+

Select one of the pre-defined or custom roles.

+
+
+
+ {selectedRoleList.fields.map(({ id }, index) => { + const { temporaryAccess } = formRoleField[index]; + const isTemporary = temporaryAccess?.isTemporary; + const isExpired = + temporaryAccess.isTemporary && + new Date() > new Date(temporaryAccess.temporaryAccessEndTime || ""); + + return ( +
+ ( + + )} + /> + + +
+ + + +
+
+ +
+
+ Configure timed access +
+ {isExpired && Expired} + ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+ + {temporaryAccess.isTemporary && ( + + )} +
+
+
+
+ { + if (selectedRoleList.fields.length > 1) { + selectedRoleList.remove(index); + } + }} + > + + +
+ ); + })} +
+
+ + {(isAllowed) => ( + + )} + + +
+
+
+
+ ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRoleForm.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRoleForm.tsx new file mode 100644 index 0000000000..e354f1015d --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/IdentityRoleForm.tsx @@ -0,0 +1,20 @@ +import { IdentityMembership } from "@app/hooks/api/identities/types"; + +import { IdentityRbacSection } from "./IdentityRbacSection"; +import { SpecificPrivilegeSection } from "./SpecificPrivilegeSection"; + +type Props = { + identityProjectMember: IdentityMembership; + onOpenUpgradeModal: (title: string) => void; +}; +export const IdentityRoleForm = ({ identityProjectMember, onOpenUpgradeModal }: Props) => { + return ( +
+ + +
+ ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx new file mode 100644 index 0000000000..83a6b6e455 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/SpecificPrivilegeSection.tsx @@ -0,0 +1,532 @@ +import { Controller, useForm } from "react-hook-form"; +import { + faArrowRotateLeft, + faCaretDown, + faCheck, + faClock, + faPlus, + faTrash +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format, formatDistance } from "date-fns"; +import ms from "ms"; +import { twMerge } from "tailwind-merge"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + Checkbox, + DeleteActionModal, + FormControl, + FormLabel, + IconButton, + Input, + Popover, + PopoverContent, + PopoverTrigger, + Select, + SelectItem, + Spinner, + Tag, + Tooltip +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { + TProjectUserPrivilege, + useCreateIdentityProjectAdditionalPrivilege, + useDeleteIdentityProjectAdditionalPrivilege, + useUpdateIdentityProjectAdditionalPrivilege +} from "@app/hooks/api"; +import { useListIdentityProjectPrivileges } from "@app/hooks/api/identityProjectAdditionalPrivilege/queries"; + +const secretPermissionSchema = z.object({ + secretPath: z.string().optional(), + environmentSlug: z.string(), + [ProjectPermissionActions.Edit]: z.boolean().optional(), + [ProjectPermissionActions.Read]: z.boolean().optional(), + [ProjectPermissionActions.Create]: z.boolean().optional(), + [ProjectPermissionActions.Delete]: z.boolean().optional(), + temporaryAccess: z.discriminatedUnion("isTemporary", [ + z.object({ + isTemporary: z.literal(true), + temporaryRange: z.string().min(1), + temporaryAccessStartTime: z.string().datetime(), + temporaryAccessEndTime: z.string().datetime().nullable().optional() + }), + z.object({ + isTemporary: z.literal(false) + }) + ]) +}); +type TSecretPermissionForm = z.infer; +const SpecificPrivilegeSecretForm = ({ + privilege, + identityId +}: { + privilege: TProjectUserPrivilege; + identityId: string; +}) => { + const { currentWorkspace } = useWorkspace(); + const projectSlug = currentWorkspace?.slug || ""; + + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "deletePrivilege" + ] as const); + const { permission } = useProjectPermission(); + const isMemberEditDisabled = permission.cannot( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Identity + ); + + const updateIdentityPrivilege = useUpdateIdentityProjectAdditionalPrivilege(); + const deleteIdentityPrivilege = useDeleteIdentityProjectAdditionalPrivilege(); + + const privilegeForm = useForm({ + resolver: zodResolver(secretPermissionSchema), + values: { + environmentSlug: privilege.permissions?.[0]?.conditions?.environment, + // secret path will be inside $glob operator + secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "", + read: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Read) + ), + edit: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Edit) + ), + create: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Create) + ), + delete: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Delete) + ), + // zod will pick it + temporaryAccess: privilege + } + }); + + const temporaryAccessField = privilegeForm.watch("temporaryAccess"); + const isTemporary = temporaryAccessField?.isTemporary; + const isExpired = + temporaryAccessField.isTemporary && + new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || ""); + + const handleUpdatePrivilege = async (data: TSecretPermissionForm) => { + if (updateIdentityPrivilege.isLoading) return; + try { + const actions = [ + { action: ProjectPermissionActions.Read, allowed: data.read }, + { action: ProjectPermissionActions.Create, allowed: data.create }, + { action: ProjectPermissionActions.Delete, allowed: data.delete }, + { action: ProjectPermissionActions.Edit, allowed: data.edit } + ]; + const conditions: Record = { environment: data.environmentSlug }; + if (data.secretPath) { + conditions.secretPath = { $glob: data.secretPath }; + } + await updateIdentityPrivilege.mutateAsync({ + privilegeDetails: { + ...data.temporaryAccess, + permissions: actions + .filter(({ allowed }) => allowed) + .map(({ action }) => ({ + action, + subject: [ProjectPermissionSub.Secrets], + conditions + })) + }, + privilegeSlug: privilege.slug, + identityId, + projectSlug + }); + createNotification({ + type: "success", + text: "Successfully updated privilege" + }); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to update privilege" + }); + } + }; + + const handleDeletePrivilege = async () => { + if (deleteIdentityPrivilege.isLoading) return; + try { + await deleteIdentityPrivilege.mutateAsync({ + identityId, + privilegeSlug: privilege.slug, + projectSlug + }); + createNotification({ + type: "success", + text: "Successfully deleted privilege" + }); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to delete privilege" + }); + } + }; + + const getAccessLabel = (exactTime = false) => { + if (isExpired) return "Access expired"; + if (!temporaryAccessField?.isTemporary) return "Permanent"; + if (exactTime) + return `Until ${format( + new Date(temporaryAccessField.temporaryAccessEndTime || ""), + "yyyy-MM-dd HH:mm:ss" + )}`; + return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); + }; + + return ( +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ ( +
+ + field.onChange(isChecked)} + /> +
+ )} + /> + ( +
+ + field.onChange(isChecked)} + /> +
+ )} + /> + ( +
+ + field.onChange(isChecked)} + /> +
+ )} + /> + ( +
+ + field.onChange(isChecked)} + /> +
+ )} + /> +
+
+ + +
+ + + +
+
+ +
+
+ Configure timed access +
+ {isExpired && Expired} + ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+ + {temporaryAccessField.isTemporary && ( + + )} +
+
+
+
+ {privilegeForm.formState.isDirty ? ( + <> + + privilegeForm.reset()} + > + + + + + + {privilegeForm.formState.isSubmitting ? ( + + ) : ( + + )} + + + + ) : ( + + handlePopUpOpen("deletePrivilege")} + > + + + + )} +
+
+
+ handlePopUpToggle("deletePrivilege", isOpen)} + deleteKey="delete" + onClose={() => handlePopUpClose("deletePrivilege")} + onDeleteApproved={handleDeletePrivilege} + /> +
+ ); +}; + +type Props = { + identityId: string; +}; + +export const SpecificPrivilegeSection = ({ identityId }: Props) => { + const { currentWorkspace } = useWorkspace(); + const projectSlug = currentWorkspace?.slug || ""; + const { data: identityPrivileges, isLoading } = useListIdentityProjectPrivileges({ + identityId, + projectSlug + }); + + const createIdentityPrivilege = useCreateIdentityProjectAdditionalPrivilege(); + + const handleCreatePrivilege = async () => { + if (createIdentityPrivilege.isLoading) return; + try { + await createIdentityPrivilege.mutateAsync({ + permissions: [ + { + action: ProjectPermissionActions.Read, + subject: [ProjectPermissionSub.Secrets], + conditions: { + environment: currentWorkspace?.environments?.[0].slug + } + } + ], + identityId, + projectSlug + }); + createNotification({ + type: "success", + text: "Successfully created privilege" + }); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create privilege" + }); + } + }; + + return ( +
+
+ Additional Privileges + {isLoading && } +
+

+ Select individual privileges to associate with the identity. +

+
+ {identityPrivileges + ?.filter(({ permissions }) => + permissions?.[0]?.subject?.includes(ProjectPermissionSub.Secrets) + ) + ?.map((privilege) => ( + + ))} +
+ + {(isAllowed) => ( + + )} + +
+ ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/index.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/index.tsx new file mode 100644 index 0000000000..f59675cb3d --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentityRoleForm/index.tsx @@ -0,0 +1 @@ +export { IdentityRoleForm } from "./IdentityRoleForm"; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityRoles.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityRoles.tsx deleted file mode 100644 index 6ff10cfa83..0000000000 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityRoles.tsx +++ /dev/null @@ -1,459 +0,0 @@ -import { useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { faCheck, faClock, faEdit, faSearch } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { twMerge } from "tailwind-merge"; -import { z } from "zod"; - -import { createNotification } from "@app/components/notifications"; -import { - Button, - Checkbox, - FormControl, - HoverCard, - HoverCardContent, - HoverCardTrigger, - IconButton, - Input, - Popover, - PopoverContent, - PopoverTrigger, - Spinner, - Tag, - Tooltip -} from "@app/components/v2"; -import { useWorkspace } from "@app/context"; -import { usePopUp } from "@app/hooks"; -import { useGetProjectRoles, useUpdateIdentityWorkspaceRole } from "@app/hooks/api"; -import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; -import { TWorkspaceUser } from "@app/hooks/api/types"; -import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types"; -import { groupBy } from "@app/lib/fn/array"; - -const temporaryRoleFormSchema = z.object({ - temporaryRange: z.string().min(1, "Required") -}); - -type TTemporaryRoleFormSchema = z.infer; - -type TTemporaryRoleFormProps = { - temporaryConfig?: { - isTemporary?: boolean; - temporaryAccessEndTime?: string | null; - temporaryAccessStartTime?: string | null; - temporaryRange?: string | null; - }; - onSetTemporary: (data: { temporaryRange: string; temporaryAccessStartTime?: string }) => void; - onRemoveTemporary: () => void; -}; - -const IdentityTemporaryRoleForm = ({ - temporaryConfig: defaultValues = {}, - onSetTemporary, - onRemoveTemporary -}: TTemporaryRoleFormProps) => { - const { popUp, handlePopUpToggle } = usePopUp(["setTempRole"] as const); - const { control, handleSubmit } = useForm({ - resolver: zodResolver(temporaryRoleFormSchema), - values: { - temporaryRange: defaultValues.temporaryRange || "1h" - } - }); - const isTemporaryFieldValue = defaultValues.isTemporary; - const isExpired = - isTemporaryFieldValue && new Date() > new Date(defaultValues.temporaryAccessEndTime || ""); - - return ( - { - handlePopUpToggle("setTempRole", isOpen); - }} - > - - - - - - - - -
-
- Set Role Temporarily -
- {isExpired && Expired} - ( - - 1m, 2h, 3d.{" "} - - More - - - } - > - - - )} - /> -
- {isTemporaryFieldValue && ( - - )} - {!isTemporaryFieldValue ? ( - - ) : ( - - )} -
-
-
-
- ); -}; - -const formSchema = z.record( - z.object({ - isChecked: z.boolean().optional(), - temporaryAccess: z.union([ - z.object({ - isTemporary: z.literal(true), - temporaryRange: z.string().min(1), - temporaryAccessStartTime: z.string().datetime(), - temporaryAccessEndTime: z.string().datetime().nullable().optional() - }), - z.boolean() - ]) - }) -); -type TForm = z.infer; - -export type TMemberRolesProp = { - disableEdit?: boolean; - identityId: string; - roles: TWorkspaceUser["roles"]; -}; - -const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; - -export const IdentityRoles = ({ - roles = [], - disableEdit = false, - identityId -}: TMemberRolesProp) => { - const { currentWorkspace } = useWorkspace(); - - const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const); - const [searchRoles, setSearchRoles] = useState(""); - - const { - handleSubmit, - control, - reset, - setValue, - formState: { isSubmitting, isDirty } - } = useForm({ - resolver: zodResolver(formSchema) - }); - - const workspaceId = currentWorkspace?.id || ""; - - const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); - const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role); - - const updateIdentityWorkspaceRole = useUpdateIdentityWorkspaceRole(); - - const handleRoleUpdate = async (data: TForm) => { - const selectedRoles = Object.keys(data) - .filter((el) => Boolean(data[el].isChecked)) - .map((el) => { - const isTemporary = Boolean(data[el].temporaryAccess); - if (!isTemporary) { - return { role: el, isTemporary: false as const }; - } - - const tempCfg = data[el].temporaryAccess as { - temporaryRange: string; - temporaryAccessStartTime: string; - }; - - return { - role: el, - isTemporary: true as const, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: tempCfg.temporaryRange, - temporaryAccessStartTime: tempCfg.temporaryAccessStartTime - }; - }); - - try { - await updateIdentityWorkspaceRole.mutateAsync({ - workspaceId, - identityId, - roles: selectedRoles - }); - createNotification({ text: "Successfully updated identity role", type: "success" }); - handlePopUpToggle("editRole"); - setSearchRoles(""); - } catch (err) { - createNotification({ text: "Failed to update identity role", type: "error" }); - } - }; - - const formatRoleName = (role: string, customRoleName?: string) => { - if (role === ProjectMembershipRole.Custom) return customRoleName; - if (role === ProjectMembershipRole.Member) return "Developer"; - return role; - }; - - return ( -
- {roles - .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE) - .map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => { - const isExpired = new Date() > new Date(temporaryAccessEndTime || ("" as string)); - return ( - -
-
{formatRoleName(role, customRoleName)}
- {isTemporary && ( -
- - - -
- )} -
-
- ); - })} - {roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && ( - - - +{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE} - - - {roles - .slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE) - .map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => { - const isExpired = new Date() > new Date(temporaryAccessEndTime || ("" as string)); - return ( - -
-
{formatRoleName(role, customRoleName)}
- {isTemporary && ( -
- - new Date(temporaryAccessEndTime as string) && - "text-red-600" - )} - /> - -
- )} -
-
- ); - })}{" "} -
-
- )} -
- { - handlePopUpToggle("editRole", isOpen); - reset(); - }} - > - {!disableEdit && ( - - - - - - )} - - {isRolesLoading ? ( -
- -
- ) : ( -
-
- {projectRoles - ?.filter( - ({ name, slug }) => - name.toLowerCase().includes(searchRoles.toLowerCase()) || - slug.toLowerCase().includes(searchRoles.toLowerCase()) - ) - ?.map(({ id, name, slug }) => { - const userProjectRoleDetails = userRolesGroupBySlug?.[slug]?.[0]; - - return ( -
-
- ( - { - field.onChange(isChecked); - setValue(`${slug}.temporaryAccess`, false); - }} - > - {name} - - )} - /> -
-
- ( - { - setValue(`${slug}.isChecked`, true, { shouldDirty: true }); - console.log(data); - field.onChange({ isTemporary: true, ...data }); - }} - onRemoveTemporary={() => { - setValue(`${slug}.isChecked`, false, { shouldDirty: true }); - field.onChange(false); - }} - /> - )} - /> -
-
- ); - })} -
-
-
- setSearchRoles(el.target.value)} - leftIcon={} - placeholder="Search roles.." - /> -
-
- -
-
-
- )} -
-
-
-
- ); -}; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentitySection.tsx deleted file mode 100644 index 4410bc6391..0000000000 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentitySection.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import Link from "next/link"; -import { faArrowUpRightFromSquare, faPlus } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; - -import { createNotification } from "@app/components/notifications"; -import { ProjectPermissionCan } from "@app/components/permissions"; -import { Button, DeleteActionModal } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; -import { withProjectPermission } from "@app/hoc"; -import { useDeleteIdentityFromWorkspace } from "@app/hooks/api"; -import { usePopUp } from "@app/hooks/usePopUp"; - -import { IdentityModal } from "./IdentityModal"; -import { IdentityTable } from "./IdentityTable"; - -export const IdentitySection = withProjectPermission( - () => { - - const { currentWorkspace } = useWorkspace(); - - const workspaceId = currentWorkspace?.id ?? ""; - - const { mutateAsync: deleteMutateAsync } = useDeleteIdentityFromWorkspace(); - - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ - "identity", - "deleteIdentity", - "upgradePlan" - ] as const); - - const onRemoveIdentitySubmit = async (identityId: string) => { - try { - await deleteMutateAsync({ - identityId, - workspaceId - }); - - createNotification({ - text: "Successfully removed identity from project", - type: "success" - }); - - handlePopUpClose("deleteIdentity"); - } catch (err) { - console.error(err); - const error = err as any; - const text = error?.response?.data?.message ?? "Failed to remove identity from project"; - - createNotification({ - text, - type: "error" - }); - } - }; - - return ( -
-
-

Identities

-
- - - Documentation{" "} - - - -
- - {(isAllowed) => ( - - )} - -
- - - handlePopUpToggle("deleteIdentity", isOpen)} - deleteKey="confirm" - onDeleteApproved={() => - onRemoveIdentitySubmit( - (popUp?.deleteIdentity?.data as { identityId: string })?.identityId - ) - } - /> -
- ); - }, - { action: ProjectPermissionActions.Read, subject: ProjectPermissionSub.Identity } -); diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityTable.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityTable.tsx deleted file mode 100644 index 24197e9fec..0000000000 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/IdentityTable.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { faServer, faXmark } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { format } from "date-fns"; - -import { ProjectPermissionCan } from "@app/components/permissions"; -import { - EmptyState, - IconButton, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr -} from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; -import { useGetWorkspaceIdentityMemberships } from "@app/hooks/api"; -import { UsePopUpState } from "@app/hooks/usePopUp"; - -import { IdentityRoles } from "./IdentityRoles"; - -type Props = { - handlePopUpOpen: ( - popUpName: keyof UsePopUpState<["deleteIdentity", "identity"]>, - data?: { - identityId?: string; - name?: string; - } - ) => void; -}; - -export const IdentityTable = ({ handlePopUpOpen }: Props) => { - const { currentWorkspace } = useWorkspace(); - const { data, isLoading } = useGetWorkspaceIdentityMemberships(currentWorkspace?.id || ""); - - return ( - - - - - - - - - - - {isLoading && } - {!isLoading && - data && - data.length > 0 && - data.map(({ identity: { id, name }, roles, createdAt }) => { - return ( - - - - - - - ); - })} - {!isLoading && data && data?.length === 0 && ( - - - - )} - -
NameRoleAdded on -
{name} - - {(isAllowed) => ( - - )} - - {format(new Date(createdAt), "yyyy-MM-dd")} - - {(isAllowed) => ( - { - handlePopUpOpen("deleteIdentity", { - identityId: id, - name - }); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - className="ml-4" - isDisabled={!isAllowed} - > - - - )} - -
- -
-
- ); -}; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/index.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/index.tsx deleted file mode 100644 index f0663a6a57..0000000000 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/IdentitySection/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { IdentitySection } from "./IdentitySection"; diff --git a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/index.tsx b/frontend/src/views/Project/MembersPage/components/IdentityTab/components/index.tsx deleted file mode 100644 index f0663a6a57..0000000000 --- a/frontend/src/views/Project/MembersPage/components/IdentityTab/components/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { IdentitySection } from "./IdentitySection"; diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx index 342b3b9203..b59286577d 100644 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberListTab.tsx @@ -2,9 +2,18 @@ import { useMemo, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import Link from "next/link"; -import { faMagnifyingGlass, faPlus, faUsers, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { + faClock, + faEdit, + faMagnifyingGlass, + faPlus, + faUsers, + faXmark +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; +import { motion } from "framer-motion"; +import { twMerge } from "tailwind-merge"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; @@ -14,6 +23,9 @@ import { DeleteActionModal, EmptyState, FormControl, + HoverCard, + HoverCardContent, + HoverCardTrigger, IconButton, Input, Modal, @@ -23,10 +35,12 @@ import { Table, TableContainer, TableSkeleton, + Tag, TBody, Td, Th, THead, + Tooltip, Tr, UpgradePlanModal } from "@app/components/v2"; @@ -46,9 +60,11 @@ import { useGetUserWsKey, useGetWorkspaceUsers } from "@app/hooks/api"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { TWorkspaceUser } from "@app/hooks/api/types"; import { ProjectVersion } from "@app/hooks/api/workspace/types"; -import { MemberRoles } from "./MemberRoles"; +import { MemberRoleForm } from "./MemberRoleForm"; const addMemberFormSchema = z.object({ orgMembershipId: z.string().trim() @@ -56,8 +72,15 @@ const addMemberFormSchema = z.object({ type TAddMemberForm = z.infer; +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 MemberListTab = () => { - const { t } = useTranslation(); const { currentOrg } = useOrganization(); @@ -77,7 +100,8 @@ export const MemberListTab = () => { const { handlePopUpToggle, popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ "addMember", "removeMember", - "upgradePlan" + "upgradePlan", + "updateRole" ] as const); const { @@ -186,7 +210,14 @@ export const MemberListTab = () => { }, [orgUsers, members]); return ( -
+

Members

@@ -223,7 +254,8 @@ export const MemberListTab = () => { {isMembersLoading && } {!isMembersLoading && - filterdUsers?.map(({ user: u, inviteEmail, id: membershipId, roles }) => { + filterdUsers?.map((projectMember, index) => { + const { user: u, inviteEmail, id: membershipId, roles } = projectMember; const name = u ? `${u.firstName} ${u.lastName}` : "-"; const email = u?.email || inviteEmail; @@ -232,44 +264,136 @@ export const MemberListTab = () => { {name} {email} - - {(isAllowed) => ( - - handlePopUpOpen("upgradePlan", { description }) +
+ {roles + .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE) + .map( + ({ + role, + customRoleName, + id, + isTemporary, + temporaryAccessEndTime + }) => { + const isExpired = + new Date() > new Date(temporaryAccessEndTime || ("" as string)); + return ( + +
+
+ {formatRoleName(role, customRoleName)} +
+ {isTemporary && ( +
+ + + +
+ )} +
+
+ ); } - membershipId={membershipId} - /> + )} + {roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && ( + + + +{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE} + + + {roles + .slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE) + .map( + ({ + role, + customRoleName, + id, + isTemporary, + temporaryAccessEndTime + }) => { + const isExpired = + new Date() > + new Date(temporaryAccessEndTime || ("" as string)); + return ( + +
+
{formatRoleName(role, customRoleName)}
+ {isTemporary && ( +
+ + + new Date( + temporaryAccessEndTime as string + ) && "text-red-600" + )} + /> + +
+ )} +
+
+ ); + } + )} +
+
)} - + {userId !== u?.id && ( + + + handlePopUpOpen("updateRole", { ...projectMember, index }) + } + > + + + + )} +
{userId !== u?.id && ( - - {(isAllowed) => ( - - handlePopUpOpen("removeMember", { username: u.username }) - } - > - - - )} - +
+ + {(isAllowed) => ( + + handlePopUpOpen("removeMember", { username: u.username }) + } + > + + + )} + +
)} @@ -343,6 +467,27 @@ export const MemberListTab = () => { )} + handlePopUpToggle("updateRole", state)} + > + + handlePopUpOpen("upgradePlan", { description })} + projectMember={ + filterdUsers?.[ + (popUp.updateRole?.data as TWorkspaceUser & { index: number })?.index + ] as TWorkspaceUser + } + /> + + { onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} text={(popUp.upgradePlan?.data as { description: string })?.description} /> -
+
); }; diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx new file mode 100644 index 0000000000..04497c62c7 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRbacSection.tsx @@ -0,0 +1,351 @@ +/* eslint-disable no-nested-ternary */ +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { faCaretDown, faClock, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format, formatDistance } from "date-fns"; +import ms from "ms"; +import { twMerge } from "tailwind-merge"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + FormControl, + IconButton, + Input, + Popover, + PopoverContent, + PopoverTrigger, + Select, + SelectItem, + Spinner, + Tag, + Tooltip +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useSubscription, + useWorkspace +} from "@app/context"; +import { useGetProjectRoles, useUpdateUserWorkspaceRole } from "@app/hooks/api"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { TWorkspaceUser } from "@app/hooks/api/types"; +import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types"; + +const roleFormSchema = z.object({ + roles: z + .object({ + slug: z.string(), + temporaryAccess: z.discriminatedUnion("isTemporary", [ + z.object({ + isTemporary: z.literal(true), + temporaryRange: z.string().min(1), + temporaryAccessStartTime: z.string().datetime(), + temporaryAccessEndTime: z.string().datetime().nullable().optional() + }), + z.object({ + isTemporary: z.literal(false) + }) + ]) + }) + .array() +}); +type TRoleForm = z.infer; + +type Props = { + projectMember: TWorkspaceUser; + onOpenUpgradeModal: (title: string) => void; +}; +export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props) => { + const { subscription } = useSubscription(); + const { currentWorkspace } = useWorkspace(); + const workspaceId = currentWorkspace?.id || ""; + const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); + const { permission } = useProjectPermission(); + const isMemberEditDisabled = permission.cannot( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Member + ); + + const roleForm = useForm({ + resolver: zodResolver(roleFormSchema), + values: { + roles: projectMember?.roles?.map(({ customRoleSlug, role, ...dto }) => ({ + slug: customRoleSlug || role, + temporaryAccess: dto.isTemporary + ? { + isTemporary: true, + temporaryRange: dto.temporaryRange, + temporaryAccessEndTime: dto.temporaryAccessEndTime, + temporaryAccessStartTime: dto.temporaryAccessStartTime + } + : { + isTemporary: dto.isTemporary + } + })) + } + }); + const selectedRoleList = useFieldArray({ + name: "roles", + control: roleForm.control + }); + + const formRoleField = roleForm.watch("roles"); + + const updateMembershipRole = useUpdateUserWorkspaceRole(); + + const handleRoleUpdate = async (data: TRoleForm) => { + if (updateMembershipRole.isLoading) return; + + const sanitizedRoles = data.roles.map((el) => { + const { isTemporary } = el.temporaryAccess; + if (!isTemporary) { + return { role: el.slug, isTemporary: false as const }; + } + return { + role: el.slug, + isTemporary: true as const, + temporaryMode: ProjectUserMembershipTemporaryMode.Relative, + temporaryRange: el.temporaryAccess.temporaryRange, + temporaryAccessStartTime: el.temporaryAccess.temporaryAccessStartTime + }; + }); + + const hasCustomRoleSelected = sanitizedRoles.some( + (el) => !Object.values(ProjectMembershipRole).includes(el.role as ProjectMembershipRole) + ); + + if (hasCustomRoleSelected && subscription && !subscription?.rbac) { + onOpenUpgradeModal( + "You can assign custom roles to members if you upgrade your Infisical plan." + ); + return; + } + + try { + await updateMembershipRole.mutateAsync({ + workspaceId, + membershipId: projectMember.id, + roles: sanitizedRoles + }); + createNotification({ text: "Successfully updated roles", type: "success" }); + roleForm.reset(undefined, { keepValues: true }); + } catch (err) { + createNotification({ text: "Failed to update role", type: "error" }); + } + }; + + if (isRolesLoading) + return ( +
+ +
+ ); + + return ( +
+
Roles
+

Select one of the pre-defined or custom roles.

+
+
+
+ {selectedRoleList.fields.map(({ id }, index) => { + const { temporaryAccess } = formRoleField[index]; + const isTemporary = temporaryAccess?.isTemporary; + const isExpired = + temporaryAccess.isTemporary && + new Date() > new Date(temporaryAccess.temporaryAccessEndTime || ""); + + return ( +
+ ( + + )} + /> + + +
+ + + +
+
+ +
+
+ Configure timed access +
+ {isExpired && Expired} + ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+ + {temporaryAccess.isTemporary && ( + + )} +
+
+
+
+ { + if (selectedRoleList.fields.length > 1) { + selectedRoleList.remove(index); + } + }} + > + + +
+ ); + })} +
+
+ + {(isAllowed) => ( + + )} + + +
+
+
+
+ ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRoleForm.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRoleForm.tsx new file mode 100644 index 0000000000..da6243281a --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/MemberRoleForm.tsx @@ -0,0 +1,20 @@ +import { TWorkspaceUser } from "@app/hooks/api/types"; + +import { MemberRbacSection } from "./MemberRbacSection"; +import { SpecificPrivilegeSection } from "./SpecificPrivilegeSection"; + +type Props = { + projectMember: TWorkspaceUser; + onOpenUpgradeModal: (title: string) => void; +}; +export const MemberRoleForm = ({ projectMember, onOpenUpgradeModal }: Props) => { + return ( +
+ + +
+ ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx new file mode 100644 index 0000000000..2df959a94d --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/SpecificPrivilegeSection.tsx @@ -0,0 +1,514 @@ +import { Controller, useForm } from "react-hook-form"; +import { + faArrowRotateLeft, + faCaretDown, + faCheck, + faClock, + faPlus, + faTrash +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format, formatDistance } from "date-fns"; +import ms from "ms"; +import { twMerge } from "tailwind-merge"; +import { z } from "zod"; + +import { TtlFormLabel } from "@app/components/features"; +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Button, + Checkbox, + DeleteActionModal, + FormControl, + FormLabel, + IconButton, + Input, + Popover, + PopoverContent, + PopoverTrigger, + Select, + SelectItem, + Spinner, + Tag, + Tooltip +} from "@app/components/v2"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { + TProjectUserPrivilege, + useCreateProjectUserAdditionalPrivilege, + useDeleteProjectUserAdditionalPrivilege, + useListProjectUserPrivileges, + useUpdateProjectUserAdditionalPrivilege +} from "@app/hooks/api"; + +const secretPermissionSchema = z.object({ + secretPath: z.string().optional(), + environmentSlug: z.string(), + [ProjectPermissionActions.Edit]: z.boolean().optional(), + [ProjectPermissionActions.Read]: z.boolean().optional(), + [ProjectPermissionActions.Create]: z.boolean().optional(), + [ProjectPermissionActions.Delete]: z.boolean().optional(), + temporaryAccess: z.discriminatedUnion("isTemporary", [ + z.object({ + isTemporary: z.literal(true), + temporaryRange: z.string().min(1), + temporaryAccessStartTime: z.string().datetime(), + temporaryAccessEndTime: z.string().datetime().nullable().optional() + }), + z.object({ + isTemporary: z.literal(false) + }) + ]) +}); +type TSecretPermissionForm = z.infer; +const SpecificPrivilegeSecretForm = ({ privilege }: { privilege: TProjectUserPrivilege }) => { + const { currentWorkspace } = useWorkspace(); + const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([ + "deletePrivilege" + ] as const); + const { permission } = useProjectPermission(); + const isMemberEditDisabled = permission.cannot( + ProjectPermissionActions.Edit, + ProjectPermissionSub.Member + ); + + const updateUserPrivilege = useUpdateProjectUserAdditionalPrivilege(); + const deleteUserPrivilege = useDeleteProjectUserAdditionalPrivilege(); + + const privilegeForm = useForm({ + resolver: zodResolver(secretPermissionSchema), + values: { + environmentSlug: privilege.permissions?.[0]?.conditions?.environment, + // secret path will be inside $glob operator + secretPath: privilege.permissions?.[0]?.conditions?.secretPath?.$glob || "", + read: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Read) + ), + edit: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Edit) + ), + create: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Create) + ), + delete: privilege.permissions?.some(({ action }) => + action.includes(ProjectPermissionActions.Delete) + ), + // zod will pick it + temporaryAccess: privilege + } + }); + + const temporaryAccessField = privilegeForm.watch("temporaryAccess"); + const isTemporary = temporaryAccessField?.isTemporary; + const isExpired = + temporaryAccessField.isTemporary && + new Date() > new Date(temporaryAccessField.temporaryAccessEndTime || ""); + + const handleUpdatePrivilege = async (data: TSecretPermissionForm) => { + if (updateUserPrivilege.isLoading) return; + try { + const actions = [ + { action: ProjectPermissionActions.Read, allowed: data.read }, + { action: ProjectPermissionActions.Create, allowed: data.create }, + { action: ProjectPermissionActions.Delete, allowed: data.delete }, + { action: ProjectPermissionActions.Edit, allowed: data.edit } + ]; + const conditions: Record = { environment: data.environmentSlug }; + if (data.secretPath) { + conditions.secretPath = { $glob: data.secretPath }; + } + await updateUserPrivilege.mutateAsync({ + privilegeId: privilege.id, + ...data.temporaryAccess, + permissions: actions + .filter(({ allowed }) => allowed) + .map(({ action }) => ({ + action, + subject: [ProjectPermissionSub.Secrets], + conditions + })), + projectMembershipId: privilege.projectMembershipId + }); + createNotification({ + type: "success", + text: "Successfully updated privilege" + }); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to update privilege" + }); + } + }; + + const handleDeletePrivilege = async () => { + if (deleteUserPrivilege.isLoading) return; + try { + await deleteUserPrivilege.mutateAsync({ + privilegeId: privilege.id, + projectMembershipId: privilege.projectMembershipId + }); + createNotification({ + type: "success", + text: "Successfully deleted privilege" + }); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to delete privilege" + }); + } + }; + + const getAccessLabel = (exactTime = false) => { + if (isExpired) return "Access expired"; + if (!temporaryAccessField?.isTemporary) return "Permanent"; + if (exactTime) + return `Until ${format( + new Date(temporaryAccessField.temporaryAccessEndTime || ""), + "yyyy-MM-dd HH:mm:ss" + )}`; + return formatDistance(new Date(temporaryAccessField.temporaryAccessEndTime || ""), new Date()); + }; + + return ( +
+
+
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ ( +
+ + field.onChange(isChecked)} + /> +
+ )} + /> + ( +
+ + field.onChange(isChecked)} + /> +
+ )} + /> + ( +
+ + field.onChange(isChecked)} + /> +
+ )} + /> + ( +
+ + field.onChange(isChecked)} + /> +
+ )} + /> +
+
+ + +
+ + + +
+
+ +
+
+ Configure timed access +
+ {isExpired && Expired} + ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+ + {temporaryAccessField.isTemporary && ( + + )} +
+
+
+
+ {privilegeForm.formState.isDirty ? ( + <> + + privilegeForm.reset()} + > + + + + + + {privilegeForm.formState.isSubmitting ? ( + + ) : ( + + )} + + + + ) : ( + + handlePopUpOpen("deletePrivilege")} + > + + + + )} +
+
+
+ handlePopUpToggle("deletePrivilege", isOpen)} + deleteKey="delete" + onClose={() => handlePopUpClose("deletePrivilege")} + onDeleteApproved={handleDeletePrivilege} + /> +
+ ); +}; + +type Props = { + membershipId: string; +}; + +export const SpecificPrivilegeSection = ({ membershipId }: Props) => { + const { data: userPrivileges, isLoading } = useListProjectUserPrivileges(membershipId); + const { currentWorkspace } = useWorkspace(); + + const createUserPrivilege = useCreateProjectUserAdditionalPrivilege(); + + const handleCreatePrivilege = async () => { + if (createUserPrivilege.isLoading) return; + try { + await createUserPrivilege.mutateAsync({ + permissions: [ + { + action: ProjectPermissionActions.Read, + subject: [ProjectPermissionSub.Secrets], + conditions: { + environment: currentWorkspace?.environments?.[0].slug + } + } + ], + projectMembershipId: membershipId + }); + createNotification({ + type: "success", + text: "Successfully created privilege" + }); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create privilege" + }); + } + }; + + return ( +
+
+ Additional Privileges + {isLoading && } +
+

+ Select individual privileges to associate with the user. +

+
+ {userPrivileges + ?.filter(({ permissions }) => + permissions?.[0]?.subject?.includes(ProjectPermissionSub.Secrets) + ) + ?.map((privilege) => ( + + ))} +
+ + {(isAllowed) => ( + + )} + +
+ ); +}; diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/index.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/index.tsx new file mode 100644 index 0000000000..765b4061c9 --- /dev/null +++ b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoleForm/index.tsx @@ -0,0 +1 @@ +export { MemberRoleForm } from "./MemberRoleForm"; diff --git a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoles.tsx b/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoles.tsx deleted file mode 100644 index 50d8cd0d93..0000000000 --- a/frontend/src/views/Project/MembersPage/components/MemberListTab/MemberRoles.tsx +++ /dev/null @@ -1,471 +0,0 @@ -import { useState } from "react"; -import { Controller, useForm } from "react-hook-form"; -import { faCheck, faClock, faEdit, faSearch } from "@fortawesome/free-solid-svg-icons"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { twMerge } from "tailwind-merge"; -import { z } from "zod"; - -import { createNotification } from "@app/components/notifications"; -import { - Button, - Checkbox, - FormControl, - HoverCard, - HoverCardContent, - HoverCardTrigger, - IconButton, - Input, - Popover, - PopoverContent, - PopoverTrigger, - Spinner, - Tag, - Tooltip -} from "@app/components/v2"; -import { useSubscription, useWorkspace } from "@app/context"; -import { usePopUp } from "@app/hooks"; -import { useGetProjectRoles, useUpdateUserWorkspaceRole } from "@app/hooks/api"; -import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; -import { TWorkspaceUser } from "@app/hooks/api/types"; -import { ProjectUserMembershipTemporaryMode } from "@app/hooks/api/workspace/types"; -import { groupBy } from "@app/lib/fn/array"; - -const temporaryRoleFormSchema = z.object({ - temporaryRange: z.string().min(1, "Required") -}); - -type TTemporaryRoleFormSchema = z.infer; - -type TTemporaryRoleFormProps = { - temporaryConfig?: { - isTemporary?: boolean; - temporaryAccessEndTime?: string | null; - temporaryAccessStartTime?: string | null; - temporaryRange?: string | null; - }; - onSetTemporary: (data: { temporaryRange: string; temporaryAccessStartTime?: string }) => void; - onRemoveTemporary: () => void; -}; - -const TemporaryRoleForm = ({ - temporaryConfig: defaultValues = {}, - onSetTemporary, - onRemoveTemporary -}: TTemporaryRoleFormProps) => { - const { popUp, handlePopUpToggle } = usePopUp(["setTempRole"] as const); - const { control, handleSubmit } = useForm({ - resolver: zodResolver(temporaryRoleFormSchema), - values: { - temporaryRange: defaultValues.temporaryRange || "1h" - } - }); - const isTemporaryFieldValue = defaultValues.isTemporary; - const isExpired = - isTemporaryFieldValue && new Date() > new Date(defaultValues.temporaryAccessEndTime || ""); - - return ( - { - handlePopUpToggle("setTempRole", isOpen); - }} - > - - - - - - - - -
-
- Configure timed access -
- {isExpired && Expired} - ( - - 1m, 2h, 3d.{" "} - - More - - - } - > - - - )} - /> -
- {isTemporaryFieldValue && ( - - )} - {!isTemporaryFieldValue ? ( - - ) : ( - - )} -
-
-
-
- ); -}; - -const formSchema = z.record( - z.object({ - isChecked: z.boolean().optional(), - temporaryAccess: z.union([ - z.object({ - isTemporary: z.literal(true), - temporaryRange: z.string().min(1), - temporaryAccessStartTime: z.string().datetime(), - temporaryAccessEndTime: z.string().datetime().nullable().optional() - }), - z.boolean() - ]) - }) -); -type TForm = z.infer; - -export type TMemberRolesProp = { - disableEdit?: boolean; - membershipId: string; - onOpenUpgradeModal: (description: string) => void; - roles: TWorkspaceUser["roles"]; -}; - -const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2; - -export const MemberRoles = ({ - roles = [], - disableEdit = false, - membershipId, - onOpenUpgradeModal -}: TMemberRolesProp) => { - const { currentWorkspace } = useWorkspace(); - - const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const); - const [searchRoles, setSearchRoles] = useState(""); - const { subscription } = useSubscription(); - - const { - handleSubmit, - control, - reset, - setValue, - formState: { isSubmitting, isDirty } - } = useForm({ - resolver: zodResolver(formSchema) - }); - - const workspaceId = currentWorkspace?.id || ""; - - const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId); - const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role); - - const updateMembershipRole = useUpdateUserWorkspaceRole(); - - const handleRoleUpdate = async (data: TForm) => { - const selectedRoles = Object.keys(data) - .filter((el) => Boolean(data[el].isChecked)) - .map((el) => { - const isTemporary = Boolean(data[el].temporaryAccess); - if (!isTemporary) { - return { role: el, isTemporary: false as const }; - } - - const tempCfg = data[el].temporaryAccess as { - temporaryRange: string; - temporaryAccessStartTime: string; - }; - - return { - role: el, - isTemporary: true as const, - temporaryMode: ProjectUserMembershipTemporaryMode.Relative, - temporaryRange: tempCfg.temporaryRange, - temporaryAccessStartTime: tempCfg.temporaryAccessStartTime - }; - }); - - const hasCustomRoleSelected = selectedRoles.some( - (el) => !Object.values(ProjectMembershipRole).includes(el.role as ProjectMembershipRole) - ); - - if (hasCustomRoleSelected && subscription && !subscription?.rbac) { - onOpenUpgradeModal( - "You can assign custom roles to members if you upgrade your Infisical plan." - ); - return; - } - - try { - await updateMembershipRole.mutateAsync({ - workspaceId, - membershipId, - roles: selectedRoles - }); - createNotification({ text: "Successfully updated role", type: "success" }); - handlePopUpToggle("editRole"); - setSearchRoles(""); - } catch (err) { - createNotification({ text: "Failed to update role", type: "error" }); - } - }; - - const formatRoleName = (role: string, customRoleName?: string) => { - if (role === ProjectMembershipRole.Custom) return customRoleName; - if (role === ProjectMembershipRole.Member) return "Developer"; - return role; - }; - - return ( -
- {roles - .slice(0, MAX_ROLES_TO_BE_SHOWN_IN_TABLE) - .map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => { - const isExpired = new Date() > new Date(temporaryAccessEndTime || ("" as string)); - return ( - -
-
{formatRoleName(role, customRoleName)}
- {isTemporary && ( -
- - - -
- )} -
-
- ); - })} - {roles.length > MAX_ROLES_TO_BE_SHOWN_IN_TABLE && ( - - - +{roles.length - MAX_ROLES_TO_BE_SHOWN_IN_TABLE} - - - {roles - .slice(MAX_ROLES_TO_BE_SHOWN_IN_TABLE) - .map(({ role, customRoleName, id, isTemporary, temporaryAccessEndTime }) => { - const isExpired = new Date() > new Date(temporaryAccessEndTime || ("" as string)); - return ( - -
-
{formatRoleName(role, customRoleName)}
- {isTemporary && ( -
- - new Date(temporaryAccessEndTime as string) && - "text-red-600" - )} - /> - -
- )} -
-
- ); - })}{" "} -
-
- )} -
- { - handlePopUpToggle("editRole", isOpen); - reset(); - }} - > - {!disableEdit && ( - - - - - - )} - - {isRolesLoading ? ( -
- -
- ) : ( -
-
- {projectRoles - ?.filter( - ({ name, slug }) => - name.toLowerCase().includes(searchRoles.toLowerCase()) || - slug.toLowerCase().includes(searchRoles.toLowerCase()) - ) - ?.map(({ id, name, slug }) => { - const userProjectRoleDetails = userRolesGroupBySlug?.[slug]?.[0]; - - return ( -
-
- ( - { - field.onChange(isChecked); - setValue(`${slug}.temporaryAccess`, false); - }} - > - {name} - - )} - /> -
-
- ( - { - setValue(`${slug}.isChecked`, true, { shouldDirty: true }); - console.log(data); - field.onChange({ isTemporary: true, ...data }); - }} - onRemoveTemporary={() => { - setValue(`${slug}.isChecked`, false, { shouldDirty: true }); - field.onChange(false); - }} - /> - )} - /> -
-
- ); - })} -
-
-
- setSearchRoles(el.target.value)} - leftIcon={} - placeholder="Search roles.." - /> -
-
- -
-
-
- )} -
-
-
-
- ); -}; diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/MultiEnvProjectPermission.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/MultiEnvProjectPermission.tsx index db18822f4c..188d81c65b 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/MultiEnvProjectPermission.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/MultiEnvProjectPermission.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react"; -import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form"; +import { Control, Controller, UseFormGetValues, UseFormSetValue, useWatch } from "react-hook-form"; import { IconProp } from "@fortawesome/fontawesome-svg-core"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { motion } from "framer-motion"; @@ -28,6 +28,7 @@ type Props = { formName: "secrets"; isNonEditable?: boolean; setValue: UseFormSetValue; + getValue: UseFormGetValues; control: Control; title: string; subtitle: string; @@ -44,6 +45,7 @@ enum Permission { export const MultiEnvProjectPermission = ({ isNonEditable, setValue, + getValue, control, formName, title, @@ -69,9 +71,12 @@ export const MultiEnvProjectPermission = ({ const handlePermissionChange = (val: Permission) => { switch (val) { - case Permission.NoAccess: - setValue(`permissions.${formName}`, undefined, { shouldDirty: true }); + case Permission.NoAccess: { + const permissions = getValue("permissions"); + if (permissions) delete permissions[formName]; + setValue("permissions", permissions, { shouldDirty: true }); break; + } case Permission.FullAccess: setValue( `permissions.${formName}`, @@ -101,7 +106,7 @@ export const MultiEnvProjectPermission = ({ className={twMerge( "rounded-md bg-mineshaft-800 px-10 py-6", (selectedPermissionCategory !== Permission.NoAccess || isCustom) && - "border-l-2 border-primary-600" + "border-l-2 border-primary-600" )} >
diff --git a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx index d7cc885660..4a57e28109 100644 --- a/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx +++ b/frontend/src/views/Project/MembersPage/components/ProjectRoleListTab/components/ProjectRoleModifySection/ProjectRoleModifySection.tsx @@ -134,6 +134,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => { register, formState: { isSubmitting, isDirty, errors }, setValue, + getValues, control } = useForm({ defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {}, @@ -232,6 +233,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
val !== "custom", { message: "Cannot use custom as its a keyword" }), permissions: z .object({ diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx index 13869b3f2d..f670ea4968 100644 --- a/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/views/SecretMainPage/components/ActionBar/ActionBar.tsx @@ -416,7 +416,7 @@ export const ActionBar = ({ {Object.keys(selectedSecrets).length} Selected
{ className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4" onSubmit={handleSubmit(onFormSubmit)} > -
-
- Allow user sign up +
+
+ Allow user signups +
+
+ Select if you want users to be able to signup freely into your Infisical instance.
{ />
{signupMode === "anyone" && ( -
-
- Restrict sign up by email domain(s) +
+
+ Restrict signup by email domain(s)