Compare commits

..

95 Commits

Author SHA1 Message Date
72f50ec399 Merge pull request #1654 from Infisical/fix-additional-privilege-slug
Move default slug init for users/identities out of fastify schema
2024-04-04 12:22:31 -07:00
effc7a3627 Move default slug init for users/identities out of fastify schema 2024-04-04 12:18:10 -07:00
510c91cef1 Update infisical-agent.mdx 2024-04-04 09:34:31 -07:00
9be5d89fcf added docs images 2024-04-03 22:55:47 -07:00
94f4497903 update access request docs 2024-04-03 22:51:48 -07:00
b5af5646ee Merge pull request #1653 from Infisical/pentest
Add separate rate limit to invite user to org
2024-04-03 18:48:24 -07:00
1554618167 Add separate rate limit to invite user 2024-04-03 18:46:29 -07:00
5fbfcdda30 Merge pull request #1651 from Infisical/daniel/cli-secrets-get-fix
Fix: CLI get secrets by name
2024-04-03 10:43:42 -07:00
cdbb3b9c47 Update secrets.go 2024-04-03 10:36:25 -07:00
0042a95b21 update docs image 2024-04-03 09:08:53 -07:00
53233e05d4 Merge pull request #1648 from Infisical/keycloak
Add documentation + option for Keycloak SAML (self-hosted)
2024-04-02 16:42:41 -07:00
4f15f9c8d3 Add support for keycloak saml on self-hosted infisical 2024-04-02 16:35:37 -07:00
97223fabe6 Merge pull request #1617 from Infisical/daniel/improve-create-project
Feat: Recursively get all secrets from all folders in specified path
2024-04-02 13:50:16 -07:00
04b312cbe4 Merge pull request #1646 from akhilmhdh/fix/disable-role-button
fix(ui): resolved multi role modal button hiding clickable
2024-04-02 13:06:57 -07:00
97e5069cf5 Merge pull request #1645 from akhilmhdh/chore/specific-privilege-api-doc
docs: added api reference for specific privilege identity
2024-04-02 12:03:48 -07:00
93146fcd96 fix(ui): resolved multi role modal button hiding clickable 2024-04-03 00:12:34 +05:30
87d98de4c1 docs: added api reference for specific privilege identity 2024-04-02 23:54:51 +05:30
26f647b948 Merge pull request #1644 from akhilmhdh/chore/aws-ssm-api
AWS SSM integration api documentation
2024-04-02 09:22:50 -07:00
80b3cdd128 add examples to integration auth docs 2024-04-02 09:21:32 -07:00
8dd85a0d65 Update requirements.mdx 2024-04-02 07:07:36 -07:00
17995d301a feat(doc): added integration and integration auth to api reference doc 2024-04-02 16:30:53 +05:30
094b48a2b1 feat(server): updated integration and integration auth with description 2024-04-02 16:29:41 +05:30
7b8bfe38f0 Merge pull request #1574 from akhilmhdh/feat/additional-privilege
feat: additional privilege for users and identity
2024-04-01 11:09:05 -07:00
9903f7c4a0 feat: fixed wrong permission type in bulk api op 2024-04-01 23:34:25 +05:30
42cd98d4d9 feat: changed update patch function to privilegeDetails for identity privilege 2024-04-01 23:13:08 +05:30
4b203e9ad3 Update postgresql.mdx 2024-04-01 10:26:25 -07:00
36bf1b2abc Fix: Renamed deep parameter to recursive 2024-04-01 10:10:49 -07:00
42fb732955 Fix: Renamed deep parameter to recursive 2024-04-01 10:10:34 -07:00
da2dcb347a Fix: Restructured recursive path functions as suggested by Akhil 2024-04-01 09:58:13 -07:00
b9482966cf Fix: Replaced merge with extend as proposed by Akhil 2024-04-01 09:52:49 -07:00
1e4b4591ed fix images in docs 2024-04-01 09:15:04 -07:00
4a325d6d96 fix image links 2024-04-01 00:08:20 -07:00
5e20573110 fix docs eyebrow 2024-03-31 23:44:35 -07:00
f623c8159d documentation revamp 2024-03-31 23:37:57 -07:00
4323407da7 Update introduction.mdx 2024-03-30 08:43:10 -04:00
4c496d5e3d Update secret-service.ts 2024-03-30 08:40:43 +01:00
d68dc4c3e0 add type=password to integration api keys 2024-03-29 16:50:18 -07:00
e64c579dfd update aws sm docs 2024-03-29 16:39:51 -07:00
d0c0d5835c feat: splitted privilege create route into two for permanent and temp to get params shape in api doc 2024-03-30 01:31:17 +05:30
af2dcdd0c7 feat: updated api description and changed slug to privilege slug 2024-03-29 23:51:26 +05:30
6c628a7265 Update aws-secret-manager.mdx 2024-03-29 10:49:10 -07:00
00f2d40803 feat(ui): changed back to relative time distance with tooltip of detailed time 2024-03-29 22:36:46 +05:30
0a66cbe729 updated docs 2024-03-29 00:10:29 -07:00
7fec7c9bf5 update docs 2024-03-28 23:11:16 -07:00
d1afec4f9a inject secrets from secret imports into integrations 2024-03-28 20:13:46 -07:00
31ad6b0c86 update style 2024-03-28 18:17:59 -07:00
e46256f45b feat: added description for all api endpoints 2024-03-28 19:55:12 +05:30
64e868a151 feat(ui): updated ui with identity privilege hooks and new role form 2024-03-28 19:55:12 +05:30
c8cbcaf10c feat(server): added identity privilege route changes with project slug 2024-03-28 19:55:12 +05:30
51716336c2 feat(ui): updated ui with new role form for users 2024-03-28 19:55:12 +05:30
6b51c7269a feat(server): removed name and description and fixed api for user privileges 2024-03-28 19:55:12 +05:30
f551a4158d feat: resolved upstream rebase conflict 2024-03-28 19:55:12 +05:30
e850b82fb3 improved admin dashboard UI 2024-03-28 19:55:12 +05:30
8f85f292db feat: improved slug with a default generator for ui and server 2024-03-28 19:55:12 +05:30
5f84de039f feat(ui): finished ui for identity additional privilege 2024-03-28 19:55:12 +05:30
8529fac098 feat(server): completed identity additional privilege 2024-03-28 19:55:12 +05:30
81cf19cb4a feat(ui): completed ui for user additional privilege 2024-03-28 19:54:00 +05:30
edbe1c8eae feat(ui): hook for new user additional privilege 2024-03-28 19:54:00 +05:30
a5039494cd feat(server): completed routes for user additional privilege 2024-03-28 19:54:00 +05:30
a908471e66 feat(server): completed user additional privilege services 2024-03-28 19:54:00 +05:30
84204c3c37 feat(server): added new user additional migration and schemas 2024-03-28 19:54:00 +05:30
4931e8579c fix image link 2024-03-27 23:13:07 -07:00
20dc243fd9 Merge pull request #1634 from Infisical/maintenanceMode
add maintenance mode
2024-03-27 21:23:51 -04:00
785a1389d9 add maintenance mode 2024-03-27 21:19:21 -04:00
5a3fc3568a fix typo for maintenance 2024-03-27 18:55:27 -04:00
497601e398 Update overview.mdx 2024-03-27 15:59:04 -04:00
8db019d2fe update dynamic secret doc 2024-03-27 13:53:41 -04:00
07d1d91110 Merge pull request #1631 from akhilmhdh/fix/dyn-superuser-remove
fix(server): resolved failing to use dynamic secret due to superuser
2024-03-27 11:19:51 -04:00
bb506fff9f remove assign statement 2024-03-27 11:11:10 -04:00
7a561bcbdf feat(server): moved dynamic secret to ee 2024-03-27 15:00:16 +05:30
8784f80fc1 fix(ui): updated error message on create dynamic secret 2024-03-27 14:25:56 +05:30
0793e70c26 fix(server): resolved failing to use dynamic secret due to superuser 2024-03-27 14:25:39 +05:30
99f8799ff4 Merge branch 'main' of https://github.com/Infisical/infisical 2024-03-26 22:55:57 -07:00
3f05c8b7ae updated dynamic secrets docs 2024-03-26 22:55:47 -07:00
6bd624a0f6 fix dynamic secret config edit 2024-03-26 22:33:55 -04:00
4a11096ea8 update dynamic secrets docs 2024-03-26 18:58:02 -07:00
1589eb8e03 fix link typo 2024-03-26 18:56:14 -07:00
b370d6e415 fix link typos 2024-03-26 18:37:00 -07:00
65937d6a17 update docs and fix typos 2024-03-26 18:26:18 -07:00
d20bc1b38a turn paywall on dynamic secret 2024-03-26 17:53:47 -04:00
882ad8729c Merge pull request #1629 from Infisical/dynamic-1
allow viewer to generate and list dynamic secret
2024-03-26 17:51:26 -04:00
75d9463ceb Merge pull request #1628 from Infisical/maintenanceApril2024
add maintenance notice
2024-03-26 15:41:40 -04:00
4f05e4ce93 Fix test case 2024-03-26 19:41:48 +01:00
2e8680c5d4 Update secret-service.ts 2024-03-26 19:36:18 +01:00
e5136c9ef5 Feat: Recursively get all secrets 2024-03-26 19:36:18 +01:00
812fe5cf31 Feat: Recursively get all secrets 2024-03-26 19:36:18 +01:00
50082e192c Feat: Recursively get all secrets, findByFolderIds DLA 2024-03-26 19:35:45 +01:00
1e1b5d655e Fix: Refactored secret fetching to be more performant 2024-03-26 19:35:45 +01:00
3befd90723 Fix: Refactor to in-memory approach 2024-03-26 19:35:45 +01:00
88549f4030 Feat: Deep search support 2024-03-26 19:35:45 +01:00
46a638cc63 FIx: Rename parameter from recursive to deep 2024-03-26 19:35:45 +01:00
566f7e4c61 Feat: Recursively get all secrets from inside path 2024-03-26 19:35:45 +01:00
9ff3210ed6 Feat: Recursively get all secrets from inside path 2024-03-26 19:35:12 +01:00
f91a6683c2 Fix: Rename parameter 2024-03-26 19:35:12 +01:00
c29cb667d7 Feat: Recursively get secrets from all nested secret paths 2024-03-26 19:35:12 +01:00
267 changed files with 6695 additions and 2079 deletions

