Merge remote-tracking branch 'origin/main' into misc/privilege-management-v2-transition

This commit is contained in:
Sheen Capadngan
2025-03-11 15:48:01 +08:00
142 changed files with 4138 additions and 2869 deletions
@@ -92,7 +92,7 @@ jobs:
exit 1 exit 1
fi fi
- name: Install openapi-diff - name: Install openapi-diff
run: go install github.com/tufin/oasdiff@latest run: go install github.com/oasdiff/oasdiff@latest
- name: Running OpenAPI Spec diff action - name: Running OpenAPI Spec diff action
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
- name: cleanup - name: cleanup
@@ -120,4 +120,3 @@ export default {
}; };
} }
}; };
+1
View File
@@ -71,6 +71,7 @@
"migrate:org": "tsx ./scripts/migrate-organization.ts", "migrate:org": "tsx ./scripts/migrate-organization.ts",
"seed:new": "tsx ./scripts/create-seed-file.ts", "seed:new": "tsx ./scripts/create-seed-file.ts",
"seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run", "seed": "knex --knexfile ./dist/db/knexfile.ts --client pg seed:run",
"seed-dev": "knex --knexfile ./src/db/knexfile.ts --client pg seed:run",
"db:reset": "npm run migration:rollback -- --all && npm run migration:latest" "db:reset": "npm run migration:rollback -- --all && npm run migration:latest"
}, },
"keywords": [], "keywords": [],
@@ -1,16 +1,11 @@
import { z } from "zod"; import { z } from "zod";
import { import { SecretApprovalRequestsReviewersSchema, SecretApprovalRequestsSchema, UsersSchema } from "@app/db/schemas";
SecretApprovalRequestsReviewersSchema,
SecretApprovalRequestsSchema,
SecretTagsSchema,
UsersSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types"; import { ApprovalStatus, RequestState } from "@app/ee/services/secret-approval-request/secret-approval-request-types";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
@@ -250,14 +245,6 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
} }
}); });
const tagSchema = SecretTagsSchema.pick({
id: true,
slug: true,
color: true
})
.array()
.optional();
server.route({ server.route({
method: "GET", method: "GET",
url: "/:id", url: "/:id",
@@ -291,7 +278,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
.omit({ _id: true, environment: true, workspace: true, type: true, version: true }) .omit({ _id: true, environment: true, workspace: true, type: true, version: true })
.extend({ .extend({
op: z.string(), op: z.string(),
tags: tagSchema, tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.nullish(), secretMetadata: ResourceMetadataSchema.nullish(),
secret: z secret: z
.object({ .object({
@@ -310,7 +297,7 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
secretKey: z.string(), secretKey: z.string(),
secretValue: z.string().optional(), secretValue: z.string().optional(),
secretComment: z.string().optional(), secretComment: z.string().optional(),
tags: tagSchema, tags: SanitizedTagSchema.array().optional(),
secretMetadata: ResourceMetadataSchema.nullish() secretMetadata: ResourceMetadataSchema.nullish()
}) })
.optional() .optional()
+2 -2
View File
@@ -1,6 +1,6 @@
import z from "zod"; import z from "zod";
import { ProjectPermissionActions } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { RAW_SECRETS } from "@app/lib/api-docs"; import { RAW_SECRETS } from "@app/lib/api-docs";
import { removeTrailingSlash } from "@app/lib/fn"; import { removeTrailingSlash } from "@app/lib/fn";
import { readLimit } from "@app/server/config/rateLimiter"; import { readLimit } from "@app/server/config/rateLimiter";
@@ -9,7 +9,7 @@ import { AuthMode } from "@app/services/auth/auth-type";
const AccessListEntrySchema = z const AccessListEntrySchema = z
.object({ .object({
allowedActions: z.nativeEnum(ProjectPermissionActions).array(), allowedActions: z.nativeEnum(ProjectPermissionSecretActions).array(),
id: z.string(), id: z.string(),
membershipId: z.string(), membershipId: z.string(),
name: z.string() name: z.string()
@@ -22,7 +22,11 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
}), }),
response: { response: {
200: z.object({ 200: z.object({
secretVersions: secretRawSchema.array() secretVersions: secretRawSchema
.extend({
secretValueHidden: z.boolean()
})
.array()
}) })
} }
}, },
@@ -37,6 +41,7 @@ export const registerSecretVersionRouter = async (server: FastifyZodProvider) =>
offset: req.query.offset, offset: req.query.offset,
secretId: req.params.secretId secretId: req.params.secretId
}); });
return { secretVersions }; return { secretVersions };
} }
}); });
+5 -7
View File
@@ -1,10 +1,10 @@
import { z } from "zod"; import { z } from "zod";
import { SecretSnapshotsSchema, SecretTagsSchema } from "@app/db/schemas"; import { SecretSnapshotsSchema } from "@app/db/schemas";
import { PROJECTS } from "@app/lib/api-docs"; import { PROJECTS } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
export const registerSnapshotRouter = async (server: FastifyZodProvider) => { export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
@@ -31,12 +31,9 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
secretVersions: secretRawSchema secretVersions: secretRawSchema
.omit({ _id: true, environment: true, workspace: true, type: true }) .omit({ _id: true, environment: true, workspace: true, type: true })
.extend({ .extend({
secretValueHidden: z.boolean(),
secretId: z.string(), secretId: z.string(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array()
id: true,
slug: true,
color: true
}).array()
}) })
.array(), .array(),
folderVersion: z.object({ id: z.string(), name: z.string() }).array(), folderVersion: z.object({ id: z.string(), name: z.string() }).array(),
@@ -55,6 +52,7 @@ export const registerSnapshotRouter = async (server: FastifyZodProvider) => {
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
id: req.params.secretSnapshotId id: req.params.secretSnapshotId
}); });
return { secretSnapshot }; return { secretSnapshot };
} }
}); });
@@ -2,6 +2,7 @@ import slugify from "@sindresorhus/slugify";
import ms from "ms"; import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { ProjectUserAdditionalPrivilegeTemporaryMode } from "@app/ee/services/project-user-additional-privilege/project-user-additional-privilege-types"; 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 { PROJECT_USER_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
@@ -23,7 +24,9 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
body: z.object({ body: z.object({
projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId), projectMembershipId: z.string().min(1).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.projectMembershipId),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug), slug: slugSchema({ min: 1, max: 60 }).optional().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions), permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.CREATE.permissions)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ z.object({
isTemporary: z.literal(false) isTemporary: z.literal(false)
@@ -81,7 +84,8 @@ export const registerUserAdditionalPrivilegeRouter = async (server: FastifyZodPr
slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug), slug: slugSchema({ min: 1, max: 60 }).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array() permissions: ProjectPermissionV2Schema.array()
.optional() .optional()
.describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions), .describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.permissions)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }), z.object({ isTemporary: z.literal(false).describe(PROJECT_USER_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary) }),
z.object({ z.object({
@@ -3,6 +3,7 @@ import ms from "ms";
import { z } from "zod"; import { z } from "zod";
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types"; import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-types";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs"; import { IDENTITY_ADDITIONAL_PRIVILEGE_V2 } from "@app/lib/api-docs";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
@@ -30,7 +31,9 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId), identityId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.identityId),
projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId), projectId: z.string().min(1).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.projectId),
slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug), slug: slugSchema({ min: 1, max: 60 }).optional().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.slug),
permissions: ProjectPermissionV2Schema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission), permissions: ProjectPermissionV2Schema.array()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.CREATE.permission)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ z.object({
isTemporary: z.literal(false) isTemporary: z.literal(false)
@@ -94,7 +97,8 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug), slug: slugSchema({ min: 1, max: 60 }).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.slug),
permissions: ProjectPermissionV2Schema.array() permissions: ProjectPermissionV2Schema.array()
.optional() .optional()
.describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission), .describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.privilegePermission)
.refine(checkForInvalidPermissionCombination),
type: z.discriminatedUnion("isTemporary", [ type: z.discriminatedUnion("isTemporary", [
z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }), z.object({ isTemporary: z.literal(false).describe(IDENTITY_ADDITIONAL_PRIVILEGE_V2.UPDATE.isTemporary) }),
z.object({ z.object({
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
import { z } from "zod"; import { z } from "zod";
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
import { PROJECT_ROLE } from "@app/lib/api-docs"; import { PROJECT_ROLE } from "@app/lib/api-docs";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -37,7 +38,9 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.CREATE.slug), .describe(PROJECT_ROLE.CREATE.slug),
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name), name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description), description: z.string().trim().nullish().describe(PROJECT_ROLE.CREATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.CREATE.permissions) permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_ROLE.CREATE.permissions)
.refine(checkForInvalidPermissionCombination)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -92,7 +95,10 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
.describe(PROJECT_ROLE.UPDATE.slug), .describe(PROJECT_ROLE.UPDATE.slug),
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name), name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description), description: z.string().trim().nullish().describe(PROJECT_ROLE.UPDATE.description),
permissions: ProjectPermissionV2Schema.array().describe(PROJECT_ROLE.UPDATE.permissions).optional() permissions: ProjectPermissionV2Schema.array()
.describe(PROJECT_ROLE.UPDATE.permissions)
.optional()
.superRefine(checkForInvalidPermissionCombination)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -1,12 +1,110 @@
import { MongoAbility } from "@casl/ability"; /* eslint-disable no-nested-ternary */
import { ForbiddenError, MongoAbility, PureAbility, subject } from "@casl/ability";
import { z } from "zod";
import { TOrganizations } from "@app/db/schemas"; import { TOrganizations } from "@app/db/schemas";
import { validatePermissionBoundary } from "@app/lib/casl/boundary"; import { validatePermissionBoundary } from "@app/lib/casl/boundary";
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type"; import { ActorAuthMethod, AuthMethod } from "@app/services/auth/auth-type";
import { OrgPermissionSet } from "./org-permission"; import { OrgPermissionSet } from "./org-permission";
import { ProjectPermissionSet } from "./project-permission"; import {
ProjectPermissionSecretActions,
ProjectPermissionSet,
ProjectPermissionSub,
ProjectPermissionV2Schema,
SecretSubjectFields
} from "./project-permission";
export function throwIfMissingSecretReadValueOrDescribePermission(
permission: MongoAbility<ProjectPermissionSet> | PureAbility,
action: Extract<
ProjectPermissionSecretActions,
ProjectPermissionSecretActions.ReadValue | ProjectPermissionSecretActions.DescribeSecret
>,
subjectFields?: SecretSubjectFields
) {
try {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeAndReadValue,
subject(ProjectPermissionSub.Secrets, subjectFields)
);
} else {
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSub.Secrets
);
}
} catch {
if (subjectFields) {
ForbiddenError.from(permission).throwUnlessCan(action, subject(ProjectPermissionSub.Secrets, subjectFields));
} else {
ForbiddenError.from(permission).throwUnlessCan(action, ProjectPermissionSub.Secrets);
}
}
}
export function hasSecretReadValueOrDescribePermission(
permission: MongoAbility<ProjectPermissionSet>,
action: Extract<
ProjectPermissionSecretActions,
ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue
>,
subjectFields?: SecretSubjectFields
) {
let canNewPermission = false;
let canOldPermission = false;
if (subjectFields) {
canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields));
canOldPermission = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
subject(ProjectPermissionSub.Secrets, subjectFields)
);
} else {
canNewPermission = permission.can(action, ProjectPermissionSub.Secrets);
canOldPermission = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSub.Secrets
);
}
return canNewPermission || canOldPermission;
}
const OptionalArrayPermissionSchema = ProjectPermissionV2Schema.array().optional();
export function checkForInvalidPermissionCombination(permissions: z.infer<typeof OptionalArrayPermissionSchema>) {
if (!permissions) return;
for (const permission of permissions) {
if (permission.subject === ProjectPermissionSub.Secrets) {
if (permission.action.includes(ProjectPermissionSecretActions.DescribeAndReadValue)) {
const hasReadValue = permission.action.includes(ProjectPermissionSecretActions.ReadValue);
const hasDescribeSecret = permission.action.includes(ProjectPermissionSecretActions.DescribeSecret);
// eslint-disable-next-line no-continue
if (!hasReadValue && !hasDescribeSecret) continue;
const hasBothDescribeAndReadValue = hasReadValue && hasDescribeSecret;
throw new BadRequestError({
message: `You have selected Read, and ${
hasBothDescribeAndReadValue
? "both Read Value and Describe Secret"
: hasReadValue
? "Read Value"
: hasDescribeSecret
? "Describe Secret"
: ""
}. You cannot select Read Value or Describe Secret if you have selected Read. The Read permission is a legacy action which has been replaced by Describe Secret and Read Value.`
});
}
}
}
return true;
}
function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) { function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
if (!actorAuthMethod) return false; if (!actorAuthMethod) return false;
@@ -54,13 +152,13 @@ const escapeHandlebarsMissingMetadata = (obj: Record<string, string>) => {
// the new privilege management system is based on the actor having the appropriate permission to perform the privilege change, // the new privilege management system is based on the actor having the appropriate permission to perform the privilege change,
// regardless of the actor's privilege level. // regardless of the actor's privilege level.
const validatePrivilegeChangeOperation = ( const validatePrivilegeChangeOperation = (
action: OrgPermissionSet[0] | ProjectPermissionSet[0], opAction: OrgPermissionSet[0] | ProjectPermissionSet[0],
subject: OrgPermissionSet[1] | ProjectPermissionSet[1], opSubject: OrgPermissionSet[1] | ProjectPermissionSet[1],
actorPermission: MongoAbility, actorPermission: MongoAbility,
managedPermission: MongoAbility managedPermission: MongoAbility
) => { ) => {
// first we ensure if the actor has the permission to manage the privilege // first we ensure if the actor has the permission to manage the privilege
if (actorPermission.can(action, subject)) { if (actorPermission.can(opAction, opSubject)) {
return { return {
isValid: true, isValid: true,
missingPermissions: [] missingPermissions: []
@@ -17,6 +17,15 @@ export enum ProjectPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum ProjectPermissionSecretActions {
DescribeAndReadValue = "read",
DescribeSecret = "describeSecret",
ReadValue = "readValue",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum ProjectPermissionCmekActions { export enum ProjectPermissionCmekActions {
Read = "read", Read = "read",
Create = "create", Create = "create",
@@ -139,7 +148,7 @@ export type IdentityManagementSubjectFields = {
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionActions, ProjectPermissionSecretActions,
ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields) ProjectPermissionSub.Secrets | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
] ]
| [ | [
@@ -453,6 +462,7 @@ const GeneralPermissionSchema = [
}) })
]; ];
// Do not update this schema anymore, as it's kept purely for backwards compatability. Update V2 schema only.
export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [ export const ProjectPermissionV1Schema = z.discriminatedUnion("subject", [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
@@ -484,7 +494,7 @@ export const ProjectPermissionV2Schema = z.discriminatedUnion("subject", [
z.object({ z.object({
subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."), subject: z.literal(ProjectPermissionSub.Secrets).describe("The entity this permission pertains to."),
inverted: z.boolean().optional().describe("Whether rule allows or forbids."), inverted: z.boolean().optional().describe("Whether rule allows or forbids."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionSecretActions).describe(
"Describe what action an entity can take." "Describe what action an entity can take."
), ),
conditions: SecretConditionV2Schema.describe( conditions: SecretConditionV2Schema.describe(
@@ -541,7 +551,6 @@ const buildAdminPermissionRules = () => {
// Admins get full access to everything // Admins get full access to everything
[ [
ProjectPermissionSub.Secrets,
ProjectPermissionSub.SecretFolders, ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.SecretImports, ProjectPermissionSub.SecretImports,
ProjectPermissionSub.SecretApproval, ProjectPermissionSub.SecretApproval,
@@ -571,7 +580,7 @@ const buildAdminPermissionRules = () => {
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
ProjectPermissionActions.Delete ProjectPermissionActions.Delete
], ],
el as ProjectPermissionSub el
); );
}); });
@@ -608,6 +617,18 @@ const buildAdminPermissionRules = () => {
ProjectPermissionSub.Identity ProjectPermissionSub.Identity
); );
can(
[
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Delete
],
ProjectPermissionSub.Secrets
);
can( can(
[ [
ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionDynamicSecretActions.ReadRootCredential,
@@ -667,10 +688,12 @@ const buildMemberPermissionRules = () => {
can( can(
[ [
ProjectPermissionActions.Read, ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionActions.Create, ProjectPermissionSecretActions.ReadValue,
ProjectPermissionActions.Delete ProjectPermissionSecretActions.Edit,
ProjectPermissionSecretActions.Create,
ProjectPermissionSecretActions.Delete
], ],
ProjectPermissionSub.Secrets ProjectPermissionSub.Secrets
); );
@@ -842,7 +865,9 @@ export const projectMemberPermissions = buildMemberPermissionRules();
const buildViewerPermissionRules = () => { const buildViewerPermissionRules = () => {
const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, rules } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionSecretActions.DescribeAndReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSub.Secrets);
can(ProjectPermissionSecretActions.ReadValue, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretFolders);
can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets); can(ProjectPermissionDynamicSecretActions.ReadRootCredential, ProjectPermissionSub.DynamicSecrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretImports);
@@ -891,7 +916,6 @@ export const buildServiceTokenProjectPermission = (
(subject) => { (subject) => {
if (canWrite) { if (canWrite) {
can(ProjectPermissionActions.Edit, subject, { can(ProjectPermissionActions.Edit, subject, {
// TODO: @Akhi
// @ts-expect-error type // @ts-expect-error type
secretPath: { $glob: secretPath }, secretPath: { $glob: secretPath },
environment environment
@@ -970,7 +994,17 @@ export const backfillPermissionV1SchemaToV2Schema = (
subject: ProjectPermissionSub.SecretImports as const subject: ProjectPermissionSub.SecretImports as const
})); }));
const secretPolicies = secretSubjects.map(({ subject, ...el }) => ({
subject: ProjectPermissionSub.Secrets as const,
...el,
action:
el.action.includes(ProjectPermissionActions.Read) && !el.action.includes(ProjectPermissionSecretActions.ReadValue)
? el.action.concat(ProjectPermissionSecretActions.ReadValue)
: el.action
}));
const secretFolderPolicies = secretSubjects const secretFolderPolicies = secretSubjects
.map(({ subject, ...el }) => ({ .map(({ subject, ...el }) => ({
...el, ...el,
// read permission is not needed anymore // read permission is not needed anymore
@@ -1012,6 +1046,7 @@ export const backfillPermissionV1SchemaToV2Schema = (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-error this is valid ts // @ts-ignore-error this is valid ts
secretImportPolicies, secretImportPolicies,
secretPolicies,
dynamicSecretPolicies, dynamicSecretPolicies,
hasReadOnlyFolder.length ? [] : secretFolderPolicies hasReadOnlyFolder.length ? [] : secretFolderPolicies
); );
@@ -58,7 +58,7 @@ import { TUserDALFactory } from "@app/services/user/user-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import { ProjectPermissionSecretActions, ProjectPermissionSub } from "../permission/project-permission";
import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal"; import { TSecretApprovalPolicyDALFactory } from "../secret-approval-policy/secret-approval-policy-dal";
import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service"; import { TSecretSnapshotServiceFactory } from "../secret-snapshot/secret-snapshot-service";
import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal"; import { TSecretApprovalRequestDALFactory } from "./secret-approval-request-dal";
@@ -77,6 +77,7 @@ import {
TSecretApprovalDetailsDTO, TSecretApprovalDetailsDTO,
TStatusChangeDTO TStatusChangeDTO
} from "./secret-approval-request-types"; } from "./secret-approval-request-types";
import { throwIfMissingSecretReadValueOrDescribePermission } from "../permission/permission-fns";
type TSecretApprovalRequestServiceFactoryDep = { type TSecretApprovalRequestServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
@@ -88,7 +89,12 @@ type TSecretApprovalRequestServiceFactoryDep = {
secretDAL: TSecretDALFactory; secretDAL: TSecretDALFactory;
secretTagDAL: Pick< secretTagDAL: Pick<
TSecretTagDALFactory, TSecretTagDALFactory,
"findManyTagsById" | "saveTagsToSecret" | "deleteTagsManySecret" | "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "findManyTagsById"
| "saveTagsToSecret"
| "deleteTagsManySecret"
| "saveTagsToSecretV2"
| "deleteTagsToSecretV2"
| "find"
>; >;
secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">; secretBlindIndexDAL: Pick<TSecretBlindIndexDALFactory, "findOne">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
@@ -106,7 +112,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@@ -912,10 +918,11 @@ export const secretApprovalRequestServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) environment,
); secretPath
});
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -1000,6 +1007,7 @@ export const secretApprovalRequestServiceFactory = ({
: keyName2BlindIndex[secretName]; : keyName2BlindIndex[secretName];
// add tags // add tags
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds; if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
return { return {
...latestSecretVersions[secretId], ...latestSecretVersions[secretId],
...el, ...el,
@@ -1363,9 +1371,9 @@ export const secretApprovalRequestServiceFactory = ({
const tagsGroupById = groupBy(tags, (i) => i.id); const tagsGroupById = groupBy(tags, (i) => i.id);
commits.forEach((commit) => { commits.forEach((commit) => {
let action = ProjectPermissionActions.Create; let action = ProjectPermissionSecretActions.Create;
if (commit.op === SecretOperations.Update) action = ProjectPermissionActions.Edit; if (commit.op === SecretOperations.Update) action = ProjectPermissionSecretActions.Edit;
if (commit.op === SecretOperations.Delete) action = ProjectPermissionActions.Delete; if (commit.op === SecretOperations.Delete) action = ProjectPermissionSecretActions.Delete;
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
action, action,
@@ -265,6 +265,7 @@ export const secretReplicationServiceFactory = ({
folderDAL, folderDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
viewSecretValue: true,
hasSecretAccess: () => true hasSecretAccess: () => true
}); });
// secrets that gets replicated across imports // secrets that gets replicated across imports
@@ -15,7 +15,11 @@ import { TSecretV2BridgeDALFactory } from "@app/services/secret-v2-bridge/secret
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "../permission/project-permission";
import { TSecretRotationDALFactory } from "./secret-rotation-dal"; import { TSecretRotationDALFactory } from "./secret-rotation-dal";
import { TSecretRotationQueueFactory } from "./secret-rotation-queue"; import { TSecretRotationQueueFactory } from "./secret-rotation-queue";
import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types"; import { TSecretRotationEncData } from "./secret-rotation-queue/secret-rotation-queue-types";
@@ -106,7 +110,7 @@ export const secretRotationServiceFactory = ({
}); });
} }
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument */
// akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error // akhilmhdh: I did this, quite strange bug with eslint. Everything do have a type stil has this error
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas"; import { ActionProjectType, TableName, TSecretTagJunctionInsert, TSecretV2TagJunctionInsert } from "@app/db/schemas";
import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto"; import { decryptSymmetric128BitHexKeyUTF8 } from "@app/lib/crypto";
@@ -12,6 +12,7 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types"; import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
import { TSecretDALFactory } from "@app/services/secret/secret-dal"; import { TSecretDALFactory } from "@app/services/secret/secret-dal";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "@app/services/secret/secret-fns";
import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal"; import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-dal";
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
@@ -22,8 +23,16 @@ import { TSecretVersionV2DALFactory } from "@app/services/secret-v2-bridge/secre
import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "@app/services/secret-v2-bridge/secret-version-tag-dal";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import {
hasSecretReadValueOrDescribePermission,
throwIfMissingSecretReadValueOrDescribePermission
} from "../permission/permission-fns";
import { TPermissionServiceFactory } from "../permission/permission-service"; import { TPermissionServiceFactory } from "../permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "../permission/project-permission";
import { import {
TGetSnapshotDataDTO, TGetSnapshotDataDTO,
TProjectSnapshotCountDTO, TProjectSnapshotCountDTO,
@@ -97,10 +106,10 @@ export const secretSnapshotServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) secretPath: path
); });
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) { if (!folder) {
@@ -134,10 +143,10 @@ export const secretSnapshotServiceFactory = ({
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder. // We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) secretPath: path
); });
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) if (!folder)
@@ -162,6 +171,7 @@ export const secretSnapshotServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const shouldUseBridge = snapshot.projectVersion === 3; const shouldUseBridge = snapshot.projectVersion === 3;
let snapshotDetails; let snapshotDetails;
if (shouldUseBridge) { if (shouldUseBridge) {
@@ -170,68 +180,112 @@ export const secretSnapshotServiceFactory = ({
projectId: snapshot.projectId projectId: snapshot.projectId
}); });
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id); const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotV2DataById(id);
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: encryptedSnapshotDetails.folderId,
envId: encryptedSnapshotDetails.environment.id
});
snapshotDetails = { snapshotDetails = {
...encryptedSnapshotDetails, ...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
...el, const canReadValue = hasSecretReadValueOrDescribePermission(
secretKey: el.key, permission,
secretValue: el.encryptedValue ProjectPermissionSecretActions.ReadValue,
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString() {
: "", environment: encryptedSnapshotDetails.environment.slug,
secretComment: el.encryptedComment secretPath: fullFolderPath,
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString() secretName: el.key,
: "" secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
})) }
);
let secretValue = "";
if (canReadValue) {
secretValue = el.encryptedValue
? secretManagerDecryptor({ cipherTextBlob: el.encryptedValue }).toString()
: "";
} else {
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
}
return {
...el,
secretKey: el.key,
secretValueHidden: !canReadValue,
secretValue,
secretComment: el.encryptedComment
? secretManagerDecryptor({ cipherTextBlob: el.encryptedComment }).toString()
: ""
};
})
}; };
} else { } else {
const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id); const encryptedSnapshotDetails = await snapshotDAL.findSecretSnapshotDataById(id);
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: encryptedSnapshotDetails.folderId,
envId: encryptedSnapshotDetails.environment.id
});
const { botKey } = await projectBotService.getBotKey(snapshot.projectId); const { botKey } = await projectBotService.getBotKey(snapshot.projectId);
if (!botKey) if (!botKey)
throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` }); throw new NotFoundError({ message: `Project bot key not found for project with ID '${snapshot.projectId}'` });
snapshotDetails = { snapshotDetails = {
...encryptedSnapshotDetails, ...encryptedSnapshotDetails,
secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => ({ secretVersions: encryptedSnapshotDetails.secretVersions.map((el) => {
...el, const secretKey = decryptSymmetric128BitHexKeyUTF8({
secretKey: decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretKeyCiphertext, ciphertext: el.secretKeyCiphertext,
iv: el.secretKeyIV, iv: el.secretKeyIV,
tag: el.secretKeyTag, tag: el.secretKeyTag,
key: botKey key: botKey
}), });
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext, const canReadValue = hasSecretReadValueOrDescribePermission(
iv: el.secretValueIV, permission,
tag: el.secretValueTag, ProjectPermissionSecretActions.ReadValue,
key: botKey {
}), environment: encryptedSnapshotDetails.environment.slug,
secretComment: secretPath: fullFolderPath,
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext secretName: secretKey,
? decryptSymmetric128BitHexKeyUTF8({ secretTags: el.tags.length ? el.tags.map((tag) => tag.slug) : undefined
ciphertext: el.secretCommentCiphertext, }
iv: el.secretCommentIV, );
tag: el.secretCommentTag,
key: botKey let secretValue = "";
})
: "" if (canReadValue) {
})) secretValue = decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key: botKey
});
} else {
secretValue = INFISICAL_SECRET_VALUE_HIDDEN_MASK;
}
return {
...el,
secretKey,
secretValueHidden: !canReadValue,
secretValue,
secretComment:
el.secretCommentTag && el.secretCommentIV && el.secretCommentCiphertext
? decryptSymmetric128BitHexKeyUTF8({
ciphertext: el.secretCommentCiphertext,
iv: el.secretCommentIV,
tag: el.secretCommentTag,
key: botKey
})
: ""
};
})
}; };
} }
const fullFolderPath = await getFullFolderPath({
folderDAL,
folderId: snapshotDetails.folderId,
envId: snapshotDetails.environment.id
});
// We need to check if the user has access to the secrets in the folder. If we don't do this, a user could theoretically access snapshot secret values even if they don't have read access to the secrets in the folder.
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: snapshotDetails.environment.slug,
secretPath: fullFolderPath
})
);
return snapshotDetails; return snapshotDetails;
}; };
+5 -1
View File
@@ -459,7 +459,8 @@ export const PROJECTS = {
workspaceId: "The ID of the project to update.", workspaceId: "The ID of the project to update.",
name: "The new name of the project.", name: "The new name of the project.",
projectDescription: "An optional description label for the project.", projectDescription: "An optional description label for the project.",
autoCapitalization: "Disable or enable auto-capitalization for the project." autoCapitalization: "Disable or enable auto-capitalization for the project.",
slug: "An optional slug for the project. (must be unique within the organization)"
}, },
GET_KEY: { GET_KEY: {
workspaceId: "The ID of the project to get the key from." workspaceId: "The ID of the project to get the key from."
@@ -666,6 +667,7 @@ export const SECRETS = {
secretPath: "The path of the secret to attach tags to.", secretPath: "The path of the secret to attach tags to.",
type: "The type of the secret to attach tags to. (shared/personal)", type: "The type of the secret to attach tags to. (shared/personal)",
environment: "The slug of the environment where the secret is located", environment: "The slug of the environment where the secret is located",
viewSecretValue: "Whether or not to retrieve the secret value.",
projectSlug: "The slug of the project where the secret is located.", projectSlug: "The slug of the project where the secret is located.",
tagSlugs: "An array of existing tag slugs to attach to the secret." tagSlugs: "An array of existing tag slugs to attach to the secret."
}, },
@@ -689,6 +691,7 @@ export const RAW_SECRETS = {
"The slug of the project to list secrets from. This parameter is only applicable by machine identities.", "The slug of the project to list secrets from. This parameter is only applicable by machine identities.",
environment: "The slug of the environment to list secrets from.", environment: "The slug of the environment to list secrets from.",
secretPath: "The secret path to list secrets from.", secretPath: "The secret path to list secrets from.",
viewSecretValue: "Whether or not to retrieve the secret value.",
includeImports: "Weather to include imported secrets or not.", includeImports: "Weather to include imported secrets or not.",
tagSlugs: "The comma separated tag slugs to filter secrets.", tagSlugs: "The comma separated tag slugs to filter secrets.",
metadataFilter: metadataFilter:
@@ -717,6 +720,7 @@ export const RAW_SECRETS = {
secretPath: "The path of the secret to get.", secretPath: "The path of the secret to get.",
version: "The version of the secret to get.", version: "The version of the secret to get.",
type: "The type of the secret to get.", type: "The type of the secret to get.",
viewSecretValue: "Whether or not to retrieve the secret value.",
includeImports: "Weather to include imported secrets or not." includeImports: "Weather to include imported secrets or not."
}, },
UPDATE: { UPDATE: {
+1
View File
@@ -1,4 +1,5 @@
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
export class DatabaseError extends Error { export class DatabaseError extends Error {
name: string; name: string;
@@ -7,6 +7,7 @@ import {
ProjectRolesSchema, ProjectRolesSchema,
ProjectsSchema, ProjectsSchema,
SecretApprovalPoliciesSchema, SecretApprovalPoliciesSchema,
SecretTagsSchema,
UsersSchema UsersSchema
} from "@app/db/schemas"; } from "@app/db/schemas";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
@@ -241,3 +242,11 @@ export const SanitizedProjectSchema = ProjectsSchema.pick({
kmsCertificateKeyId: true, kmsCertificateKeyId: true,
auditLogsRetentionDays: true auditLogsRetentionDays: true
}); });
export const SanitizedTagSchema = SecretTagsSchema.pick({
id: true,
slug: true,
color: true
}).extend({
name: z.string()
});
+6 -1
View File
@@ -118,7 +118,12 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
querystring: z.object({ querystring: z.object({
searchTerm: z.string().default(""), searchTerm: z.string().default(""),
offset: z.coerce.number().default(0), offset: z.coerce.number().default(0),
limit: z.coerce.number().max(100).default(20) limit: z.coerce.number().max(100).default(20),
// TODO: remove this once z.coerce.boolean() is supported
adminsOnly: z
.string()
.transform((val) => val === "true")
.default("false")
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -1,10 +1,11 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { z } from "zod"; import { z } from "zod";
import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema, SecretTagsSchema } from "@app/db/schemas"; import { ActionProjectType, SecretFoldersSchema, SecretImportsSchema } from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { import {
ProjectPermissionDynamicSecretActions, ProjectPermissionDynamicSecretActions,
ProjectPermissionSecretActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
import { DASHBOARD } from "@app/lib/api-docs"; import { DASHBOARD } from "@app/lib/api-docs";
@@ -15,7 +16,7 @@ import { secretsLimit } from "@app/server/config/rateLimiter";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry"; import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log"; import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedDynamicSecretSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas"; import { SanitizedDynamicSecretSchema, SanitizedTagSchema, secretRawSchema } from "@app/server/routes/sanitizedSchemas";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema"; import { ResourceMetadataSchema } from "@app/services/resource-metadata/resource-metadata-schema";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
@@ -116,16 +117,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(), dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional(), .optional(),
@@ -294,6 +289,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
secrets = await server.services.secret.getSecretsRawMultiEnv({ secrets = await server.services.secret.getSecretsRawMultiEnv({
viewSecretValue: true,
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
@@ -393,6 +389,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(), .optional(),
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(), search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(), tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
viewSecretValue: booleanSchema.default(true),
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets), includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders), includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets), includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
@@ -410,16 +407,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(), dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(),
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional(), .optional(),
@@ -601,23 +592,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}); });
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
const secretsRaw = await server.services.secret.getSecretsRaw({ secrets = (
actorId: req.permission.id, await server.services.secret.getSecretsRaw({
actor: req.permission.type, actorId: req.permission.id,
actorOrgId: req.permission.orgId, actor: req.permission.type,
environment, viewSecretValue: req.query.viewSecretValue,
actorAuthMethod: req.permission.authMethod, throwOnMissingReadValuePermission: false,
projectId, actorOrgId: req.permission.orgId,
path: secretPath, environment,
orderBy, actorAuthMethod: req.permission.authMethod,
orderDirection, projectId,
search, path: secretPath,
limit: remainingLimit, orderBy,
offset: adjustedOffset, orderDirection,
tagSlugs: tags search,
}); limit: remainingLimit,
offset: adjustedOffset,
secrets = secretsRaw.secrets; tagSlugs: tags
})
).secrets;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
projectId, projectId,
@@ -696,16 +689,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(), .optional(),
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional() .optional()
@@ -749,6 +736,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
const secrets = await server.services.secret.getSecretsRawByFolderMappings( const secrets = await server.services.secret.getSecretsRawByFolderMappings(
{ {
filterByAction: ProjectPermissionSecretActions.DescribeSecret,
projectId, projectId,
folderMappings, folderMappings,
filters: { filters: {
@@ -846,6 +834,52 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/accessible-secrets",
config: {
rateLimit: secretsLimit
},
schema: {
querystring: z.object({
projectId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
filterByAction: z
.enum([ProjectPermissionSecretActions.DescribeSecret, ProjectPermissionSecretActions.ReadValue])
.default(ProjectPermissionSecretActions.ReadValue)
}),
response: {
200: z.object({
secrets: secretRawSchema
.extend({
secretPath: z.string().optional(),
secretValueHidden: z.boolean()
})
.array()
.optional()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { projectId, environment, secretPath, filterByAction } = req.query;
const { secrets } = await server.services.secret.getAccessibleSecrets({
actorId: req.permission.id,
actor: req.permission.type,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
environment,
secretPath,
projectId,
filterByAction
});
return { secrets };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/secrets-by-keys", url: "/secrets-by-keys",
@@ -862,22 +896,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
projectId: z.string().trim(), projectId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash), secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
keys: z.string().trim().transform(decodeURIComponent) keys: z.string().trim().transform(decodeURIComponent),
viewSecretValue: booleanSchema.default(false)
}), }),
response: { response: {
200: z.object({ 200: z.object({
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array() .array()
.optional() .optional()
@@ -886,7 +915,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}, },
onRequest: verifyAuth([AuthMode.JWT]), onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => { handler: async (req) => {
const { secretPath, projectId, environment } = req.query; const { secretPath, projectId, environment, viewSecretValue } = req.query;
const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? []; const keys = req.query.keys?.split(",").filter((key) => Boolean(key.trim())) ?? [];
if (!keys.length) throw new BadRequestError({ message: "One or more keys required" }); if (!keys.length) throw new BadRequestError({ message: "One or more keys required" });
@@ -895,6 +924,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
actorOrgId: req.permission.orgId, actorOrgId: req.permission.orgId,
viewSecretValue,
environment, environment,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId, projectId,
@@ -6,6 +6,7 @@ import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validateSignUpAuthorization } from "@app/services/auth/auth-fns"; import { validateSignUpAuthorization } from "@app/services/auth/auth-fns";
import { AuthMode } from "@app/services/auth/auth-type"; import { AuthMode } from "@app/services/auth/auth-type";
import { UserEncryption } from "@app/services/user/user-types";
export const registerPasswordRouter = async (server: FastifyZodProvider) => { export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({ server.route({
@@ -113,20 +114,16 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
message: z.string(),
user: UsersSchema, user: UsersSchema,
token: z.string() token: z.string(),
userEncryptionVersion: z.nativeEnum(UserEncryption)
}) })
} }
}, },
handler: async (req) => { handler: async (req) => {
const { token, user } = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code); const passwordReset = await server.services.password.verifyPasswordResetEmail(req.body.email, req.body.code);
return { return passwordReset;
message: "Successfully verified email",
user,
token
};
} }
}); });
+13 -2
View File
@@ -307,7 +307,17 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
.max(256, { message: "Description must be 256 or fewer characters" }) .max(256, { message: "Description must be 256 or fewer characters" })
.optional() .optional()
.describe(PROJECTS.UPDATE.projectDescription), .describe(PROJECTS.UPDATE.projectDescription),
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization) autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization),
slug: z
.string()
.trim()
.regex(
/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/,
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
)
.max(64, { message: "Slug must be 64 characters or fewer" })
.optional()
.describe(PROJECTS.UPDATE.slug)
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -325,7 +335,8 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
update: { update: {
name: req.body.name, name: req.body.name,
description: req.body.description, description: req.body.description,
autoCapitalization: req.body.autoCapitalization autoCapitalization: req.body.autoCapitalization,
slug: req.body.slug
}, },
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
actorId: req.permission.id, actorId: req.permission.id,
+2
View File
@@ -3,6 +3,7 @@ import { registerIdentityOrgRouter } from "./identity-org-router";
import { registerIdentityProjectRouter } from "./identity-project-router"; import { registerIdentityProjectRouter } from "./identity-project-router";
import { registerMfaRouter } from "./mfa-router"; import { registerMfaRouter } from "./mfa-router";
import { registerOrgRouter } from "./organization-router"; import { registerOrgRouter } from "./organization-router";
import { registerPasswordRouter } from "./password-router";
import { registerProjectMembershipRouter } from "./project-membership-router"; import { registerProjectMembershipRouter } from "./project-membership-router";
import { registerProjectRouter } from "./project-router"; import { registerProjectRouter } from "./project-router";
import { registerServiceTokenRouter } from "./service-token-router"; import { registerServiceTokenRouter } from "./service-token-router";
@@ -12,6 +13,7 @@ export const registerV2Routes = async (server: FastifyZodProvider) => {
await server.register(registerMfaRouter, { prefix: "/auth" }); await server.register(registerMfaRouter, { prefix: "/auth" });
await server.register(registerUserRouter, { prefix: "/users" }); await server.register(registerUserRouter, { prefix: "/users" });
await server.register(registerServiceTokenRouter, { prefix: "/service-token" }); await server.register(registerServiceTokenRouter, { prefix: "/service-token" });
await server.register(registerPasswordRouter, { prefix: "/password" });
await server.register( await server.register(
async (orgRouter) => { async (orgRouter) => {
await orgRouter.register(registerOrgRouter); await orgRouter.register(registerOrgRouter);
@@ -0,0 +1,53 @@
import { z } from "zod";
import { authRateLimit } from "@app/server/config/rateLimiter";
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
import { AuthMode } from "@app/services/auth/auth-type";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { ResetPasswordV2Type } from "@app/services/auth/auth-password-type";
export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/password-reset",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
newPassword: z.string().trim()
})
},
handler: async (req) => {
const token = validatePasswordResetAuthorization(req.headers.authorization);
await server.services.password.resetPasswordV2({
type: ResetPasswordV2Type.Recovery,
newPassword: req.body.newPassword,
userId: token.userId
});
}
});
server.route({
method: "POST",
url: "/user/password-reset",
schema: {
body: z.object({
oldPassword: z.string().trim(),
newPassword: z.string().trim()
})
},
config: {
rateLimit: authRateLimit
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
await server.services.password.resetPasswordV2({
type: ResetPasswordV2Type.LoggedInReset,
userId: req.permission.id,
newPassword: req.body.newPassword,
oldPassword: req.body.oldPassword
});
}
});
};
+58 -99
View File
@@ -1,13 +1,7 @@
import picomatch from "picomatch"; import picomatch from "picomatch";
import { z } from "zod"; import { z } from "zod";
import { import { SecretApprovalRequestsSchema, SecretsSchema, SecretType, ServiceTokenScopes } from "@app/db/schemas";
SecretApprovalRequestsSchema,
SecretsSchema,
SecretTagsSchema,
SecretType,
ServiceTokenScopes
} from "@app/db/schemas";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs"; import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -23,7 +17,7 @@ import { SecretOperations, SecretProtectionType } from "@app/services/secret/sec
import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; import { SecretUpdateMode } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types"; import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { secretRawSchema } from "../sanitizedSchemas"; import { SanitizedTagSchema, secretRawSchema } from "../sanitizedSchemas";
const SecretReferenceNode = z.object({ const SecretReferenceNode = z.object({
key: z.string(), key: z.string(),
@@ -31,6 +25,14 @@ const SecretReferenceNode = z.object({
environment: z.string(), environment: z.string(),
secretPath: z.string() secretPath: z.string()
}); });
const convertStringBoolean = (defaultValue: boolean = false) => {
return z
.enum(["true", "false"])
.default(defaultValue ? "true" : "false")
.transform((value) => value === "true");
};
type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] }; type TSecretReferenceNode = z.infer<typeof SecretReferenceNode> & { children: TSecretReferenceNode[] };
const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({ const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReferenceNode.extend({
@@ -75,17 +77,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
z.object({ tags: SanitizedTagSchema.array()
tags: SecretTagsSchema.pick({ })
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
})
)
}) })
} }
}, },
@@ -139,13 +133,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
}) })
}) })
} }
@@ -247,21 +235,10 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug), workspaceSlug: z.string().trim().optional().describe(RAW_SECRETS.LIST.workspaceSlug),
environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment), environment: z.string().trim().optional().describe(RAW_SECRETS.LIST.environment),
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.LIST.secretPath),
expandSecretReferences: z viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.LIST.viewSecretValue),
.enum(["true", "false"]) expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.LIST.expand),
.default("false") recursive: convertStringBoolean().describe(RAW_SECRETS.LIST.recursive),
.transform((value) => value === "true") include_imports: convertStringBoolean().describe(RAW_SECRETS.LIST.includeImports),
.describe(RAW_SECRETS.LIST.expand),
recursive: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.recursive),
include_imports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.LIST.includeImports),
tagSlugs: z tagSlugs: z
.string() .string()
.describe(RAW_SECRETS.LIST.tagSlugs) .describe(RAW_SECRETS.LIST.tagSlugs)
@@ -274,15 +251,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema secrets: secretRawSchema
.extend({ .extend({
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretValueHidden: z.boolean(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional()
}) })
.array(), .array(),
imports: z imports: z
@@ -293,6 +264,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secrets: secretRawSchema secrets: secretRawSchema
.omit({ createdAt: true, updatedAt: true }) .omit({ createdAt: true, updatedAt: true })
.extend({ .extend({
secretValueHidden: z.boolean(),
secretMetadata: ResourceMetadataSchema.optional() secretMetadata: ResourceMetadataSchema.optional()
}) })
.array() .array()
@@ -342,6 +314,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
expandSecretReferences: req.query.expandSecretReferences, expandSecretReferences: req.query.expandSecretReferences,
actorAuthMethod: req.permission.authMethod, actorAuthMethod: req.permission.authMethod,
projectId: workspaceId, projectId: workspaceId,
viewSecretValue: req.query.viewSecretValue,
path: secretPath, path: secretPath,
metadataFilter: req.query.metadataFilter, metadataFilter: req.query.metadataFilter,
includeImports: req.query.include_imports, includeImports: req.query.include_imports,
@@ -376,6 +349,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
} }
}); });
} }
return { secrets, imports }; return { secrets, imports };
} }
}); });
@@ -394,14 +368,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
secret: secretRawSchema.extend({ secret: secretRawSchema.extend({
secretPath: z.string(), secretPath: z.string(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array().optional(),
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional(),
secretMetadata: ResourceMetadataSchema.optional() secretMetadata: ResourceMetadataSchema.optional()
}) })
}) })
@@ -445,28 +412,15 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath), secretPath: z.string().trim().default("/").transform(removeTrailingSlash).describe(RAW_SECRETS.GET.secretPath),
version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version), version: z.coerce.number().optional().describe(RAW_SECRETS.GET.version),
type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type), type: z.nativeEnum(SecretType).default(SecretType.Shared).describe(RAW_SECRETS.GET.type),
expandSecretReferences: z viewSecretValue: convertStringBoolean(true).describe(RAW_SECRETS.GET.viewSecretValue),
.enum(["true", "false"]) expandSecretReferences: convertStringBoolean().describe(RAW_SECRETS.GET.expand),
.default("false") include_imports: convertStringBoolean().describe(RAW_SECRETS.GET.includeImports)
.transform((value) => value === "true")
.describe(RAW_SECRETS.GET.expand),
include_imports: z
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
.describe(RAW_SECRETS.GET.includeImports)
}), }),
response: { response: {
200: z.object({ 200: z.object({
secret: secretRawSchema.extend({ secret: secretRawSchema.extend({
tags: SecretTagsSchema.pick({ secretValueHidden: z.boolean(),
id: true, tags: SanitizedTagSchema.array().optional(),
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
.optional(),
secretMetadata: ResourceMetadataSchema.optional() secretMetadata: ResourceMetadataSchema.optional()
}) })
}) })
@@ -498,6 +452,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
expandSecretReferences: req.query.expandSecretReferences, expandSecretReferences: req.query.expandSecretReferences,
environment, environment,
projectId: workspaceId, projectId: workspaceId,
viewSecretValue: req.query.viewSecretValue,
projectSlug: workspaceSlug, projectSlug: workspaceSlug,
path: secretPath, path: secretPath,
secretName: req.params.secretName, secretName: req.params.secretName,
@@ -704,7 +659,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secret: secretRawSchema secret: secretRawSchema.extend({
secretValueHidden: z.boolean()
})
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -800,7 +757,9 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secret: secretRawSchema secret: secretRawSchema.extend({
secretValueHidden: z.boolean()
})
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -822,6 +781,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
if (secretOperation.type === SecretProtectionType.Approval) { if (secretOperation.type === SecretProtectionType.Approval) {
return { approval: secretOperation.approval }; return { approval: secretOperation.approval };
} }
const { secret } = secretOperation; const { secret } = secretOperation;
await server.services.auditLog.createAuditLog({ await server.services.auditLog.createAuditLog({
@@ -884,13 +844,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
workspace: z.string(), workspace: z.string(),
environment: z.string(), environment: z.string(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
tags: SecretTagsSchema.pick({ tags: SanitizedTagSchema.array()
id: true,
slug: true,
color: true
})
.extend({ name: z.string() })
.array()
}) })
.array(), .array(),
imports: z imports: z
@@ -986,10 +940,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretPath: z.string().trim().default("/").transform(removeTrailingSlash), secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
type: z.nativeEnum(SecretType).default(SecretType.Shared), type: z.nativeEnum(SecretType).default(SecretType.Shared),
version: z.coerce.number().optional(), version: z.coerce.number().optional(),
include_imports: z include_imports: convertStringBoolean()
.enum(["true", "false"])
.default("false")
.transform((value) => value === "true")
}), }),
response: { response: {
200: z.object({ 200: z.object({
@@ -1260,6 +1211,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
z.object({ z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( secret: SecretsSchema.omit({ secretBlindIndex: true }).merge(
z.object({ z.object({
secretValueHidden: z.boolean(),
_id: z.string(), _id: z.string(),
workspace: z.string(), workspace: z.string(),
environment: z.string() environment: z.string()
@@ -1429,13 +1381,12 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secret: SecretsSchema.omit({ secretBlindIndex: true }).merge( secret: SecretsSchema.omit({ secretBlindIndex: true }).extend({
z.object({ _id: z.string(),
_id: z.string(), secretValueHidden: z.boolean(),
workspace: z.string(), workspace: z.string(),
environment: z.string() environment: z.string()
}) })
)
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -1747,7 +1698,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() secrets: SecretsSchema.omit({ secretBlindIndex: true }).extend({ secretValueHidden: z.boolean() }).array()
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -1862,7 +1813,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: SecretsSchema.omit({ secretBlindIndex: true }).array() secrets: SecretsSchema.omit({ secretBlindIndex: true })
.extend({
secretValueHidden: z.boolean()
})
.array()
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -2124,7 +2079,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: secretRawSchema.array() secrets: secretRawSchema.extend({ secretValueHidden: z.boolean() }).array()
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
@@ -2246,7 +2201,11 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.union([ 200: z.union([
z.object({ z.object({
secrets: secretRawSchema.array() secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean()
})
.array()
}), }),
z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled") z.object({ approval: SecretApprovalRequestsSchema }).describe("When secret protection policy is enabled")
]) ])
+30
View File
@@ -45,6 +45,36 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid
if (decodedToken.userId !== userId) throw new UnauthorizedError(); if (decodedToken.userId !== userId) throw new UnauthorizedError();
}; };
export const validatePasswordResetAuthorization = (token?: string) => {
if (!token) throw new UnauthorizedError();
const appCfg = getConfig();
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>token?.split(" ", 2) ?? [null, null];
if (AUTH_TOKEN_TYPE === null) {
throw new UnauthorizedError({ message: "Missing Authorization Header in the request header." });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
throw new UnauthorizedError({
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
});
}
if (AUTH_TOKEN_VALUE === null) {
throw new UnauthorizedError({
message: "Missing Authorization Body in the request header"
});
}
const decodedToken = jwt.verify(AUTH_TOKEN_VALUE, appCfg.AUTH_SECRET) as AuthModeProviderSignUpTokenPayload;
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) {
throw new UnauthorizedError({
message: `The provided authentication token type is not supported.`
});
}
return decodedToken;
};
export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => { export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
if (isLocked) { if (isLocked) {
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
@@ -4,6 +4,8 @@ import jwt from "jsonwebtoken";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas"; import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
@@ -12,14 +14,18 @@ import { TokenType } from "../auth-token/auth-token-types";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TTotpConfigDALFactory } from "../totp/totp-config-dal"; import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { UserEncryption } from "../user/user-types";
import { TAuthDALFactory } from "./auth-dal"; import { TAuthDALFactory } from "./auth-dal";
import { import {
ResetPasswordV2Type,
TChangePasswordDTO, TChangePasswordDTO,
TCreateBackupPrivateKeyDTO, TCreateBackupPrivateKeyDTO,
TResetPasswordV2DTO,
TResetPasswordViaBackupKeyDTO, TResetPasswordViaBackupKeyDTO,
TSetupPasswordViaBackupKeyDTO TSetupPasswordViaBackupKeyDTO
} from "./auth-password-type"; } from "./auth-password-type";
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type"; import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
import { logger } from "@app/lib/logger";
type TAuthPasswordServiceFactoryDep = { type TAuthPasswordServiceFactoryDep = {
authDAL: TAuthDALFactory; authDAL: TAuthDALFactory;
@@ -114,26 +120,31 @@ export const authPaswordServiceFactory = ({
* Email password reset flow via email. Step 1 send email * Email password reset flow via email. Step 1 send email
*/ */
const sendPasswordResetEmail = async (email: string) => { const sendPasswordResetEmail = async (email: string) => {
const user = await userDAL.findUserByUsername(email); const sendEmail = async () => {
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts const user = await userDAL.findUserByUsername(email);
if (!user || (user && !user.isAccepted)) return;
const cfg = getConfig(); if (user && user.isAccepted) {
const token = await tokenService.createTokenForUser({ const cfg = getConfig();
type: TokenType.TOKEN_EMAIL_PASSWORD_RESET, const token = await tokenService.createTokenForUser({
userId: user.id type: TokenType.TOKEN_EMAIL_PASSWORD_RESET,
}); userId: user.id
});
await smtpService.sendMail({ await smtpService.sendMail({
template: SmtpTemplates.ResetPassword, template: SmtpTemplates.ResetPassword,
recipients: [email], recipients: [email],
subjectLine: "Infisical password reset", subjectLine: "Infisical password reset",
substitutions: { substitutions: {
email, email,
token, token,
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : "" callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-reset` : ""
}
});
} }
}); };
// note(daniel): run in background to prevent timing attacks
void sendEmail().catch((err) => logger.error(err, "Failed to send password reset email"));
}; };
/* /*
@@ -142,6 +153,11 @@ export const authPaswordServiceFactory = ({
const verifyPasswordResetEmail = async (email: string, code: string) => { const verifyPasswordResetEmail = async (email: string, code: string) => {
const cfg = getConfig(); const cfg = getConfig();
const user = await userDAL.findUserByUsername(email); const user = await userDAL.findUserByUsername(email);
const userEnc = await userDAL.findUserEncKeyByUserId(user.id);
if (!userEnc) throw new BadRequestError({ message: "Failed to find user encryption data" });
// ignore as user is not found to avoid an outside entity to identify infisical registered accounts // ignore as user is not found to avoid an outside entity to identify infisical registered accounts
if (!user || (user && !user.isAccepted)) { if (!user || (user && !user.isAccepted)) {
throw new Error("Failed email verification for pass reset"); throw new Error("Failed email verification for pass reset");
@@ -162,8 +178,91 @@ export const authPaswordServiceFactory = ({
{ expiresIn: cfg.JWT_SIGNUP_LIFETIME } { expiresIn: cfg.JWT_SIGNUP_LIFETIME }
); );
return { token, user }; return { token, user, userEncryptionVersion: userEnc.encryptionVersion as UserEncryption };
}; };
const resetPasswordV2 = async ({ userId, newPassword, type, oldPassword }: TResetPasswordV2DTO) => {
const cfg = getConfig();
const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user) {
throw new BadRequestError({ message: `User encryption key not found for user with ID '${userId}'` });
}
if (!user.hashedPassword) {
throw new BadRequestError({ message: "Unable to reset password, no password is set" });
}
if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
}
// we check the old password if the user is resetting their password while logged in
if (type === ResetPasswordV2Type.LoggedInReset) {
if (!oldPassword) {
throw new BadRequestError({ message: "Current password is required." });
}
const isValid = await bcrypt.compare(oldPassword, user.hashedPassword);
if (!isValid) {
throw new BadRequestError({ message: "Incorrect current password." });
}
}
const newHashedPassword = await bcrypt.hash(newPassword, cfg.BCRYPT_SALT_ROUND);
// we need to get the original private key first for v2
let privateKey: string;
if (
user.serverEncryptedPrivateKey &&
user.serverEncryptedPrivateKeyTag &&
user.serverEncryptedPrivateKeyIV &&
user.serverEncryptedPrivateKeyEncoding &&
user.encryptionVersion === UserEncryption.V2
) {
privateKey = infisicalSymmetricDecrypt({
iv: user.serverEncryptedPrivateKeyIV,
tag: user.serverEncryptedPrivateKeyTag,
ciphertext: user.serverEncryptedPrivateKey,
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
} else {
throw new BadRequestError({
message: "Cannot reset password without current credentials or recovery method",
name: "Reset password"
});
}
const encKeys = await generateUserSrpKeys(user.username, newPassword, {
publicKey: user.publicKey,
privateKey
});
const { tag, iv, ciphertext, encoding } = infisicalSymmetricEncypt(privateKey);
await userDAL.updateUserEncryptionByUserId(userId, {
hashedPassword: newHashedPassword,
// srp params
salt: encKeys.salt,
verifier: encKeys.verifier,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
serverEncryptedPrivateKey: ciphertext,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyEncoding: encoding
});
await tokenService.revokeAllMySessions(userId);
};
/* /*
* Reset password of a user via backup key * Reset password of a user via backup key
* */ * */
@@ -391,6 +490,7 @@ export const authPaswordServiceFactory = ({
createBackupPrivateKey, createBackupPrivateKey,
getBackupPrivateKeyOfUser, getBackupPrivateKeyOfUser,
sendPasswordSetupEmail, sendPasswordSetupEmail,
setupPassword setupPassword,
resetPasswordV2
}; };
}; };
@@ -13,6 +13,18 @@ export type TChangePasswordDTO = {
password: string; password: string;
}; };
export enum ResetPasswordV2Type {
Recovery = "recovery",
LoggedInReset = "logged-in-reset"
}
export type TResetPasswordV2DTO = {
type: ResetPasswordV2Type;
userId: string;
newPassword: string;
oldPassword?: string;
};
export type TResetPasswordViaBackupKeyDTO = { export type TResetPasswordViaBackupKeyDTO = {
userId: string; userId: string;
protectedKey: string; protectedKey: string;
@@ -31,9 +31,9 @@ export type TImportDataIntoInfisicalDTO = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
@@ -27,9 +27,9 @@ export type TExternalMigrationQueueFactoryDep = {
projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">; projectEnvDAL: Pick<TProjectEnvDALFactory, "find" | "findLastEnvPosition" | "create" | "findOne">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "create">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "create" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany" | "create">;
folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findOne" | "findById">; folderDAL: Pick<TSecretFolderDALFactory, "create" | "findBySecretPath" | "findOne" | "findById">;
@@ -68,7 +68,8 @@ const getIntegrationSecretsV2 = async (
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
secretImportDAL, secretImportDAL,
secretImports, secretImports,
hasSecretAccess: () => true hasSecretAccess: () => true,
viewSecretValue: true
}); });
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
@@ -1,8 +1,13 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { NotFoundError } from "@app/lib/errors"; import { NotFoundError } from "@app/lib/errors";
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
@@ -91,13 +96,10 @@ export const integrationServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: sourceEnvironment,
subject(ProjectPermissionSub.Secrets, { secretPath
environment: sourceEnvironment, });
secretPath
})
);
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath); const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
if (!folder) { if (!folder) {
@@ -174,13 +176,10 @@ export const integrationServiceFactory = ({
const newSecretPath = secretPath || integration.secretPath; const newSecretPath = secretPath || integration.secretPath;
if (environment || secretPath) { if (environment || secretPath) {
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: newEnvironment,
subject(ProjectPermissionSub.Secrets, { secretPath: newSecretPath
environment: newEnvironment, });
secretPath: newSecretPath
})
);
} }
const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath); const folder = await folderDAL.findBySecretPath(integration.projectId, newEnvironment, newSecretPath);
@@ -10,8 +10,13 @@ import {
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service"; import { TProjectTemplateServiceFactory } from "@app/ee/services/project-template/project-template-service";
import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types"; import { InfisicalProjectTemplate } from "@app/ee/services/project-template/project-template-types";
import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal"; import { TSshCertificateAuthorityDALFactory } from "@app/ee/services/ssh/ssh-certificate-authority-dal";
@@ -563,11 +568,24 @@ export const projectServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Settings);
if (update.slug) {
const existingProject = await projectDAL.findOne({
slug: update.slug,
orgId: actorOrgId
});
if (existingProject && existingProject.id !== project.id) {
throw new BadRequestError({
message: `Failed to update project slug. The project "${existingProject.name}" with the slug "${existingProject.slug}" already exists in your organization. Please choose a unique slug for your project.`
});
}
}
const updatedProject = await projectDAL.updateById(project.id, { const updatedProject = await projectDAL.updateById(project.id, {
name: update.name, name: update.name,
description: update.description, description: update.description,
autoCapitalization: update.autoCapitalization, autoCapitalization: update.autoCapitalization,
enforceCapitalization: update.autoCapitalization enforceCapitalization: update.autoCapitalization,
slug: update.slug
}); });
return updatedProject; return updatedProject;
@@ -747,7 +765,7 @@ export const projectServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.Any actionProjectType: ActionProjectType.Any
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
const project = await projectDAL.findProjectById(projectId); const project = await projectDAL.findProjectById(projectId);
@@ -82,6 +82,7 @@ export type TUpdateProjectDTO = {
name?: string; name?: string;
description?: string; description?: string;
autoCapitalization?: boolean; autoCapitalization?: boolean;
slug?: string;
}; };
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
@@ -3,6 +3,7 @@ import { groupBy, unique } from "@app/lib/fn";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretDALFactory } from "../secret/secret-dal";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal"; import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-dal";
import { TSecretImportDALFactory } from "./secret-import-dal"; import { TSecretImportDALFactory } from "./secret-import-dal";
@@ -32,6 +33,12 @@ type TSecretImportSecretsV2 = {
folderId: string | undefined; folderId: string | undefined;
importFolderId: string; importFolderId: string;
secrets: (TSecretsV2 & { secrets: (TSecretsV2 & {
secretTags: {
slug: string;
name: string;
color?: string | null;
id: string;
}[];
workspace: string; workspace: string;
environment: string; environment: string;
_id: string; _id: string;
@@ -39,6 +46,7 @@ type TSecretImportSecretsV2 = {
// akhilmhdh: yes i know you can put ?. // akhilmhdh: yes i know you can put ?.
// But for somereason ts consider ? and undefined explicit as different just ts things // But for somereason ts consider ? and undefined explicit as different just ts things
secretValue: string; secretValue: string;
secretValueHidden: boolean;
secretComment: string; secretComment: string;
secretMetadata?: ResourceMetadataDTO; secretMetadata?: ResourceMetadataDTO;
})[]; })[];
@@ -150,12 +158,14 @@ export const fnSecretsV2FromImports = async ({
secretImportDAL, secretImportDAL,
decryptor, decryptor,
expandSecretReferences, expandSecretReferences,
hasSecretAccess hasSecretAccess,
viewSecretValue
}: { }: {
secretImports: (Omit<TSecretImports, "importEnv"> & { secretImports: (Omit<TSecretImports, "importEnv"> & {
importEnv: { id: string; slug: string; name: string }; importEnv: { id: string; slug: string; name: string };
})[]; })[];
folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findByManySecretPath">;
viewSecretValue: boolean;
secretDAL: Pick<TSecretV2BridgeDALFactory, "find">; secretDAL: Pick<TSecretV2BridgeDALFactory, "find">;
secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">; secretImportDAL: Pick<TSecretImportDALFactory, "findByFolderIds">;
decryptor: (value?: Buffer | null) => string; decryptor: (value?: Buffer | null) => string;
@@ -168,9 +178,14 @@ export const fnSecretsV2FromImports = async ({
hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean; hasSecretAccess: (environment: string, secretPath: string, secretName: string, secretTagSlugs: string[]) => boolean;
}) => { }) => {
const cyclicDetector = new Set(); const cyclicDetector = new Set();
const stack: { secretImports: typeof rootSecretImports; depth: number; parentImportedSecrets: TSecretsV2[] }[] = [ const stack: {
{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] } secretImports: typeof rootSecretImports;
]; depth: number;
parentImportedSecrets: (TSecretsV2 & {
secretValueHidden: boolean;
secretTags: { slug: string; name: string; id: string; color?: string | null }[];
})[];
}[] = [{ secretImports: rootSecretImports, depth: 0, parentImportedSecrets: [] }];
const processedImports: TSecretImportSecretsV2[] = []; const processedImports: TSecretImportSecretsV2[] = [];
@@ -229,7 +244,9 @@ export const fnSecretsV2FromImports = async ({
.map((item) => ({ .map((item) => ({
...item, ...item,
secretKey: item.key, secretKey: item.key,
secretValue: decryptor(item.encryptedValue), secretValue: viewSecretValue ? decryptor(item.encryptedValue) : INFISICAL_SECRET_VALUE_HIDDEN_MASK,
secretValueHidden: !viewSecretValue,
secretTags: item.tags,
secretComment: decryptor(item.encryptedComment), secretComment: decryptor(item.encryptedComment),
environment: importEnv.slug, environment: importEnv.slug,
workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend. workspace: "", // This field should not be used, it's only here to keep the older Python SDK versions backwards compatible with the new Postgres backend.
@@ -267,6 +284,8 @@ export const fnSecretsV2FromImports = async ({
processedImport.secrets = unique(processedImport.secrets, (i) => i.key); processedImport.secrets = unique(processedImport.secrets, (i) => i.key);
return Promise.allSettled( return Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => { processedImport.secrets.map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecretReferences({ const expandedSecretValue = await expandSecretReferences({
value: decryptedSecret.secretValue, value: decryptedSecret.secretValue,
secretPath: processedImport.secretPath, secretPath: processedImport.secretPath,
@@ -4,8 +4,16 @@ import { ForbiddenError, subject } from "@casl/ability";
import { ActionProjectType, TableName } from "@app/db/schemas"; import { ActionProjectType, TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
hasSecretReadValueOrDescribePermission,
throwIfMissingSecretReadValueOrDescribePermission
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service"; import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
@@ -89,13 +97,11 @@ export const secretImportServiceFactory = ({
); );
// check if user has permission to import from target path // check if user has permission to import from target path
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
ProjectPermissionActions.Read, environment: data.environment,
subject(ProjectPermissionSub.Secrets, { secretPath: data.path
environment: data.environment, });
secretPath: data.path
})
);
if (isReplication) { if (isReplication) {
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
if (!plan.secretApproval) { if (!plan.secretApproval) {
@@ -401,13 +407,10 @@ export const secretImportServiceFactory = ({
if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" }); if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" });
// check if user has permission to import from target path // check if user has permission to import from target path
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
ProjectPermissionActions.Read, environment: secretImportDoc.importEnv.slug,
subject(ProjectPermissionSub.Secrets, { secretPath: secretImportDoc.importPath
environment: secretImportDoc.importEnv.slug, });
secretPath: secretImportDoc.importPath
})
);
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
@@ -595,14 +598,12 @@ export const secretImportServiceFactory = ({
// so anything based on this order will also be in right position // so anything based on this order will also be in right position
const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false }); const secretImports = await secretImportDAL.find({ folderId: folder.id, isReplication: false });
const allowedImports = secretImports.filter((el) => const allowedImports = secretImports.filter((el) =>
permission.can( hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: el.importEnv.slug,
subject(ProjectPermissionSub.Secrets, { secretPath: el.importPath
environment: el.importEnv.slug, })
secretPath: el.importPath
})
)
); );
return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL }); return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL });
}; };
@@ -642,20 +643,19 @@ export const secretImportServiceFactory = ({
const importedSecrets = await fnSecretsV2FromImports({ const importedSecrets = await fnSecretsV2FromImports({
secretImports, secretImports,
folderDAL, folderDAL,
viewSecretValue: true,
secretDAL: secretV2BridgeDAL, secretDAL: secretV2BridgeDAL,
secretImportDAL, secretImportDAL,
decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""), decryptor: (value) => (value ? secretManagerDecryptor({ cipherTextBlob: value }).toString() : ""),
hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) => hasSecretAccess: (expandEnvironment, expandSecretPath, expandSecretKey, expandSecretTags) =>
permission.can( hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: expandEnvironment,
subject(ProjectPermissionSub.Secrets, { secretPath: expandSecretPath,
environment: expandEnvironment, secretName: expandSecretKey,
secretPath: expandSecretPath, secretTags: expandSecretTags
secretName: expandSecretKey, })
secretTags: expandSecretTags
})
)
}); });
return importedSecrets; return importedSecrets;
} }
@@ -666,13 +666,10 @@ export const secretImportServiceFactory = ({
}); });
const allowedImports = secretImports.filter((el) => const allowedImports = secretImports.filter((el) =>
permission.can( hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: el.importEnv.slug,
subject(ProjectPermissionSub.Secrets, { secretPath: el.importPath
environment: el.importEnv.slug, })
secretPath: el.importPath
})
)
); );
const importedSecrets = await fnSecretsFromImports({ const importedSecrets = await fnSecretsFromImports({
allowedImports, allowedImports,
@@ -683,7 +680,10 @@ export const secretImportServiceFactory = ({
return importedSecrets.map((el) => ({ return importedSecrets.map((el) => ({
...el, ...el,
secrets: el.secrets.map((encryptedSecret) => secrets: el.secrets.map((encryptedSecret) =>
decryptSecretRaw({ ...encryptedSecret, workspace: projectId, environment, secretPath }, botKey) decryptSecretRaw(
{ ...encryptedSecret, workspace: projectId, environment, secretPath, secretValueHidden: false },
botKey
)
) )
})); }));
}; };
@@ -249,7 +249,8 @@ export const secretSyncQueueFactory = ({
expandSecretReferences, expandSecretReferences,
secretImportDAL, secretImportDAL,
secretImports, secretImports,
hasSecretAccess: () => true hasSecretAccess: () => true,
viewSecretValue: true
}); });
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
@@ -1,9 +1,10 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { throwIfMissingSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { import {
ProjectPermissionActions, ProjectPermissionSecretActions,
ProjectPermissionSecretSyncActions, ProjectPermissionSecretSyncActions,
ProjectPermissionSub ProjectPermissionSub
} from "@app/ee/services/permission/project-permission"; } from "@app/ee/services/permission/project-permission";
@@ -178,13 +179,10 @@ export const secretSyncServiceFactory = ({
ProjectPermissionSub.SecretSyncs ProjectPermissionSub.SecretSyncs
); );
ForbiddenError.from(projectPermission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(projectPermission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { secretPath
environment, });
secretPath
})
);
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
@@ -269,13 +267,10 @@ export const secretSyncServiceFactory = ({
if (!updatedEnvironment || !updatedSecretPath) if (!updatedEnvironment || !updatedSecretPath)
throw new BadRequestError({ message: "Must specify both source environment and secret path" }); throw new BadRequestError({ message: "Must specify both source environment and secret path" });
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: updatedEnvironment,
subject(ProjectPermissionSub.Secrets, { secretPath: updatedSecretPath
environment: updatedEnvironment, });
secretPath: updatedSecretPath
})
);
const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath); const newFolder = await folderDAL.findBySecretPath(secretSync.projectId, updatedEnvironment, updatedSecretPath);
@@ -47,6 +47,7 @@ export const secretTagDALFactory = (db: TDbClient) => {
throw new DatabaseError({ error, name: "Find all by ids" }); throw new DatabaseError({ error, name: "Find all by ids" });
} }
}; };
return { return {
...secretTagOrm, ...secretTagOrm,
saveTagsToSecret: secretJnTagOrm.insertMany, saveTagsToSecret: secretJnTagOrm.insertMany,
@@ -8,6 +8,7 @@ import { logger } from "@app/lib/logger";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema"; import { ResourceMetadataDTO } from "../resource-metadata/resource-metadata-schema";
import { INFISICAL_SECRET_VALUE_HIDDEN_MASK } from "../secret/secret-fns";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal"; import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types"; import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
@@ -108,6 +109,7 @@ export const fnSecretBulkInsert = async ({
[`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id [`${TableName.SecretV2}Id` as const]: newSecretGroupedByKeyName[key][0].id
})) }))
); );
const secretVersions = await secretVersionDAL.insertMany( const secretVersions = await secretVersionDAL.insertMany(
sanitizedInputSecrets.map((el) => ({ sanitizedInputSecrets.map((el) => ({
...el, ...el,
@@ -146,6 +148,7 @@ export const fnSecretBulkInsert = async ({
if (newSecretTags.length) { if (newSecretTags.length) {
const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx); const secTags = await secretTagDAL.saveTagsToSecretV2(newSecretTags, tx);
const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId); const secVersionsGroupBySecId = groupBy(secretVersions, (i) => i.secretId);
const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({ const newSecretVersionTags = secTags.flatMap(({ secrets_v2Id, secret_tagsId }) => ({
[`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id, [`${TableName.SecretVersionV2}Id` as const]: secVersionsGroupBySecId[secrets_v2Id][0].id,
[`${TableName.SecretTag}Id` as const]: secret_tagsId [`${TableName.SecretTag}Id` as const]: secret_tagsId
@@ -154,7 +157,16 @@ export const fnSecretBulkInsert = async ({
await secretVersionTagDAL.insertMany(newSecretVersionTags, tx); await secretVersionTagDAL.insertMany(newSecretVersionTags, tx);
} }
return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); const secretsWithTags = await secretDAL.find(
{
$in: {
[`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id)
}
},
{ tx }
);
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
}; };
export const fnSecretBulkUpdate = async ({ export const fnSecretBulkUpdate = async ({
@@ -300,7 +312,15 @@ export const fnSecretBulkUpdate = async ({
tx tx
); );
return newSecrets.map((secret) => ({ ...secret, _id: secret.id })); const secretsWithTags = await secretDAL.find(
{
$in: {
[`${TableName.SecretV2}.id` as "id"]: newSecrets.map((s) => s.id)
}
},
{ tx }
);
return secretsWithTags.map((secret) => ({ ...secret, _id: secret.id }));
}; };
export const fnSecretBulkDelete = async ({ export const fnSecretBulkDelete = async ({
@@ -533,7 +553,7 @@ export const expandSecretReferencesFactory = ({
const referredValue = await fetchSecret(environment, secretPath, secretKey); const referredValue = await fetchSecret(environment, secretPath, secretKey);
if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags)) if (!canExpandValue(environment, secretPath, secretKey, referredValue.tags))
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to.` message: `You are attempting to reference secret named ${secretKey} from environment ${environment} in path ${secretPath} which you do not have access to read value on.`
}); });
const cacheKey = getCacheUniqueKey(environment, secretPath); const cacheKey = getCacheUniqueKey(environment, secretPath);
@@ -552,7 +572,7 @@ export const expandSecretReferencesFactory = ({
const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey); const referedValue = await fetchSecret(secretReferenceEnvironment, secretReferencePath, secretReferenceKey);
if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags)) if (!canExpandValue(secretReferenceEnvironment, secretReferencePath, secretReferenceKey, referedValue.tags))
throw new ForbiddenRequestError({ throw new ForbiddenRequestError({
message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to.` message: `You are attempting to reference secret named ${secretReferenceKey} from environment ${secretReferenceEnvironment} in path ${secretReferencePath} which you do not have access to read value on.`
}); });
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath); const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
@@ -646,13 +666,13 @@ export const reshapeBridgeSecret = (
name: string; name: string;
}[]; }[];
secretMetadata?: ResourceMetadataDTO; secretMetadata?: ResourceMetadataDTO;
} },
secretValueHidden: boolean
) => ({ ) => ({
secretKey: secret.key, secretKey: secret.key,
secretPath, secretPath,
workspace: workspaceId, workspace: workspaceId,
environment, environment,
secretValue: secret.value || "",
secretComment: secret.comment || "", secretComment: secret.comment || "",
version: secret.version, version: secret.version,
type: secret.type, type: secret.type,
@@ -674,5 +694,15 @@ export const reshapeBridgeSecret = (
metadata: secret.metadata, metadata: secret.metadata,
secretMetadata: secret.secretMetadata, secretMetadata: secret.secretMetadata,
createdAt: secret.createdAt, createdAt: secret.createdAt,
updatedAt: secret.updatedAt updatedAt: secret.updatedAt,
...(secretValueHidden
? {
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK,
secretValueHidden: true
}
: {
secretValue: secret.value || "",
secretValueHidden: false
})
}); });
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas"; import { SecretType, TSecretsV2, TSecretsV2Insert, TSecretsV2Update } from "@app/db/schemas";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { OrderByDirection, TProjectPermission } from "@app/lib/types";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { SecretsOrderBy } from "@app/services/secret/secret-types"; import { SecretsOrderBy } from "@app/services/secret/secret-types";
@@ -36,6 +37,8 @@ export type TGetSecretsDTO = {
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[]; tagSlugs?: string[];
viewSecretValue: boolean;
throwOnMissingReadValuePermission?: boolean;
metadataFilter?: { metadataFilter?: {
key?: string; key?: string;
value?: string; value?: string;
@@ -48,6 +51,11 @@ export type TGetSecretsDTO = {
keys?: string[]; keys?: string[];
} & TProjectPermission; } & TProjectPermission;
export type TGetSecretsMissingReadValuePermissionDTO = Omit<
TGetSecretsDTO,
"viewSecretValue" | "recursive" | "expandSecretReferences"
>;
export type TGetASecretDTO = { export type TGetASecretDTO = {
secretName: string; secretName: string;
path: string; path: string;
@@ -57,6 +65,7 @@ export type TGetASecretDTO = {
includeImports?: boolean; includeImports?: boolean;
version?: number; version?: number;
projectId: string; projectId: string;
viewSecretValue: boolean;
} & Omit<TProjectPermission, "projectId">; } & Omit<TProjectPermission, "projectId">;
export type TCreateSecretDTO = TProjectPermission & { export type TCreateSecretDTO = TProjectPermission & {
@@ -164,9 +173,9 @@ export type TFnSecretBulkInsert = {
} }
>; >;
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences">; secretDAL: Pick<TSecretV2BridgeDALFactory, "insertMany" | "upsertSecretReferences" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
actor?: { actor?: {
type: string; type: string;
@@ -192,9 +201,9 @@ export type TFnSecretBulkUpdate = {
data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO }; data: TRequireReferenceIfValue & { tags?: string[]; secretMetadata?: ResourceMetadataDTO };
}[]; }[];
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">; resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences">; secretDAL: Pick<TSecretV2BridgeDALFactory, "bulkUpdate" | "upsertSecretReferences" | "find">;
secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">; secretVersionDAL: Pick<TSecretVersionV2DALFactory, "insertMany">;
secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2">; secretTagDAL: Pick<TSecretTagDALFactory, "saveTagsToSecretV2" | "deleteTagsToSecretV2" | "find">;
secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
actor?: { actor?: {
type: string; type: string;
@@ -340,4 +349,12 @@ export type TGetSecretsRawByFolderMappingsDTO = {
folderMappings: { folderId: string; path: string; environment: string }[]; folderMappings: { folderId: string; path: string; environment: string }[];
userId: string; userId: string;
filters: TFindSecretsByFolderIdsFilter; filters: TFindSecretsByFolderIdsFilter;
filterByAction?: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
}; };
export type TGetAccessibleSecretsDTO = {
environment: string;
projectId: string;
secretPath: string;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;
@@ -2,9 +2,9 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas"; import { SecretVersionsV2Schema, TableName, TSecretVersionsV2, TSecretVersionsV2Update } from "@app/db/schemas";
import { BadRequestError, DatabaseError } from "@app/lib/errors"; import { BadRequestError, DatabaseError } from "@app/lib/errors";
import { ormify, selectAllTableCols, TFindOpt } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
@@ -13,6 +13,58 @@ export type TSecretVersionV2DALFactory = ReturnType<typeof secretVersionV2Bridge
export const secretVersionV2BridgeDALFactory = (db: TDbClient) => { export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2); const secretVersionV2Orm = ormify(db, TableName.SecretVersionV2);
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersionsV2> = {}) => {
try {
const query = (tx || db.replicaNode())(TableName.SecretVersionV2)
.where(`${TableName.SecretVersionV2}.secretId`, secretId)
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.select(selectAllTableCols(TableName.SecretVersionV2))
.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"));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsV2Schema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretVersionV2}: FindBySecretId` });
}
};
// This will fetch all latest secret versions from a folder // This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try { try {
@@ -135,6 +187,17 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
`${TableName.SecretVersionV2}.userActorId` `${TableName.SecretVersionV2}.userActorId`
) )
.leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`) .leftJoin(TableName.Identity, `${TableName.Identity}.id`, `${TableName.SecretVersionV2}.identityActorId`)
.leftJoin(TableName.SecretV2, `${TableName.SecretVersionV2}.secretId`, `${TableName.SecretV2}.id`)
.leftJoin(
TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`,
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
)
.leftJoin(
TableName.SecretTag,
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
`${TableName.SecretTag}.id`
)
.where((qb) => { .where((qb) => {
void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId); void qb.where(`${TableName.SecretVersionV2}.secretId`, secretId);
void qb.where(`${TableName.ProjectMembership}.projectId`, projectId); void qb.where(`${TableName.ProjectMembership}.projectId`, projectId);
@@ -145,9 +208,12 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
}) })
.select( .select(
selectAllTableCols(TableName.SecretVersionV2), selectAllTableCols(TableName.SecretVersionV2),
`${TableName.Users}.username as userActorName`, db.ref("username").withSchema(TableName.Users).as("userActorName"),
`${TableName.Identity}.name as identityActorName`, db.ref("name").withSchema(TableName.Identity).as("identityActorName"),
`${TableName.ProjectMembership}.id as membershipId` db.ref("id").withSchema(TableName.ProjectMembership).as("membershipId"),
db.ref("id").withSchema(TableName.SecretTag).as("tagId"),
db.ref("color").withSchema(TableName.SecretTag).as("tagColor"),
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug")
); );
if (limit) void query.limit(limit); if (limit) void query.limit(limit);
@@ -162,14 +228,33 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
); );
} }
const docs: Array< const docs = await query;
TSecretVersionsV2 & {
userActorName: string | undefined | null; const data = sqlNestRelationships({
identityActorName: string | undefined | null; data: docs,
membershipId: string | undefined | null; key: "id",
} parentMapper: (el) => ({
> = await query; _id: el.id,
return docs; ...SecretVersionsV2Schema.parse(el),
userActorName: el.userActorName,
identityActorName: el.identityActorName,
membershipId: el.membershipId
}),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" }); throw new DatabaseError({ error, name: "FindVersionsBySecretIdWithActors" });
} }
@@ -181,6 +266,7 @@ export const secretVersionV2BridgeDALFactory = (db: TDbClient) => {
findLatestVersionMany, findLatestVersionMany,
bulkUpdate, bulkUpdate,
findLatestVersionByFolderId, findLatestVersionByFolderId,
findVersionsBySecretIdWithActors findVersionsBySecretIdWithActors,
findBySecretId
}; };
}; };
+44 -1
View File
@@ -169,6 +169,48 @@ export const secretDALFactory = (db: TDbClient) => {
} }
}; };
const findManySecretsWithTags = async (
filter: {
secretIds: string[];
type: SecretType;
},
tx?: Knex
) => {
try {
const secrets = await (tx || db.replicaNode())(TableName.Secret)
.whereIn(`${TableName.Secret}.id` as "id", filter.secretIds)
.where("type", filter.type)
.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"));
const data = sqlNestRelationships({
data: secrets,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: "get many secrets with tags" });
}
};
const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => { const findByFolderIds = async (folderIds: string[], userId?: string, tx?: Knex) => {
try { 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) // 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)
@@ -443,6 +485,7 @@ export const secretDALFactory = (db: TDbClient) => {
upsertSecretReferences, upsertSecretReferences,
findReferencedSecretReferences, findReferencedSecretReferences,
findAllProjectSecretValues, findAllProjectSecretValues,
pruneSecretReminders pruneSecretReminders,
findManySecretsWithTags
}; };
}; };
+38 -15
View File
@@ -1,5 +1,4 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import { subject } from "@casl/ability";
import path from "path"; import path from "path";
import { import {
@@ -12,8 +11,9 @@ import {
TSecretFolders, TSecretFolders,
TSecrets TSecrets
} from "@app/db/schemas"; } from "@app/db/schemas";
import { hasSecretReadValueOrDescribePermission } from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { import {
buildSecretBlindIndexFromName, buildSecretBlindIndexFromName,
@@ -51,6 +51,8 @@ import {
TUpdateManySecretsRawFnFactory TUpdateManySecretsRawFnFactory
} from "./secret-types"; } from "./secret-types";
export const INFISICAL_SECRET_VALUE_HIDDEN_MASK = "<hidden-by-infisical>";
export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => { export const generateSecretBlindIndexBySalt = async (secretName: string, secretBlindIndexDoc: TSecretBlindIndexes) => {
const appCfg = getConfig(); const appCfg = getConfig();
const secretBlindIndex = await buildSecretBlindIndexFromName({ const secretBlindIndex = await buildSecretBlindIndexFromName({
@@ -189,13 +191,10 @@ export const recursivelyGetSecretPaths = ({
// Filter out paths that the user does not have permission to access, and paths that are not in the current path // 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( const allowedPaths = paths.filter(
(folder) => (folder) =>
permission.can( hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { secretPath: folder.path
environment, }) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
secretPath: folder.path
})
) && folder.path.startsWith(currentPath === "/" ? "" : currentPath)
); );
return allowedPaths; return allowedPaths;
@@ -344,6 +343,7 @@ export const interpolateSecrets = ({ projectId, secretEncKey, secretDAL, folderD
export const decryptSecretRaw = ( export const decryptSecretRaw = (
secret: TSecrets & { secret: TSecrets & {
secretValueHidden: boolean;
workspace: string; workspace: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -362,12 +362,14 @@ export const decryptSecretRaw = (
key key
}); });
const secretValue = decryptSymmetric128BitHexKeyUTF8({ const secretValue = !secret.secretValueHidden
ciphertext: secret.secretValueCiphertext, ? decryptSymmetric128BitHexKeyUTF8({
iv: secret.secretValueIV, ciphertext: secret.secretValueCiphertext,
tag: secret.secretValueTag, iv: secret.secretValueIV,
key tag: secret.secretValueTag,
}); key
})
: INFISICAL_SECRET_VALUE_HIDDEN_MASK;
let secretComment = ""; let secretComment = "";
@@ -385,6 +387,7 @@ export const decryptSecretRaw = (
secretPath: secret.secretPath, secretPath: secret.secretPath,
workspace: secret.workspace, workspace: secret.workspace,
environment: secret.environment, environment: secret.environment,
secretValueHidden: secret.secretValueHidden,
secretValue, secretValue,
secretComment, secretComment,
version: secret.version, version: secret.version,
@@ -1198,3 +1201,23 @@ export const fnDeleteProjectSecretReminders = async (
} }
} }
}; };
export const conditionallyHideSecretValue = (
shouldHideValue: boolean,
{
secretValueCiphertext,
secretValueIV,
secretValueTag
}: {
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}
) => {
return {
secretValueCiphertext: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueCiphertext,
secretValueIV: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueIV,
secretValueTag: shouldHideValue ? INFISICAL_SECRET_VALUE_HIDDEN_MASK : secretValueTag,
secretValueHidden: shouldHideValue
};
};
+2 -1
View File
@@ -403,7 +403,8 @@ export const secretQueueFactory = ({
expandSecretReferences, expandSecretReferences,
secretImportDAL, secretImportDAL,
secretImports, secretImports,
hasSecretAccess: () => true hasSecretAccess: () => true,
viewSecretValue: true
}); });
for (let i = importedSecrets.length - 1; i >= 0; i -= 1) { for (let i = importedSecrets.length - 1; i >= 0; i -= 1) {
+293 -99
View File
@@ -6,14 +6,23 @@ import {
ActionProjectType, ActionProjectType,
ProjectMembershipRole, ProjectMembershipRole,
ProjectUpgradeStatus, ProjectUpgradeStatus,
ProjectVersion,
SecretEncryptionAlgo, SecretEncryptionAlgo,
SecretKeyEncoding, SecretKeyEncoding,
SecretsSchema, SecretsSchema,
SecretType SecretType
} from "@app/db/schemas"; } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
hasSecretReadValueOrDescribePermission,
throwIfMissingSecretReadValueOrDescribePermission
} from "@app/ee/services/permission/permission-fns";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service"; import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal"; import { TSecretApprovalRequestDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-dal";
import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal"; import { TSecretApprovalRequestSecretDALFactory } from "@app/ee/services/secret-approval-request/secret-approval-request-secret-dal";
@@ -48,6 +57,7 @@ import { TSecretV2BridgeServiceFactory } from "../secret-v2-bridge/secret-v2-bri
import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types"; import { TGetSecretReferencesTreeDTO } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretDALFactory } from "./secret-dal"; import { TSecretDALFactory } from "./secret-dal";
import { import {
conditionallyHideSecretValue,
decryptSecretRaw, decryptSecretRaw,
fnSecretBlindIndexCheck, fnSecretBlindIndexCheck,
fnSecretBulkDelete, fnSecretBulkDelete,
@@ -71,6 +81,7 @@ import {
TDeleteManySecretRawDTO, TDeleteManySecretRawDTO,
TDeleteSecretDTO, TDeleteSecretDTO,
TDeleteSecretRawDTO, TDeleteSecretRawDTO,
TGetAccessibleSecretsDTO,
TGetASecretByIdRawDTO, TGetASecretByIdRawDTO,
TGetASecretDTO, TGetASecretDTO,
TGetASecretRawDTO, TGetASecretRawDTO,
@@ -205,7 +216,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -323,7 +334,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -445,7 +456,23 @@ export const secretServiceFactory = ({
environmentSlug: folder.environment.slug environmentSlug: folder.environment.slug
}); });
} }
return { ...updatedSecret[0], workspace: projectId, environment, secretPath: path };
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return {
...updatedSecret[0],
...conditionallyHideSecretValue(secretValueHidden, updatedSecret[0]),
workspace: projectId,
environment,
secretPath: path
};
}; };
const deleteSecret = async ({ const deleteSecret = async ({
@@ -468,7 +495,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -541,7 +568,23 @@ export const secretServiceFactory = ({
}); });
} }
return { ...deletedSecret[0], _id: deletedSecret[0].id, workspace: projectId, environment, secretPath: path }; const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return {
...deletedSecret[0],
...conditionallyHideSecretValue(secretValueHidden, deletedSecret[0]),
_id: deletedSecret[0].id,
workspace: projectId,
environment,
secretPath: path
};
}; };
const getSecrets = async ({ const getSecrets = async ({
@@ -589,10 +632,10 @@ export const secretServiceFactory = ({
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p })); paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
} else { } else {
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) secretPath: path
); });
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) return { secrets: [], imports: [] }; if (!folder) return { secrets: [], imports: [] };
@@ -614,13 +657,10 @@ export const secretServiceFactory = ({
// if its service token allow full access over imported one // if its service token allow full access over imported one
actor === ActorType.SERVICE actor === ActorType.SERVICE
? true ? true
: permission.can( : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: importEnv.slug,
subject(ProjectPermissionSub.Secrets, { secretPath: importPath
environment: importEnv.slug, })
secretPath: importPath
})
)
); );
const importedSecrets = await fnSecretsFromImports({ const importedSecrets = await fnSecretsFromImports({
allowedImports, allowedImports,
@@ -671,10 +711,11 @@ export const secretServiceFactory = ({
actorOrgId, actorOrgId,
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) secretPath: path
); });
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, path);
if (!folder) if (!folder)
throw new NotFoundError({ throw new NotFoundError({
@@ -721,14 +762,12 @@ export const secretServiceFactory = ({
// if its service token allow full access over imported one // if its service token allow full access over imported one
actor === ActorType.SERVICE actor === ActorType.SERVICE
? true ? true
: permission.can( : hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
ProjectPermissionActions.Read, environment: importEnv.slug,
subject(ProjectPermissionSub.Secrets, { secretPath: importPath
environment: importEnv.slug, })
secretPath: importPath
})
)
); );
const importedSecrets = await fnSecretsFromImports({ const importedSecrets = await fnSecretsFromImports({
allowedImports, allowedImports,
secretDAL, secretDAL,
@@ -740,6 +779,7 @@ export const secretServiceFactory = ({
if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) { if (secretBlindIndex === importedSecrets[i].secrets[j].secretBlindIndex) {
return { return {
...importedSecrets[i].secrets[j], ...importedSecrets[i].secrets[j],
secretValueHidden: false,
workspace: projectId, workspace: projectId,
environment: importedSecrets[i].environment, environment: importedSecrets[i].environment,
secretPath: importedSecrets[i].secretPath secretPath: importedSecrets[i].secretPath
@@ -750,7 +790,13 @@ export const secretServiceFactory = ({
} }
if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` }); if (!secret) throw new NotFoundError({ message: `Secret with name '${secretName}' not found` });
return { ...secret, workspace: projectId, environment, secretPath: path }; return {
...secret,
secretValueHidden: false, // Always false because we check permission at the beginning of the function
workspace: projectId,
environment,
secretPath: path
};
}; };
const createManySecret = async ({ const createManySecret = async ({
@@ -772,7 +818,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -860,7 +906,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -902,8 +948,8 @@ export const secretServiceFactory = ({
if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" }); if (tagIds.length !== tags.length) throw new NotFoundError({ message: "One or more tags not found" });
const references = await getSecretReference(projectId); const references = await getSecretReference(projectId);
const secrets = await secretDAL.transaction(async (tx) => const secrets = await secretDAL.transaction(async (tx) => {
fnSecretBulkUpdate({ const updatedSecrets = await fnSecretBulkUpdate({
folderId, folderId,
projectId, projectId,
tx, tx,
@@ -933,8 +979,22 @@ export const secretServiceFactory = ({
secretVersionDAL, secretVersionDAL,
secretTagDAL, secretTagDAL,
secretVersionTagDAL secretVersionTagDAL
}) });
);
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return updatedSecrets.map((secret) => ({
...secret,
...conditionallyHideSecretValue(secretValueHidden, secret)
}));
});
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
@@ -968,7 +1028,7 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
); );
@@ -1019,8 +1079,19 @@ export const secretServiceFactory = ({
}); });
} }
} }
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{
environment,
secretPath: path
}
);
return secrets; return secrets.map((secret) => ({
...secret,
...conditionallyHideSecretValue(secretValueHidden, secret)
}));
}); });
await snapshotService.performSnapshot(folderId); await snapshotService.performSnapshot(folderId);
@@ -1181,6 +1252,7 @@ export const secretServiceFactory = ({
secretName, secretName,
path: secretPath, path: secretPath,
environment, environment,
viewSecretValue: false,
type: "shared" type: "shared"
}); });
@@ -1195,12 +1267,25 @@ export const secretServiceFactory = ({
| (typeof groupPermissions)[number] | (typeof groupPermissions)[number]
) => { ) => {
const allowedActions = [ const allowedActions = [
ProjectPermissionActions.Read, ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionActions.Delete, ProjectPermissionSecretActions.ReadValue,
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Delete,
ProjectPermissionActions.Edit ProjectPermissionSecretActions.Create,
].filter((action) => ProjectPermissionSecretActions.Edit
entityPermission.permission.can( ].filter((action) => {
if (
action === ProjectPermissionSecretActions.DescribeSecret ||
action === ProjectPermissionSecretActions.ReadValue
) {
return hasSecretReadValueOrDescribePermission(entityPermission.permission, action, {
environment,
secretPath,
secretName,
secretTags: secret?.tags?.map((el) => el.slug)
});
}
return entityPermission.permission.can(
action, action,
subject(ProjectPermissionSub.Secrets, { subject(ProjectPermissionSub.Secrets, {
environment, environment,
@@ -1208,8 +1293,8 @@ export const secretServiceFactory = ({
secretName, secretName,
secretTags: secret?.tags?.map((el) => el.slug) secretTags: secret?.tags?.map((el) => el.slug)
}) })
) );
); });
return { return {
...entityPermission, ...entityPermission,
@@ -1228,6 +1313,39 @@ export const secretServiceFactory = ({
return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess }; return { users: usersWithAccess, identities: identitiesWithAccess, groups: groupsWithAccess };
}; };
const getAccessibleSecrets = async ({
projectId,
secretPath,
actor,
actorId,
actorOrgId,
actorAuthMethod,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
if (!shouldUseSecretV2Bridge) {
throw new BadRequestError({
message: "Project version does not support this endpoint.",
name: "ProjectVersionNotSupported"
});
}
const secrets = await secretV2BridgeService.getAccessibleSecrets({
projectId,
secretPath,
environment,
filterByAction,
actor,
actorId,
actorOrgId,
actorAuthMethod
});
return secrets;
};
const getSecretsRaw = async ({ const getSecretsRaw = async ({
projectId, projectId,
path, path,
@@ -1235,11 +1353,13 @@ export const secretServiceFactory = ({
actorId, actorId,
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
viewSecretValue,
environment, environment,
includeImports, includeImports,
expandSecretReferences, expandSecretReferences,
recursive, recursive,
tagSlugs = [], tagSlugs = [],
throwOnMissingReadValuePermission = true,
...paramsV2 ...paramsV2
}: TGetSecretsRawDTO) => { }: TGetSecretsRawDTO) => {
const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { botKey, shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
@@ -1250,6 +1370,8 @@ export const secretServiceFactory = ({
actorId, actorId,
actor, actor,
actorOrgId, actorOrgId,
viewSecretValue,
throwOnMissingReadValuePermission,
environment, environment,
path, path,
recursive, recursive,
@@ -1258,6 +1380,7 @@ export const secretServiceFactory = ({
tagSlugs, tagSlugs,
...paramsV2 ...paramsV2
}); });
return { secrets, imports }; return { secrets, imports };
} }
@@ -1286,14 +1409,20 @@ export const secretServiceFactory = ({
recursive recursive
}); });
const decryptedSecrets = secrets.map((el) => decryptSecretRaw(el, botKey)); const decryptedSecrets = secrets.map((el) => decryptSecretRaw({ ...el, secretValueHidden: false }, botKey));
const filteredSecrets = tagSlugs.length const filteredSecrets = tagSlugs.length
? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug)))) ? decryptedSecrets.filter((secret) => Boolean(secret.tags?.find((el) => tagSlugs.includes(el.slug))))
: decryptedSecrets; : decryptedSecrets;
const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => { const processedImports = (imports || [])?.map(({ secrets: importedSecrets, ...el }) => {
const decryptedImportSecrets = importedSecrets.map((sec) => const decryptedImportSecrets = importedSecrets.map((sec) =>
decryptSecretRaw( decryptSecretRaw(
{ ...sec, environment: el.environment, workspace: projectId, secretPath: el.secretPath }, {
...sec,
environment: el.environment,
workspace: projectId,
secretPath: el.secretPath,
secretValueHidden: false
},
botKey botKey
) )
); );
@@ -1304,6 +1433,7 @@ export const secretServiceFactory = ({
const importedEntries = decryptedImportSecrets.reduce( const importedEntries = decryptedImportSecrets.reduce(
( (
accum: { accum: {
secretValueHidden: boolean;
secretKey: string; secretKey: string;
secretPath: string; secretPath: string;
workspace: string; workspace: string;
@@ -1347,6 +1477,7 @@ export const secretServiceFactory = ({
Object.keys(secretsGroupByPath).map((groupedPath) => Object.keys(secretsGroupByPath).map((groupedPath) =>
Promise.allSettled( Promise.allSettled(
secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => { secretsGroupByPath[groupedPath].map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecret({ const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue, value: decryptedSecret.secretValue,
secretPath: groupedPath, secretPath: groupedPath,
@@ -1363,6 +1494,7 @@ export const secretServiceFactory = ({
processedImports.map((processedImport) => processedImports.map((processedImport) =>
Promise.allSettled( Promise.allSettled(
processedImport.secrets.map(async (decryptedSecret, index) => { processedImport.secrets.map(async (decryptedSecret, index) => {
if (decryptedSecret.secretValueHidden) return;
const expandedSecretValue = await expandSecret({ const expandedSecretValue = await expandSecret({
value: decryptedSecret.secretValue, value: decryptedSecret.secretValue,
secretPath: path, secretPath: path,
@@ -1400,6 +1532,7 @@ export const secretServiceFactory = ({
path, path,
actor, actor,
environment, environment,
viewSecretValue,
projectId: workspaceId, projectId: workspaceId,
expandSecretReferences, expandSecretReferences,
projectSlug, projectSlug,
@@ -1419,6 +1552,7 @@ export const secretServiceFactory = ({
includeImports, includeImports,
actorAuthMethod, actorAuthMethod,
path, path,
viewSecretValue,
actorOrgId, actorOrgId,
actor, actor,
actorId, actorId,
@@ -1449,6 +1583,7 @@ export const secretServiceFactory = ({
message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`, message: `Project bot for project with ID '${projectId}' not found. Please upgrade your project.`,
name: "bot_not_found_error" name: "bot_not_found_error"
}); });
const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey); const decryptedSecret = decryptSecretRaw(encryptedSecret, botKey);
if (expandSecretReferences) { if (expandSecretReferences) {
@@ -1467,7 +1602,10 @@ export const secretServiceFactory = ({
decryptedSecret.secretValue = expandedSecretValue || ""; decryptedSecret.secretValue = expandedSecretValue || "";
} }
return { secretMetadata: undefined, ...decryptedSecret }; return {
secretMetadata: undefined,
...decryptedSecret
};
}; };
const createSecretRaw = async ({ const createSecretRaw = async ({
@@ -1618,7 +1756,16 @@ export const secretServiceFactory = ({
tags: tagIds tags: tagIds
}); });
return { type: SecretProtectionType.Direct as const, secret: decryptSecretRaw(secret, botKey) }; return {
type: SecretProtectionType.Direct as const,
secret: decryptSecretRaw(
{
...secret,
secretValueHidden: false
},
botKey
)
};
}; };
const updateSecretRaw = async ({ const updateSecretRaw = async ({
@@ -2014,7 +2161,7 @@ export const secretServiceFactory = ({
return { return {
type: SecretProtectionType.Direct as const, type: SecretProtectionType.Direct as const,
secrets: secrets.map((secret) => secrets: secrets.map((secret) =>
decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath }, botKey) decryptSecretRaw({ ...secret, workspace: projectId, environment, secretPath, secretValueHidden: false }, botKey)
) )
}; };
}; };
@@ -2303,6 +2450,12 @@ export const secretServiceFactory = ({
const folder = await folderDAL.findById(secret.folderId); const folder = await folderDAL.findById(secret.folderId);
if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` }); if (!folder) throw new NotFoundError({ message: `Folder with ID '${secret.folderId}' not found` });
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]);
if (!folderWithPath) {
throw new NotFoundError({ message: `Folder with ID '${folder.id}' not found` });
}
const { botKey } = await projectBotService.getBotKey(folder.projectId); const { botKey } = await projectBotService.getBotKey(folder.projectId);
if (!botKey) if (!botKey)
throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` }); throw new NotFoundError({ message: `Project bot for project with ID '${folder.projectId}' not found` });
@@ -2316,18 +2469,43 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
const secretVersions = await secretVersionDAL.find({ secretId }, { offset, limit, sort: [["createdAt", "desc"]] }); const secretVersions = await secretVersionDAL.findBySecretId(secretId, {
return secretVersions.map((el) => offset,
decryptSecretRaw( limit,
sort: [["createdAt", "desc"]]
});
return secretVersions.map((el) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key: botKey
});
const secretValueHidden = !hasSecretReadValueOrDescribePermission(
permission,
ProjectPermissionSecretActions.ReadValue,
{ {
environment: folder.environment.envSlug,
secretPath: folderWithPath.path,
secretName: secretKey,
...(el.tags?.length && {
secretTags: el.tags.map((tag) => tag.slug)
})
}
);
return decryptSecretRaw(
{
secretValueHidden,
...el, ...el,
workspace: folder.projectId, workspace: folder.projectId,
environment: folder.environment.envSlug, environment: folder.environment.envSlug,
secretPath: "/" secretPath: folderWithPath.path
}, },
botKey botKey
) );
); });
}; };
const attachTags = async ({ const attachTags = async ({
@@ -2353,7 +2531,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -2459,7 +2637,7 @@ export const secretServiceFactory = ({
}); });
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
@@ -2625,7 +2803,7 @@ export const secretServiceFactory = ({
message: `Project with slug '${projectSlug}' not found` message: `Project with slug '${projectSlug}' not found`
}); });
} }
if (project.version === 3) { if (project.version === ProjectVersion.V3) {
return secretV2BridgeService.moveSecrets({ return secretV2BridgeService.moveSecrets({
sourceEnvironment, sourceEnvironment,
sourceSecretPath, sourceSecretPath,
@@ -2650,30 +2828,6 @@ export const secretServiceFactory = ({
actionProjectType: ActionProjectType.SecretManager actionProjectType: ActionProjectType.SecretManager
}); });
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment,
secretPath: destinationSecretPath
})
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment: destinationEnvironment,
secretPath: destinationSecretPath
})
);
const { botKey } = await projectBotService.getBotKey(project.id); const { botKey } = await projectBotService.getBotKey(project.id);
if (!botKey) { if (!botKey) {
throw new NotFoundError({ throw new NotFoundError({
@@ -2701,11 +2855,9 @@ export const secretServiceFactory = ({
}); });
} }
const sourceSecrets = await secretDAL.find({ const sourceSecrets = await secretDAL.findManySecretsWithTags({
type: SecretType.Shared, type: SecretType.Shared,
$in: { secretIds
id: secretIds
}
}); });
if (sourceSecrets.length !== secretIds.length) { if (sourceSecrets.length !== secretIds.length) {
@@ -2714,21 +2866,62 @@ export const secretServiceFactory = ({
}); });
} }
const decryptedSourceSecrets = sourceSecrets.map((secret) => ({ const sourceActions = [
...secret, ProjectPermissionSecretActions.Delete,
secretKey: decryptSymmetric128BitHexKeyUTF8({ ProjectPermissionSecretActions.DescribeSecret,
ProjectPermissionSecretActions.ReadValue
] as const;
const destinationActions = [ProjectPermissionSecretActions.Create, ProjectPermissionSecretActions.Edit] as const;
const decryptedSourceSecrets = sourceSecrets.map((secret) => {
const secretKey = decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretKeyCiphertext, ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV, iv: secret.secretKeyIV,
tag: secret.secretKeyTag, tag: secret.secretKeyTag,
key: botKey key: botKey
}), });
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext, for (const destinationAction of destinationActions) {
iv: secret.secretValueIV, ForbiddenError.from(permission).throwUnlessCan(
tag: secret.secretValueTag, destinationAction,
key: botKey subject(ProjectPermissionSub.Secrets, {
}) environment: destinationEnvironment,
})); secretPath: destinationSecretPath
})
);
}
for (const sourceAction of sourceActions) {
if (
sourceAction === ProjectPermissionSecretActions.ReadValue ||
sourceAction === ProjectPermissionSecretActions.DescribeSecret
) {
throwIfMissingSecretReadValueOrDescribePermission(permission, sourceAction, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
});
} else {
ForbiddenError.from(permission).throwUnlessCan(
sourceAction,
subject(ProjectPermissionSub.Secrets, {
environment: sourceEnvironment,
secretPath: sourceSecretPath
})
);
}
}
return {
...secret,
secretKey,
secretValue: decryptSymmetric128BitHexKeyUTF8({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key: botKey
})
};
});
let isSourceUpdated = false; let isSourceUpdated = false;
let isDestinationUpdated = false; let isDestinationUpdated = false;
@@ -3102,6 +3295,7 @@ export const secretServiceFactory = ({
getSecretReferenceTree, getSecretReferenceTree,
getSecretsRawByFolderMappings, getSecretsRawByFolderMappings,
getSecretAccessList, getSecretAccessList,
getSecretByIdRaw getSecretByIdRaw,
getAccessibleSecrets
}; };
}; };
+12 -2
View File
@@ -20,6 +20,7 @@ import { TSecretV2BridgeDALFactory } from "../secret-v2-bridge/secret-v2-bridge-
import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types"; import { SecretUpdateMode } from "../secret-v2-bridge/secret-v2-bridge-types";
import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal"; import { TSecretVersionV2DALFactory } from "../secret-v2-bridge/secret-version-dal";
import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal"; import { TSecretVersionV2TagDALFactory } from "../secret-v2-bridge/secret-version-tag-dal";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">; type TPartialSecret = Pick<TSecrets, "id" | "secretReminderRepeatDays" | "secretReminderNote">;
@@ -180,10 +181,18 @@ export enum SecretsOrderBy {
Name = "name" // "key" for secrets but using name for use across resources Name = "name" // "key" for secrets but using name for use across resources
} }
export type TGetAccessibleSecretsDTO = {
secretPath: string;
environment: string;
filterByAction: ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue;
} & TProjectPermission;
export type TGetSecretsRawDTO = { export type TGetSecretsRawDTO = {
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
path: string; path: string;
environment: string; environment: string;
viewSecretValue: boolean;
throwOnMissingReadValuePermission?: boolean;
includeImports?: boolean; includeImports?: boolean;
recursive?: boolean; recursive?: boolean;
tagSlugs?: string[]; tagSlugs?: string[];
@@ -209,6 +218,7 @@ export type TGetASecretRawDTO = {
secretName: string; secretName: string;
path: string; path: string;
environment: string; environment: string;
viewSecretValue: boolean;
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
type: "shared" | "personal"; type: "shared" | "personal";
includeImports?: boolean; includeImports?: boolean;
@@ -417,7 +427,7 @@ export type TCreateManySecretsRawFnFactory = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@@ -454,7 +464,7 @@ export type TUpdateManySecretsRawFnFactory = {
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
secretV2BridgeDAL: Pick< secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory, TSecretV2BridgeDALFactory,
"insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" "insertMany" | "upsertSecretReferences" | "findBySecretKeys" | "bulkUpdate" | "deleteMany" | "find"
>; >;
secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">; secretVersionV2BridgeDAL: Pick<TSecretVersionV2DALFactory, "insertMany" | "findLatestVersionMany">;
secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">; secretVersionTagV2BridgeDAL: Pick<TSecretVersionV2TagDALFactory, "insertMany">;
@@ -1,9 +1,9 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas"; import { SecretVersionsSchema, TableName, TSecretVersions, TSecretVersionsUpdate } from "@app/db/schemas";
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
import { ormify, selectAllTableCols } from "@app/lib/knex"; import { ormify, selectAllTableCols, sqlNestRelationships, TFindOpt } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { QueueName } from "@app/queue"; import { QueueName } from "@app/queue";
@@ -12,6 +12,50 @@ export type TSecretVersionDALFactory = ReturnType<typeof secretVersionDALFactory
export const secretVersionDALFactory = (db: TDbClient) => { export const secretVersionDALFactory = (db: TDbClient) => {
const secretVersionOrm = ormify(db, TableName.SecretVersion); const secretVersionOrm = ormify(db, TableName.SecretVersion);
const findBySecretId = async (secretId: string, { offset, limit, sort, tx }: TFindOpt<TSecretVersions> = {}) => {
try {
const query = (tx || db.replicaNode())(TableName.SecretVersion)
.where(`${TableName.SecretVersion}.secretId`, secretId)
.leftJoin(TableName.Secret, `${TableName.SecretVersion}.secretId`, `${TableName.Secret}.id`)
.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.SecretVersion))
.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"));
if (limit) void query.limit(limit);
if (offset) void query.offset(offset);
if (sort) {
void query.orderBy(sort.map(([column, order, nulls]) => ({ column: column as string, order, nulls })));
}
const docs = await query;
const data = sqlNestRelationships({
data: docs,
key: "id",
parentMapper: (el) => ({ _id: el.id, ...SecretVersionsSchema.parse(el) }),
childrenMapper: [
{
key: "tagId",
label: "tags" as const,
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
id,
color,
slug,
name: slug
})
}
]
});
return data;
} catch (error) {
throw new DatabaseError({ error, name: `${TableName.SecretVersion}: FindBySecretId` });
}
};
// This will fetch all latest secret versions from a folder // This will fetch all latest secret versions from a folder
const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => { const findLatestVersionByFolderId = async (folderId: string, tx?: Knex) => {
try { try {
@@ -149,6 +193,7 @@ export const secretVersionDALFactory = (db: TDbClient) => {
findLatestVersionMany, findLatestVersionMany,
bulkUpdate, bulkUpdate,
findLatestVersionByFolderId, findLatestVersionByFolderId,
findBySecretId,
bulkUpdateNoVersionIncrement bulkUpdateNoVersionIncrement
}; };
}; };
@@ -5,7 +5,11 @@ import bcrypt from "bcrypt";
import { ActionProjectType } from "@app/db/schemas"; import { ActionProjectType } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import {
ProjectPermissionActions,
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/ee/services/permission/project-permission";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors"; import { ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
@@ -67,7 +71,7 @@ export const serviceTokenServiceFactory = ({
scopes.forEach(({ environment, secretPath }) => { scopes.forEach(({ environment, secretPath }) => {
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionSecretActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
}); });
@@ -271,12 +271,13 @@ export const superAdminServiceFactory = ({
return { token, user: userInfo, organization }; return { token, user: userInfo, organization };
}; };
const getUsers = ({ offset, limit, searchTerm }: TAdminGetUsersDTO) => { const getUsers = ({ offset, limit, searchTerm, adminsOnly }: TAdminGetUsersDTO) => {
return userDAL.getUsersByFilter({ return userDAL.getUsersByFilter({
limit, limit,
offset, offset,
searchTerm, searchTerm,
sortBy: "username" sortBy: "username",
adminsOnly
}); });
}; };
@@ -20,6 +20,7 @@ export type TAdminGetUsersDTO = {
offset: number; offset: number;
limit: number; limit: number;
searchTerm: string; searchTerm: string;
adminsOnly: boolean;
}; };
export enum LoginMethod { export enum LoginMethod {
+8 -1
View File
@@ -23,15 +23,18 @@ export const userDALFactory = (db: TDbClient) => {
limit, limit,
offset, offset,
searchTerm, searchTerm,
sortBy sortBy,
adminsOnly
}: { }: {
limit: number; limit: number;
offset: number; offset: number;
searchTerm: string; searchTerm: string;
sortBy?: keyof TUsers; sortBy?: keyof TUsers;
adminsOnly: boolean;
}) => { }) => {
try { try {
let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false); let query = db.replicaNode()(TableName.Users).where("isGhost", "=", false);
if (searchTerm) { if (searchTerm) {
query = query.where((qb) => { query = query.where((qb) => {
void qb void qb
@@ -42,6 +45,10 @@ export const userDALFactory = (db: TDbClient) => {
}); });
} }
if (adminsOnly) {
query = query.where("superAdmin", true);
}
if (sortBy) { if (sortBy) {
query = query.orderBy(sortBy); query = query.orderBy(sortBy);
} }
+1 -1
View File
@@ -868,4 +868,4 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
+11 -10
View File
@@ -1,10 +1,6 @@
package cmd package cmd
import ( import (
// "fmt"
// "github.com/Infisical/infisical-merge/packages/api"
// "github.com/Infisical/infisical-merge/packages/models"
"context" "context"
"fmt" "fmt"
"os" "os"
@@ -14,13 +10,8 @@ import (
"github.com/Infisical/infisical-merge/packages/gateway" "github.com/Infisical/infisical-merge/packages/gateway"
"github.com/Infisical/infisical-merge/packages/util" "github.com/Infisical/infisical-merge/packages/util"
"github.com/rs/zerolog/log"
// "github.com/Infisical/infisical-merge/packages/visualize"
// "github.com/rs/zerolog/log"
// "github.com/go-resty/resty/v2"
"github.com/posthog/posthog-go" "github.com/posthog/posthog-go"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -40,6 +31,16 @@ var gatewayCmd = &cobra.Command{
util.HandleError(fmt.Errorf("Token not found")) util.HandleError(fmt.Errorf("Token not found"))
} }
domain, err := cmd.Flags().GetString("domain")
if err != nil {
util.HandleError(err, "Unable to parse domain flag")
}
// Try to install systemd service if possible
if err := gateway.InstallGatewaySystemdService(token.Token, domain); err != nil {
log.Warn().Msgf("Failed to install systemd service: %v", err)
}
Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION)) Telemetry.CaptureEvent("cli-command:gateway", posthog.NewProperties().Set("version", util.CLI_VERSION))
sigCh := make(chan os.Signal, 1) sigCh := make(chan os.Signal, 1)
+3 -16
View File
@@ -79,13 +79,9 @@ func (g *Gateway) ConnectWithRelay() error {
// Dial TURN Server // Dial TURN Server
if relayPort == "5349" { if relayPort == "5349" {
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(g.config.CertificateChain))
log.Info().Msgf("Provided relay port %s. Using TLS", relayPort) log.Info().Msgf("Provided relay port %s. Using TLS", relayPort)
conn, err := dtls.Dial("udp", turnAddr, &dtls.Config{ conn, err := dtls.Dial("udp", turnAddr, &dtls.Config{
ServerName: relayAddress, ServerName: relayAddress,
RootCAs: caCertPool,
}) })
if err != nil { if err != nil {
return fmt.Errorf("Failed to connect with relay server: %w", err) return fmt.Errorf("Failed to connect with relay server: %w", err)
@@ -193,7 +189,7 @@ func (g *Gateway) Listen(ctx context.Context) error {
log.Printf("Listener started on %s", quicListener.Addr()) log.Printf("Listener started on %s", quicListener.Addr())
g.registerRelayIsActive(ctx, quicListener.Addr().String(), errCh) g.registerRelayIsActive(ctx, errCh)
log.Info().Msg("Gateway started successfully") log.Info().Msg("Gateway started successfully")
@@ -330,7 +326,7 @@ func (g *Gateway) createPermissionForStaticIps(staticIps string) error {
return nil return nil
} }
func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh chan error) error { func (g *Gateway) registerRelayIsActive(ctx context.Context, errCh chan error) error {
ticker := time.NewTicker(15 * time.Second) ticker := time.NewTicker(15 * time.Second)
maxFailures := 3 maxFailures := 3
failures := 0 failures := 0
@@ -345,13 +341,7 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh
return return
case <-ticker.C: case <-ticker.C:
log.Debug().Msg("Performing relay connection health check") log.Debug().Msg("Performing relay connection health check")
ctxTimeout, cancel := context.WithTimeout(ctx, 5*time.Second) err := g.createPermissionForStaticIps(g.config.InfisicalStaticIp)
defer cancel()
// Try to establish a QUIC connection
conn, err := quic.DialAddr(ctxTimeout, addr, &tls.Config{
InsecureSkipVerify: false, // Skip certificate verification
NextProtos: []string{"infisical-gateway"},
}, nil)
if err != nil && !strings.Contains(err.Error(), "tls:") { if err != nil && !strings.Contains(err.Error(), "tls:") {
failures++ failures++
log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions") log.Warn().Err(err).Int("failures", failures).Msg("Failed to refresh TURN permissions")
@@ -361,9 +351,6 @@ func (g *Gateway) registerRelayIsActive(ctx context.Context, addr string, errCh
} }
continue continue
} }
if conn != nil {
defer conn.CloseWithError(0, "All good")
}
} }
} }
}() }()
+82
View File
@@ -0,0 +1,82 @@
package gateway
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"github.com/rs/zerolog/log"
)
const systemdServiceTemplate = `[Unit]
Description=Infisical Gateway Service
After=network.target
[Service]
Type=simple
EnvironmentFile=/etc/infisical/gateway.conf
ExecStart=/usr/local/bin/infisical gateway
Restart=on-failure
InaccessibleDirectories=/home
PrivateTmp=yes
LimitCORE=infinity
LimitNOFILE=1000000
LimitNPROC=60000
LimitRTPRIO=infinity
LimitRTTIME=7000000
[Install]
WantedBy=multi-user.target
`
func InstallGatewaySystemdService(token string, domain string) error {
if runtime.GOOS != "linux" {
log.Info().Msg("Skipping systemd service installation - not on Linux")
return nil
}
if os.Geteuid() != 0 {
log.Info().Msg("Skipping systemd service installation - not running as root/sudo")
return nil
}
configDir := "/etc/infisical"
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("failed to create config directory: %v", err)
}
configContent := fmt.Sprintf("INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN=%s\n", token)
if domain != "" {
configContent += fmt.Sprintf("INFISICAL_API_URL=%s\n", domain)
} else {
configContent += "INFISICAL_API_URL=\n"
}
configPath := filepath.Join(configDir, "gateway.conf")
if err := os.WriteFile(configPath, []byte(configContent), 0600); err != nil {
return fmt.Errorf("failed to write config file: %v", err)
}
servicePath := "/etc/systemd/system/infisical-gateway.service"
if _, err := os.Stat(servicePath); err == nil {
log.Info().Msg("Systemd service file already exists")
return nil
}
if err := os.WriteFile(servicePath, []byte(systemdServiceTemplate), 0644); err != nil {
return fmt.Errorf("failed to write systemd service file: %v", err)
}
reloadCmd := exec.Command("systemctl", "daemon-reload")
if err := reloadCmd.Run(); err != nil {
return fmt.Errorf("failed to reload systemd: %v", err)
}
log.Info().Msg("Successfully installed systemd service")
log.Info().Msg("To start the service, run: sudo systemctl start infisical-gateway")
log.Info().Msg("To enable the service on boot, run: sudo systemctl enable infisical-gateway")
return nil
}
@@ -1,93 +0,0 @@
import { useTranslation } from "react-i18next";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useToggle } from "@app/hooks";
import { generateUserBackupKey } from "@app/lib/crypto";
import { createNotification } from "../notifications";
import { generateBackupPDFAsync } from "../utilities/generateBackupPDF";
import { Button } from "../v2";
interface DownloadBackupPDFStepProps {
incrementStep: () => void;
email: string;
password: string;
name: string;
}
/**
* This is the step of the signup flow where the user downloads the backup pdf
* @param {object} obj
* @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
* @param {string} obj.email - user's email
* @param {string} obj.password - user's password
* @param {string} obj.name - user's name
* @returns
*/
export default function DonwloadBackupPDFStep({
incrementStep,
email,
password,
name
}: DownloadBackupPDFStepProps): JSX.Element {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useToggle();
const handleBackupKeyGenerate = async () => {
try {
setIsLoading.on();
const generatedKey = await generateUserBackupKey(email, password);
await generateBackupPDFAsync({
generatedKey,
personalEmail: email,
personalName: name
});
incrementStep();
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to generate backup key"
});
} finally {
setIsLoading.off();
}
};
return (
<div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
<p className="flex flex-col items-center justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
<FontAwesomeIcon
icon={faWarning}
className="mb-6 ml-2 mr-3 pt-1 text-6xl text-bunker-200"
/>
{t("signup.step4-message")}
</p>
<div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
<div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
<span className="mb-2">
{t("signup.step4-description1")} {t("signup.step4-description3")}
</span>
</div>
<div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
<div className="text-l w-full py-1 text-lg">
<Button
onClick={handleBackupKeyGenerate}
size="sm"
isFullWidth
isLoading={isLoading}
isDisabled={isLoading}
className="h-12"
colorSchema="primary"
variant="outline_bg"
>
Download PDF
</Button>
</div>
</div>
</div>
</div>
);
}
@@ -9,9 +9,7 @@ import { Button, FormControl, Input, TextArea } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useUpdateProject } from "@app/hooks/api"; import { useUpdateProject } from "@app/hooks/api";
import { CopyButton } from "./CopyButton"; const baseFormSchema = z.object({
const formSchema = z.object({
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"), name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
description: z description: z
.string() .string()
@@ -20,31 +18,55 @@ const formSchema = z.object({
.optional() .optional()
}); });
type FormData = z.infer<typeof formSchema>; const formSchemaWithSlug = baseFormSchema.extend({
slug: z
.string()
.min(1, "Required")
.max(64, "Too long, maximum length is 64 characters")
.regex(
/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/,
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
)
});
export const ProjectOverviewChangeSection = () => { type BaseFormData = z.infer<typeof baseFormSchema>;
type FormDataWithSlug = z.infer<typeof formSchemaWithSlug>;
type Props = {
showSlugField?: boolean;
};
export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { mutateAsync, isPending } = useUpdateProject(); const { mutateAsync, isPending } = useUpdateProject();
const { handleSubmit, control, reset, watch } = useForm<BaseFormData | FormDataWithSlug>({
resolver: zodResolver(showSlugField ? formSchemaWithSlug : baseFormSchema)
});
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) }); const currentSlug = showSlugField ? watch("slug") : currentWorkspace?.slug;
useEffect(() => { useEffect(() => {
if (currentWorkspace) { if (currentWorkspace) {
reset({ reset({
name: currentWorkspace.name, name: currentWorkspace.name,
description: currentWorkspace.description ?? "" description: currentWorkspace.description ?? "",
...(showSlugField && { slug: currentWorkspace.slug })
}); });
} }
}, [currentWorkspace]); }, [currentWorkspace, showSlugField]);
const onFormSubmit = async ({ name, description }: FormData) => { const onFormSubmit = async (data: BaseFormData | FormDataWithSlug) => {
try { try {
if (!currentWorkspace?.id) return; if (!currentWorkspace?.id) return;
await mutateAsync({ await mutateAsync({
projectID: currentWorkspace.id, projectID: currentWorkspace.id,
newProjectName: name, newProjectName: data.name,
newProjectDescription: description newProjectDescription: data.description,
...(showSlugField &&
"slug" in data && {
newSlug: data.slug !== currentWorkspace.slug ? data.slug : undefined
})
}); });
createNotification({ createNotification({
@@ -65,20 +87,34 @@ export const ProjectOverviewChangeSection = () => {
<div className="justify-betweens flex"> <div className="justify-betweens flex">
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2> <h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
<div className="space-x-2"> <div className="space-x-2">
<CopyButton <Button
value={currentWorkspace?.slug || ""} variant="outline_bg"
hoverText="Click to project slug" size="sm"
notificationText="Copied project slug to clipboard" onClick={() => {
navigator.clipboard.writeText(currentSlug || "");
createNotification({
text: "Copied project slug to clipboard",
type: "success"
});
}}
title="Click to copy project slug"
> >
Copy Project Slug Copy Project Slug
</CopyButton> </Button>
<CopyButton <Button
value={currentWorkspace?.id || ""} variant="outline_bg"
hoverText="Click to project ID" size="sm"
notificationText="Copied project ID to clipboard" onClick={() => {
navigator.clipboard.writeText(currentWorkspace?.id || "");
createNotification({
text: "Copied project ID to clipboard",
type: "success"
});
}}
title="Click to copy project ID"
> >
Copy Project ID Copy Project ID
</CopyButton> </Button>
</div> </div>
</div> </div>
<div> <div>
@@ -113,6 +149,38 @@ export const ProjectOverviewChangeSection = () => {
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>
</div> </div>
{showSlugField && (
<div className="flex w-full flex-row items-end gap-4">
<div className="w-full max-w-md">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Project}
>
{(isAllowed) => (
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Project slug"
>
<Input
placeholder="Project slug"
{...field}
className="bg-mineshaft-800"
isDisabled={!isAllowed}
/>
</FormControl>
)}
control={control}
name="slug"
/>
)}
</ProjectPermissionCan>
</div>
</div>
)}
<div className="flex w-full flex-row items-end gap-4"> <div className="flex w-full flex-row items-end gap-4">
<div className="w-full max-w-md"> <div className="w-full max-w-md">
<ProjectPermissionCan <ProjectPermissionCan
@@ -1,13 +1,21 @@
import { useState } from "react"; /* eslint-disable no-nested-ternary */
import { faChevronRight, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; import { useEffect, useState } from "react";
import {
faChevronRight,
faExclamationTriangle,
faEye,
faEyeSlash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as Collapsible from "@radix-ui/react-collapsible"; import * as Collapsible from "@radix-ui/react-collapsible";
import { AxiosError } from "axios";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2"; import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { useGetSecretReferenceTree } from "@app/hooks/api"; import { useGetSecretReferenceTree } from "@app/hooks/api";
import { TSecretReferenceTraceNode } from "@app/hooks/api/types"; import { ApiErrorTypes, TApiErrors, TSecretReferenceTraceNode } from "@app/hooks/api/types";
import style from "./SecretReferenceDetails.module.css"; import style from "./SecretReferenceDetails.module.css";
@@ -84,7 +92,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const projectId = currentWorkspace?.id || ""; const projectId = currentWorkspace?.id || "";
const { data, isPending } = useGetSecretReferenceTree({ const { data, isPending, isError, error } = useGetSecretReferenceTree({
secretPath, secretPath,
environmentSlug: environment, environmentSlug: environment,
projectId, projectId,
@@ -94,6 +102,26 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
const tree = data?.tree; const tree = data?.tree;
const secretValue = data?.value; const secretValue = data?.value;
useEffect(() => {
if (error instanceof AxiosError) {
const err = error?.response?.data as TApiErrors;
if (err?.error === ApiErrorTypes.CustomForbiddenError) {
createNotification({
title: "You don't have permission to view reference tree",
text: "You don't have permission to view one or more of the referenced secrets.",
type: "error"
});
return;
}
createNotification({
title: "Error fetching secret reference tree",
text: "Please try again later.",
type: "error"
});
}
}, [error]);
if (isPending) { if (isPending) {
return ( return (
<div className="flex items-center justify-center py-4"> <div className="flex items-center justify-center py-4">
@@ -114,11 +142,16 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
</FormControl> </FormControl>
<FormLabel className="mb-2" label="Reference Tree" /> <FormLabel className="mb-2" label="Reference Tree" />
<div className="thin-scrollbar relative max-h-96 overflow-auto rounded-md border border-mineshaft-600 bg-bunker-700 py-6 text-sm text-mineshaft-200"> <div className="thin-scrollbar relative max-h-96 overflow-auto rounded-md border border-mineshaft-600 bg-bunker-700 py-6 text-sm text-mineshaft-200">
{tree && ( {isError ? (
<div className="flex items-center justify-center py-4">
<FontAwesomeIcon icon={faExclamationTriangle} className="mr-2 text-red-500" />
<p className="text-red-500">Error fetching secret reference tree</p>
</div>
) : tree ? (
<ul className={style.tree}> <ul className={style.tree}>
<SecretReferenceNode node={tree} isRoot secretKey={secretKey} /> <SecretReferenceNode node={tree} isRoot secretKey={secretKey} />
</ul> </ul>
)} ) : null}
</div> </div>
<div className="mt-2 text-sm text-mineshaft-400"> <div className="mt-2 text-sm text-mineshaft-400">
Click a secret key to view its sub-references. Click a secret key to view its sub-references.
@@ -34,12 +34,12 @@ const passwordCheck = async ({
const tests = [ const tests = [
{ {
name: "tooShort", name: "tooShort",
validator: (pwd: string) => pwd.length >= 14, validator: (pwd: string) => pwd?.length >= 14,
setError: setPasswordErrorTooShort setError: setPasswordErrorTooShort
}, },
{ {
name: "tooLong", name: "tooLong",
validator: (pwd: string) => pwd.length < 101, validator: (pwd: string) => pwd?.length < 101,
setError: setPasswordErrorTooLong setError: setPasswordErrorTooLong
}, },
{ {
+22
View File
@@ -0,0 +1,22 @@
import { twMerge } from "tailwind-merge";
import { Tooltip } from "../Tooltip/Tooltip";
interface IProps {
className?: string;
tooltipText?: string;
}
export const Blur = ({ className, tooltipText }: IProps) => {
return (
<Tooltip content={tooltipText} isDisabled={!tooltipText}>
<div
className={twMerge("flex w-80 flex-grow items-center py-1 pl-4 pr-2", className)}
tabIndex={0}
role="button"
>
<span className="blur">********</span>
</div>
</Tooltip>
);
};
@@ -0,0 +1 @@
export { Blur } from "./Blur";
@@ -120,6 +120,7 @@ export const InfisicalSecretInput = forwardRef<HTMLTextAreaElement, Props>(
const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused; const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused;
const { data: secrets } = useGetProjectSecrets({ const { data: secrets } = useGetProjectSecrets({
viewSecretValue: false,
environment: suggestionSource.environment || "", environment: suggestionSource.environment || "",
secretPath: suggestionSource.secretPath || "", secretPath: suggestionSource.secretPath || "",
workspaceId, workspaceId,
@@ -7,6 +7,15 @@ export enum ProjectPermissionActions {
Delete = "delete" Delete = "delete"
} }
export enum ProjectPermissionSecretActions {
DescribeAndReadValue = "read",
DescribeSecret = "describeSecret",
ReadValue = "readValue",
Create = "create",
Edit = "edit",
Delete = "delete"
}
export enum ProjectPermissionDynamicSecretActions { export enum ProjectPermissionDynamicSecretActions {
ReadRootCredential = "read-root-credential", ReadRootCredential = "read-root-credential",
CreateRootCredential = "create-root-credential", CreateRootCredential = "create-root-credential",
@@ -162,7 +171,7 @@ export type SecretImportSubjectFields = {
export type ProjectPermissionSet = export type ProjectPermissionSet =
| [ | [
ProjectPermissionActions, ProjectPermissionSecretActions,
( (
| ProjectPermissionSub.Secrets | ProjectPermissionSub.Secrets
| (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields) | (ForcedSubject<ProjectPermissionSub.Secrets> & SecretSubjectFields)
+1
View File
@@ -50,6 +50,7 @@ export type TUpdateAdminSlackConfigDTO = {
export type AdminGetUsersFilters = { export type AdminGetUsersFilters = {
limit: number; limit: number;
searchTerm: string; searchTerm: string;
adminsOnly: boolean;
}; };
export type AdminSlackConfig = { export type AdminSlackConfig = {
+2
View File
@@ -2,6 +2,8 @@ export {
useGetAuthToken, useGetAuthToken,
useOauthTokenExchange, useOauthTokenExchange,
useResetPassword, useResetPassword,
useResetPasswordV2,
useResetUserPasswordV2,
useSelectOrganization, useSelectOrganization,
useSendMfaToken, useSendMfaToken,
useSendPasswordResetEmail, useSendPasswordResetEmail,
+27 -1
View File
@@ -22,12 +22,15 @@ import {
LoginLDAPRes, LoginLDAPRes,
MfaMethod, MfaMethod,
ResetPasswordDTO, ResetPasswordDTO,
ResetPasswordV2DTO,
ResetUserPasswordV2DTO,
SendMfaTokenDTO, SendMfaTokenDTO,
SetupPasswordDTO, SetupPasswordDTO,
SRP1DTO, SRP1DTO,
SRPR1Res, SRPR1Res,
TOauthTokenExchangeDTO, TOauthTokenExchangeDTO,
UserAgentType, UserAgentType,
UserEncryptionVersion,
VerifyMfaTokenDTO, VerifyMfaTokenDTO,
VerifyMfaTokenRes, VerifyMfaTokenRes,
VerifySignupInviteDTO VerifySignupInviteDTO
@@ -247,7 +250,10 @@ export const useSendPasswordResetEmail = () => {
export const useVerifyPasswordResetCode = () => { export const useVerifyPasswordResetCode = () => {
return useMutation({ return useMutation({
mutationFn: async ({ email, code }: { email: string; code: string }) => { mutationFn: async ({ email, code }: { email: string; code: string }) => {
const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", { const { data } = await apiRequest.post<{
token: string;
userEncryptionVersion: UserEncryptionVersion;
}>("/api/v1/password/email/password-reset-verify", {
email, email,
code code
}); });
@@ -302,6 +308,26 @@ export const useResetPassword = () => {
}); });
}; };
export const useResetPasswordV2 = () => {
return useMutation({
mutationFn: async (details: ResetPasswordV2DTO) => {
await apiRequest.post("/api/v2/password/password-reset", details, {
headers: {
Authorization: `Bearer ${details.verificationToken}`
}
});
}
});
};
export const useResetUserPasswordV2 = () => {
return useMutation({
mutationFn: async (details: ResetUserPasswordV2DTO) => {
await apiRequest.post("/api/v2/password/user/password-reset", details);
}
});
};
export const changePassword = async (details: ChangePasswordDTO) => { export const changePassword = async (details: ChangePasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/change-password", details); const { data } = await apiRequest.post("/api/v1/password/change-password", details);
return data; return data;
+15
View File
@@ -3,6 +3,11 @@ export type GetAuthTokenAPI = {
organizationId?: string; organizationId?: string;
}; };
export enum UserEncryptionVersion {
V1 = 1,
V2 = 2
}
export type SendMfaTokenDTO = { export type SendMfaTokenDTO = {
email: string; email: string;
}; };
@@ -136,6 +141,16 @@ export type ResetPasswordDTO = {
password: string; password: string;
}; };
export type ResetPasswordV2DTO = {
newPassword: string;
verificationToken: string;
};
export type ResetUserPasswordV2DTO = {
oldPassword: string;
newPassword: string;
};
export type SetupPasswordDTO = { export type SetupPasswordDTO = {
protectedKey: string; protectedKey: string;
protectedKeyIV: string; protectedKeyIV: string;
@@ -1,4 +1,5 @@
export { export {
useGetAccessibleSecrets,
useGetProjectSecretsDetails, useGetProjectSecretsDetails,
useGetProjectSecretsOverview, useGetProjectSecretsOverview,
useGetProjectSecretsQuickSearch useGetProjectSecretsQuickSearch
@@ -11,6 +11,7 @@ import {
DashboardSecretsOrderBy, DashboardSecretsOrderBy,
TDashboardProjectSecretsQuickSearch, TDashboardProjectSecretsQuickSearch,
TDashboardProjectSecretsQuickSearchResponse, TDashboardProjectSecretsQuickSearchResponse,
TGetAccessibleSecretsDTO,
TGetDashboardProjectSecretsByKeys, TGetDashboardProjectSecretsByKeys,
TGetDashboardProjectSecretsDetailsDTO, TGetDashboardProjectSecretsDetailsDTO,
TGetDashboardProjectSecretsOverviewDTO, TGetDashboardProjectSecretsOverviewDTO,
@@ -20,6 +21,8 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries"; import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
import { groupBy, unique } from "@app/lib/fn/array"; import { groupBy, unique } from "@app/lib/fn/array";
import { SecretV3Raw } from "../types";
export const dashboardKeys = { export const dashboardKeys = {
all: () => ["dashboard"] as const, all: () => ["dashboard"] as const,
getDashboardSecrets: ({ getDashboardSecrets: ({
@@ -58,6 +61,17 @@ export const dashboardKeys = {
...dashboardKeys.getDashboardSecrets({ projectId, secretPath }), ...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
"quick-search", "quick-search",
params params
] as const,
getAccessibleSecrets: ({
projectId,
secretPath,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) =>
[
...dashboardKeys.all(),
"accessible-secrets",
{ projectId, secretPath, environment, filterByAction }
] as const ] as const
}; };
@@ -207,6 +221,7 @@ export const useGetProjectSecretsDetails = (
search = "", search = "",
includeSecrets, includeSecrets,
includeFolders, includeFolders,
viewSecretValue,
includeImports, includeImports,
includeDynamicSecrets, includeDynamicSecrets,
tags tags
@@ -231,6 +246,7 @@ export const useGetProjectSecretsDetails = (
limit, limit,
orderBy, orderBy,
orderDirection, orderDirection,
viewSecretValue,
offset, offset,
projectId, projectId,
environment, environment,
@@ -247,6 +263,7 @@ export const useGetProjectSecretsDetails = (
limit, limit,
orderBy, orderBy,
orderDirection, orderDirection,
viewSecretValue,
offset, offset,
projectId, projectId,
environment, environment,
@@ -292,6 +309,22 @@ export const fetchProjectSecretsQuickSearch = async ({
return data; return data;
}; };
const fetchAccessibleSecrets = async ({
projectId,
secretPath,
environment,
filterByAction
}: TGetAccessibleSecretsDTO) => {
const { data } = await apiRequest.get<{ secrets: SecretV3Raw[] }>(
"/api/v1/dashboard/accessible-secrets",
{
params: { projectId, secretPath, environment, filterByAction }
}
);
return data.secrets;
};
export const useGetProjectSecretsQuickSearch = ( export const useGetProjectSecretsQuickSearch = (
{ {
projectId, projectId,
@@ -354,3 +387,32 @@ export const useGetProjectSecretsQuickSearch = (
placeholderData: (previousData) => previousData placeholderData: (previousData) => previousData
}); });
}; };
export const useGetAccessibleSecrets = ({
projectId,
secretPath,
environment,
filterByAction,
options
}: TGetAccessibleSecretsDTO & {
options?: Omit<
UseQueryOptions<
SecretV3Raw[],
unknown,
SecretV3Raw[],
ReturnType<typeof dashboardKeys.getAccessibleSecrets>
>,
"queryKey" | "queryFn"
>;
}) => {
return useQuery({
...options,
queryKey: dashboardKeys.getAccessibleSecrets({
projectId,
secretPath,
environment,
filterByAction
}),
queryFn: () => fetchAccessibleSecrets({ projectId, secretPath, environment, filterByAction })
});
};
+11
View File
@@ -1,3 +1,4 @@
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { TSecretFolder } from "@app/hooks/api/secretFolders/types"; import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
@@ -69,6 +70,7 @@ export type TGetDashboardProjectSecretsDetailsDTO = Omit<
TGetDashboardProjectSecretsOverviewDTO, TGetDashboardProjectSecretsOverviewDTO,
"environments" "environments"
> & { > & {
viewSecretValue: boolean;
environment: string; environment: string;
includeImports?: boolean; includeImports?: boolean;
tags: Record<string, boolean>; tags: Record<string, boolean>;
@@ -100,3 +102,12 @@ export type TGetDashboardProjectSecretsByKeys = {
environment: string; environment: string;
keys: string[]; keys: string[];
}; };
export type TGetAccessibleSecretsDTO = {
projectId: string;
secretPath: string;
environment: string;
filterByAction:
| ProjectPermissionSecretActions.DescribeSecret
| ProjectPermissionSecretActions.ReadValue;
};
+1 -5
View File
@@ -10,18 +10,14 @@ import {
formatedConditionsOperatorNames, formatedConditionsOperatorNames,
PermissionConditionOperators PermissionConditionOperators
} from "@app/context/ProjectPermissionContext/types"; } from "@app/context/ProjectPermissionContext/types";
import { camelCaseToSpaces } from "@app/lib/fn/string";
import { ApiErrorTypes, TApiErrors } from "./types"; import { ApiErrorTypes, TApiErrors } from "./types";
// this is saved in react-query cache // this is saved in react-query cache
export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"]; export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"]; export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"]; export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
const camelCaseToSpaces = (input: string) => {
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
};
export const queryClient = new QueryClient({ export const queryClient = new QueryClient({
mutationCache: new MutationCache({ mutationCache: new MutationCache({
onError: (error) => { onError: (error) => {
@@ -79,6 +79,7 @@ export const decryptSecrets = (
id: encSecret.id, id: encSecret.id,
env: encSecret.environment, env: encSecret.environment,
key: secretKey, key: secretKey,
secretValueHidden: encSecret.secretValueHidden,
value: secretValue, value: secretValue,
tags: encSecret.tags, tags: encSecret.tags,
comment: secretComment, comment: secretComment,
@@ -137,6 +137,7 @@ export const useGetImportedSecretsSingleEnv = ({
env: encSecret.environment, env: encSecret.environment,
key: encSecret.secretKey, key: encSecret.secretKey,
value: encSecret.secretValue, value: encSecret.secretValue,
secretValueHidden: encSecret.secretValueHidden,
tags: encSecret.tags, tags: encSecret.tags,
comment: encSecret.secretComment, comment: encSecret.secretComment,
createdAt: encSecret.createdAt, createdAt: encSecret.createdAt,
@@ -176,6 +177,7 @@ export const useGetImportedSecretsAllEnvs = ({
env: encSecret.environment, env: encSecret.environment,
key: encSecret.secretKey, key: encSecret.secretKey,
value: encSecret.secretValue, value: encSecret.secretValue,
secretValueHidden: encSecret.secretValueHidden,
tags: encSecret.tags, tags: encSecret.tags,
comment: encSecret.secretComment, comment: encSecret.secretComment,
createdAt: encSecret.createdAt, createdAt: encSecret.createdAt,
@@ -75,6 +75,7 @@ export const useGetSnapshotSecrets = ({ snapshotId }: TSnapshotDataProps) =>
id: secretVersion.secretId, id: secretVersion.secretId,
env: data.environment.slug, env: data.environment.slug,
key: secretVersion.secretKey, key: secretVersion.secretKey,
secretValueHidden: secretVersion.secretValueHidden,
value: secretVersion.secretValue || "", value: secretVersion.secretValue || "",
tags: secretVersion.tags, tags: secretVersion.tags,
comment: secretVersion.secretComment, comment: secretVersion.secretComment,
+19 -5
View File
@@ -26,8 +26,13 @@ import {
export const secretKeys = { export const secretKeys = {
// this is also used in secretSnapshot part // this is also used in secretSnapshot part
getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) => getProjectSecret: ({
[{ workspaceId, environment, secretPath }, "secrets"] as const, workspaceId,
environment,
secretPath,
viewSecretValue
}: TGetProjectSecretsKey) =>
[{ workspaceId, environment, secretPath, viewSecretValue }, "secrets"] as const,
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const, getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const,
getSecretAccessList: ({ getSecretAccessList: ({
workspaceId, workspaceId,
@@ -44,13 +49,15 @@ export const fetchProjectSecrets = async ({
environment, environment,
secretPath, secretPath,
includeImports, includeImports,
expandSecretReferences expandSecretReferences,
viewSecretValue
}: TGetProjectSecretsKey) => { }: TGetProjectSecretsKey) => {
const { data } = await apiRequest.get<SecretV3RawResponse>("/api/v3/secrets/raw", { const { data } = await apiRequest.get<SecretV3RawResponse>("/api/v3/secrets/raw", {
params: { params: {
environment, environment,
workspaceId, workspaceId,
secretPath, secretPath,
viewSecretValue,
expandSecretReferences, expandSecretReferences,
include_imports: includeImports include_imports: includeImports
} }
@@ -68,6 +75,7 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
env: el.environment, env: el.environment,
key: el.secretKey, key: el.secretKey,
value: el.secretValue, value: el.secretValue,
secretValueHidden: el.secretValueHidden,
tags: el.tags || [], tags: el.tags || [],
comment: el.secretComment || "", comment: el.secretComment || "",
reminderRepeatDays: el.secretReminderRepeatDays, reminderRepeatDays: el.secretReminderRepeatDays,
@@ -107,6 +115,7 @@ export const useGetProjectSecrets = ({
workspaceId, workspaceId,
environment, environment,
secretPath, secretPath,
viewSecretValue,
options options
}: TGetProjectSecretsDTO & { }: TGetProjectSecretsDTO & {
options?: Omit< options?: Omit<
@@ -123,8 +132,13 @@ export const useGetProjectSecrets = ({
...options, ...options,
// wait for all values to be available // wait for all values to be available
enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true), enabled: Boolean(workspaceId && environment) && (options?.enabled ?? true),
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }), queryKey: secretKeys.getProjectSecret({
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath }), workspaceId,
environment,
secretPath,
viewSecretValue
}),
queryFn: () => fetchProjectSecrets({ workspaceId, environment, secretPath, viewSecretValue }),
select: useCallback( select: useCallback(
(data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => mergePersonalSecrets(data.secrets), (data: Awaited<ReturnType<typeof fetchProjectSecrets>>) => mergePersonalSecrets(data.secrets),
[] []
+5
View File
@@ -19,6 +19,7 @@ export type EncryptedSecret = {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHidden: boolean;
__v: number; __v: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
@@ -37,6 +38,7 @@ export type SecretV3RawSanitized = {
version: number; version: number;
key: string; key: string;
value?: string; value?: string;
secretValueHidden: boolean;
comment?: string; comment?: string;
reminderRepeatDays?: number | null; reminderRepeatDays?: number | null;
reminderNote?: string | null; reminderNote?: string | null;
@@ -61,6 +63,7 @@ export type SecretV3Raw = {
environment: string; environment: string;
version: number; version: number;
type: string; type: string;
secretValueHidden: boolean;
secretKey: string; secretKey: string;
secretPath: string; secretPath: string;
secretValue?: string; secretValue?: string;
@@ -95,6 +98,7 @@ export type SecretVersions = {
envId: string; envId: string;
secretKey: string; secretKey: string;
secretValue?: string; secretValue?: string;
secretValueHidden: boolean;
secretComment?: string; secretComment?: string;
tags: WsTag[]; tags: WsTag[];
__v: number; __v: number;
@@ -115,6 +119,7 @@ export type TGetProjectSecretsKey = {
environment: string; environment: string;
secretPath?: string; secretPath?: string;
includeImports?: boolean; includeImports?: boolean;
viewSecretValue?: boolean;
expandSecretReferences?: boolean; expandSecretReferences?: boolean;
}; };
+8 -1
View File
@@ -47,7 +47,8 @@ export enum ApiErrorTypes {
PermissionBoundaryError = "PermissionBoundaryError", PermissionBoundaryError = "PermissionBoundaryError",
BadRequestError = "BadRequest", BadRequestError = "BadRequest",
UnauthorizedError = "UnauthorizedError", UnauthorizedError = "UnauthorizedError",
ForbiddenError = "PermissionDenied" ForbiddenError = "PermissionDenied",
CustomForbiddenError = "ForbiddenError"
} }
export type TApiErrors = export type TApiErrors =
@@ -70,6 +71,12 @@ export type TApiErrors =
details: PureAbility["rules"]; details: PureAbility["rules"];
statusCode: 403; statusCode: 403;
} }
| {
reqId: string;
error: ApiErrorTypes.CustomForbiddenError;
message: string;
statusCode: 403;
}
| { | {
reqId: string; reqId: string;
statusCode: 400; statusCode: 400;
+3 -2
View File
@@ -251,12 +251,13 @@ export const useUpdateProject = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<Workspace, object, UpdateProjectDTO>({ return useMutation<Workspace, object, UpdateProjectDTO>({
mutationFn: async ({ projectID, newProjectName, newProjectDescription }) => { mutationFn: async ({ projectID, newProjectName, newProjectDescription, newSlug }) => {
const { data } = await apiRequest.patch<{ workspace: Workspace }>( const { data } = await apiRequest.patch<{ workspace: Workspace }>(
`/api/v1/workspace/${projectID}`, `/api/v1/workspace/${projectID}`,
{ {
name: newProjectName, name: newProjectName,
description: newProjectDescription description: newProjectDescription,
slug: newSlug
} }
); );
return data.workspace; return data.workspace;
@@ -74,6 +74,7 @@ export type UpdateProjectDTO = {
projectID: string; projectID: string;
newProjectName: string; newProjectName: string;
newProjectDescription?: string; newProjectDescription?: string;
newSlug?: string;
}; };
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number }; export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
@@ -30,10 +30,13 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuTrigger, DropdownMenuTrigger,
Modal,
ModalContent,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { envConfig } from "@app/config/env"; import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context"; import { useOrganization, useSubscription, useUser } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { import {
useGetOrganizations, useGetOrganizations,
@@ -50,6 +53,7 @@ import { ProjectType } from "@app/hooks/api/workspace/types";
import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils"; import { navigateUserToOrg } from "@app/pages/auth/LoginPage/Login.utils";
import { MenuIconButton } from "../MenuIconButton"; import { MenuIconButton } from "../MenuIconButton";
import { ServerAdminsPanel } from "../ServerAdminsPanel/ServerAdminsPanel";
const getPlan = (subscription: SubscriptionPlan) => { const getPlan = (subscription: SubscriptionPlan) => {
if (subscription.dynamicSecret) return "Enterprise Plan"; if (subscription.dynamicSecret) return "Enterprise Plan";
@@ -77,6 +81,11 @@ export const INFISICAL_SUPPORT_OPTIONS = [
<FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />, <FontAwesomeIcon key={4} className="pr-4 text-sm" icon={faEnvelope} />,
"Email Support", "Email Support",
"mailto:[email protected]" "mailto:[email protected]"
],
[
<FontAwesomeIcon key={5} className="pr-4 text-sm" icon={faUsers} />,
"Instance Admins",
"server-admins"
] ]
]; ];
@@ -89,6 +98,7 @@ export const MinimizedOrgSidebar = () => {
const [openSupport, setOpenSupport] = useState(false); const [openSupport, setOpenSupport] = useState(false);
const [openUser, setOpenUser] = useState(false); const [openUser, setOpenUser] = useState(false);
const [openOrg, setOpenOrg] = useState(false); const [openOrg, setOpenOrg] = useState(false);
const [showAdminsModal, setShowAdminsModal] = useState(false);
const { user } = useUser(); const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl(); const { mutateAsync } = useGetOrgTrialUrl();
@@ -410,21 +420,39 @@ export const MinimizedOrgSidebar = () => {
side="right" side="right"
className="p-1" className="p-1"
> >
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => ( {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => {
<DropdownMenuItem key={url as string}> if (url === "server-admins" && isInfisicalCloud()) {
<a return null;
target="_blank" }
rel="noopener noreferrer" return (
href={String(url)} <DropdownMenuItem key={url as string}>
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200" {url === "server-admins" ? (
> <button
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md"> type="button"
{icon} onClick={() => setShowAdminsModal(true)}
<div className="text-sm">{text}</div> className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
</div> >
</a> <div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
</DropdownMenuItem> {icon}
))} <div className="text-sm">{text}</div>
</div>
</button>
) : (
<a
target="_blank"
rel="noopener noreferrer"
href={String(url)}
className="flex w-full items-center rounded-md font-normal text-mineshaft-300 duration-200"
>
<div className="relative flex w-full cursor-pointer select-none items-center justify-start rounded-md">
{icon}
<div className="text-sm">{text}</div>
</div>
</a>
)}
</DropdownMenuItem>
);
})}
{envConfig.PLATFORM_VERSION && ( {envConfig.PLATFORM_VERSION && (
<div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200"> <div className="mb-2 mt-2 w-full cursor-default pl-5 text-sm duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" /> <FontAwesomeIcon icon={faInfo} className="mr-4 px-[0.1rem]" />
@@ -540,6 +568,13 @@ export const MinimizedOrgSidebar = () => {
</div> </div>
</nav> </nav>
</aside> </aside>
<Modal isOpen={showAdminsModal} onOpenChange={setShowAdminsModal}>
<ModalContent title="Server Administrators" subTitle="View all server administrators">
<div className="mb-2">
<ServerAdminsPanel />
</div>
</ModalContent>
</Modal>
<CreateOrgModal <CreateOrgModal
isOpen={popUp?.createOrg?.isOpen} isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)} onClose={() => handlePopUpToggle("createOrg", false)}
@@ -0,0 +1,85 @@
import { useState } from "react";
import { faMagnifyingGlass } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Input,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useDebounce } from "@app/hooks";
import { useGetOrgUsers } from "@app/hooks/api";
export const ServerAdminsPanel = () => {
const [searchUserFilter, setSearchUserFilter] = useState("");
const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
const { currentOrg } = useOrganization();
const { data: orgUsers, isPending } = useGetOrgUsers(currentOrg?.id || "");
const adminUsers = orgUsers?.filter((orgUser) => {
const isSuperAdmin = orgUser.user.superAdmin;
const matchesSearch = debounedSearchTerm
? orgUser.user.email?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) ||
orgUser.user.firstName?.toLowerCase().includes(debounedSearchTerm.toLowerCase()) ||
orgUser.user.lastName?.toLowerCase().includes(debounedSearchTerm.toLowerCase())
: true;
return isSuperAdmin && matchesSearch;
});
const isEmpty = !isPending && (!adminUsers || adminUsers.length === 0);
return (
<div className="flex h-full flex-col">
<div className="mb-4 px-4">
<Input
value={searchUserFilter}
onChange={(e) => setSearchUserFilter(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search server admins..."
className="w-full"
/>
</div>
<div className="flex-1 px-2">
<TableContainer className="flex max-h-[30vh] flex-col overflow-auto">
<Table className="w-full table-fixed">
<THead className="sticky top-0 bg-bunker-800">
<Tr>
<Th className="w-1/2">Name</Th>
<Th className="w-1/2">Email</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={2} innerKey="admins" />}
{!isPending &&
adminUsers?.map(({ user }) => {
const name =
user.firstName || user.lastName
? `${user.firstName} ${user.lastName}`
: user.username;
return (
<Tr key={`admin-${user.id}`}>
<Td className="w-1/2 break-words">{name}</Td>
<Td className="w-1/2 break-words">{user.email}</Td>
</Tr>
);
})}
</TBody>
</Table>
{isEmpty && (
<div className="flex h-32 items-center justify-center text-sm text-mineshaft-400">
No server administrators found
</div>
)}
</TableContainer>
</div>
</div>
);
};
+36
View File
@@ -0,0 +1,36 @@
import { MongoAbility, subject } from "@casl/ability";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext";
import {
ProjectPermissionSecretActions,
ProjectPermissionSub,
SecretSubjectFields
} from "@app/context/ProjectPermissionContext/types";
export function hasSecretReadValueOrDescribePermission(
permission: MongoAbility<ProjectPermissionSet>,
action: Extract<
ProjectPermissionSecretActions,
ProjectPermissionSecretActions.DescribeSecret | ProjectPermissionSecretActions.ReadValue
>,
subjectFields?: SecretSubjectFields
) {
let canNewPermission = false;
let canOldPermission = false;
if (subjectFields) {
canNewPermission = permission.can(action, subject(ProjectPermissionSub.Secrets, subjectFields));
canOldPermission = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
subject(ProjectPermissionSub.Secrets, subjectFields)
);
} else {
canNewPermission = permission.can(action, ProjectPermissionSub.Secrets);
canOldPermission = permission.can(
ProjectPermissionSecretActions.DescribeAndReadValue,
ProjectPermissionSub.Secrets
);
}
return canNewPermission || canOldPermission;
}
+4
View File
@@ -7,3 +7,7 @@ export const formatReservedPaths = (secretPath: string) => {
} }
return secretPath; return secretPath;
}; };
export const camelCaseToSpaces = (input: string) => {
return input.replace(/([a-z])([A-Z])/g, "$1 $2");
};
@@ -1,6 +1,14 @@
import { useState } from "react"; import { useState } from "react";
import { faMagnifyingGlass, faUsers, faEllipsis } from "@fortawesome/free-solid-svg-icons"; import {
faCheckCircle,
faEllipsis,
faFilter,
faMagnifyingGlass,
faUsers,
faUserShield
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
@@ -8,7 +16,13 @@ import {
Badge, Badge,
Button, Button,
DeleteActionModal, DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
EmptyState, EmptyState,
IconButton,
Input, Input,
Table, Table,
TableContainer, TableContainer,
@@ -17,11 +31,7 @@ import {
Td, Td,
Th, Th,
THead, THead,
Tr, Tr
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2"; } from "@app/components/v2";
import { useSubscription, useUser } from "@app/context"; import { useSubscription, useUser } from "@app/context";
import { useDebounce, usePopUp } from "@app/hooks"; import { useDebounce, usePopUp } from "@app/hooks";
@@ -48,6 +58,7 @@ const UserPanelTable = ({
) => void; ) => void;
}) => { }) => {
const [searchUserFilter, setSearchUserFilter] = useState(""); const [searchUserFilter, setSearchUserFilter] = useState("");
const [adminsOnly, setAdminsOnly] = useState(false);
const { user } = useUser(); const { user } = useUser();
const userId = user?.id || ""; const userId = user?.id || "";
const [debounedSearchTerm] = useDebounce(searchUserFilter, 500); const [debounedSearchTerm] = useDebounce(searchUserFilter, 500);
@@ -55,18 +66,55 @@ const UserPanelTable = ({
const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({ const { data, isPending, isFetchingNextPage, hasNextPage, fetchNextPage } = useAdminGetUsers({
limit: 20, limit: 20,
searchTerm: debounedSearchTerm searchTerm: debounedSearchTerm,
adminsOnly
}); });
const isEmpty = !isPending && !data?.pages?.[0].length; const isEmpty = !isPending && !data?.pages?.[0].length;
const isTableFiltered = Boolean(adminsOnly);
return ( return (
<> <>
<Input <div className="flex gap-2">
value={searchUserFilter} <Input
onChange={(e) => setSearchUserFilter(e.target.value)} value={searchUserFilter}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />} onChange={(e) => setSearchUserFilter(e.target.value)}
placeholder="Search users..." leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
/> placeholder="Search users..."
className="flex-1"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Filter Users"
variant="plain"
size="sm"
className={twMerge(
"flex h-10 w-11 items-center justify-center overflow-hidden border border-mineshaft-600 bg-mineshaft-800 p-0 transition-all hover:border-primary/60 hover:bg-primary/10",
isTableFiltered && "border-primary/50 text-primary"
)}
>
<FontAwesomeIcon icon={faFilter} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="p-0">
<DropdownMenuLabel>Filter By</DropdownMenuLabel>
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
setAdminsOnly(!adminsOnly);
}}
icon={adminsOnly && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faUserShield} className="text-yellow-700" />
<span>Server Admins</span>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="mt-4"> <div className="mt-4">
<TableContainer> <TableContainer>
<Table> <Table>
@@ -1,4 +1,3 @@
import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -8,16 +7,13 @@ import { AnimatePresence, motion } from "framer-motion";
import { z } from "zod"; import { z } from "zod";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { generateBackupPDFAsync } from "@app/components/utilities/generateBackupPDF";
// TODO(akhilmhdh): rewrite this into module functions in lib // TODO(akhilmhdh): rewrite this into module functions in lib
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, ContentLoader, FormControl, Input } from "@app/components/v2"; import { Button, ContentLoader, FormControl, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context"; import { useServerConfig } from "@app/context";
import { useCreateAdminUser, useSelectOrganization } from "@app/hooks/api"; import { useCreateAdminUser, useSelectOrganization } from "@app/hooks/api";
import { generateUserBackupKey, generateUserPassKey } from "@app/lib/crypto"; import { generateUserPassKey } from "@app/lib/crypto";
import { DownloadBackupKeys } from "./components/DownloadBackupKeys";
const formSchema = z const formSchema = z
.object({ .object({
@@ -34,25 +30,17 @@ const formSchema = z
type TFormSchema = z.infer<typeof formSchema>; type TFormSchema = z.infer<typeof formSchema>;
enum SignupSteps {
DetailsForm = "details-form",
BackupKey = "backup-key"
}
export const SignUpPage = () => { export const SignUpPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
control, control,
handleSubmit, handleSubmit,
getValues,
formState: { isSubmitting } formState: { isSubmitting }
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}); });
const [step, setStep] = useState(SignupSteps.DetailsForm);
const { config } = useServerConfig(); const { config } = useServerConfig();
const { mutateAsync: createAdminUser } = useCreateAdminUser(); const { mutateAsync: createAdminUser } = useCreateAdminUser();
const { mutateAsync: selectOrganization } = useSelectOrganization(); const { mutateAsync: selectOrganization } = useSelectOrganization();
@@ -84,7 +72,7 @@ export const SignUpPage = () => {
// Will be refactored in next iteration to make it url based rather than local storage ones // Will be refactored in next iteration to make it url based rather than local storage ones
// Part of migration to nextjs 14 // Part of migration to nextjs 14
localStorage.setItem("orgData.id", res.organization.id); localStorage.setItem("orgData.id", res.organization.id);
setStep(SignupSteps.BackupKey); navigate({ to: "/admin" });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
createNotification({ createNotification({
@@ -94,27 +82,7 @@ export const SignUpPage = () => {
} }
}; };
const handleBackupKeyGenerate = async () => { if (config?.initialized) return <ContentLoader text="Redirecting to admin page..." />;
try {
const { email, password, firstName, lastName } = getValues();
const generatedKey = await generateUserBackupKey(email, password);
await generateBackupPDFAsync({
generatedKey,
personalEmail: email,
personalName: `${firstName} ${lastName}`
});
navigate({ to: "/admin" });
} catch (err) {
console.log(err);
createNotification({
type: "error",
text: "Failed to generate backup"
});
}
};
if (config?.initialized && step === SignupSteps.DetailsForm)
return <ContentLoader text="Redirecting to admin page..." />;
return ( return (
<div className="flex max-h-screen min-h-screen flex-col justify-center overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6"> <div className="flex max-h-screen min-h-screen flex-col justify-center overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700 px-6">
@@ -127,56 +95,28 @@ export const SignUpPage = () => {
</Helmet> </Helmet>
<div className="flex items-center justify-center"> <div className="flex items-center justify-center">
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{step === SignupSteps.DetailsForm && ( <motion.div
<motion.div className="text-mineshaft-200"
className="text-mineshaft-200" key="panel-1"
key="panel-1" transition={{ duration: 0.15 }}
transition={{ duration: 0.15 }} initial={{ opacity: 0, translateX: 30 }}
initial={{ opacity: 0, translateX: 30 }} animate={{ opacity: 1, translateX: 0 }}
animate={{ opacity: 1, translateX: 0 }} exit={{ opacity: 0, translateX: 30 }}
exit={{ opacity: 0, translateX: 30 }} >
> <div className="flex flex-col items-center space-y-2 text-center">
<div className="flex flex-col items-center space-y-2 text-center"> <img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" />
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical logo" /> <div className="pt-4 text-4xl">Welcome to Infisical</div>
<div className="pt-4 text-4xl">Welcome to Infisical</div> <div className="pb-4 text-bunker-300">Create your first Super Admin Account</div>
<div className="pb-4 text-bunker-300">Create your first Super Admin Account</div> </div>
</div> <form onSubmit={handleSubmit(handleFormSubmit)}>
<form onSubmit={handleSubmit(handleFormSubmit)}> <div className="mt-8">
<div className="mt-8"> <div className="flex items-center space-x-4">
<div className="flex items-center space-x-4">
<Controller
control={control}
name="firstName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="First name"
errorText={error?.message}
isError={Boolean(error)}
>
<Input isFullWidth size="md" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="lastName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Last name"
errorText={error?.message}
isError={Boolean(error)}
>
<Input isFullWidth size="md" {...field} />
</FormControl>
)}
/>
</div>
<Controller <Controller
control={control} control={control}
name="email" name="firstName"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Email" label="First name"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
> >
@@ -186,56 +126,66 @@ export const SignUpPage = () => {
/> />
<Controller <Controller
control={control} control={control}
name="password" name="lastName"
render={({ field, fieldState: { error } }) => ( render={({ field, fieldState: { error } }) => (
<FormControl <FormControl
label="Password" label="Last name"
errorText={error?.message} errorText={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
> >
<Input isFullWidth size="md" type="password" {...field} /> <Input isFullWidth size="md" {...field} />
</FormControl>
)}
/>
<Controller
control={control}
name="confirmPassword"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Confirm password"
errorText={error?.message}
isError={Boolean(error)}
>
<Input isFullWidth size="md" type="password" {...field} />
</FormControl> </FormControl>
)} )}
/> />
</div> </div>
<Button <Controller
type="submit" control={control}
colorSchema="primary" name="email"
variant="outline_bg" render={({ field, fieldState: { error } }) => (
isFullWidth <FormControl label="Email" errorText={error?.message} isError={Boolean(error)}>
className="mt-4" <Input isFullWidth size="md" {...field} />
isLoading={isSubmitting} </FormControl>
> )}
Continue />
</Button> <Controller
</form> control={control}
</motion.div> name="password"
)} render={({ field, fieldState: { error } }) => (
{step === SignupSteps.BackupKey && ( <FormControl
<motion.div label="Password"
className="text-mineshaft-200" errorText={error?.message}
key="panel-2" isError={Boolean(error)}
transition={{ duration: 0.15 }} >
initial={{ opacity: 0, translateX: 30 }} <Input isFullWidth size="md" type="password" {...field} />
animate={{ opacity: 1, translateX: 0 }} </FormControl>
exit={{ opacity: 0, translateX: 30 }} )}
> />
<DownloadBackupKeys onGenerate={handleBackupKeyGenerate} /> <Controller
</motion.div> control={control}
)} name="confirmPassword"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Confirm password"
errorText={error?.message}
isError={Boolean(error)}
>
<Input isFullWidth size="md" type="password" {...field} />
</FormControl>
)}
/>
</div>
<Button
type="submit"
colorSchema="primary"
variant="outline_bg"
isFullWidth
className="mt-4"
isLoading={isSubmitting}
>
Continue
</Button>
</form>
</motion.div>
</AnimatePresence> </AnimatePresence>
</div> </div>
</div> </div>
@@ -1,56 +0,0 @@
import { useTranslation } from "react-i18next";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "@app/components/v2";
import { useToggle } from "@app/hooks";
type Props = {
onGenerate: () => Promise<void>;
};
export const DownloadBackupKeys = ({ onGenerate }: Props): JSX.Element => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useToggle();
return (
<div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
<p className="flex flex-col items-center justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
<FontAwesomeIcon
icon={faWarning}
className="mb-6 ml-2 mr-3 pt-1 text-6xl text-bunker-200"
/>
{t("signup.step4-message")}
</p>
<div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
<div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
<span className="mb-2">
{t("signup.step4-description1")} {t("signup.step4-description3")}
</span>
</div>
<div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
<div className="text-l w-full py-1 text-lg">
<Button
onClick={async () => {
try {
setIsLoading.on();
await onGenerate();
} finally {
setIsLoading.off();
}
}}
size="sm"
isFullWidth
className="h-12"
colorSchema="primary"
variant="outline_bg"
isLoading={isLoading}
>
Download PDF
</Button>
</div>
</div>
</div>
</div>
);
};
@@ -1 +0,0 @@
export { DownloadBackupKeys } from "./DownloadBackupKeys";
@@ -1,396 +1,75 @@
import crypto from "crypto"; import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { FormEvent, useState } from "react"; import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import jsrp from "jsrp";
import InputField from "@app/components/basic/InputField"; import { ConfirmEmailStep } from "./components/ConfirmEmailStep";
import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck"; import { EnterPasswordStep } from "./components/EnterPasswordStep";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { InputBackupKeyStep } from "./components/InputBackupKeyStep";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { Button } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useResetPassword, useVerifyPasswordResetCode } from "@app/hooks/api";
import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
// eslint-disable-next-line new-cap enum Steps {
const client = new jsrp.client(); ConfirmEmail = 1,
InputBackupKey = 2,
EnterNewPassword = 3
}
const formData = z.object({
verificationToken: z.string(),
privateKey: z.string(),
userEncryptionVersion: z.nativeEnum(UserEncryptionVersion)
});
type TFormData = z.infer<typeof formData>;
export const PasswordResetPage = () => { export const PasswordResetPage = () => {
const [verificationToken, setVerificationToken] = useState(""); const { watch, setValue } = useForm<TFormData>({
const [step, setStep] = useState(1); resolver: zodResolver(formData)
const [loading, setLoading] = useState(false); });
const [backupKey, setBackupKey] = useState("");
const [privateKey, setPrivateKey] = useState("");
const [newPassword, setNewPassword] = useState("");
const [backupKeyError, setBackupKeyError] = useState(false);
const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(false);
const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false);
const [passwordErrorNoLetterChar, setPasswordErrorNoLetterChar] = useState(false);
const [passwordErrorNoNumOrSpecialChar, setPasswordErrorNoNumOrSpecialChar] = useState(false);
const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false);
const [passwordErrorEscapeChar, setPasswordErrorEscapeChar] = useState(false);
const [passwordErrorLowEntropy, setPasswordErrorLowEntropy] = useState(false);
const [passwordErrorBreached, setPasswordErrorBreached] = useState(false);
const verificationToken = watch("verificationToken");
const encryptionVersion = watch("userEncryptionVersion");
const privateKey = watch("privateKey");
const [step, setStep] = useState<Steps>(Steps.ConfirmEmail);
const navigate = useNavigate(); const navigate = useNavigate();
const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
const {
mutateAsync: verifyPasswordResetCodeMutateAsync,
isPending: isVerifyPasswordResetLoading
} = useVerifyPasswordResetCode();
const { mutateAsync: resetPasswordMutateAsync } = useResetPassword();
const parsedUrl = search;
const token = parsedUrl.token as string;
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
// Decrypt the private key with a backup key
const getEncryptedKeyHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const result = await getBackupEncryptedPrivateKey({ verificationToken });
setPrivateKey(
Aes256Gcm.decrypt({
ciphertext: result.encryptedPrivateKey,
iv: result.iv,
tag: result.tag,
secret: backupKey
})
);
setStep(3);
} catch (err) {
console.error(err);
setBackupKeyError(true);
}
};
// If everything is correct, reset the password
const resetPasswordHandler = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const errorCheck = await passwordCheck({
password: newPassword,
setPasswordErrorTooShort,
setPasswordErrorTooLong,
setPasswordErrorNoLetterChar,
setPasswordErrorNoNumOrSpecialChar,
setPasswordErrorRepeatedChar,
setPasswordErrorEscapeChar,
setPasswordErrorLowEntropy,
setPasswordErrorBreached
});
if (!errorCheck) {
client.init(
{
username: email,
password: newPassword
},
async () => {
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
const derivedKey = await deriveArgonKey({
password: newPassword,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
await resetPasswordMutateAsync({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
verificationToken,
password: newPassword
});
navigate({ to: "/login" });
setLoading(false);
});
}
);
}
};
// Click a button to confirm email
const stepConfirmEmail = (
<div className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 py-6 drop-shadow-xl md:max-w-lg md:px-6">
<p className="mb-8 flex justify-center bg-gradient-to-br from-sky-400 to-primary bg-clip-text text-center text-4xl font-semibold text-transparent">
Confirm your email
</p>
<img
src="/images/envelope.svg"
style={{ height: "262px", width: "410px" }}
alt="verify email"
/>
<div className="mx-auto mb-2 mt-4 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
<Button
onClick={async () => {
try {
const response = await verifyPasswordResetCodeMutateAsync({
email,
code: token
});
setVerificationToken(response.token);
setStep(2);
} catch (err) {
console.log("ERROR", err);
navigate({ to: "/email-not-verified" });
}
}}
isLoading={isVerifyPasswordResetLoading}
size="lg"
>
Confirm Email
</Button>
</div>
</div>
);
// Input backup key
const stepInputBackupKey = (
<form
onSubmit={getEncryptedKeyHandler}
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
>
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Enter your backup key
</p>
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
You can find it in your emergency kit. You had to download the emergency kit during
signup.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
<InputField
label="Backup Key"
onChangeHandler={setBackupKey}
type="password"
value={backupKey}
placeholder=""
isRequired
error={backupKeyError}
errorText="Something is wrong with the backup key"
/>
</div>
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button type="submit" size="lg">
Submit Backup Key
</Button>
</div>
</div>
</form>
);
// Enter new password
const stepEnterNewPassword = (
<form
onSubmit={resetPasswordHandler}
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
>
<p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Enter new password
</p>
<div className="mt-1 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
Make sure you save it somewhere safe.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
<InputField
label="New Password"
onChangeHandler={(password) => {
setNewPassword(password);
passwordCheck({
password,
setPasswordErrorTooShort,
setPasswordErrorTooLong,
setPasswordErrorNoLetterChar,
setPasswordErrorNoNumOrSpecialChar,
setPasswordErrorRepeatedChar,
setPasswordErrorEscapeChar,
setPasswordErrorLowEntropy,
setPasswordErrorBreached
});
}}
type="password"
value={newPassword}
isRequired
error={
passwordErrorTooShort &&
passwordErrorTooLong &&
passwordErrorNoLetterChar &&
passwordErrorNoNumOrSpecialChar &&
passwordErrorRepeatedChar &&
passwordErrorEscapeChar &&
passwordErrorLowEntropy &&
passwordErrorBreached
}
autoComplete="new-password"
id="new-password"
/>
</div>
{passwordErrorTooShort ||
passwordErrorTooLong ||
passwordErrorNoLetterChar ||
passwordErrorNoNumOrSpecialChar ||
passwordErrorRepeatedChar ||
passwordErrorEscapeChar ||
passwordErrorLowEntropy ||
passwordErrorBreached ? (
<div className="mx-2 mb-2 mt-3 flex w-full max-w-md flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400">Password should contain:</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorTooShort ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}>
at least 14 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorTooLong ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}>
at most 100 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNoLetterChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at least 1 letter character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNoNumOrSpecialChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorNoNumOrSpecialChar ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
at least 1 number or special character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorRepeatedChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at most 3 repeated, consecutive characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorEscapeChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
No escape characters allowed.
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowEntropy ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`}
>
Password contains personal info.
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorBreached ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorBreached ? "text-gray-400" : "text-gray-600"} text-sm`}>
Password was found in a data breach.
</div>
</div>
</div>
) : (
<div className="py-2" />
)}
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button type="submit" onClick={() => setLoading(true)} size="lg" isLoading={loading}>
Submit New Password
</Button>
</div>
</div>
</form>
);
return ( return (
<div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800"> <div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800">
{step === 1 && stepConfirmEmail} {step === Steps.ConfirmEmail && (
{step === 2 && stepInputBackupKey} <ConfirmEmailStep
{step === 3 && stepEnterNewPassword} onComplete={(verifyToken, userEncryptionVersion) => {
setValue("verificationToken", verifyToken);
setValue("userEncryptionVersion", userEncryptionVersion);
if (userEncryptionVersion === UserEncryptionVersion.V2) {
setStep(Steps.EnterNewPassword);
} else {
setStep(Steps.InputBackupKey);
}
}}
/>
)}
{step === Steps.InputBackupKey && (
<InputBackupKeyStep
verificationToken={verificationToken}
onComplete={(key) => {
setValue("privateKey", key);
setStep(Steps.EnterNewPassword);
}}
/>
)}
{step === Steps.EnterNewPassword && (
<EnterPasswordStep
verificationToken={verificationToken}
privateKey={privateKey}
encryptionVersion={encryptionVersion}
onComplete={() => {
navigate({ to: "/login" });
}}
/>
)}
</div> </div>
); );
}; };
@@ -0,0 +1,54 @@
import { useNavigate, useSearch } from "@tanstack/react-router";
import { Button } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useVerifyPasswordResetCode } from "@app/hooks/api";
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
type Props = {
onComplete: (verificationToken: string, encryptionVersion: UserEncryptionVersion) => void;
};
export const ConfirmEmailStep = ({ onComplete }: Props) => {
const navigate = useNavigate();
const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
const { token, to: email } = search;
const {
mutateAsync: verifyPasswordResetCodeMutateAsync,
isPending: isVerifyPasswordResetLoading
} = useVerifyPasswordResetCode();
return (
<div className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 py-6 drop-shadow-xl md:max-w-lg md:px-6">
<p className="mb-8 flex justify-center bg-gradient-to-br from-sky-400 to-primary bg-clip-text text-center text-4xl font-semibold text-transparent">
Confirm your email
</p>
<img
src="/images/envelope.svg"
style={{ height: "262px", width: "410px" }}
alt="verify email"
/>
<div className="mx-auto mb-2 mt-4 flex max-h-24 max-w-md flex-col items-center justify-center px-4 text-lg md:p-2">
<Button
onClick={async () => {
try {
const response = await verifyPasswordResetCodeMutateAsync({
email,
code: token
});
onComplete(response.token, response.userEncryptionVersion);
} catch (err) {
console.log("ERROR", err);
navigate({ to: "/email-not-verified" });
}
}}
isLoading={isVerifyPasswordResetLoading}
size="lg"
>
Confirm Email
</Button>
</div>
</div>
);
};
@@ -0,0 +1,325 @@
import crypto from "crypto";
import { Controller, useForm } from "react-hook-form";
import { faCheck, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { useSearch } from "@tanstack/react-router";
import jsrp from "jsrp";
import { z } from "zod";
import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { Button, FormControl, Input } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useResetPassword, useResetPasswordV2 } from "@app/hooks/api";
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
const formData = z.object({
password: z.string(),
passwordErrorTooShort: z.boolean().optional(),
passwordErrorTooLong: z.boolean().optional(),
passwordErrorNoLetterChar: z.boolean().optional(),
passwordErrorNoNumOrSpecialChar: z.boolean().optional(),
passwordErrorRepeatedChar: z.boolean().optional(),
passwordErrorEscapeChar: z.boolean().optional(),
passwordErrorLowEntropy: z.boolean().optional(),
passwordErrorBreached: z.boolean()
});
type TFormData = z.infer<typeof formData>;
type Props = {
verificationToken: string;
privateKey: string;
encryptionVersion: UserEncryptionVersion;
onComplete: () => void;
};
export const EnterPasswordStep = ({
verificationToken,
encryptionVersion,
privateKey,
onComplete
}: Props) => {
const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id });
const { to: email } = search;
const {
control,
watch,
handleSubmit,
setValue,
formState: { isSubmitting }
} = useForm<TFormData>({
resolver: zodResolver(formData)
});
const { mutateAsync: resetPassword, isPending: isLoading } = useResetPassword();
const { mutateAsync: resetPasswordV2, isPending: isLoadingV2 } = useResetPasswordV2();
const passwordErrorTooShort = watch("passwordErrorTooShort");
const passwordErrorTooLong = watch("passwordErrorTooLong");
const passwordErrorNoLetterChar = watch("passwordErrorNoLetterChar");
const passwordErrorNoNumOrSpecialChar = watch("passwordErrorNoNumOrSpecialChar");
const passwordErrorRepeatedChar = watch("passwordErrorRepeatedChar");
const passwordErrorEscapeChar = watch("passwordErrorEscapeChar");
const passwordErrorLowEntropy = watch("passwordErrorLowEntropy");
const passwordErrorBreached = watch("passwordErrorBreached");
const isPasswordError =
passwordErrorTooShort ||
passwordErrorTooLong ||
passwordErrorNoLetterChar ||
passwordErrorNoNumOrSpecialChar ||
passwordErrorRepeatedChar ||
passwordErrorEscapeChar ||
passwordErrorLowEntropy ||
passwordErrorBreached;
const handlePasswordCheck = async (checkPassword: string) => {
const errorCheck = await passwordCheck({
password: checkPassword,
setPasswordErrorTooShort: (v) => setValue("passwordErrorTooShort", v),
setPasswordErrorTooLong: (v) => setValue("passwordErrorTooLong", v),
setPasswordErrorNoLetterChar: (v) => setValue("passwordErrorNoLetterChar", v),
setPasswordErrorNoNumOrSpecialChar: (v) => setValue("passwordErrorNoNumOrSpecialChar", v),
setPasswordErrorRepeatedChar: (v) => setValue("passwordErrorRepeatedChar", v),
setPasswordErrorEscapeChar: (v) => setValue("passwordErrorEscapeChar", v),
setPasswordErrorLowEntropy: (v) => setValue("passwordErrorLowEntropy", v),
setPasswordErrorBreached: (v) => setValue("passwordErrorBreached", v)
});
return errorCheck;
};
const resetPasswordHandler = async (data: TFormData) => {
const errorCheck = await handlePasswordCheck(data.password);
if (errorCheck) return;
if (encryptionVersion === UserEncryptionVersion.V2) {
await resetPasswordV2({
newPassword: data.password,
verificationToken
});
} else {
// eslint-disable-next-line new-cap
const client = new jsrp.client();
client.init(
{
username: email,
password: data.password
},
async () => {
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
const derivedKey = await deriveArgonKey({
password: data.password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
await resetPassword({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
verificationToken,
password: data.password
});
});
}
);
}
onComplete();
};
return (
<form
onSubmit={handleSubmit(resetPasswordHandler)}
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
>
<p className="mx-auto flex w-max justify-center text-2xl font-semibold text-bunker-100 md:text-3xl">
Enter new password
</p>
<div className="mt-1 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="flex w-max max-w-md justify-center text-sm text-gray-400">
Make sure you save it somewhere safe.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
<Controller
control={control}
name="password"
render={({ field }) => (
<FormControl
className="w-full"
label="New Password"
isRequired
isError={isPasswordError}
>
<Input
{...field}
onChange={(e) => {
field.onChange(e);
handlePasswordCheck(e.target.value);
}}
type="password"
/>
</FormControl>
)}
/>
</div>
{passwordErrorTooShort ||
passwordErrorTooLong ||
passwordErrorNoLetterChar ||
passwordErrorNoNumOrSpecialChar ||
passwordErrorRepeatedChar ||
passwordErrorEscapeChar ||
passwordErrorLowEntropy ||
passwordErrorBreached ? (
<div className="mx-2 mb-2 mt-3 flex w-full max-w-md flex-col items-start rounded-md bg-white/5 px-2 py-2">
<div className="mb-1 text-sm text-gray-400">Password should contain:</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorTooShort ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}>
at least 14 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorTooLong ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}>
at most 100 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNoLetterChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at least 1 letter character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNoNumOrSpecialChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorNoNumOrSpecialChar ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
at least 1 number or special character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorRepeatedChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at most 3 repeated, consecutive characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorEscapeChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
No escape characters allowed.
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowEntropy ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`}
>
Password contains personal info.
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorBreached ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div className={`${passwordErrorBreached ? "text-gray-400" : "text-gray-600"} text-sm`}>
Password was found in a data breach.
</div>
</div>
</div>
) : (
<div className="py-2" />
)}
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button
type="submit"
colorSchema="secondary"
isLoading={isSubmitting || isLoading || isLoadingV2}
isDisabled={isSubmitting || isLoading || isLoadingV2}
>
Change Password
</Button>
</div>
</div>
</form>
);
};
@@ -0,0 +1,87 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { Button, FormControl, Input } from "@app/components/v2";
import { getBackupEncryptedPrivateKey } from "@app/hooks/api/auth/queries";
type Props = {
verificationToken: string;
onComplete: (privateKey: string) => void;
};
const formData = z.object({
backupKey: z.string()
});
type TFormData = z.infer<typeof formData>;
export const InputBackupKeyStep = ({ verificationToken, onComplete }: Props) => {
const { control, handleSubmit, setError } = useForm<TFormData>({
resolver: zodResolver(formData)
});
const getEncryptedKeyHandler = async (data: z.infer<typeof formData>) => {
try {
const result = await getBackupEncryptedPrivateKey({ verificationToken });
const privateKey = Aes256Gcm.decrypt({
ciphertext: result.encryptedPrivateKey,
iv: result.iv,
tag: result.tag,
secret: data.backupKey
});
onComplete(privateKey);
// setStep(3);
} catch (err) {
console.error(err);
setError("backupKey", { message: "Failed to decrypt private key" });
}
};
return (
<form
onSubmit={handleSubmit(getEncryptedKeyHandler)}
className="mx-1 my-32 flex w-full max-w-xs flex-col items-center rounded-xl bg-bunker px-4 pb-3 pt-6 drop-shadow-xl md:max-w-lg md:px-6"
>
<p className="mx-auto mb-4 flex w-max justify-center text-2xl font-semibold text-bunker-100">
Enter your backup key
</p>
<div className="mt-4 flex flex-row items-center justify-center md:mx-2 md:pb-4">
<p className="flex w-full px-4 text-center text-sm text-gray-400 sm:max-w-md">
You can find it in your emergency kit. You had to download the emergency kit during
signup.
</p>
</div>
<div className="mt-4 flex max-h-24 w-full items-center justify-center rounded-lg md:mt-0 md:max-h-28 md:p-2">
<Controller
control={control}
name="backupKey"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
className="w-full"
isError={Boolean(error)}
errorText={error?.message}
label="Backup Key"
>
<Input
className="w-full"
value={value}
onChange={onChange}
placeholder="08af467b815ffa412f2c98cc3326acdb"
/>
</FormControl>
)}
/>
</div>
<div className="mx-auto mt-4 flex max-h-20 w-full max-w-md flex-col items-center justify-center text-sm md:p-2">
<div className="text-l m-8 mt-6 px-8 py-3 text-lg">
<Button type="submit" colorSchema="secondary">
Submit Backup Key
</Button>
</div>
</div>
</form>
);
};
@@ -4,7 +4,7 @@ import crypto from "crypto";
import { useState } from "react"; import { useState } from "react";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { faWarning, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useSearch } from "@tanstack/react-router"; import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import jsrp from "jsrp"; import jsrp from "jsrp";
@@ -16,7 +16,6 @@ import InputField from "@app/components/basic/InputField";
import checkPassword from "@app/components/utilities/checks/password/checkPassword"; import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage"; import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button } from "@app/components/v2"; import { Button } from "@app/components/v2";
@@ -54,8 +53,6 @@ export const SignupInvitePage = () => {
const [lastNameError, setLastNameError] = useState(false); const [lastNameError, setLastNameError] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState(1); const [step, setStep] = useState(1);
const [, setBackupKeyError] = useState(false);
const [, setBackupKeyIssued] = useState(false);
const [errors, setErrors] = useState<Errors>({}); const [errors, setErrors] = useState<Errors>({});
const [shouldShowMfa, toggleShowMfa] = useToggle(false); const [shouldShowMfa, toggleShowMfa] = useToggle(false);
@@ -205,7 +202,9 @@ export const SignupInvitePage = () => {
localStorage.setItem("orgData.id", orgId); localStorage.setItem("orgData.id", orgId);
setStep(3); navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
});
}; };
await completeSignupFlow(); await completeSignupFlow();
@@ -367,44 +366,6 @@ export const SignupInvitePage = () => {
</div> </div>
); );
// Step 4 of the sign up process (download the emergency kit pdf)
const step4 = (
<div className="h-7/12 mx-1 mb-36 flex w-full max-w-xs flex-col items-center rounded-xl border border-mineshaft-600 bg-mineshaft-800 px-4 pb-6 pt-8 drop-shadow-xl md:mb-16 md:max-w-lg md:px-6">
<p className="flex justify-center bg-gradient-to-br from-white to-mineshaft-300 bg-clip-text text-center text-4xl font-semibold text-transparent">
Save your Emergency Kit
</p>
<div className="text-md mt-4 flex w-full max-w-md flex-col items-center justify-center rounded-md px-2 text-gray-400 md:mt-8">
<div>
If you get locked out of your account, your Emergency Kit is the only way to sign in.
</div>
<div className="mt-3">We recommend you download it and keep it somewhere safe.</div>
</div>
<div className="mx-auto mt-4 flex w-full max-w-xs flex-row items-center rounded-md bg-white/10 p-2 text-gray-400 md:max-w-md">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-4 text-4xl" />
It contains your Secret Key which we cannot access or recover for you if you lose it.
</div>
<div className="mx-auto mt-4 flex max-h-24 max-w-max flex-col items-center justify-center px-2 py-3 text-lg md:px-4 md:py-5">
<Button
onClick={async () => {
await issueBackupKey({
email,
password,
personalName: `${firstName} ${lastName}`,
setBackupKeyError,
setBackupKeyIssued
});
navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
});
}}
size="lg"
>
Download PDF
</Button>
</div>
</div>
);
return ( return (
<div className="flex h-screen flex-col items-center justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700"> <div className="flex h-screen flex-col items-center justify-center bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
<Helmet> <Helmet>
@@ -425,7 +386,8 @@ export const SignupInvitePage = () => {
<img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" /> <img src="/images/gradientLogo.svg" height={90} width={120} alt="Infisical Logo" />
</div> </div>
</Link> </Link>
{step === 1 ? stepConfirmEmail : step === 2 ? main : step4} {step === 1 && stepConfirmEmail}
{step === 2 && main}
</> </>
)} )}
</div> </div>
@@ -5,7 +5,6 @@ import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import CodeInputStep from "@app/components/auth/CodeInputStep"; import CodeInputStep from "@app/components/auth/CodeInputStep";
import DownloadBackupPDF from "@app/components/auth/DonwloadBackupPDFStep";
import EnterEmailStep from "@app/components/auth/EnterEmailStep"; import EnterEmailStep from "@app/components/auth/EnterEmailStep";
import InitialSignupStep from "@app/components/auth/InitialSignupStep"; import InitialSignupStep from "@app/components/auth/InitialSignupStep";
import TeamInviteStep from "@app/components/auth/TeamInviteStep"; import TeamInviteStep from "@app/components/auth/TeamInviteStep";
@@ -72,7 +71,7 @@ export const SignUpPage = () => {
incrementStep(); incrementStep();
} }
if (!serverDetails?.emailConfigured && step === 5) { if (!serverDetails?.emailConfigured && step === 4) {
navigate({ navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const to: `/organization/${ProjectType.SecretManager}/overview` as const
}); });
@@ -119,17 +118,6 @@ export const SignUpPage = () => {
); );
} }
if (registerStep === 4) {
return (
<DownloadBackupPDF
incrementStep={incrementStep}
email={email}
password={password}
name={name}
/>
);
}
if (serverDetails?.emailConfigured) { if (serverDetails?.emailConfigured) {
return <TeamInviteStep />; return <TeamInviteStep />;
} }
@@ -6,7 +6,6 @@ import { jwtDecode } from "jwt-decode";
import { ROUTE_PATHS } from "@app/const/routes"; import { ROUTE_PATHS } from "@app/const/routes";
import { BackupPDFStep } from "./components/BackupPDFStep";
import { EmailConfirmationStep } from "./components/EmailConfirmationStep"; import { EmailConfirmationStep } from "./components/EmailConfirmationStep";
import { UserInfoSSOStep } from "./components/UserInfoSSOStep"; import { UserInfoSSOStep } from "./components/UserInfoSSOStep";
@@ -57,14 +56,9 @@ export const SignupSsoPage = () => {
providerOrganizationName={organizationName} providerOrganizationName={organizationName}
password={password} password={password}
setPassword={setPassword} setPassword={setPassword}
setStep={setStep}
providerAuthToken={token} providerAuthToken={token}
/> />
); );
case 2:
return (
<BackupPDFStep email={username} password={password} name={`${firstName} ${lastName}`} />
);
default: default:
return <div />; return <div />;
} }
@@ -1,71 +0,0 @@
import { useTranslation } from "react-i18next";
import { faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
import { Button } from "@app/components/v2";
import { ProjectType } from "@app/hooks/api/workspace/types";
interface DownloadBackupPDFStepProps {
email: string;
password: string;
name: string;
}
/**
* This is the step of the signup flow where the user downloads the backup pdf
* @param {object} obj
* @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
* @param {string} obj.email - user's email
* @param {string} obj.password - user's password
* @param {string} obj.name - user's name
* @returns
*/
export const BackupPDFStep = ({ email, password, name }: DownloadBackupPDFStepProps) => {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<div className="mx-auto mb-36 flex h-full w-full flex-col items-center md:mb-16 md:px-6">
<p className="flex justify-center bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-xl font-medium text-transparent">
<FontAwesomeIcon icon={faWarning} className="ml-2 mr-3 pt-1 text-2xl text-bunker-200" />
{t("signup.step4-message")}
</p>
<div className="text-md mt-8 flex w-full max-w-md flex-col items-center justify-center rounded-md border border-mineshaft-600 bg-mineshaft-800 pb-2 text-center text-bunker-300 md:min-w-[24rem] lg:w-1/6">
<div className="m-2 mx-auto mt-4 flex w-full flex-row items-center rounded-md px-3 text-center text-bunker-300 md:mt-8 md:min-w-[23rem] lg:w-1/6">
<span className="mb-2">
{t("signup.step4-description1")} {t("signup.step4-description3")}
</span>
</div>
<div className="mx-auto mb-2 mt-2 flex w-full flex-col items-center justify-center px-3 text-center text-sm md:mb-4 md:mt-4 md:min-w-[20rem] md:max-w-md md:text-left lg:w-1/6">
<div className="text-l w-full py-1 text-lg">
<Button
onClick={async () => {
await issueBackupKey({
email,
password,
personalName: name,
setBackupKeyError: () => {},
setBackupKeyIssued: () => {}
});
navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
});
}}
size="sm"
isFullWidth
className="h-12"
colorSchema="primary"
variant="outline_bg"
>
{" "}
Download PDF{" "}
</Button>
</div>
</div>
</div>
</div>
);
};
@@ -1 +0,0 @@
export { BackupPDFStep } from "./BackupPDFStep";
@@ -2,6 +2,7 @@ import crypto from "crypto";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router";
import jsrp from "jsrp"; import jsrp from "jsrp";
import nacl from "tweetnacl"; import nacl from "tweetnacl";
import { encodeBase64 } from "tweetnacl-util"; import { encodeBase64 } from "tweetnacl-util";
@@ -17,12 +18,12 @@ import { useToggle } from "@app/hooks";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries"; import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types"; import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries"; import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { ProjectType } from "@app/hooks/api/workspace/types";
// eslint-disable-next-line new-cap // eslint-disable-next-line new-cap
const client = new jsrp.client(); const client = new jsrp.client();
type Props = { type Props = {
setStep: (step: number) => void;
username: string; username: string;
password: string; password: string;
setPassword: (value: string) => void; setPassword: (value: string) => void;
@@ -50,7 +51,6 @@ export const UserInfoSSOStep = ({
providerOrganizationName, providerOrganizationName,
password, password,
setPassword, setPassword,
setStep,
providerAuthToken providerAuthToken
}: Props) => { }: Props) => {
const [nameError, setNameError] = useState(false); const [nameError, setNameError] = useState(false);
@@ -63,6 +63,7 @@ export const UserInfoSSOStep = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { mutateAsync: selectOrganization } = useSelectOrganization(); const { mutateAsync: selectOrganization } = useSelectOrganization();
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const navigate = useNavigate();
useEffect(() => { useEffect(() => {
const randomPassword = crypto.randomBytes(32).toString("hex"); const randomPassword = crypto.randomBytes(32).toString("hex");
@@ -202,7 +203,9 @@ export const UserInfoSSOStep = ({
} }
localStorage.setItem("orgData.id", orgId); localStorage.setItem("orgData.id", orgId);
setStep(2); navigate({
to: `/organization/${ProjectType.SecretManager}/overview` as const
});
} catch (error) { } catch (error) {
setIsLoading(false); setIsLoading(false);
console.error(error); console.error(error);

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