mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 10:35:17 +00:00
Merge remote-tracking branch 'origin/main' into misc/privilege-management-v2-transition
This commit is contained in:
@@ -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 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
+18
-10
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,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()
|
||||||
|
});
|
||||||
|
|||||||
@@ -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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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")
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+89
-21
@@ -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
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,6 +2,8 @@ export {
|
|||||||
useGetAuthToken,
|
useGetAuthToken,
|
||||||
useOauthTokenExchange,
|
useOauthTokenExchange,
|
||||||
useResetPassword,
|
useResetPassword,
|
||||||
|
useResetPasswordV2,
|
||||||
|
useResetUserPasswordV2,
|
||||||
useSelectOrganization,
|
useSelectOrganization,
|
||||||
useSendMfaToken,
|
useSendMfaToken,
|
||||||
useSendPasswordResetEmail,
|
useSendPasswordResetEmail,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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),
|
||||||
[]
|
[]
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
+50
-15
@@ -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)}
|
||||||
|
|||||||
+85
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
-56
@@ -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";
|
|
||||||
+6
-3
@@ -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
Reference in New Issue
Block a user