View File

@ -3,9 +3,13 @@ import "fastify";
import { TUsers } from "@app/db/schemas";
import { TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
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 { 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";
@ -21,8 +25,6 @@ import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service";
import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { TDynamicSecretServiceFactory } from "@app/services/dynamic-secret/dynamic-secret-service";
import { TDynamicSecretLeaseServiceFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TIdentityServiceFactory } from "@app/services/identity/identity-service";
import { TIdentityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { TIdentityProjectServiceFactory } from "@app/services/identity-project/identity-project-service";
@ -121,6 +123,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

View File

@ -38,6 +38,9 @@ import {
TIdentityOrgMemberships,
TIdentityOrgMembershipsInsert,
TIdentityOrgMembershipsUpdate,
TIdentityProjectAdditionalPrivilege,
TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate,
TIdentityProjectMembershipRole,
TIdentityProjectMembershipRoleInsert,
TIdentityProjectMembershipRoleUpdate,
@ -92,6 +95,9 @@ import {
TProjects,
TProjectsInsert,
TProjectsUpdate,
TProjectUserAdditionalPrivilege,
TProjectUserAdditionalPrivilegeInsert,
TProjectUserAdditionalPrivilegeUpdate,
TProjectUserMembershipRoles,
TProjectUserMembershipRolesInsert,
TProjectUserMembershipRolesUpdate,
@ -239,6 +245,11 @@ declare module "knex/types/tables" {
TProjectUserMembershipRolesUpdate
>;
[TableName.ProjectRoles]: Knex.CompositeTableType<TProjectRoles, TProjectRolesInsert, TProjectRolesUpdate>;
[TableName.ProjectUserAdditionalPrivilege]: Knex.CompositeTableType<
TProjectUserAdditionalPrivilege,
TProjectUserAdditionalPrivilegeInsert,
TProjectUserAdditionalPrivilegeUpdate
>;
[TableName.ProjectKeys]: Knex.CompositeTableType<TProjectKeys, TProjectKeysInsert, TProjectKeysUpdate>;
[TableName.Secret]: Knex.CompositeTableType<TSecrets, TSecretsInsert, TSecretsUpdate>;
[TableName.SecretBlindIndex]: Knex.CompositeTableType<
@ -294,6 +305,11 @@ declare module "knex/types/tables" {
TIdentityProjectMembershipRoleInsert,
TIdentityProjectMembershipRoleUpdate
>;
[TableName.IdentityProjectAdditionalPrivilege]: Knex.CompositeTableType<
TIdentityProjectAdditionalPrivilege,
TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate
>;
[TableName.ScimToken]: Knex.CompositeTableType<TScimTokens, TScimTokensInsert, TScimTokensUpdate>;
[TableName.SecretApprovalPolicy]: Knex.CompositeTableType<
TSecretApprovalPolicies,

View File

@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
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<void> {
await dropOnUpdateTrigger(knex, TableName.ProjectUserAdditionalPrivilege);
await knex.schema.dropTableIfExists(TableName.ProjectUserAdditionalPrivilege);
}

View File

@ -0,0 +1,32 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
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<void> {
await dropOnUpdateTrigger(knex, TableName.IdentityProjectAdditionalPrivilege);
await knex.schema.dropTableIfExists(TableName.IdentityProjectAdditionalPrivilege);
}

View File

@ -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<typeof IdentityProjectAdditionalPrivilegeSchema>;
export type TIdentityProjectAdditionalPrivilegeInsert = Omit<
z.input<typeof IdentityProjectAdditionalPrivilegeSchema>,
TImmutableDBKeys
>;
export type TIdentityProjectAdditionalPrivilegeUpdate = Partial<
Omit<z.input<typeof IdentityProjectAdditionalPrivilegeSchema>, TImmutableDBKeys>
>;

View File

@ -10,6 +10,7 @@ export * from "./git-app-org";
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";
@ -28,6 +29,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";

View File

@ -20,6 +20,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",
@ -43,6 +44,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",

View File

@ -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<typeof ProjectUserAdditionalPrivilegeSchema>;
export type TProjectUserAdditionalPrivilegeInsert = Omit<
z.input<typeof ProjectUserAdditionalPrivilegeSchema>,
TImmutableDBKeys
>;
export type TProjectUserAdditionalPrivilegeUpdate = Partial<
Omit<z.input<typeof ProjectUserAdditionalPrivilegeSchema>, TImmutableDBKeys>
>;

View File

@ -6,10 +6,9 @@ import { DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { SanitizedDynamicSecretSchema } from "../sanitizedSchemas";
export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",

View File

@ -3,14 +3,13 @@ import ms from "ms";
import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
import { DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type";
import { DynamicSecretProviderSchema } from "@app/services/dynamic-secret/providers/models";
import { SanitizedDynamicSecretSchema } from "../sanitizedSchemas";
export const registerDynamicSecretRouter = async (server: FastifyZodProvider) => {
server.route({

View File

@ -0,0 +1,310 @@
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()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.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,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
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()
.refine((val) => val.toLowerCase() === val, "Must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.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,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
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<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
}))
};
}
return { privileges };
}
});
};

View File

@ -1,3 +1,6 @@
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-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";
@ -13,6 +16,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
@ -34,10 +38,26 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerSecretRotationProviderRouter, {
prefix: "/secret-rotation-providers"
});
await server.register(
async (dynamicSecretRouter) => {
await dynamicSecretRouter.register(registerDynamicSecretRouter);
await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
},
{ prefix: "/dynamic-secrets" }
);
await server.register(registerSamlRouter, { prefix: "/sso" });
await server.register(registerScimRouter, { prefix: "/scim" });
await server.register(registerLdapRouter, { prefix: "/ldap" });
await server.register(registerSecretScanningRouter, { prefix: "/secret-scanning" });
await server.register(registerSecretRotationRouter, { prefix: "/secret-rotations" });
await server.register(registerSecretVersionRouter, { prefix: "/secret" });
await server.register(
async (privilegeRouter) => {
await privilegeRouter.register(registerUserAdditionalPrivilegeRouter, { prefix: "/users" });
await privilegeRouter.register(registerIdentityProjectAdditionalPrivilegeRouter, { prefix: "/identity" });
},
{ prefix: "/additional-privilege" }
);
};

View File

@ -0,0 +1,237 @@
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()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.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,
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
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()
.refine((v) => v.toLowerCase() === v, "Slug must be lowercase")
.refine((v) => slugify(v) === v, {
message: "Slug must be a valid slug"
})
.optional()
.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,
slug: req.body.slug ? slugify(req.body.slug) : `privilege-${slugify(alphaNumericNanoId(12))}`,
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 };
}
});
};

View File

@ -8,11 +8,12 @@ import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services
import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TDynamicSecretDALFactory } from "../dynamic-secret/dynamic-secret-dal";
import { DynamicSecretProviders, TDynamicProviderFns } from "../dynamic-secret/providers/models";
import { TProjectDALFactory } from "../project/project-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TDynamicSecretLeaseDALFactory } from "./dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "./dynamic-secret-lease-queue";
import {
@ -248,6 +249,7 @@ export const dynamicSecretLeaseServiceFactory = ({
if ((revokeResponse as { error?: Error })?.error) {
const { error } = revokeResponse as { error?: Error };
logger.error("Failed to revoke lease", { error: error?.message });
const deletedDynamicSecretLease = await dynamicSecretLeaseDAL.updateById(dynamicSecretLease.id, {
status: DynamicSecretLeaseStatus.FailedDeletion,
statusDetails: error?.message?.slice(0, 255)

View File

@ -6,11 +6,11 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { TDynamicSecretLeaseDALFactory } from "../dynamic-secret-lease/dynamic-secret-lease-dal";
import { TDynamicSecretLeaseQueueServiceFactory } from "../dynamic-secret-lease/dynamic-secret-lease-queue";
import { TProjectDALFactory } from "../project/project-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TDynamicSecretDALFactory } from "./dynamic-secret-dal";
import {
DynamicSecretStatus,

View File

@ -23,7 +23,17 @@ export const SqlDatabaseProvider = (): TDynamicProviderFns => {
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
const providerInputs = await DynamicSecretSqlDBSchema.parseAsync(inputs);
if (providerInputs.host === "localhost" || providerInputs.host === "127.0.0.1" || dbHost === providerInputs.host)
if (
// localhost
providerInputs.host === "localhost" ||
providerInputs.host === "127.0.0.1" ||
// database infisical uses
dbHost === providerInputs.host ||
// internal ips
providerInputs.host === "host.docker.internal" ||
providerInputs.host.match(/^10\.\d+\.\d+\.\d+/) ||
providerInputs.host.match(/^192\.168\.\d+\.\d+/)
)
throw new BadRequestError({ message: "Invalid db host" });
return providerInputs;
};

View File

@ -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;
};

View File

@ -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<TIdentityProjectDALFactory, "findOne" | "findById">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
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
};
};

View File

@ -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<TProjectPermission, "projectId">;
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<TProjectPermission, "projectId"> & {
slug: string;
identityId: string;
projectSlug: string;
};
export type TGetIdentityPrivilegeDetailsDTO = Omit<TProjectPermission, "projectId"> & {
slug: string;
identityId: string;
projectSlug: string;
};
export type TListIdentityPrivilegesDTO = Omit<TProjectPermission, "projectId"> & {
identityId: string;
projectSlug: string;
};

View File

@ -15,7 +15,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
membersUsed: 0,
environmentLimit: null,
environmentsUsed: 0,
dynamicSecret: true,
dynamicSecret: false,
secretVersioning: true,
pitRecovery: false,
ipAllowlisting: false,

View File

@ -27,7 +27,7 @@ export type TFeatureSet = {
tier: -1;
workspaceLimit: null;
workspacesUsed: 0;
dynamicSecret: true;
dynamicSecret: false;
memberLimit: null;
membersUsed: 0;
environmentLimit: null;

View File

@ -56,6 +56,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)
@ -69,9 +74,22 @@ export const permissionDALFactory = (db: TDbClient) => {
db.ref("updatedAt").withSchema(TableName.ProjectMembership).as("membershipUpdatedAt"),
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,
@ -102,15 +120,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" });
}
@ -129,6 +176,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,
@ -144,9 +196,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,
@ -171,16 +242,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" });
}

View File

@ -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"]) {

View File

@ -180,10 +180,12 @@ export const permissionServiceFactory = ({
authMethod: ActorAuthMethod,
userOrgId?: string
): Promise<TProjectPermissionRT<ActorType.USER>> => {
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(

View File

@ -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<typeof projectUserAdditionalPrivilegeDALFactory>;
export const projectUserAdditionalPrivilegeDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.ProjectUserAdditionalPrivilege);
return orm;
};

View File

@ -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<TProjectMembershipDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
};
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
};
};

View File

@ -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<TProjectPermission, "projectId">;
export type TUpdateUserPrivilegeDTO = { privilegeId: string } & Omit<TProjectPermission, "projectId"> &
Partial<{
permissions: unknown;
slug: string;
isTemporary: boolean;
temporaryMode: ProjectUserAdditionalPrivilegeTemporaryMode.Relative;
temporaryRange: string;
temporaryAccessStartTime: string;
}>;
export type TDeleteUserPrivilegeDTO = Omit<TProjectPermission, "projectId"> & { privilegeId: string };
export type TGetUserPrivilegeDetailsDTO = Omit<TProjectPermission, "projectId"> & { privilegeId: string };
export type TListUserPrivilegesDTO = Omit<TProjectPermission, "projectId"> & { projectMembershipId: string };

View File

@ -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(

View File

@ -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 = {

View File

@ -90,7 +90,17 @@ export const secretRotationDbFn = async ({
const appCfg = getConfig();
const ssl = ca ? { rejectUnauthorized: false, ca } : undefined;
if (host === "localhost" || host === "127.0.0.1" || getDbConnectionHost(appCfg.DB_CONNECTION_URI) === host)
const dbHost = appCfg.DB_HOST || getDbConnectionHost(appCfg.DB_CONNECTION_URI);
if (
host === "localhost" ||
host === "127.0.0.1" ||
// database infisical uses
dbHost === host ||
// internal ips
host === "host.docker.internal" ||
host.match(/^10\.\d+\.\d+\.\d+/) ||
host.match(/^192\.168\.\d+\.\d+/)
)
throw new Error("Invalid db host");
const db = knex({

View File

@ -215,6 +215,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.",
@ -397,3 +398,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."
}
};

View File

@ -114,7 +114,8 @@ const envSchema = z
.enum(["true", "false"])
.transform((val) => val === "true")
.optional(),
INFISICAL_CLOUD: zodStrBool.default("false")
INFISICAL_CLOUD: zodStrBool.default("false"),
MAINTENANCE_MODE: zodStrBool.default("false")
})
.transform((data) => ({
...data,

View File

@ -24,6 +24,7 @@ import { fastifyErrHandler } from "./plugins/error-handler";
import { registerExternalNextjs } from "./plugins/external-nextjs";
import { serializerCompiler, validatorCompiler, ZodTypeProvider } from "./plugins/fastify-zod";
import { fastifyIp } from "./plugins/ip";
import { maintenanceMode } from "./plugins/maintenanceMode";
import { fastifySwagger } from "./plugins/swagger";
import { registerRoutes } from "./routes";
@ -72,6 +73,8 @@ export const main = async ({ db, smtp, logger, queue, keyStore }: TMain) => {
}
await server.register(helmet, { contentSecurityPolicy: false });
await server.register(maintenanceMode);
await server.register(registerRoutes, { smtp, queue, db, keyStore });
if (appCfg.isProductionMode) {

View File

@ -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
};

View File

@ -0,0 +1,12 @@
import fp from "fastify-plugin";
import { getConfig } from "@app/lib/config/env";
export const maintenanceMode = fp(async (fastify) => {
fastify.addHook("onRequest", async (req) => {
const serverEnvs = getConfig();
if (req.url !== "/api/v1/auth/checkAuth" && req.method !== "GET" && serverEnvs.MAINTENANCE_MODE) {
throw new Error("Infisical is in maintenance mode. Please try again later.");
}
});
});

View File

@ -5,12 +5,22 @@ import { registerV1EERoutes } from "@app/ee/routes/v1";
import { auditLogDALFactory } from "@app/ee/services/audit-log/audit-log-dal";
import { auditLogQueueServiceFactory } from "@app/ee/services/audit-log/audit-log-queue";
import { auditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-service";
import { dynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
import { dynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-service";
import { buildDynamicSecretProviders } from "@app/ee/services/dynamic-secret/providers";
import { dynamicSecretLeaseDALFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-dal";
import { dynamicSecretLeaseQueueServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-queue";
import { dynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
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";
@ -47,12 +57,6 @@ import { authPaswordServiceFactory } from "@app/services/auth/auth-password-serv
import { authSignupServiceFactory } from "@app/services/auth/auth-signup-service";
import { tokenDALFactory } from "@app/services/auth-token/auth-token-dal";
import { tokenServiceFactory } from "@app/services/auth-token/auth-token-service";
import { dynamicSecretDALFactory } from "@app/services/dynamic-secret/dynamic-secret-dal";
import { dynamicSecretServiceFactory } from "@app/services/dynamic-secret/dynamic-secret-service";
import { buildDynamicSecretProviders } from "@app/services/dynamic-secret/providers";
import { dynamicSecretLeaseDALFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-dal";
import { dynamicSecretLeaseQueueServiceFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-queue";
import { dynamicSecretLeaseServiceFactory } from "@app/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { identityDALFactory } from "@app/services/identity/identity-dal";
import { identityOrgDALFactory } from "@app/services/identity/identity-org-dal";
import { identityServiceFactory } from "@app/services/identity/identity-service";
@ -149,6 +153,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);
@ -174,6 +179,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);
@ -345,6 +351,11 @@ export const registerRoutes = async (
projectRoleDAL,
licenseService
});
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
permissionService,
projectMembershipDAL,
projectUserAdditionalPrivilegeDAL
});
const projectKeyService = projectKeyServiceFactory({
permissionService,
projectKeyDAL,
@ -479,6 +490,7 @@ export const registerRoutes = async (
snapshotService,
secretQueueService,
secretImportDAL,
projectEnvDAL,
projectBotService
});
const sarService = secretApprovalRequestServiceFactory({
@ -548,6 +560,12 @@ export const registerRoutes = async (
identityProjectMembershipRoleDAL,
projectRoleDAL
});
const identityProjectAdditionalPrivilegeService = identityProjectAdditionalPrivilegeServiceFactory({
projectDAL,
identityProjectAdditionalPrivilegeDAL,
permissionService,
identityProjectDAL
});
const identityUaService = identityUaServiceFactory({
identityOrgMembershipDAL,
permissionService,
@ -638,7 +656,9 @@ export const registerRoutes = async (
trustedIp: trustedIpService,
scim: scimService,
secretBlindIndex: secretBlindIndexService,
telemetry: telemetryService
telemetry: telemetryService,
projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService,
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService
});
server.decorate<FastifyZodProvider["store"]>("store", {

View File

@ -16,13 +16,16 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
schema: {
response: {
200: z.object({
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true })
config: SuperAdminSchema.omit({ createdAt: true, updatedAt: true }).merge(
z.object({ isMigrationModeOn: z.boolean() })
)
})
}
},
handler: async () => {
const config = await getServerCfg();
return { config };
const serverEnvs = getConfig();
return { config: { ...config, isMigrationModeOn: serverEnvs.MAINTENANCE_MODE } };
}
});

View File

@ -1,8 +1,6 @@
import { registerAdminRouter } from "./admin-router";
import { registerAuthRoutes } from "./auth-router";
import { registerProjectBotRouter } from "./bot-router";
import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router";
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerIdentityAccessTokenRouter } from "./identity-access-token-router";
import { registerIdentityRouter } from "./identity-router";
import { registerIdentityUaRouter } from "./identity-ua";
@ -54,14 +52,6 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
{ prefix: "/workspace" }
);
await server.register(
async (dynamicSecretRouter) => {
await dynamicSecretRouter.register(registerDynamicSecretRouter);
await dynamicSecretRouter.register(registerDynamicSecretLeaseRouter, { prefix: "/leases" });
},
{ prefix: "/dynamic-secrets" }
);
await server.register(registerProjectBotRouter, { prefix: "/bot" });
await server.register(registerIntegrationRouter, { prefix: "/integration" });
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });

View File

@ -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({

View File

@ -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,

View File

@ -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({

View File

@ -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: {

View File

@ -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,

View File

@ -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),

View File

@ -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({

View File

@ -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"
}

View File

@ -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),

View File

@ -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
};

View File

@ -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
};
};

View File

@ -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<TPermissionServiceFactory, "getProjectPermission">;
folderDAL: Pick<TSecretFolderDALFactory, "findBySecretPath" | "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
};
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,

View File

@ -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<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }> = {};
importedSecrets.forEach(({ secrets: secs }) => {

View File

@ -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<TSecretFolderDALFactory, "findBySecretPath" | "updateById" | "findById" | "findByManySecretPath">;
projectDAL: Pick<TProjectDALFactory, "checkProjectUpgradeStatus" | "findProjectBySlug">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne">;
folderDAL: Pick<
TSecretFolderDALFactory,
"findBySecretPath" | "updateById" | "findById" | "findByManySecretPath" | "find"
>;
secretBlindIndexDAL: TSecretBlindIndexDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets" | "handleSecretReminder" | "removeSecretReminder">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
secretImportDAL: Pick<TSecretImportDALFactory, "find">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "findByFolderIds">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "insertMany">;
};
export type TSecretServiceFactory = ReturnType<typeof secretServiceFactory>;
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
)
)
}))
};

View File

@ -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 = {

View File

@ -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")
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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 ")

View File

@ -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)

View File

@ -93,6 +93,7 @@ type GetAllSecretsParameters struct {
WorkspaceId string
SecretsPath string
IncludeImport bool
Recursive bool
}
type GetAllFoldersParameters struct {

View File

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

View File

@ -0,0 +1,4 @@
---
title: "Create Permanent"
openapi: "POST /api/v1/additional-privilege/identity/permanent"
---

View File

@ -0,0 +1,4 @@
---
title: "Create Temporary"
openapi: "POST /api/v1/additional-privilege/identity/temporary"
---

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/additional-privilege/identity"
---

View File

@ -0,0 +1,4 @@
---
title: "Find By Privilege Slug"
openapi: "GET /api/v1/additional-privilege/identity/{privilegeSlug}"
---

View File

@ -0,0 +1,4 @@
---
title: "List"
openapi: "GET /api/v1/additional-privilege/identity"
---

View File

@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/additional-privilege/identity"
---

View File

@ -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.
<Tabs>
<Tab title="AWS Secrets manager">
<ParamField body="integration" type="string" initialValue="aws-secret-manager" required>
This value must be **aws-secret-manager**.
</ParamField>
<ParamField body="workspaceId" type="string" required>
Infisical project id for the integration.
</ParamField>
<ParamField body="accessId" type="string" required>
The AWS IAM User Access ID.
</ParamField>
<ParamField body="accessToken" type="string" required>
The AWS IAM User Access Secret Key.
</ParamField>
</Tab>
<Tab title="GCP Secrets manager">
Coming Soon
</Tab>
<Tab title="Heroku">
Coming Soon
</Tab>
</Tabs>

View File

@ -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.
<Tabs>
<Tab title="AWS Secrets manager">
<ParamField body="integrationAuthId" type="string" required>
The ID of the integration auth object for authentication with AWS.
Refer [Create Integration Auth](./create-auth) for more info
</ParamField>
<ParamField body="isActive" type="boolean">
Whether the integration should be active or inactive
</ParamField>
<ParamField body="app" type="string" required>
The secret name used when saving secret in AWS SSM. Used for naming and can be arbitrary.
</ParamField>
<ParamField body="region" type="string" required>
The AWS region of the SSM. Example: `us-east-1`
</ParamField>
<ParamField body="sourceEnvironment" type="string" required>
The Infisical environment slug from where secrets will be synced from. Example: `dev`
</ParamField>
<ParamField body="secretPath" type="string" required>
The Infisical folder path from where secrets will be synced from. Example: `/some/path`. The root of the environment is `/`.
</ParamField>
</Tab>
<Tab title="GCP Secrets manager">
Coming Soon
</Tab>
<Tab title="Heroku">
Coming Soon
</Tab>
</Tabs>

View File

@ -0,0 +1,4 @@
---
title: "Delete Auth By ID"
openapi: "DELETE /api/v1/integration-auth/{integrationAuthId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Delete Auth"
openapi: "DELETE /api/v1/integration-auth"
---

View File

@ -0,0 +1,4 @@
---
title: "Delete"
openapi: "DELETE /api/v1/integration/{integrationId}"
---

View File

@ -0,0 +1,4 @@
---
title: "Get Auth By ID"
openapi: "GET /api/v1/integration-auth/{integrationAuthId}"
---

View File

@ -0,0 +1,4 @@
---
title: "List Auth"
openapi: "GET /api/v1/workspace/{workspaceId}/authorizations"
---

View File

@ -0,0 +1,4 @@
---
title: "Update"
openapi: "PATCH /api/v1/integration/{integrationId}"
---

View File

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

View File

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

View File

@ -0,0 +1,107 @@
---
mode: 'custom'
---
export function openSearch() {
document.getElementById('search-bar-entry').click();
}
<div
className="relative w-full flex items-center justify-center"
style={{ height: '24rem', backgroundColor: '#1F1F33', overflow: 'hidden' }}
>
<div style={{ flex: 'none' }}>
<img
src="/images/background.png"
style={{ height: '68rem', width: '68rem' }}
/>
</div>
<div style={{ position: 'absolute', textAlign: 'center' }}>
<div
style={{
color: 'white',
fontWeight: '400',
fontSize: '48px',
margin: '0',
}}
>
Infisical Documentation
</div>
<p
style={{
color: 'white',
fontWeight: '400',
fontSize: '20px',
opacity: '0.7',
}}
>
What can we help you build?
</p>
<button
type="button"
className="mx-auto w-full flex items-center text-sm leading-6 shadow-sm text-gray-400 bg-white ring-1 ring-gray-400/20 focus:outline-primary"
id="home-search-entry"
style={{
maxWidth: '24rem',
borderRadius: '4px',
marginTop: '3rem',
paddingLeft: '0.75rem',
paddingRight: '0.75rem',
paddingTop: '0.75rem',
paddingBottom: '0.75rem',
}}
onClick={openSearch}
>
<svg
className="h-4 w-4 ml-1.5 mr-3 flex-none bg-gray-500 hover:bg-gray-600 dark:bg-white/50 dark:hover:bg-white/70"
style={{
maskImage:
'url("https://mintlify.b-cdn.net/v6.5.1/solid/magnifying-glass.svg")',
maskRepeat: 'no-repeat',
maskPosition: 'center center',
}}
/>
Start a chat with us...
</button>
</div>
</div>
<div style={{marginTop: '6rem', marginBottom: '8rem', maxWidth: '70rem', marginLeft: 'auto',
marginRight: 'auto', paddingLeft: '1.25rem',
paddingRight: '1.25rem' }}>
<div
style={{
textAlign: 'center',
fontSize: '24px',
fontWeight: '600',
color: '#121142',
marginBottom: '3rem',
}}
>
Choose a topic below or simply{' '}
<span className="text-primary">get started</span>
</div>
<CardGroup cols={3}>
<Card title="Getting Started" icon="book-open" href="/guides">
Practical guides and best practices to get you up and running quickly.
</Card>
<Card title="API Reference" icon="code-simple" href="/reference">
Comprehensive details about the Infisical API.
</Card>
<Card title="Security" icon="code-simple" href="/reference">
Learn more about Infisical's architecture and underlying security.
</Card>
<Card title="Self-hosting" icon="link-simple" href="/integrations">
Read self-hosting instruction for Infisical.
</Card>
<Card title="Integrations" icon="link-simple" href="/integrations">
Infisical's growing number of third-party integrations.
</Card>
<Card title="Releases" icon="party-horn" href="/release-notes">
News about features and changes in Pinecone and related tools.
</Card>
</CardGroup>
</div>

View File

@ -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
<Card
href="/documentation/getting-started/platform"
title="Platform"
icon="laptop"
color="#dc2626"
>
Store secrets like API keys, database credentials, environment variables with Infisical
</Card>
## Access secrets
Start managing secrets securely with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself.
<CardGroup cols={2}>
<Card href="../../cli/overview" title="Command Line Interface (CLI)" icon="square-terminal" color="#3775a9">
Inject secrets into any application process/environment
<Card
title="Infisical Cloud"
href="https://app.infisical.com/signup"
icon="cloud"
color="#000000"
>
Get started with Infisical Cloud in just a few minutes.
</Card>
<Card
href="/self-hosting/overview"
title="Self-hosting"
icon="server"
color="#000000"
>
Self-host Infisical on your own infrastructure.
</Card>
</CardGroup>
## 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.
<CardGroup cols={2}>
<Card href="../../cli/overview" title="Command Line Interface (CLI)" icon="square-terminal" color="#000000">
Inject secrets into any application process/environment.
</Card>
<Card
title="SDKs"
href="/documentation/getting-started/sdks"
icon="boxes-stacked"
color="#3c8639"
color="#000000"
>
Fetch secrets with any programming language on demand
Fetch secrets with any programming language on demand.
</Card>
<Card href="../../integrations/platforms/docker-intro" title="Docker" icon="docker" color="#0078d3">
Inject secrets into Docker containers
<Card href="../../integrations/platforms/docker-intro" title="Docker" icon="docker" color="#000000">
Inject secrets into Docker containers.
</Card>
<Card
href="../../integrations/platforms/kubernetes"
title="Kubernetes"
icon="server"
color="#3775a9"
color="#000000"
>
Fetch and save secrets as native Kubernetes secrets
Fetch and save secrets as native Kubernetes secrets.
</Card>
<Card
href="/documentation/getting-started/api"
title="REST API"
icon="cloud"
color="#3775a9"
color="#000000"
>
Fetch secrets via HTTP request
</Card>
</CardGroup>
## Resources
<CardGroup cols={2}>
<Card
href="/self-hosting/overview"
title="Self-hosting"
icon="server"
color="#0285c7"
>
Learn how to configure and deploy Infisical
</Card>
<Card
href="/documentation/guides/introduction"
title="Guide"
icon="book-open"
color="#dc2626"
>
Explore guides for every language and stack
Fetch secrets via HTTP request.
</Card>
<Card
href="/integrations/overview"
title="Native Integrations"
icon="clouds"
color="#dc2626"
color="#000000"
>
Explore integrations for GitHub, Vercel, Netlify, and more
</Card>
<Card
href="/integrations/overview"
title="Frameworks"
icon="plug"
color="#dc2626"
>
Explore integrations for Next.js, Express, Django, and more
</Card>
<Card
href="/cli/scanning-overview"
title="Secret scanning"
icon="satellite-dish"
color="#0285c7"
>
Scan and prevent 140+ secret type leaks in your codebase
</Card>
<Card
href="https://calendly.com/team-infisical/infisical-demo"
title="Contact Us"
icon="user-headset"
color="#0285c7"
>
Questions? Need help setting up? Book a 1x1 meeting with us
Explore integrations for GitHub, Vercel, AWS, and more.
</Card>
</CardGroup>

View File

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

View File

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

View File

@ -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)
<Note>
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.

View File

@ -0,0 +1,22 @@
---
title: "Access Requests"
description: "Learn how to request access to sensitive resources in Infisical."
---
In certain situations, developers need to expand their access to a 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 (also known as eligible approvers) to a certain sensitive folder or environment.
![Create Access Request Policy Modal](/images/platform/access-controls/create-access-request-policy.png)
![Access Request Policies](/images/platform/access-controls/access-request-policies.png)
2. When a developer requests access to one of such sensitive resources, the request is visible in the dashboard, and the corresponding eligible approvers get an email notification about it.
![Access Request Create](/images/platform/access-controls/request-access.png)
![Access Request Dashboard](/images/platform/access-controls/access-requests-pending.png)
3. An eligible approver can approve or reject the access request.
![Access Request Review](/images/platform/access-controls/review-access-request.png)
4. As soon as the request is approved, developer is able to access the sought resources.
![Access Request Dashboard](/images/platform/access-controls/access-requests-completed.png)

View File

@ -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)

View File

@ -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.
<CardGroup cols={2}>
<Card
title="Role-based Access Controls"
href="./role-based-access-controls"
icon="address-book"
color="#000000"
>
Manage user and machine identitity permissions through predefined roles.
</Card>
<Card
title="Additional Privileges"
href="./additional-privileges"
icon="ballot-check"
color="#000000"
>
Add specific privileges to users and machines on top of their roles.
</Card>
<Card
title="Temporary Access"
href="./temporary-access"
icon="clock"
color="#000000"
>
Grant timed access to roles and specific privileges.
</Card>
<Card
title="Access Requests"
href="./access-requests"
icon="check"
color="#000000"
>
Enable users to request (temporary) access to sensitive resources.
</Card>
<Card
title="Approval Workflows"
href="/documentation/platform/pr-workflows"
icon="thumbs-up"
color="#000000"
>
Set up review policies for secret changes in sensitive environments.
</Card>
<Card
title="Audit Logs"
href="/documentation/platform/audit-logs"
icon="list"
color="#000000"
>
Track every action performed by user and machine identities in Infisical.
</Card>
</CardGroup>

View File

@ -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).
<Note>
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.
</Note>
![project member custom role](/images/platform/rbac/project-member-custom-role.png)

View File

@ -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)
<Note>
Every user and machine identity should always have at least one permanent role attached to it.
</Note>

View File

@ -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."
---
<Info>
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.
</Info>
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.

View File

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

View File

@ -0,0 +1,30 @@
---
title: "Overview"
description: "Learn how to generate secrets dynamically on-demand."
---
## Introduction
Contrary to static key-value secrets, which require manual input of data into the secure Infisical storage, dynamic secrets are generated on-demand upon access.
Dynamic secrets are unique to every identity using them. Such secrets come are generated only at the moment they are retrieved, eliminating the possibility of theft or reuse by another identity. Thanks to Infisical's integrated revocation capabilities, dynamic secrets can be promptly invalidated post-use, significantly reducing their lifespan.
## Benefits of Dynamic Secrets
This approach offers several advantages in terms of security and management:
- **Enhanced Security**: By frequently changing secrets, dynamic secrets minimize the risk associated with secret compromise. Even if an attacker manages to obtain a secret, it would likely be invalid by the time they attempt to use it.
- **Reduced Secret Lifetime**: The limited validity period of dynamic secrets means that they are less valuable targets for attackers. This inherently reduces the time window during which a secret can be exploited.
- **Automated Management**: Dynamic secrets enable automated systems to handle the generation, distribution, revocation, and rotation of secrets without human intervention, thus reducing the risk of human error.
- **Auditing and Traceability**: The generation of dynamic secrets can be tightly controlled and monitored. This allows for detailed auditing of who accessed what secret and when, improving overall security posture and compliance with regulatory standards.
- **Scalability**: Dynamic secret management systems can scale more effectively to handle a large number of services and applications, as they automate much of the overhead associated with manual secret management.
Dynamic secrets are particularly useful in environments with stringent security requirements, such as cloud environments, distributed systems, and microservices architectures, where they help to manage database credentials, API keys, service tokens, and other types of secrets.
## Infisical Dynamic Secret Templates
1. [PostgreSQL](./postgresql)

View File

@ -0,0 +1,98 @@
---
title: "PostgreSQL"
description: "Learn how to dynamically generate PostgreSQL Database user passwords."
---
The Infisical PostgreSQL secret rotation allows you to automatically rotate your PostgreSQL database user's password at a predefined interval.
## Prerequisite
1. Create a user with the required permission in your SQL instance.
## Set up Dynamic Secrets with PostgreSQL
<Steps>
<Step title="Open Secret Overview Dashboard">
Open the Secret Overview dashboard and select the environment in which you would like to add a dynamic secret.
</Step>
<Step title="Click on the `Add Dynamic Secret` button">
![Add Dynamic Secret Button](../../../images/platform/dynamic-secrets/add-dynamic-secret-button.png)
</Step>
<Step title="Select `SQL Database`">
![Dynamic Secret Modal](../../../images/platform/dynamic-secrets/dynamic-secret-modal.png)
</Step>
<Step title="Provide the inputs for dynamic secret parameters">
<ParamField path="Secret Name" type="string" required>
Name by which you want the secret to be referenced
</ParamField>
<ParamField path="Default TTL" type="string" required>
Default time-to-live for a generated secret (it is possible to modify this value when a secret is generate)
</ParamField>
<ParamField path="Max TTL" type="string" required>
Maximum time-to-live for a generated secret
</ParamField>
<ParamField path="Service" type="string" required>
Choose the service you want to generate dynamic secrets for
</ParamField>
<ParamField path="Host" type="string" required>
Database host
</ParamField>
<ParamField path="Port" type="number" required>
Database port
</ParamField>
<ParamField path="User" type="string" required>
Username that will be used to create dynamic secrets
</ParamField>
<ParamField path="Password" type="string" required>
Password that will be used to create dynamic secrets
</ParamField>
<ParamField path="Database Name" type="string" required>
Name of the database for which you want to create dynamic secrets
</ParamField>
<ParamField path="CA(SSL)" type="string">
A CA may be required if your DB requires it for incoming connections. AWS RDS instances with default settings will requires a CA which can be downloaded [here](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html#UsingWithRDS.SSL.CertificatesAllRegions).
</ParamField>
![Dynamic Secret Setup Modal](../../../images/platform/dynamic-secrets/dynamic-secret-setup-modal.png)
</Step>
<Step title="(Optional) Modify SQL Statements">
If you want to provide specific privileges for the future generated dynamic secrets, you are able to specify them as SQL statements.
![Modify SQL Statements Modal](../../../images/platform/dynamic-secrets/modify-sql-statements.png)
</Step>
<Step title="Click `Submit`">
After submitting the form, you will see a dynamic secret creates in the dashboard.
<Note>
If this step fails, you might have to add the CA certficate.
</Note>
![Dynamic Secret](../../../images/platform/dynamic-secrets/dynamic-secret.png)
</Step>
<Step title="Generate dynamic secrets">
Now that the dynamic secret is created, you can start generating unique secret values by specifying the Time-to-live within the predefined range.
![Provision Lease](../../../images/platform/dynamic-secrets/provision-lease.png)
After you click the `Submit` button, a new secret lease will be generated and the Database User and Database Password will be shown.
![Provision Lease](../../../images/platform/dynamic-secrets/lease-values.png)
</Step>
<Step title="Audit or Revoke Leases">
As soon as you have generated a few secret leases, you will be able to access them by clicking `Generate` on the dynamic secret row. In this modal, you are able to see the expiration time or delete a secret preemptively.
![Provision Lease](../../../images/platform/dynamic-secrets/lease-data.png)
</Step>
</Steps>

View File

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

View File

@ -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.
<Note>
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.
</Note>
## 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
<AccordionGroup>
<Accordion title="What is the difference between an identity and service token?">
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.
</Accordion>
<Accordion title="Why can I not create, read, update, or delete an identity?">
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.
</Accordion>
</AccordionGroup>

View File

@ -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."
---
<Note>
Currently, identities can only be used to make authenticated requests to the Infisical API, SDKs, and Agent. They do not work with clients such as CLI, K8s Operator, Terraform Provider, 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.
</Note>
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**
<AccordionGroup>
<Accordion title="What is the difference between an identity and service token?">
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.
</Accordion>
<Accordion title="Why can I not create, read, update, or delete an identity?">
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.
</Accordion>
</AccordionGroup>
<CardGroup cols={2}>
<Card href="./user-identities" title="People (User Identities)" icon="people" color="#000000">
Learn more about the concept on user identities in Infisical.
</Card>
<Card
title="Machine Identities"
href="./machine-identities"
icon="computer"
color="#000000"
>
Understand the concept of machine identities in Infisical.
</Card>
</CardGroup>

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