mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-22 10:12:15 +00:00
Compare commits
54 Commits
fix/gitlab
...
create-pul
Author | SHA1 | Date | |
---|---|---|---|
|
356bffad35 | ||
|
bbb21c95f6 | ||
|
394340c599 | ||
|
30039b97b5 | ||
|
71d4935c0f | ||
|
aa193adf48 | ||
|
dbac4b4567 | ||
|
df38e79590 | ||
|
8f778403b4 | ||
|
1068e6024d | ||
|
286426b240 | ||
|
b5b778e241 | ||
|
f85a35fde8 | ||
|
3b40f37f50 | ||
|
4e51a3b784 | ||
|
387981ea87 | ||
|
81b0c8bc12 | ||
|
06dca77be2 | ||
|
b79ed28bb8 | ||
|
7c6b6653f5 | ||
|
6055661515 | ||
|
f3eda1fd13 | ||
|
60178a6ba6 | ||
|
3e6d43e4df | ||
|
be68ecc25d | ||
|
b2ad7cc7c0 | ||
|
6c6c436cc6 | ||
|
01ea41611b | ||
|
dc7bf9674a | ||
|
b6814b67b0 | ||
|
5234a89612 | ||
|
45bb2f0fcc | ||
|
4c7e218d0d | ||
|
0371a57548 | ||
|
7d0eb9a0fd | ||
|
44b14756b1 | ||
|
1a4f8b23ff | ||
|
51f4047207 | ||
|
4567e505ec | ||
|
0fc4fb8858 | ||
|
fe4cc950d3 | ||
|
a0f678a295 | ||
|
3639a7fc18 | ||
|
59c8dc3cda | ||
|
7a955e3fae | ||
|
ee5130f56c | ||
|
719f3beab0 | ||
|
b9a6f94eea | ||
|
c0daa11aeb | ||
|
9b2b6d61be | ||
|
a0d9331e67 | ||
|
8ec8b1ce2f | ||
|
e3dae9d498 | ||
|
41d72d5dc6 |
@@ -55,6 +55,7 @@ VOLUME /app/.next/cache/images
|
|||||||
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
|
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
|
||||||
COPY --from=frontend-builder /app/public ./public
|
COPY --from=frontend-builder /app/public ./public
|
||||||
RUN chown non-root-user:nodejs ./public/data
|
RUN chown non-root-user:nodejs ./public/data
|
||||||
|
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
|
||||||
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
|
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
@@ -93,9 +94,18 @@ RUN mkdir frontend-build
|
|||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM base AS production
|
FROM base AS production
|
||||||
|
RUN apk add --upgrade --no-cache ca-certificates
|
||||||
RUN addgroup --system --gid 1001 nodejs \
|
RUN addgroup --system --gid 1001 nodejs \
|
||||||
&& adduser --system --uid 1001 non-root-user
|
&& adduser --system --uid 1001 non-root-user
|
||||||
|
|
||||||
|
# Give non-root-user permission to update SSL certs
|
||||||
|
RUN chown -R non-root-user /etc/ssl/certs
|
||||||
|
RUN chown non-root-user /etc/ssl/certs/ca-certificates.crt
|
||||||
|
RUN chmod -R u+rwx /etc/ssl/certs
|
||||||
|
RUN chmod u+rw /etc/ssl/certs/ca-certificates.crt
|
||||||
|
RUN chown non-root-user /usr/sbin/update-ca-certificates
|
||||||
|
RUN chmod u+rx /usr/sbin/update-ca-certificates
|
||||||
|
|
||||||
## set pre baked keys
|
## set pre baked keys
|
||||||
ARG POSTHOG_API_KEY
|
ARG POSTHOG_API_KEY
|
||||||
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
||||||
|
2
backend/src/@types/fastify.d.ts
vendored
2
backend/src/@types/fastify.d.ts
vendored
@@ -52,6 +52,7 @@ import { TSecretServiceFactory } from "@app/services/secret/secret-service";
|
|||||||
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
|
import { TSecretBlindIndexServiceFactory } from "@app/services/secret-blind-index/secret-blind-index-service";
|
||||||
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
import { TSecretFolderServiceFactory } from "@app/services/secret-folder/secret-folder-service";
|
||||||
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
import { TSecretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
||||||
|
import { TSecretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
|
||||||
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
import { TSecretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
||||||
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
|
import { TServiceTokenServiceFactory } from "@app/services/service-token/service-token-service";
|
||||||
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
import { TSuperAdminServiceFactory } from "@app/services/super-admin/super-admin-service";
|
||||||
@@ -143,6 +144,7 @@ declare module "fastify" {
|
|||||||
dynamicSecretLease: TDynamicSecretLeaseServiceFactory;
|
dynamicSecretLease: TDynamicSecretLeaseServiceFactory;
|
||||||
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
|
projectUserAdditionalPrivilege: TProjectUserAdditionalPrivilegeServiceFactory;
|
||||||
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
|
identityProjectAdditionalPrivilege: TIdentityProjectAdditionalPrivilegeServiceFactory;
|
||||||
|
secretSharing: TSecretSharingServiceFactory;
|
||||||
};
|
};
|
||||||
// this is exclusive use for middlewares in which we need to inject data
|
// this is exclusive use for middlewares in which we need to inject data
|
||||||
// everywhere else access using service layer
|
// everywhere else access using service layer
|
||||||
|
4
backend/src/@types/knex.d.ts
vendored
4
backend/src/@types/knex.d.ts
vendored
@@ -186,6 +186,9 @@ import {
|
|||||||
TSecretScanningGitRisks,
|
TSecretScanningGitRisks,
|
||||||
TSecretScanningGitRisksInsert,
|
TSecretScanningGitRisksInsert,
|
||||||
TSecretScanningGitRisksUpdate,
|
TSecretScanningGitRisksUpdate,
|
||||||
|
TSecretSharing,
|
||||||
|
TSecretSharingInsert,
|
||||||
|
TSecretSharingUpdate,
|
||||||
TSecretsInsert,
|
TSecretsInsert,
|
||||||
TSecretSnapshotFolders,
|
TSecretSnapshotFolders,
|
||||||
TSecretSnapshotFoldersInsert,
|
TSecretSnapshotFoldersInsert,
|
||||||
@@ -328,6 +331,7 @@ declare module "knex/types/tables" {
|
|||||||
TSecretFolderVersionsInsert,
|
TSecretFolderVersionsInsert,
|
||||||
TSecretFolderVersionsUpdate
|
TSecretFolderVersionsUpdate
|
||||||
>;
|
>;
|
||||||
|
[TableName.SecretSharing]: Knex.CompositeTableType<TSecretSharing, TSecretSharingInsert, TSecretSharingUpdate>;
|
||||||
[TableName.SecretTag]: Knex.CompositeTableType<TSecretTags, TSecretTagsInsert, TSecretTagsUpdate>;
|
[TableName.SecretTag]: Knex.CompositeTableType<TSecretTags, TSecretTagsInsert, TSecretTagsUpdate>;
|
||||||
[TableName.SecretImport]: Knex.CompositeTableType<TSecretImports, TSecretImportsInsert, TSecretImportsUpdate>;
|
[TableName.SecretImport]: Knex.CompositeTableType<TSecretImports, TSecretImportsInsert, TSecretImportsUpdate>;
|
||||||
[TableName.Integration]: Knex.CompositeTableType<TIntegrations, TIntegrationsInsert, TIntegrationsUpdate>;
|
[TableName.Integration]: Knex.CompositeTableType<TIntegrations, TIntegrationsInsert, TIntegrationsUpdate>;
|
||||||
|
@@ -0,0 +1,43 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasConsecutiveFailedMfaAttempts = await knex.schema.hasColumn(TableName.Users, "consecutiveFailedMfaAttempts");
|
||||||
|
const hasIsLocked = await knex.schema.hasColumn(TableName.Users, "isLocked");
|
||||||
|
const hasTemporaryLockDateEnd = await knex.schema.hasColumn(TableName.Users, "temporaryLockDateEnd");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
if (!hasConsecutiveFailedMfaAttempts) {
|
||||||
|
t.integer("consecutiveFailedMfaAttempts").defaultTo(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasIsLocked) {
|
||||||
|
t.boolean("isLocked").defaultTo(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasTemporaryLockDateEnd) {
|
||||||
|
t.dateTime("temporaryLockDateEnd").nullable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasConsecutiveFailedMfaAttempts = await knex.schema.hasColumn(TableName.Users, "consecutiveFailedMfaAttempts");
|
||||||
|
const hasIsLocked = await knex.schema.hasColumn(TableName.Users, "isLocked");
|
||||||
|
const hasTemporaryLockDateEnd = await knex.schema.hasColumn(TableName.Users, "temporaryLockDateEnd");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.Users, (t) => {
|
||||||
|
if (hasConsecutiveFailedMfaAttempts) {
|
||||||
|
t.dropColumn("consecutiveFailedMfaAttempts");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasIsLocked) {
|
||||||
|
t.dropColumn("isLocked");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasTemporaryLockDateEnd) {
|
||||||
|
t.dropColumn("temporaryLockDateEnd");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,21 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const doesSecretVersionIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "secretVersionId");
|
||||||
|
if (await knex.schema.hasTable(TableName.SnapshotSecret)) {
|
||||||
|
await knex.schema.alterTable(TableName.SnapshotSecret, (t) => {
|
||||||
|
if (doesSecretVersionIdExist) t.index("secretVersionId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const doesSecretVersionIdExist = await knex.schema.hasColumn(TableName.SnapshotSecret, "secretVersionId");
|
||||||
|
if (await knex.schema.hasTable(TableName.SnapshotSecret)) {
|
||||||
|
await knex.schema.alterTable(TableName.SnapshotSecret, (t) => {
|
||||||
|
if (doesSecretVersionIdExist) t.dropIndex("secretVersionId");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
29
backend/src/db/migrations/20240529184641_secret
Normal file
29
backend/src/db/migrations/20240529184641_secret
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
import { createOnUpdateTrigger } from "../utils";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (!(await knex.schema.hasTable(TableName.SecretSharing))) {
|
||||||
|
await knex.schema.createTable(TableName.SecretSharing, (t) => {
|
||||||
|
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
|
||||||
|
t.string("name").notNullable();
|
||||||
|
t.text("encryptedValue").notNullable();
|
||||||
|
t.text("iv").notNullable();
|
||||||
|
t.text("tag").notNullable();
|
||||||
|
t.text("hashedHex").notNullable();
|
||||||
|
t.timestamp("expiresAt").notNullable();
|
||||||
|
t.uuid("userId").notNullable();
|
||||||
|
t.uuid("orgId").notNullable();
|
||||||
|
t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE");
|
||||||
|
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
|
||||||
|
t.timestamps(true, true, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
await createOnUpdateTrigger(knex, TableName.SecretSharing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
await knex.schema.dropTableIfExists(TableName.SecretSharing);
|
||||||
|
}
|
@@ -60,6 +60,7 @@ export * from "./secret-imports";
|
|||||||
export * from "./secret-rotation-outputs";
|
export * from "./secret-rotation-outputs";
|
||||||
export * from "./secret-rotations";
|
export * from "./secret-rotations";
|
||||||
export * from "./secret-scanning-git-risks";
|
export * from "./secret-scanning-git-risks";
|
||||||
|
export * from "./secret-sharing";
|
||||||
export * from "./secret-snapshot-folders";
|
export * from "./secret-snapshot-folders";
|
||||||
export * from "./secret-snapshot-secrets";
|
export * from "./secret-snapshot-secrets";
|
||||||
export * from "./secret-snapshots";
|
export * from "./secret-snapshots";
|
||||||
|
@@ -29,6 +29,7 @@ export enum TableName {
|
|||||||
ProjectKeys = "project_keys",
|
ProjectKeys = "project_keys",
|
||||||
Secret = "secrets",
|
Secret = "secrets",
|
||||||
SecretReference = "secret_references",
|
SecretReference = "secret_references",
|
||||||
|
SecretSharing = "secret_sharing",
|
||||||
SecretBlindIndex = "secret_blind_indexes",
|
SecretBlindIndex = "secret_blind_indexes",
|
||||||
SecretVersion = "secret_versions",
|
SecretVersion = "secret_versions",
|
||||||
SecretFolder = "secret_folders",
|
SecretFolder = "secret_folders",
|
||||||
|
26
backend/src/db/schemas/secret-sharing.ts
Normal file
26
backend/src/db/schemas/secret-sharing.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Code generated by automation script, DO NOT EDIT.
|
||||||
|
// Automated by pulling database and generating zod schema
|
||||||
|
// To update. Just run npm run generate:schema
|
||||||
|
// Written by akhilmhdh.
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { TImmutableDBKeys } from "./models";
|
||||||
|
|
||||||
|
export const SecretSharingSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
name: z.string(),
|
||||||
|
encryptedValue: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
|
expiresAt: z.date(),
|
||||||
|
userId: z.string().uuid(),
|
||||||
|
orgId: z.string().uuid(),
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TSecretSharing = z.infer<typeof SecretSharingSchema>;
|
||||||
|
export type TSecretSharingInsert = Omit<z.input<typeof SecretSharingSchema>, TImmutableDBKeys>;
|
||||||
|
export type TSecretSharingUpdate = Partial<Omit<z.input<typeof SecretSharingSchema>, TImmutableDBKeys>>;
|
@@ -22,7 +22,10 @@ export const UsersSchema = z.object({
|
|||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
isGhost: z.boolean().default(false),
|
isGhost: z.boolean().default(false),
|
||||||
username: z.string(),
|
username: z.string(),
|
||||||
isEmailVerified: z.boolean().default(false).nullable().optional()
|
isEmailVerified: z.boolean().default(false).nullable().optional(),
|
||||||
|
consecutiveFailedMfaAttempts: z.number().optional(),
|
||||||
|
isLocked: z.boolean().optional(),
|
||||||
|
temporaryLockDateEnd: z.date().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TUsers = z.infer<typeof UsersSchema>;
|
export type TUsers = z.infer<typeof UsersSchema>;
|
||||||
|
@@ -5,10 +5,15 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
import { IdentityProjectAdditionalPrivilegeTemporaryMode } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-types";
|
||||||
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
import { IDENTITY_ADDITIONAL_PRIVILEGE } from "@app/lib/api-docs";
|
||||||
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
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 { ProjectPermissionSchema, SanitizedIdentityPrivilegeSchema } from "@app/server/routes/sanitizedSchemas";
|
import {
|
||||||
|
ProjectPermissionSchema,
|
||||||
|
ProjectSpecificPrivilegePermissionSchema,
|
||||||
|
SanitizedIdentityPrivilegeSchema
|
||||||
|
} from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
|
export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -39,7 +44,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||||
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
permissions: ProjectPermissionSchema.array()
|
||||||
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||||
|
.optional(),
|
||||||
|
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
|
||||||
|
IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.privilegePermission
|
||||||
|
).optional()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -49,6 +59,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const { permissions, privilegePermission } = req.body;
|
||||||
|
if (!permissions && !privilegePermission) {
|
||||||
|
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = privilegePermission
|
||||||
|
? privilegePermission.actions.map((action) => ({
|
||||||
|
action,
|
||||||
|
subject: privilegePermission.subject,
|
||||||
|
conditions: privilegePermission.conditions
|
||||||
|
}))
|
||||||
|
: permissions!;
|
||||||
const privilege = await server.services.identityProjectAdditionalPrivilege.create({
|
const privilege = await server.services.identityProjectAdditionalPrivilege.create({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -57,7 +79,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
...req.body,
|
...req.body,
|
||||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||||
isTemporary: false,
|
isTemporary: false,
|
||||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
permissions: JSON.stringify(packRules(permission))
|
||||||
});
|
});
|
||||||
return { privilege };
|
return { privilege };
|
||||||
}
|
}
|
||||||
@@ -90,7 +112,12 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.slug),
|
||||||
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions),
|
permissions: ProjectPermissionSchema.array()
|
||||||
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.permissions)
|
||||||
|
.optional(),
|
||||||
|
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
|
||||||
|
IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.privilegePermission
|
||||||
|
).optional(),
|
||||||
temporaryMode: z
|
temporaryMode: z
|
||||||
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.CREATE.temporaryMode),
|
||||||
@@ -111,6 +138,19 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
|
const { permissions, privilegePermission } = req.body;
|
||||||
|
if (!permissions && !privilegePermission) {
|
||||||
|
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = privilegePermission
|
||||||
|
? privilegePermission.actions.map((action) => ({
|
||||||
|
action,
|
||||||
|
subject: privilegePermission.subject,
|
||||||
|
conditions: privilegePermission.conditions
|
||||||
|
}))
|
||||||
|
: permissions!;
|
||||||
|
|
||||||
const privilege = await server.services.identityProjectAdditionalPrivilege.create({
|
const privilege = await server.services.identityProjectAdditionalPrivilege.create({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -119,7 +159,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
...req.body,
|
...req.body,
|
||||||
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
slug: req.body.slug ? slugify(req.body.slug) : slugify(alphaNumericNanoId(12)),
|
||||||
isTemporary: true,
|
isTemporary: true,
|
||||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
permissions: JSON.stringify(packRules(permission))
|
||||||
});
|
});
|
||||||
return { privilege };
|
return { privilege };
|
||||||
}
|
}
|
||||||
@@ -156,13 +196,16 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
})
|
})
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.newSlug),
|
||||||
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
permissions: ProjectPermissionSchema.array().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.permissions),
|
||||||
|
privilegePermission: ProjectSpecificPrivilegePermissionSchema.describe(
|
||||||
|
IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.privilegePermission
|
||||||
|
).optional(),
|
||||||
isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
|
isTemporary: z.boolean().describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.isTemporary),
|
||||||
temporaryMode: z
|
temporaryMode: z
|
||||||
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
.nativeEnum(IdentityProjectAdditionalPrivilegeTemporaryMode)
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryMode),
|
||||||
temporaryRange: z
|
temporaryRange: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => ms(val) > 0, "Temporary range must be a positive number")
|
.refine((val) => typeof val === "undefined" || ms(val) > 0, "Temporary range must be a positive number")
|
||||||
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
|
.describe(IDENTITY_ADDITIONAL_PRIVILEGE.UPDATE.temporaryRange),
|
||||||
temporaryAccessStartTime: z
|
temporaryAccessStartTime: z
|
||||||
.string()
|
.string()
|
||||||
@@ -179,7 +222,18 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const updatedInfo = req.body.privilegeDetails;
|
const { permissions, privilegePermission, ...updatedInfo } = req.body.privilegeDetails;
|
||||||
|
if (!permissions && !privilegePermission) {
|
||||||
|
throw new BadRequestError({ message: "Permission or privilegePermission must be provided" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const permission = privilegePermission
|
||||||
|
? privilegePermission.actions.map((action) => ({
|
||||||
|
action,
|
||||||
|
subject: privilegePermission.subject,
|
||||||
|
conditions: privilegePermission.conditions
|
||||||
|
}))
|
||||||
|
: permissions!;
|
||||||
const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({
|
const privilege = await server.services.identityProjectAdditionalPrivilege.updateBySlug({
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
@@ -190,7 +244,7 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F
|
|||||||
projectSlug: req.body.projectSlug,
|
projectSlug: req.body.projectSlug,
|
||||||
data: {
|
data: {
|
||||||
...updatedInfo,
|
...updatedInfo,
|
||||||
permissions: updatedInfo?.permissions ? JSON.stringify(packRules(updatedInfo.permissions)) : undefined
|
permissions: permission ? JSON.stringify(packRules(permission)) : undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return { privilege };
|
return { privilege };
|
||||||
|
@@ -23,7 +23,7 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
|||||||
.min(1)
|
.min(1)
|
||||||
.trim()
|
.trim()
|
||||||
.refine(
|
.refine(
|
||||||
(val) => !Object.keys(OrgMembershipRole).includes(val),
|
(val) => !Object.values(OrgMembershipRole).includes(val as OrgMembershipRole),
|
||||||
"Please choose a different slug, the slug you have entered is reserved"
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
)
|
)
|
||||||
.refine((v) => slugify(v) === v, {
|
.refine((v) => slugify(v) === v, {
|
||||||
|
@@ -1,146 +1,232 @@
|
|||||||
|
import { packRules } from "@casl/ability/extra";
|
||||||
|
import slugify from "@sindresorhus/slugify";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
import { ProjectMembershipRole, ProjectMembershipsSchema, ProjectRolesSchema } from "@app/db/schemas";
|
||||||
|
import { PROJECT_ROLE } 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 { ProjectPermissionSchema, SanitizedRoleSchema } from "@app/server/routes/sanitizedSchemas";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/:projectId/roles",
|
url: "/:projectSlug/roles",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Create a project role",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim()
|
projectSlug: z.string().trim().describe(PROJECT_ROLE.CREATE.projectSlug)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z.string().trim(),
|
slug: z
|
||||||
name: z.string().trim(),
|
.string()
|
||||||
description: z.string().trim().optional(),
|
.toLowerCase()
|
||||||
permissions: z.any().array()
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.refine(
|
||||||
|
(val) => !Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||||
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
|
)
|
||||||
|
.refine((v) => slugify(v) === v, {
|
||||||
|
message: "Slug must be a valid"
|
||||||
|
})
|
||||||
|
.describe(PROJECT_ROLE.CREATE.slug),
|
||||||
|
name: z.string().min(1).trim().describe(PROJECT_ROLE.CREATE.name),
|
||||||
|
description: z.string().trim().optional().describe(PROJECT_ROLE.CREATE.description),
|
||||||
|
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.CREATE.permissions)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
role: ProjectRolesSchema
|
role: SanitizedRoleSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const role = await server.services.projectRole.createRole(
|
const role = await server.services.projectRole.createRole({
|
||||||
req.permission.type,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
req.permission.id,
|
actorId: req.permission.id,
|
||||||
req.params.projectId,
|
actorOrgId: req.permission.orgId,
|
||||||
req.body,
|
actor: req.permission.type,
|
||||||
req.permission.authMethod,
|
projectSlug: req.params.projectSlug,
|
||||||
req.permission.orgId
|
data: {
|
||||||
);
|
...req.body,
|
||||||
|
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||||
|
}
|
||||||
|
});
|
||||||
return { role };
|
return { role };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
url: "/:projectId/roles/:roleId",
|
url: "/:projectSlug/roles/:roleId",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Update a project role",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim(),
|
projectSlug: z.string().trim().describe(PROJECT_ROLE.UPDATE.projectSlug),
|
||||||
roleId: z.string().trim()
|
roleId: z.string().trim().describe(PROJECT_ROLE.UPDATE.roleId)
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
slug: z.string().trim().optional(),
|
slug: z
|
||||||
name: z.string().trim().optional(),
|
.string()
|
||||||
description: z.string().trim().optional(),
|
.toLowerCase()
|
||||||
permissions: z.any().array()
|
.trim()
|
||||||
|
.optional()
|
||||||
|
.describe(PROJECT_ROLE.UPDATE.slug)
|
||||||
|
.refine(
|
||||||
|
(val) =>
|
||||||
|
typeof val === "undefined" ||
|
||||||
|
!Object.values(ProjectMembershipRole).includes(val as ProjectMembershipRole),
|
||||||
|
"Please choose a different slug, the slug you have entered is reserved"
|
||||||
|
)
|
||||||
|
.refine((val) => typeof val === "undefined" || slugify(val) === val, {
|
||||||
|
message: "Slug must be a valid"
|
||||||
|
}),
|
||||||
|
name: z.string().trim().optional().describe(PROJECT_ROLE.UPDATE.name),
|
||||||
|
permissions: ProjectPermissionSchema.array().describe(PROJECT_ROLE.UPDATE.permissions)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
role: ProjectRolesSchema
|
role: SanitizedRoleSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const role = await server.services.projectRole.updateRole(
|
const role = await server.services.projectRole.updateRole({
|
||||||
req.permission.type,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
req.permission.id,
|
actorId: req.permission.id,
|
||||||
req.params.projectId,
|
actorOrgId: req.permission.orgId,
|
||||||
req.params.roleId,
|
actor: req.permission.type,
|
||||||
req.body,
|
projectSlug: req.params.projectSlug,
|
||||||
req.permission.authMethod,
|
roleId: req.params.roleId,
|
||||||
req.permission.orgId
|
data: {
|
||||||
);
|
...req.body,
|
||||||
|
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||||
|
}
|
||||||
|
});
|
||||||
return { role };
|
return { role };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: "/:projectId/roles/:roleId",
|
url: "/:projectSlug/roles/:roleId",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: writeLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
|
description: "Delete a project role",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim(),
|
projectSlug: z.string().trim().describe(PROJECT_ROLE.DELETE.projectSlug),
|
||||||
roleId: z.string().trim()
|
roleId: z.string().trim().describe(PROJECT_ROLE.DELETE.roleId)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
role: ProjectRolesSchema
|
role: SanitizedRoleSchema
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const role = await server.services.projectRole.deleteRole(
|
const role = await server.services.projectRole.deleteRole({
|
||||||
req.permission.type,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
req.permission.id,
|
actorId: req.permission.id,
|
||||||
req.params.projectId,
|
actorOrgId: req.permission.orgId,
|
||||||
req.params.roleId,
|
actor: req.permission.type,
|
||||||
req.permission.authMethod,
|
projectSlug: req.params.projectSlug,
|
||||||
req.permission.orgId
|
roleId: req.params.roleId
|
||||||
);
|
});
|
||||||
return { role };
|
return { role };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/:projectId/roles",
|
url: "/:projectSlug/roles",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "List project role",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
projectSlug: z.string().trim().describe(PROJECT_ROLE.LIST.projectSlug)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
roles: ProjectRolesSchema.omit({ permissions: true }).array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const roles = await server.services.projectRole.listRoles({
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
projectSlug: req.params.projectSlug
|
||||||
|
});
|
||||||
|
return { roles };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:projectSlug/roles/slug/:slug",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: readLimit
|
rateLimit: readLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
projectId: z.string().trim()
|
projectSlug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.projectSlug),
|
||||||
|
slug: z.string().trim().describe(PROJECT_ROLE.GET_ROLE_BY_SLUG.roleSlug)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
data: z.object({
|
role: SanitizedRoleSchema
|
||||||
roles: ProjectRolesSchema.omit({ permissions: true })
|
|
||||||
.merge(z.object({ permissions: z.unknown() }))
|
|
||||||
.array()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const roles = await server.services.projectRole.listRoles(
|
const role = await server.services.projectRole.getRoleBySlug({
|
||||||
req.permission.type,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
req.permission.id,
|
actorId: req.permission.id,
|
||||||
req.params.projectId,
|
actorOrgId: req.permission.orgId,
|
||||||
req.permission.authMethod,
|
actor: req.permission.type,
|
||||||
req.permission.orgId
|
projectSlug: req.params.projectSlug,
|
||||||
);
|
roleSlug: req.params.slug
|
||||||
return { data: { roles } };
|
});
|
||||||
|
return { role };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -519,7 +519,8 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
|||||||
projectSlug: "The slug of the project of the identity in.",
|
projectSlug: "The slug of the project of the identity in.",
|
||||||
identityId: "The ID of the identity to create.",
|
identityId: "The ID of the identity to create.",
|
||||||
slug: "The slug of the privilege to create.",
|
slug: "The slug of the privilege to create.",
|
||||||
permissions: `The permission object for the privilege.
|
permissions: `@deprecated - use privilegePermission
|
||||||
|
The permission object for the privilege.
|
||||||
- Read secrets
|
- Read secrets
|
||||||
\`\`\`
|
\`\`\`
|
||||||
{ "permissions": [{"action": "read", "subject": "secrets"]}
|
{ "permissions": [{"action": "read", "subject": "secrets"]}
|
||||||
@@ -533,6 +534,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
|||||||
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
|
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
|
||||||
\`\`\`
|
\`\`\`
|
||||||
`,
|
`,
|
||||||
|
privilegePermission: "The permission object for the privilege.",
|
||||||
isPackPermission: "Whether the server should pack(compact) the permission object.",
|
isPackPermission: "Whether the server should pack(compact) the permission object.",
|
||||||
isTemporary: "Whether the privilege is temporary.",
|
isTemporary: "Whether the privilege is temporary.",
|
||||||
temporaryMode: "Type of temporary access given. Types: relative",
|
temporaryMode: "Type of temporary access given. Types: relative",
|
||||||
@@ -544,7 +546,8 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
|||||||
identityId: "The ID of the identity to update.",
|
identityId: "The ID of the identity to update.",
|
||||||
slug: "The slug of the privilege to update.",
|
slug: "The slug of the privilege to update.",
|
||||||
newSlug: "The new slug of the privilege to update.",
|
newSlug: "The new slug of the privilege to update.",
|
||||||
permissions: `The permission object for the privilege.
|
permissions: `@deprecated - use privilegePermission
|
||||||
|
The permission object for the privilege.
|
||||||
- Read secrets
|
- Read secrets
|
||||||
\`\`\`
|
\`\`\`
|
||||||
{ "permissions": [{"action": "read", "subject": "secrets"]}
|
{ "permissions": [{"action": "read", "subject": "secrets"]}
|
||||||
@@ -558,6 +561,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE = {
|
|||||||
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
|
- { "permissions": [{"action": "read", "subject": "secrets", "conditions": { "environment": "dev", "secretPath": { "$glob": "/" } }}] }
|
||||||
\`\`\`
|
\`\`\`
|
||||||
`,
|
`,
|
||||||
|
privilegePermission: "The permission object for the privilege.",
|
||||||
isTemporary: "Whether the privilege is temporary.",
|
isTemporary: "Whether the privilege is temporary.",
|
||||||
temporaryMode: "Type of temporary access given. Types: relative",
|
temporaryMode: "Type of temporary access given. Types: relative",
|
||||||
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
|
temporaryRange: "TTL for the temporay time. Eg: 1m, 1h, 1d",
|
||||||
@@ -715,3 +719,32 @@ export const AUDIT_LOG_STREAMS = {
|
|||||||
id: "The ID of the audit log stream to get details."
|
id: "The ID of the audit log stream to get details."
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PROJECT_ROLE = {
|
||||||
|
CREATE: {
|
||||||
|
projectSlug: "Slug of the project to create the role for.",
|
||||||
|
slug: "The slug of the role.",
|
||||||
|
name: "The name of the role.",
|
||||||
|
description: "The description for the role.",
|
||||||
|
permissions: "The permissions assigned to the role."
|
||||||
|
},
|
||||||
|
UPDATE: {
|
||||||
|
projectSlug: "Slug of the project to update the role for.",
|
||||||
|
roleId: "The ID of the role to update",
|
||||||
|
slug: "The slug of the role.",
|
||||||
|
name: "The name of the role.",
|
||||||
|
description: "The description for the role.",
|
||||||
|
permissions: "The permissions assigned to the role."
|
||||||
|
},
|
||||||
|
DELETE: {
|
||||||
|
projectSlug: "Slug of the project to delete this role for.",
|
||||||
|
roleId: "The ID of the role to update"
|
||||||
|
},
|
||||||
|
GET_ROLE_BY_SLUG: {
|
||||||
|
projectSlug: "The slug of the project.",
|
||||||
|
roleSlug: "The slug of the role to get details"
|
||||||
|
},
|
||||||
|
LIST: {
|
||||||
|
projectSlug: "The slug of the project to list the roles of."
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@@ -52,9 +52,25 @@ export const inviteUserRateLimit: RateLimitOptions = {
|
|||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mfaRateLimit: RateLimitOptions = {
|
||||||
|
timeWindow: 60 * 1000,
|
||||||
|
max: 20,
|
||||||
|
keyGenerator: (req) => {
|
||||||
|
return req.headers.authorization?.split(" ")[1] || req.realIp;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const creationLimit: RateLimitOptions = {
|
export const creationLimit: RateLimitOptions = {
|
||||||
// identity, project, org
|
// identity, project, org
|
||||||
timeWindow: 60 * 1000,
|
timeWindow: 60 * 1000,
|
||||||
max: 30,
|
max: 30,
|
||||||
keyGenerator: (req) => req.realIp
|
keyGenerator: (req) => req.realIp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Public endpoints to avoid brute force attacks
|
||||||
|
export const publicEndpointLimit: RateLimitOptions = {
|
||||||
|
// Shared Secrets
|
||||||
|
timeWindow: 60 * 1000,
|
||||||
|
max: 30,
|
||||||
|
keyGenerator: (req) => req.realIp
|
||||||
|
};
|
||||||
|
@@ -130,6 +130,8 @@ import { secretFolderServiceFactory } from "@app/services/secret-folder/secret-f
|
|||||||
import { secretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
|
import { secretFolderVersionDALFactory } from "@app/services/secret-folder/secret-folder-version-dal";
|
||||||
import { secretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
|
import { secretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
|
||||||
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
import { secretImportServiceFactory } from "@app/services/secret-import/secret-import-service";
|
||||||
|
import { secretSharingDALFactory } from "@app/services/secret-sharing/secret-sharing-dal";
|
||||||
|
import { secretSharingServiceFactory } from "@app/services/secret-sharing/secret-sharing-service";
|
||||||
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
import { secretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
|
||||||
import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
import { secretTagServiceFactory } from "@app/services/secret-tag/secret-tag-service";
|
||||||
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
|
import { serviceTokenDALFactory } from "@app/services/service-token/service-token-dal";
|
||||||
@@ -253,6 +255,7 @@ export const registerRoutes = async (
|
|||||||
const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db);
|
const groupProjectMembershipRoleDAL = groupProjectMembershipRoleDALFactory(db);
|
||||||
const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
|
const userGroupMembershipDAL = userGroupMembershipDALFactory(db);
|
||||||
const secretScanningDAL = secretScanningDALFactory(db);
|
const secretScanningDAL = secretScanningDALFactory(db);
|
||||||
|
const secretSharingDAL = secretSharingDALFactory(db);
|
||||||
const licenseDAL = licenseDALFactory(db);
|
const licenseDAL = licenseDALFactory(db);
|
||||||
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
const dynamicSecretDAL = dynamicSecretDALFactory(db);
|
||||||
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
const dynamicSecretLeaseDAL = dynamicSecretLeaseDALFactory(db);
|
||||||
@@ -523,7 +526,8 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
projectUserMembershipRoleDAL,
|
projectUserMembershipRoleDAL,
|
||||||
identityProjectMembershipRoleDAL
|
identityProjectMembershipRoleDAL,
|
||||||
|
projectDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const snapshotService = secretSnapshotServiceFactory({
|
const snapshotService = secretSnapshotServiceFactory({
|
||||||
@@ -611,6 +615,12 @@ export const registerRoutes = async (
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
projectBotService
|
projectBotService
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const secretSharingService = secretSharingServiceFactory({
|
||||||
|
permissionService,
|
||||||
|
secretSharingDAL
|
||||||
|
});
|
||||||
|
|
||||||
const sarService = secretApprovalRequestServiceFactory({
|
const sarService = secretApprovalRequestServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectBotService,
|
projectBotService,
|
||||||
@@ -784,7 +794,8 @@ export const registerRoutes = async (
|
|||||||
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
identityAccessTokenDAL
|
identityAccessTokenDAL,
|
||||||
|
secretSharingDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
await superAdminService.initServerCfg();
|
await superAdminService.initServerCfg();
|
||||||
@@ -850,7 +861,8 @@ export const registerRoutes = async (
|
|||||||
secretBlindIndex: secretBlindIndexService,
|
secretBlindIndex: secretBlindIndexService,
|
||||||
telemetry: telemetryService,
|
telemetry: telemetryService,
|
||||||
projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService,
|
projectUserAdditionalPrivilege: projectUserAdditionalPrivilegeService,
|
||||||
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService
|
identityProjectAdditionalPrivilege: identityProjectAdditionalPrivilegeService,
|
||||||
|
secretSharing: secretSharingService
|
||||||
});
|
});
|
||||||
|
|
||||||
server.decorate<FastifyZodProvider["store"]>("store", {
|
server.decorate<FastifyZodProvider["store"]>("store", {
|
||||||
|
@@ -4,6 +4,7 @@ import {
|
|||||||
DynamicSecretsSchema,
|
DynamicSecretsSchema,
|
||||||
IdentityProjectAdditionalPrivilegeSchema,
|
IdentityProjectAdditionalPrivilegeSchema,
|
||||||
IntegrationAuthsSchema,
|
IntegrationAuthsSchema,
|
||||||
|
ProjectRolesSchema,
|
||||||
SecretApprovalPoliciesSchema,
|
SecretApprovalPoliciesSchema,
|
||||||
UsersSchema
|
UsersSchema
|
||||||
} from "@app/db/schemas";
|
} from "@app/db/schemas";
|
||||||
@@ -88,10 +89,38 @@ export const ProjectPermissionSchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ProjectSpecificPrivilegePermissionSchema = z.object({
|
||||||
|
actions: z
|
||||||
|
.nativeEnum(ProjectPermissionActions)
|
||||||
|
.describe("Describe what action an entity can take. Possible actions: create, edit, delete, and read")
|
||||||
|
.array()
|
||||||
|
.min(1),
|
||||||
|
subject: z
|
||||||
|
.enum([ProjectPermissionSub.Secrets])
|
||||||
|
.describe("The entity this permission pertains to. Possible options: secrets, environments"),
|
||||||
|
conditions: z
|
||||||
|
.object({
|
||||||
|
environment: z.string().describe("The environment slug this permission should allow."),
|
||||||
|
secretPath: z
|
||||||
|
.object({
|
||||||
|
$glob: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.describe("The secret path this permission should allow. Can be a glob pattern such as /folder-name/*/** ")
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.describe("When specified, only matching conditions will be allowed to access given resource.")
|
||||||
|
});
|
||||||
|
|
||||||
export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({
|
export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({
|
||||||
permissions: UnpackedPermissionSchema.array()
|
permissions: UnpackedPermissionSchema.array()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const SanitizedRoleSchema = ProjectRolesSchema.extend({
|
||||||
|
permissions: UnpackedPermissionSchema.array()
|
||||||
|
});
|
||||||
|
|
||||||
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
export const SanitizedDynamicSecretSchema = DynamicSecretsSchema.omit({
|
||||||
inputIV: true,
|
inputIV: true,
|
||||||
inputTag: true,
|
inputTag: true,
|
||||||
|
@@ -19,6 +19,7 @@ import { registerProjectMembershipRouter } from "./project-membership-router";
|
|||||||
import { registerProjectRouter } from "./project-router";
|
import { registerProjectRouter } from "./project-router";
|
||||||
import { registerSecretFolderRouter } from "./secret-folder-router";
|
import { registerSecretFolderRouter } from "./secret-folder-router";
|
||||||
import { registerSecretImportRouter } from "./secret-import-router";
|
import { registerSecretImportRouter } from "./secret-import-router";
|
||||||
|
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
||||||
import { registerSecretTagRouter } from "./secret-tag-router";
|
import { registerSecretTagRouter } from "./secret-tag-router";
|
||||||
import { registerSsoRouter } from "./sso-router";
|
import { registerSsoRouter } from "./sso-router";
|
||||||
import { registerUserActionRouter } from "./user-action-router";
|
import { registerUserActionRouter } from "./user-action-router";
|
||||||
@@ -65,4 +66,5 @@ export const registerV1Routes = async (server: FastifyZodProvider) => {
|
|||||||
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
await server.register(registerIntegrationAuthRouter, { prefix: "/integration-auth" });
|
||||||
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
|
await server.register(registerWebhookRouter, { prefix: "/webhooks" });
|
||||||
await server.register(registerIdentityRouter, { prefix: "/identities" });
|
await server.register(registerIdentityRouter, { prefix: "/identities" });
|
||||||
|
await server.register(registerSecretSharingRouter, { prefix: "/secret-sharing" });
|
||||||
};
|
};
|
||||||
|
139
backend/src/server/routes/v1/secret-sharing-router.ts
Normal file
139
backend/src/server/routes/v1/secret-sharing-router.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretSharingSchema } from "@app/db/schemas";
|
||||||
|
import { publicEndpointLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
export const registerSecretSharingRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
response: {
|
||||||
|
200: z.array(SecretSharingSchema)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const sharedSecrets = await req.server.services.secretSharing.getSharedSecrets({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
return sharedSecrets;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/public/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: publicEndpointLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string().uuid()
|
||||||
|
}),
|
||||||
|
querystring: z.object({
|
||||||
|
hashedHex: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: SecretSharingSchema.pick({ name: true, encryptedValue: true, iv: true, tag: true, expiresAt: true })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const sharedSecret = await req.server.services.secretSharing.getActiveSharedSecretByIdAndHashedHex(
|
||||||
|
req.params.id,
|
||||||
|
req.query.hashedHex
|
||||||
|
);
|
||||||
|
if (!sharedSecret) return undefined;
|
||||||
|
return {
|
||||||
|
name: sharedSecret.name,
|
||||||
|
encryptedValue: sharedSecret.encryptedValue,
|
||||||
|
iv: sharedSecret.iv,
|
||||||
|
tag: sharedSecret.tag,
|
||||||
|
expiresAt: sharedSecret.expiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string(),
|
||||||
|
encryptedValue: z.string(),
|
||||||
|
iv: z.string(),
|
||||||
|
tag: z.string(),
|
||||||
|
hashedHex: z.string(),
|
||||||
|
expiresAt: z.string().refine((date) => new Date(date) > new Date(), {
|
||||||
|
message: "Expires at should be a future date"
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string().uuid()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { name, encryptedValue, iv, tag, hashedHex, expiresAt } = req.body;
|
||||||
|
const sharedSecret = await req.server.services.secretSharing.createSharedSecret({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
name,
|
||||||
|
encryptedValue,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
|
expiresAt: new Date(expiresAt)
|
||||||
|
});
|
||||||
|
return { id: sharedSecret.id };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:sharedSecretId",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
sharedSecretId: z.string().uuid()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: SecretSharingSchema
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { sharedSecretId } = req.params;
|
||||||
|
const deletedSharedSecret = await req.server.services.secretSharing.deleteSharedSecretById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
sharedSecretId
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...deletedSharedSecret };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
@@ -1,11 +1,15 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas";
|
||||||
import { readLimit } from "@app/server/config/rateLimiter";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { authRateLimit, readLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/",
|
url: "/",
|
||||||
@@ -25,4 +29,29 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { user };
|
return { user };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:userId/unlock",
|
||||||
|
config: {
|
||||||
|
rateLimit: authRateLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
token: z.string().trim()
|
||||||
|
}),
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handler: async (req, res) => {
|
||||||
|
try {
|
||||||
|
await server.services.user.unlockUser(req.params.userId, req.query.token);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`User unlock failed for ${req.params.userId}`);
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
return res.redirect(`${appCfg.SITE_URL}/login`);
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
import { mfaRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthModeMfaJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
||||||
@@ -34,7 +34,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/mfa/send",
|
url: "/mfa/send",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: mfaRateLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
response: {
|
response: {
|
||||||
@@ -53,7 +53,7 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
|
|||||||
url: "/mfa/verify",
|
url: "/mfa/verify",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
config: {
|
config: {
|
||||||
rateLimit: writeLimit
|
rateLimit: mfaRateLimit
|
||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
|
@@ -13,8 +13,9 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo
|
|||||||
|
|
||||||
type TAuthTokenServiceFactoryDep = {
|
type TAuthTokenServiceFactoryDep = {
|
||||||
tokenDAL: TTokenDALFactory;
|
tokenDAL: TTokenDALFactory;
|
||||||
userDAL: Pick<TUserDALFactory, "findById">;
|
userDAL: Pick<TUserDALFactory, "findById" | "transaction">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
|
export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
|
||||||
|
|
||||||
export const getTokenConfig = (tokenType: TokenType) => {
|
export const getTokenConfig = (tokenType: TokenType) => {
|
||||||
@@ -53,6 +54,11 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
|||||||
const expiresAt = new Date(new Date().getTime() + 86400000);
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
return { token, expiresAt };
|
return { token, expiresAt };
|
||||||
}
|
}
|
||||||
|
case TokenType.TOKEN_USER_UNLOCK: {
|
||||||
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
|
const expiresAt = new Date(new Date().getTime() + 259200000);
|
||||||
|
return { token, expiresAt };
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
const token = crypto.randomBytes(16).toString("hex");
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
const expiresAt = new Date();
|
const expiresAt = new Date();
|
||||||
|
@@ -3,7 +3,8 @@ export enum TokenType {
|
|||||||
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
TOKEN_EMAIL_VERIFICATION = "emailVerification", // unverified -> verified
|
||||||
TOKEN_EMAIL_MFA = "emailMfa",
|
TOKEN_EMAIL_MFA = "emailMfa",
|
||||||
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
||||||
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset"
|
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
|
||||||
|
TOKEN_USER_UNLOCK = "userUnlock"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TCreateTokenForUserDTO = {
|
export type TCreateTokenForUserDTO = {
|
||||||
|
@@ -44,3 +44,27 @@ export const validateSignUpAuthorization = (token: string, userId: string, valid
|
|||||||
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError();
|
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw new UnauthorizedError();
|
||||||
if (decodedToken.userId !== userId) throw new UnauthorizedError();
|
if (decodedToken.userId !== userId) throw new UnauthorizedError();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?: Date | null) => {
|
||||||
|
if (isLocked) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
name: "User Locked",
|
||||||
|
message:
|
||||||
|
"User is locked due to multiple failed login attempts. An email has been sent to you in order to unlock your account. You can also reset your password to unlock your account."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (temporaryLockDateEnd) {
|
||||||
|
const timeDiff = new Date().getTime() - temporaryLockDateEnd.getTime();
|
||||||
|
if (timeDiff < 0) {
|
||||||
|
const secondsDiff = (-1 * timeDiff) / 1000;
|
||||||
|
const timeDisplay =
|
||||||
|
secondsDiff > 60 ? `${Math.ceil(secondsDiff / 60)} minutes` : `${Math.ceil(secondsDiff)} seconds`;
|
||||||
|
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
name: "User Locked",
|
||||||
|
message: `User is temporary locked due to multiple failed login attempts. Try again after ${timeDisplay}. You can also reset your password now to proceed.`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@@ -4,7 +4,7 @@ import { TUsers, UserDeviceSchema } from "@app/db/schemas";
|
|||||||
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
|
||||||
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 { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
import { TTokenDALFactory } from "../auth-token/auth-token-dal";
|
import { TTokenDALFactory } from "../auth-token/auth-token-dal";
|
||||||
@@ -13,7 +13,7 @@ import { TokenType } from "../auth-token/auth-token-types";
|
|||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { validateProviderAuthToken } from "./auth-fns";
|
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns";
|
||||||
import {
|
import {
|
||||||
TLoginClientProofDTO,
|
TLoginClientProofDTO,
|
||||||
TLoginGenServerPublicKeyDTO,
|
TLoginGenServerPublicKeyDTO,
|
||||||
@@ -212,6 +212,9 @@ export const authLoginServiceFactory = ({
|
|||||||
});
|
});
|
||||||
// send multi factor auth token if they it enabled
|
// send multi factor auth token if they it enabled
|
||||||
if (userEnc.isMfaEnabled && userEnc.email) {
|
if (userEnc.isMfaEnabled && userEnc.email) {
|
||||||
|
const user = await userDAL.findById(userEnc.userId);
|
||||||
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
|
|
||||||
const mfaToken = jwt.sign(
|
const mfaToken = jwt.sign(
|
||||||
{
|
{
|
||||||
authMethod,
|
authMethod,
|
||||||
@@ -300,28 +303,111 @@ export const authLoginServiceFactory = ({
|
|||||||
const resendMfaToken = async (userId: string) => {
|
const resendMfaToken = async (userId: string) => {
|
||||||
const user = await userDAL.findById(userId);
|
const user = await userDAL.findById(userId);
|
||||||
if (!user || !user.email) return;
|
if (!user || !user.email) return;
|
||||||
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
await sendUserMfaCode({
|
await sendUserMfaCode({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
email: user.email
|
email: user.email
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processFailedMfaAttempt = async (userId: string) => {
|
||||||
|
try {
|
||||||
|
const updatedUser = await userDAL.transaction(async (tx) => {
|
||||||
|
const PROGRESSIVE_DELAY_INTERVAL = 3;
|
||||||
|
const user = await userDAL.updateById(userId, { $incr: { consecutiveFailedMfaAttempts: 1 } }, tx);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new Error("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressiveDelaysInMins = [5, 30, 60];
|
||||||
|
|
||||||
|
// lock user when failed attempt exceeds threshold
|
||||||
|
if (
|
||||||
|
user.consecutiveFailedMfaAttempts &&
|
||||||
|
user.consecutiveFailedMfaAttempts >= PROGRESSIVE_DELAY_INTERVAL * (progressiveDelaysInMins.length + 1)
|
||||||
|
) {
|
||||||
|
return userDAL.updateById(
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
isLocked: true,
|
||||||
|
temporaryLockDateEnd: null
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// delay user only when failed MFA attempts is a multiple of configured delay interval
|
||||||
|
if (user.consecutiveFailedMfaAttempts && user.consecutiveFailedMfaAttempts % PROGRESSIVE_DELAY_INTERVAL === 0) {
|
||||||
|
const delayIndex = user.consecutiveFailedMfaAttempts / PROGRESSIVE_DELAY_INTERVAL - 1;
|
||||||
|
return userDAL.updateById(
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
temporaryLockDateEnd: new Date(new Date().getTime() + progressiveDelaysInMins[delayIndex] * 60 * 1000)
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedUser;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "Process failed MFA Attempt" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Multi factor authentication verification of code
|
* Multi factor authentication verification of code
|
||||||
* Third step of login in which user completes with mfa
|
* Third step of login in which user completes with mfa
|
||||||
* */
|
* */
|
||||||
const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
|
const verifyMfaToken = async ({ userId, mfaToken, mfaJwtToken, ip, userAgent, orgId }: TVerifyMfaTokenDTO) => {
|
||||||
await tokenService.validateTokenForUser({
|
const appCfg = getConfig();
|
||||||
type: TokenType.TOKEN_EMAIL_MFA,
|
const user = await userDAL.findById(userId);
|
||||||
userId,
|
enforceUserLockStatus(Boolean(user.isLocked), user.temporaryLockDateEnd);
|
||||||
code: mfaToken
|
|
||||||
});
|
try {
|
||||||
|
await tokenService.validateTokenForUser({
|
||||||
|
type: TokenType.TOKEN_EMAIL_MFA,
|
||||||
|
userId,
|
||||||
|
code: mfaToken
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const updatedUser = await processFailedMfaAttempt(userId);
|
||||||
|
if (updatedUser.isLocked) {
|
||||||
|
if (updatedUser.email) {
|
||||||
|
const unlockToken = await tokenService.createTokenForUser({
|
||||||
|
type: TokenType.TOKEN_USER_UNLOCK,
|
||||||
|
userId: updatedUser.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
template: SmtpTemplates.UnlockAccount,
|
||||||
|
subjectLine: "Unlock your Infisical account",
|
||||||
|
recipients: [updatedUser.email],
|
||||||
|
substitutions: {
|
||||||
|
token: unlockToken,
|
||||||
|
callback_url: `${appCfg.SITE_URL}/api/v1/user/${updatedUser.id}/unlock`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
const decodedToken = jwt.verify(mfaJwtToken, getConfig().AUTH_SECRET) as AuthModeMfaJwtTokenPayload;
|
const decodedToken = jwt.verify(mfaJwtToken, getConfig().AUTH_SECRET) as AuthModeMfaJwtTokenPayload;
|
||||||
|
|
||||||
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
||||||
if (!userEnc) throw new Error("Failed to authenticate user");
|
if (!userEnc) throw new Error("Failed to authenticate user");
|
||||||
|
|
||||||
|
// reset lock states
|
||||||
|
await userDAL.updateById(userId, {
|
||||||
|
consecutiveFailedMfaAttempts: 0,
|
||||||
|
temporaryLockDateEnd: null
|
||||||
|
});
|
||||||
|
|
||||||
const token = await generateUserTokens({
|
const token = await generateUserTokens({
|
||||||
user: {
|
user: {
|
||||||
...userEnc,
|
...userEnc,
|
||||||
|
@@ -174,6 +174,12 @@ export const authPaswordServiceFactory = ({
|
|||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await userDAL.updateById(userId, {
|
||||||
|
isLocked: false,
|
||||||
|
temporaryLockDateEnd: null,
|
||||||
|
consecutiveFailedMfaAttempts: 0
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError, subject } from "@casl/ability";
|
||||||
|
|
||||||
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, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
@@ -66,6 +66,11 @@ export const integrationServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment: sourceEnvironment, secretPath })
|
||||||
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
|
const folder = await folderDAL.findBySecretPath(integrationAuth.projectId, sourceEnvironment, secretPath);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
||||||
|
|
||||||
@@ -123,6 +128,11 @@ export const integrationServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Integrations);
|
||||||
|
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionActions.Read,
|
||||||
|
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||||
|
);
|
||||||
|
|
||||||
const folder = await folderDAL.findBySecretPath(integration.projectId, environment, secretPath);
|
const folder = await folderDAL.findBySecretPath(integration.projectId, environment, secretPath);
|
||||||
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
if (!folder) throw new BadRequestError({ message: "Folder path not found" });
|
||||||
|
|
||||||
|
@@ -1,25 +1,30 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError, MongoAbility, RawRuleOf } from "@casl/ability";
|
||||||
import { packRules } from "@casl/ability/extra";
|
import { PackRule, packRules, unpackRules } from "@casl/ability/extra";
|
||||||
|
|
||||||
import { ProjectMembershipRole, TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas";
|
import { ProjectMembershipRole } from "@app/db/schemas";
|
||||||
|
import { UnpackedPermissionSchema } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import {
|
import {
|
||||||
projectAdminPermissions,
|
projectAdminPermissions,
|
||||||
projectMemberPermissions,
|
projectMemberPermissions,
|
||||||
projectNoAccessPermissions,
|
projectNoAccessPermissions,
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionSet,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
projectViewerPermission
|
projectViewerPermission
|
||||||
} from "@app/ee/services/permission/project-permission";
|
} from "@app/ee/services/permission/project-permission";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod } from "../auth/auth-type";
|
||||||
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
import { TIdentityProjectMembershipRoleDALFactory } from "../identity-project/identity-project-membership-role-dal";
|
||||||
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||||
import { TProjectRoleDALFactory } from "./project-role-dal";
|
import { TProjectRoleDALFactory } from "./project-role-dal";
|
||||||
|
import { TCreateRoleDTO, TDeleteRoleDTO, TGetRoleBySlugDTO, TListRolesDTO, TUpdateRoleDTO } from "./project-role-types";
|
||||||
|
|
||||||
type TProjectRoleServiceFactoryDep = {
|
type TProjectRoleServiceFactoryDep = {
|
||||||
projectRoleDAL: TProjectRoleDALFactory;
|
projectRoleDAL: TProjectRoleDALFactory;
|
||||||
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getUserProjectPermission">;
|
||||||
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
|
identityProjectMembershipRoleDAL: TIdentityProjectMembershipRoleDALFactory;
|
||||||
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
|
projectUserMembershipRoleDAL: TProjectUserMembershipRoleDALFactory;
|
||||||
@@ -27,20 +32,68 @@ type TProjectRoleServiceFactoryDep = {
|
|||||||
|
|
||||||
export type TProjectRoleServiceFactory = ReturnType<typeof projectRoleServiceFactory>;
|
export type TProjectRoleServiceFactory = ReturnType<typeof projectRoleServiceFactory>;
|
||||||
|
|
||||||
|
const unpackPermissions = (permissions: unknown) =>
|
||||||
|
UnpackedPermissionSchema.array().parse(
|
||||||
|
unpackRules((permissions || []) as PackRule<RawRuleOf<MongoAbility<ProjectPermissionSet>>>[])
|
||||||
|
);
|
||||||
|
|
||||||
|
const getPredefinedRoles = (projectId: string, roleFilter?: ProjectMembershipRole) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
|
||||||
|
projectId,
|
||||||
|
name: "Admin",
|
||||||
|
slug: ProjectMembershipRole.Admin,
|
||||||
|
permissions: projectAdminPermissions,
|
||||||
|
description: "Full administrative access over a project",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
|
||||||
|
projectId,
|
||||||
|
name: "Developer",
|
||||||
|
slug: ProjectMembershipRole.Member,
|
||||||
|
permissions: projectMemberPermissions,
|
||||||
|
description: "Limited read/write role in a project",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
|
||||||
|
projectId,
|
||||||
|
name: "Viewer",
|
||||||
|
slug: ProjectMembershipRole.Viewer,
|
||||||
|
permissions: projectViewerPermission,
|
||||||
|
description: "Only read role in a project",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
|
||||||
|
projectId,
|
||||||
|
name: "No Access",
|
||||||
|
slug: ProjectMembershipRole.NoAccess,
|
||||||
|
permissions: projectNoAccessPermissions,
|
||||||
|
description: "No access to any resources in the project",
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
}
|
||||||
|
].filter(({ slug }) => !roleFilter || roleFilter.includes(slug));
|
||||||
|
};
|
||||||
|
|
||||||
export const projectRoleServiceFactory = ({
|
export const projectRoleServiceFactory = ({
|
||||||
projectRoleDAL,
|
projectRoleDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
identityProjectMembershipRoleDAL,
|
identityProjectMembershipRoleDAL,
|
||||||
projectUserMembershipRoleDAL
|
projectUserMembershipRoleDAL,
|
||||||
|
projectDAL
|
||||||
}: TProjectRoleServiceFactoryDep) => {
|
}: TProjectRoleServiceFactoryDep) => {
|
||||||
const createRole = async (
|
const createRole = async ({ projectSlug, data, actor, actorId, actorAuthMethod, actorOrgId }: TCreateRoleDTO) => {
|
||||||
actor: ActorType,
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
actorId: string,
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
projectId: string,
|
const projectId = project.id;
|
||||||
data: Omit<TProjectRolesInsert, "projectId">,
|
|
||||||
actorAuthMethod: ActorAuthMethod,
|
|
||||||
actorOrgId: string | undefined
|
|
||||||
) => {
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -53,21 +106,54 @@ export const projectRoleServiceFactory = ({
|
|||||||
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
|
if (existingRole) throw new BadRequestError({ name: "Create Role", message: "Duplicate role" });
|
||||||
const role = await projectRoleDAL.create({
|
const role = await projectRoleDAL.create({
|
||||||
...data,
|
...data,
|
||||||
projectId,
|
projectId
|
||||||
permissions: JSON.stringify(data.permissions)
|
|
||||||
});
|
});
|
||||||
return role;
|
return { ...role, permissions: unpackPermissions(role.permissions) };
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRole = async (
|
const getRoleBySlug = async ({
|
||||||
actor: ActorType,
|
actor,
|
||||||
actorId: string,
|
actorId,
|
||||||
projectId: string,
|
projectSlug,
|
||||||
roleId: string,
|
actorAuthMethod,
|
||||||
data: Omit<TOrgRolesUpdate, "orgId">,
|
actorOrgId,
|
||||||
actorAuthMethod: ActorAuthMethod,
|
roleSlug
|
||||||
actorOrgId: string | undefined
|
}: TGetRoleBySlugDTO) => {
|
||||||
) => {
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
const projectId = project.id;
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||||
|
if (roleSlug !== "custom" && Object.values(ProjectMembershipRole).includes(roleSlug as ProjectMembershipRole)) {
|
||||||
|
const predefinedRole = getPredefinedRoles(projectId, roleSlug as ProjectMembershipRole)[0];
|
||||||
|
return { ...predefinedRole, permissions: UnpackedPermissionSchema.array().parse(predefinedRole.permissions) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRole = await projectRoleDAL.findOne({ slug: roleSlug, projectId });
|
||||||
|
if (!customRole) throw new BadRequestError({ message: "Role not found" });
|
||||||
|
return { ...customRole, permissions: unpackPermissions(customRole.permissions) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRole = async ({
|
||||||
|
roleId,
|
||||||
|
projectSlug,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
data
|
||||||
|
}: TUpdateRoleDTO) => {
|
||||||
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
|
const projectId = project.id;
|
||||||
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -81,22 +167,16 @@ export const projectRoleServiceFactory = ({
|
|||||||
if (existingRole && existingRole.id !== roleId)
|
if (existingRole && existingRole.id !== roleId)
|
||||||
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
|
throw new BadRequestError({ name: "Update Role", message: "Duplicate role" });
|
||||||
}
|
}
|
||||||
const [updatedRole] = await projectRoleDAL.update(
|
const [updatedRole] = await projectRoleDAL.update({ id: roleId, projectId }, data);
|
||||||
{ id: roleId, projectId },
|
|
||||||
{ ...data, permissions: data.permissions ? JSON.stringify(data.permissions) : undefined }
|
|
||||||
);
|
|
||||||
if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
if (!updatedRole) throw new BadRequestError({ message: "Role not found", name: "Update role" });
|
||||||
return updatedRole;
|
return { ...updatedRole, permissions: unpackPermissions(updatedRole.permissions) };
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRole = async (
|
const deleteRole = async ({ actor, actorId, actorAuthMethod, actorOrgId, projectSlug, roleId }: TDeleteRoleDTO) => {
|
||||||
actor: ActorType,
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
actorId: string,
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
projectId: string,
|
const projectId = project.id;
|
||||||
roleId: string,
|
|
||||||
actorAuthMethod: ActorAuthMethod,
|
|
||||||
actorOrgId: string | undefined
|
|
||||||
) => {
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -125,16 +205,14 @@ export const projectRoleServiceFactory = ({
|
|||||||
const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId });
|
const [deletedRole] = await projectRoleDAL.delete({ id: roleId, projectId });
|
||||||
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Delete role" });
|
if (!deletedRole) throw new BadRequestError({ message: "Role not found", name: "Delete role" });
|
||||||
|
|
||||||
return deletedRole;
|
return { ...deletedRole, permissions: unpackPermissions(deletedRole.permissions) };
|
||||||
};
|
};
|
||||||
|
|
||||||
const listRoles = async (
|
const listRoles = async ({ projectSlug, actorOrgId, actorAuthMethod, actorId, actor }: TListRolesDTO) => {
|
||||||
actor: ActorType,
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
||||||
actorId: string,
|
if (!project) throw new BadRequestError({ message: "Project not found" });
|
||||||
projectId: string,
|
const projectId = project.id;
|
||||||
actorAuthMethod: ActorAuthMethod,
|
|
||||||
actorOrgId: string | undefined
|
|
||||||
) => {
|
|
||||||
const { permission } = await permissionService.getProjectPermission(
|
const { permission } = await permissionService.getProjectPermission(
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
@@ -144,52 +222,7 @@ export const projectRoleServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||||
const customRoles = await projectRoleDAL.find({ projectId });
|
const customRoles = await projectRoleDAL.find({ projectId });
|
||||||
const roles = [
|
const roles = [...getPredefinedRoles(projectId), ...(customRoles || [])];
|
||||||
{
|
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c69", // dummy userid
|
|
||||||
projectId,
|
|
||||||
name: "Admin",
|
|
||||||
slug: ProjectMembershipRole.Admin,
|
|
||||||
description: "Complete administration access over the project",
|
|
||||||
permissions: packRules(projectAdminPermissions),
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c70", // dummy user for zod validation in response
|
|
||||||
projectId,
|
|
||||||
name: "Developer",
|
|
||||||
slug: ProjectMembershipRole.Member,
|
|
||||||
description: "Non-administrative role in an project",
|
|
||||||
permissions: packRules(projectMemberPermissions),
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c71", // dummy user for zod validation in response
|
|
||||||
projectId,
|
|
||||||
name: "Viewer",
|
|
||||||
slug: ProjectMembershipRole.Viewer,
|
|
||||||
description: "Non-administrative role in an project",
|
|
||||||
permissions: packRules(projectViewerPermission),
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "b11b49a9-09a9-4443-916a-4246f9ff2c72", // dummy user for zod validation in response
|
|
||||||
projectId,
|
|
||||||
name: "No Access",
|
|
||||||
slug: "no-access",
|
|
||||||
description: "No access to any resources in the project",
|
|
||||||
permissions: packRules(projectNoAccessPermissions),
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
},
|
|
||||||
...(customRoles || []).map(({ permissions, ...data }) => ({
|
|
||||||
...data,
|
|
||||||
permissions
|
|
||||||
}))
|
|
||||||
];
|
|
||||||
|
|
||||||
return roles;
|
return roles;
|
||||||
};
|
};
|
||||||
@@ -209,5 +242,5 @@ export const projectRoleServiceFactory = ({
|
|||||||
return { permissions: packRules(permission.rules), membership };
|
return { permissions: packRules(permission.rules), membership };
|
||||||
};
|
};
|
||||||
|
|
||||||
return { createRole, updateRole, deleteRole, listRoles, getUserPermission };
|
return { createRole, updateRole, deleteRole, listRoles, getUserPermission, getRoleBySlug };
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,27 @@
|
|||||||
|
import { TOrgRolesUpdate, TProjectRolesInsert } from "@app/db/schemas";
|
||||||
|
import { TProjectPermission } from "@app/lib/types";
|
||||||
|
|
||||||
|
export type TCreateRoleDTO = {
|
||||||
|
data: Omit<TProjectRolesInsert, "projectId">;
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TGetRoleBySlugDTO = {
|
||||||
|
roleSlug: string;
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TUpdateRoleDTO = {
|
||||||
|
roleId: string;
|
||||||
|
data: Omit<TOrgRolesUpdate, "orgId">;
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TDeleteRoleDTO = {
|
||||||
|
roleId: string;
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TListRolesDTO = {
|
||||||
|
projectSlug: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
@@ -3,10 +3,12 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||||
|
|
||||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||||
|
import { TSecretSharingDALFactory } from "../secret-sharing/secret-sharing-dal";
|
||||||
|
|
||||||
type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
||||||
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
|
auditLogDAL: Pick<TAuditLogDALFactory, "pruneAuditLog">;
|
||||||
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
|
identityAccessTokenDAL: Pick<TIdentityAccessTokenDALFactory, "removeExpiredTokens">;
|
||||||
|
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -15,12 +17,14 @@ export type TDailyResourceCleanUpQueueServiceFactory = ReturnType<typeof dailyRe
|
|||||||
export const dailyResourceCleanUpQueueServiceFactory = ({
|
export const dailyResourceCleanUpQueueServiceFactory = ({
|
||||||
auditLogDAL,
|
auditLogDAL,
|
||||||
queueService,
|
queueService,
|
||||||
identityAccessTokenDAL
|
identityAccessTokenDAL,
|
||||||
|
secretSharingDAL
|
||||||
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
}: TDailyResourceCleanUpQueueServiceFactoryDep) => {
|
||||||
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
queueService.start(QueueName.DailyResourceCleanUp, async () => {
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task started`);
|
||||||
await auditLogDAL.pruneAuditLog();
|
await auditLogDAL.pruneAuditLog();
|
||||||
await identityAccessTokenDAL.removeExpiredTokens();
|
await identityAccessTokenDAL.removeExpiredTokens();
|
||||||
|
await secretSharingDAL.pruneExpiredSharedSecrets();
|
||||||
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
27
backend/src/services/secret-sharing/secret-sharing-dal.ts
Normal file
27
backend/src/services/secret-sharing/secret-sharing-dal.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TDbClient } from "@app/db";
|
||||||
|
import { TableName } from "@app/db/schemas";
|
||||||
|
import { DatabaseError } from "@app/lib/errors";
|
||||||
|
import { ormify } from "@app/lib/knex";
|
||||||
|
|
||||||
|
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
|
||||||
|
|
||||||
|
export const secretSharingDALFactory = (db: TDbClient) => {
|
||||||
|
const sharedSecretOrm = ormify(db, TableName.SecretSharing);
|
||||||
|
|
||||||
|
const pruneExpiredSharedSecrets = async (tx?: Knex) => {
|
||||||
|
try {
|
||||||
|
const today = new Date();
|
||||||
|
const docs = await (tx || db)(TableName.SecretSharing).where("expiresAt", "<", today).del();
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "pruneExpiredSharedSecrets" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...sharedSecretOrm,
|
||||||
|
pruneExpiredSharedSecrets
|
||||||
|
};
|
||||||
|
};
|
@@ -0,0 +1,66 @@
|
|||||||
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { UnauthorizedError } from "@app/lib/errors";
|
||||||
|
|
||||||
|
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||||
|
import { TCreateSharedSecretDTO, TDeleteSharedSecretDTO, TSharedSecretPermission } from "./secret-sharing-types";
|
||||||
|
|
||||||
|
type TSecretSharingServiceFactoryDep = {
|
||||||
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
|
secretSharingDAL: TSecretSharingDALFactory;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
||||||
|
|
||||||
|
export const secretSharingServiceFactory = ({
|
||||||
|
permissionService,
|
||||||
|
secretSharingDAL
|
||||||
|
}: TSecretSharingServiceFactoryDep) => {
|
||||||
|
const createSharedSecret = async (createSharedSecretInput: TCreateSharedSecretDTO) => {
|
||||||
|
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, name, encryptedValue, iv, tag, hashedHex, expiresAt } =
|
||||||
|
createSharedSecretInput;
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||||
|
const newSharedSecret = await secretSharingDAL.create({
|
||||||
|
name,
|
||||||
|
encryptedValue,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
|
expiresAt,
|
||||||
|
userId: actorId,
|
||||||
|
orgId
|
||||||
|
});
|
||||||
|
return { id: newSharedSecret.id };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSharedSecrets = async (getSharedSecretsInput: TSharedSecretPermission) => {
|
||||||
|
const { actor, actorId, orgId, actorAuthMethod, actorOrgId } = getSharedSecretsInput;
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||||
|
const userSharedSecrets = await secretSharingDAL.find({ userId: actorId, orgId }, { sort: [["expiresAt", "asc"]] });
|
||||||
|
return userSharedSecrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActiveSharedSecretByIdAndHashedHex = async (sharedSecretId: string, hashedHex: string) => {
|
||||||
|
const sharedSecret = await secretSharingDAL.findOne({ id: sharedSecretId, hashedHex });
|
||||||
|
if (sharedSecret && sharedSecret.expiresAt < new Date()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return sharedSecret;
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteSharedSecretById = async (deleteSharedSecretInput: TDeleteSharedSecretDTO) => {
|
||||||
|
const { actor, actorId, orgId, actorAuthMethod, actorOrgId, sharedSecretId } = deleteSharedSecretInput;
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
if (!permission) throw new UnauthorizedError({ name: "User not in org" });
|
||||||
|
const deletedSharedSecret = await secretSharingDAL.deleteById(sharedSecretId);
|
||||||
|
return deletedSharedSecret;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSharedSecret,
|
||||||
|
getSharedSecrets,
|
||||||
|
deleteSharedSecretById,
|
||||||
|
getActiveSharedSecretByIdAndHashedHex
|
||||||
|
};
|
||||||
|
};
|
22
backend/src/services/secret-sharing/secret-sharing-types.ts
Normal file
22
backend/src/services/secret-sharing/secret-sharing-types.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
|
export type TSharedSecretPermission = {
|
||||||
|
actor: ActorType;
|
||||||
|
actorId: string;
|
||||||
|
actorAuthMethod: ActorAuthMethod;
|
||||||
|
actorOrgId: string;
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateSharedSecretDTO = {
|
||||||
|
name: string;
|
||||||
|
encryptedValue: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
hashedHex: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
} & TSharedSecretPermission;
|
||||||
|
|
||||||
|
export type TDeleteSharedSecretDTO = {
|
||||||
|
sharedSecretId: string;
|
||||||
|
} & TSharedSecretPermission;
|
@@ -21,6 +21,7 @@ export enum SmtpTemplates {
|
|||||||
EmailVerification = "emailVerification.handlebars",
|
EmailVerification = "emailVerification.handlebars",
|
||||||
SecretReminder = "secretReminder.handlebars",
|
SecretReminder = "secretReminder.handlebars",
|
||||||
EmailMfa = "emailMfa.handlebars",
|
EmailMfa = "emailMfa.handlebars",
|
||||||
|
UnlockAccount = "unlockAccount.handlebars",
|
||||||
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
AccessApprovalRequest = "accessApprovalRequest.handlebars",
|
||||||
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
HistoricalSecretList = "historicalSecretLeakIncident.handlebars",
|
||||||
NewDeviceJoin = "newDevice.handlebars",
|
NewDeviceJoin = "newDevice.handlebars",
|
||||||
|
16
backend/src/services/smtp/templates/unlockAccount.handlebars
Normal file
16
backend/src/services/smtp/templates/unlockAccount.handlebars
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Your Infisical account has been locked</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Unlock your Infisical account</h2>
|
||||||
|
<p>Your account has been temporarily locked due to multiple failed login attempts. </h2>
|
||||||
|
<a href="{{callback_url}}?token={{token}}">To unlock your account, follow the link here</a>
|
||||||
|
<p>If these attempts were not made by you, reset your password immediately.</p>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@@ -207,6 +207,19 @@ export const userServiceFactory = ({
|
|||||||
return userAction;
|
return userAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const unlockUser = async (userId: string, token: string) => {
|
||||||
|
await tokenService.validateTokenForUser({
|
||||||
|
userId,
|
||||||
|
code: token,
|
||||||
|
type: TokenType.TOKEN_USER_UNLOCK
|
||||||
|
});
|
||||||
|
|
||||||
|
await userDAL.update(
|
||||||
|
{ id: userId },
|
||||||
|
{ consecutiveFailedMfaAttempts: 0, isLocked: false, temporaryLockDateEnd: null }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sendEmailVerificationCode,
|
sendEmailVerificationCode,
|
||||||
verifyEmailVerificationCode,
|
verifyEmailVerificationCode,
|
||||||
@@ -216,6 +229,7 @@ export const userServiceFactory = ({
|
|||||||
deleteMe,
|
deleteMe,
|
||||||
getMe,
|
getMe,
|
||||||
createUserAction,
|
createUserAction,
|
||||||
getUserAction
|
getUserAction,
|
||||||
|
unlockUser
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -15,7 +15,6 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"runtime"
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"text/template"
|
"text/template"
|
||||||
@@ -257,19 +256,6 @@ func WriteBytesToFile(data *bytes.Buffer, outputPath string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendAPIEndpoint(address string) string {
|
|
||||||
// Ensure the address does not already end with "/api"
|
|
||||||
if strings.HasSuffix(address, "/api") {
|
|
||||||
return address
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the address ends with a slash and append accordingly
|
|
||||||
if address[len(address)-1] == '/' {
|
|
||||||
return address + "api"
|
|
||||||
}
|
|
||||||
return address + "/api"
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseAgentConfig(configFile []byte) (*Config, error) {
|
func ParseAgentConfig(configFile []byte) (*Config, error) {
|
||||||
var rawConfig struct {
|
var rawConfig struct {
|
||||||
Infisical InfisicalConfig `yaml:"infisical"`
|
Infisical InfisicalConfig `yaml:"infisical"`
|
||||||
@@ -290,7 +276,7 @@ func ParseAgentConfig(configFile []byte) (*Config, error) {
|
|||||||
rawConfig.Infisical.Address = DEFAULT_INFISICAL_CLOUD_URL
|
rawConfig.Infisical.Address = DEFAULT_INFISICAL_CLOUD_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
config.INFISICAL_URL = appendAPIEndpoint(rawConfig.Infisical.Address)
|
config.INFISICAL_URL = util.AppendAPIEndpoint(rawConfig.Infisical.Address)
|
||||||
|
|
||||||
log.Info().Msgf("Infisical instance address set to %s", rawConfig.Infisical.Address)
|
log.Info().Msgf("Infisical instance address set to %s", rawConfig.Infisical.Address)
|
||||||
|
|
||||||
|
@@ -101,7 +101,7 @@ var loginCmd = &cobra.Command{
|
|||||||
//set domainQuery to false
|
//set domainQuery to false
|
||||||
if !overrideDomain {
|
if !overrideDomain {
|
||||||
domainQuery = false
|
domainQuery = false
|
||||||
config.INFISICAL_URL = config.INFISICAL_URL_MANUAL_OVERRIDE
|
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL_MANUAL_OVERRIDE)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -43,6 +43,7 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.")
|
rootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.")
|
||||||
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
silent, err := cmd.Flags().GetBool("silent")
|
silent, err := cmd.Flags().GetBool("silent")
|
||||||
|
config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err)
|
util.HandleError(err)
|
||||||
}
|
}
|
||||||
|
@@ -170,6 +170,11 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "Unable to get your local config details")
|
util.HandleError(err, "Unable to get your local config details")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secretType, err := cmd.Flags().GetString("type")
|
||||||
|
if err != nil || (secretType != util.SECRET_TYPE_SHARED && secretType != util.SECRET_TYPE_PERSONAL) {
|
||||||
|
util.HandleError(err, "Unable to parse secret type")
|
||||||
|
}
|
||||||
|
|
||||||
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.HandleError(err, "Unable to authenticate")
|
util.HandleError(err, "Unable to authenticate")
|
||||||
@@ -179,6 +184,7 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
httpClient := resty.New().
|
httpClient := resty.New().
|
||||||
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
|
SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
|
||||||
SetHeader("Accept", "application/json")
|
SetHeader("Accept", "application/json")
|
||||||
@@ -223,7 +229,16 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
secretsToModify := []api.Secret{}
|
secretsToModify := []api.Secret{}
|
||||||
secretOperations := []SecretSetOperation{}
|
secretOperations := []SecretSetOperation{}
|
||||||
|
|
||||||
secretByKey := getSecretsByKeys(secrets)
|
sharedSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets))
|
||||||
|
personalSecretMapByName := make(map[string]models.SingleEnvironmentVariable, len(secrets))
|
||||||
|
|
||||||
|
for _, secret := range secrets {
|
||||||
|
if secret.Type == util.SECRET_TYPE_PERSONAL {
|
||||||
|
personalSecretMapByName[secret.Key] = secret
|
||||||
|
} else {
|
||||||
|
sharedSecretMapByName[secret.Key] = secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, arg := range args {
|
for _, arg := range args {
|
||||||
splitKeyValueFromArg := strings.SplitN(arg, "=", 2)
|
splitKeyValueFromArg := strings.SplitN(arg, "=", 2)
|
||||||
@@ -251,7 +266,16 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
util.HandleError(err, "unable to encrypt your secrets")
|
util.HandleError(err, "unable to encrypt your secrets")
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingSecret, ok := secretByKey[key]; ok {
|
var existingSecret models.SingleEnvironmentVariable
|
||||||
|
var doesSecretExist bool
|
||||||
|
|
||||||
|
if secretType == util.SECRET_TYPE_SHARED {
|
||||||
|
existingSecret, doesSecretExist = sharedSecretMapByName[key]
|
||||||
|
} else {
|
||||||
|
existingSecret, doesSecretExist = personalSecretMapByName[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
if doesSecretExist {
|
||||||
// case: secret exists in project so it needs to be modified
|
// case: secret exists in project so it needs to be modified
|
||||||
encryptedSecretDetails := api.Secret{
|
encryptedSecretDetails := api.Secret{
|
||||||
ID: existingSecret.ID,
|
ID: existingSecret.ID,
|
||||||
@@ -291,7 +315,7 @@ var secretsSetCmd = &cobra.Command{
|
|||||||
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
|
SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce),
|
||||||
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
|
SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag),
|
||||||
SecretValueHash: hashedValue,
|
SecretValueHash: hashedValue,
|
||||||
Type: util.SECRET_TYPE_SHARED,
|
Type: secretType,
|
||||||
PlainTextKey: key,
|
PlainTextKey: key,
|
||||||
}
|
}
|
||||||
secretsToCreate = append(secretsToCreate, encryptedSecretDetails)
|
secretsToCreate = append(secretsToCreate, encryptedSecretDetails)
|
||||||
@@ -781,6 +805,7 @@ func init() {
|
|||||||
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
secretsCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets")
|
||||||
secretsCmd.AddCommand(secretsSetCmd)
|
secretsCmd.AddCommand(secretsSetCmd)
|
||||||
secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path")
|
secretsSetCmd.Flags().String("path", "/", "set secrets within a folder path")
|
||||||
|
secretsSetCmd.Flags().String("type", util.SECRET_TYPE_SHARED, "the type of secret to create: personal or shared")
|
||||||
|
|
||||||
// Only supports logged in users (JWT auth)
|
// Only supports logged in users (JWT auth)
|
||||||
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) {
|
||||||
|
@@ -237,7 +237,7 @@ func NewDomainPrompt() (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return domain, nil
|
return util.AppendAPIEndpoint(domain), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoggedInUsersPrompt(profiles []string) (string, error) {
|
func LoggedInUsersPrompt(profiles []string) (string, error) {
|
||||||
|
@@ -88,7 +88,7 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) {
|
|||||||
//configFile.LoggedInUserDomain
|
//configFile.LoggedInUserDomain
|
||||||
//if not empty set as infisical url
|
//if not empty set as infisical url
|
||||||
if configFile.LoggedInUserDomain != "" {
|
if configFile.LoggedInUserDomain != "" {
|
||||||
config.INFISICAL_URL = configFile.LoggedInUserDomain
|
config.INFISICAL_URL = AppendAPIEndpoint(configFile.LoggedInUserDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
isAuthenticated := api.CallIsAuthenticated(httpClient)
|
isAuthenticated := api.CallIsAuthenticated(httpClient)
|
||||||
|
@@ -233,3 +233,16 @@ func getCurrentBranch() (string, error) {
|
|||||||
}
|
}
|
||||||
return path.Base(strings.TrimSpace(out.String())), nil
|
return path.Base(strings.TrimSpace(out.String())), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func AppendAPIEndpoint(address string) string {
|
||||||
|
// Ensure the address does not already end with "/api"
|
||||||
|
if strings.HasSuffix(address, "/api") {
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the address ends with a slash and append accordingly
|
||||||
|
if address[len(address)-1] == '/' {
|
||||||
|
return address + "api"
|
||||||
|
}
|
||||||
|
return address + "/api"
|
||||||
|
}
|
||||||
|
4
docs/api-reference/endpoints/project-roles/create.mdx
Normal file
4
docs/api-reference/endpoints/project-roles/create.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Create"
|
||||||
|
openapi: "POST /api/v1/workspace/{projectSlug}/roles"
|
||||||
|
---
|
4
docs/api-reference/endpoints/project-roles/delete.mdx
Normal file
4
docs/api-reference/endpoints/project-roles/delete.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Delete"
|
||||||
|
openapi: "DELETE /api/v1/workspace/{projectSlug}/roles/{roleId}"
|
||||||
|
---
|
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Get By Slug"
|
||||||
|
openapi: "GET /api/v1/workspace/{projectSlug}/roles/slug/{slug}"
|
||||||
|
---
|
4
docs/api-reference/endpoints/project-roles/list.mdx
Normal file
4
docs/api-reference/endpoints/project-roles/list.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "List"
|
||||||
|
openapi: "GET /api/v1/workspace/{projectSlug}/roles"
|
||||||
|
---
|
4
docs/api-reference/endpoints/project-roles/update.mdx
Normal file
4
docs/api-reference/endpoints/project-roles/update.mdx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: "Update"
|
||||||
|
openapi: "PATCH /api/v1/workspace/{projectSlug}/roles/{roleId}"
|
||||||
|
---
|
@@ -153,6 +153,16 @@ $ infisical secrets set STRIPE_API_KEY=sjdgwkeudyjwe DOMAIN=example.com HASH=jeb
|
|||||||
```
|
```
|
||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
|
<Accordion title="--type">
|
||||||
|
Used to select the type of secret to create. This could be either personal or shared (defaults to shared)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example
|
||||||
|
infisical secrets set DOMAIN=example.com --type=personal
|
||||||
|
```
|
||||||
|
|
||||||
|
</Accordion>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="infisical secrets delete">
|
<Accordion title="infisical secrets delete">
|
||||||
|
44
docs/documentation/platform/secret-sharing.mdx
Normal file
44
docs/documentation/platform/secret-sharing.mdx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
title: "Secret Sharing"
|
||||||
|
sidebarTitle: "Secret Sharing"
|
||||||
|
description: "Learn how to share time-bound secrets securely with anybody on the internet."
|
||||||
|
---
|
||||||
|
|
||||||
|
Developers often need to share secrets with their team members, contractors, or other third parties. This can be a risky process, as secrets can be easily leaked or misused. Infisical provides a secure way to share secrets with anybody on the internet in a time-bound manner.
|
||||||
|
|
||||||
|
## Share a Secret
|
||||||
|
|
||||||
|
1. Navigate to the **Projects** page.
|
||||||
|
2. Click on the **Secret Sharing** tab from the sidebar.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Click on the **Share Secret** button.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Infisical does not store the secret you share. This is a part of our Zero
|
||||||
|
Knowledge Architecture.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
4. Enter the secret you want to share and set the expiration time. Click on the **Share Secret** button.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Secret once set cannot be changed. This is to ensure that the secret is not
|
||||||
|
tampered with.
|
||||||
|
</Note>
|
||||||
|
|
||||||
|
5. Copy the link and share it with the intended recipient. Anybody with the link can access the secret before its expiration time. Hence, it is recommended to share the link only with the intended recipient.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Access a Shared Secret
|
||||||
|
|
||||||
|
Just click on the link you received to access the secret. The secret will be displayed on the screen & for how long it is valid.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Delete a Shared Secret
|
||||||
|
|
||||||
|
In the **Secret Sharing** tab, click on the **Delete** button next to the secret you want to delete. This will delete the secret immediately & the link will no longer be accessible.
|
BIN
docs/images/platform/secret-sharing/copy-url.png
Normal file
BIN
docs/images/platform/secret-sharing/copy-url.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 54 KiB |
BIN
docs/images/platform/secret-sharing/new-secret.png
Normal file
BIN
docs/images/platform/secret-sharing/new-secret.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
BIN
docs/images/platform/secret-sharing/overview.png
Normal file
BIN
docs/images/platform/secret-sharing/overview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 157 KiB |
BIN
docs/images/platform/secret-sharing/public-view.png
Normal file
BIN
docs/images/platform/secret-sharing/public-view.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 221 KiB |
@@ -137,6 +137,7 @@
|
|||||||
"documentation/platform/secret-rotation/aws-iam"
|
"documentation/platform/secret-rotation/aws-iam"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"documentation/platform/secret-sharing",
|
||||||
{
|
{
|
||||||
"group": "Dynamic Secrets",
|
"group": "Dynamic Secrets",
|
||||||
"pages": [
|
"pages": [
|
||||||
@@ -476,6 +477,16 @@
|
|||||||
"api-reference/endpoints/project-identities/delete-identity-membership"
|
"api-reference/endpoints/project-identities/delete-identity-membership"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"group": "Project Roles",
|
||||||
|
"pages": [
|
||||||
|
"api-reference/endpoints/project-roles/create",
|
||||||
|
"api-reference/endpoints/project-roles/update",
|
||||||
|
"api-reference/endpoints/project-roles/delete",
|
||||||
|
"api-reference/endpoints/project-roles/get-by-slug",
|
||||||
|
"api-reference/endpoints/project-roles/list"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"group": "Environments",
|
"group": "Environments",
|
||||||
"pages": [
|
"pages": [
|
||||||
|
@@ -23,7 +23,8 @@ export const publicPaths = [
|
|||||||
"/login/provider/success", // TODO: change
|
"/login/provider/success", // TODO: change
|
||||||
"/login/provider/error", // TODO: change
|
"/login/provider/error", // TODO: change
|
||||||
"/login/sso",
|
"/login/sso",
|
||||||
"/admin/signup"
|
"/admin/signup",
|
||||||
|
"/shared/secret/[id]"
|
||||||
];
|
];
|
||||||
|
|
||||||
export const languageMap = {
|
export const languageMap = {
|
||||||
|
@@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import { setAuthToken } from "@app/reactQuery";
|
import { setAuthToken } from "@app/reactQuery";
|
||||||
|
|
||||||
import { organizationKeys } from "../organization/queries";
|
import { organizationKeys } from "../organization/queries";
|
||||||
|
import { workspaceKeys } from "../workspace/queries";
|
||||||
import {
|
import {
|
||||||
ChangePasswordDTO,
|
ChangePasswordDTO,
|
||||||
CompleteAccountDTO,
|
CompleteAccountDTO,
|
||||||
@@ -78,7 +79,10 @@ export const useSelectOrganization = () => {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
|
queryClient.invalidateQueries([
|
||||||
|
organizationKeys.getUserOrganizations,
|
||||||
|
workspaceKeys.getAllUserWorkspace
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -12,21 +12,30 @@ export type TIdentityProjectPrivilege = {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
permissions?: TProjectPermission[];
|
permissions?: TProjectPermission[];
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
isTemporary: true;
|
isTemporary: true;
|
||||||
temporaryMode: string;
|
temporaryMode: string;
|
||||||
temporaryRange: string;
|
temporaryRange: string;
|
||||||
temporaryAccessStartTime: string;
|
temporaryAccessStartTime: string;
|
||||||
temporaryAccessEndTime?: string;
|
temporaryAccessEndTime?: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
isTemporary: false;
|
isTemporary: false;
|
||||||
temporaryMode?: null;
|
temporaryMode?: null;
|
||||||
temporaryRange?: null;
|
temporaryRange?: null;
|
||||||
temporaryAccessStartTime?: null;
|
temporaryAccessStartTime?: null;
|
||||||
temporaryAccessEndTime?: null;
|
temporaryAccessEndTime?: null;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type TProjectSpecificPrivilegePermission = {
|
||||||
|
conditions: {
|
||||||
|
environment: string;
|
||||||
|
secretPath?: { $glob: string };
|
||||||
|
};
|
||||||
|
actions: string[];
|
||||||
|
subject: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TCreateIdentityProjectPrivilegeDTO = {
|
export type TCreateIdentityProjectPrivilegeDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
@@ -36,14 +45,16 @@ export type TCreateIdentityProjectPrivilegeDTO = {
|
|||||||
temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode;
|
temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode;
|
||||||
temporaryRange?: string;
|
temporaryRange?: string;
|
||||||
temporaryAccessStartTime?: string;
|
temporaryAccessStartTime?: string;
|
||||||
permissions: TProjectPermission[];
|
privilegePermission: TProjectSpecificPrivilegePermission;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateIdentityProjectPrivlegeDTO = {
|
export type TUpdateIdentityProjectPrivlegeDTO = {
|
||||||
projectSlug: string;
|
projectSlug: string;
|
||||||
identityId: string;
|
identityId: string;
|
||||||
privilegeSlug: string;
|
privilegeSlug: string;
|
||||||
privilegeDetails: Partial<Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">>;
|
privilegeDetails: Partial<
|
||||||
|
Omit<TCreateIdentityProjectPrivilegeDTO, "projectMembershipId" | "projectId">
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteIdentityProjectPrivilegeDTO = {
|
export type TDeleteIdentityProjectPrivilegeDTO = {
|
||||||
|
@@ -8,6 +8,7 @@ export {
|
|||||||
} from "./mutation";
|
} from "./mutation";
|
||||||
export {
|
export {
|
||||||
useGetOrgRoles,
|
useGetOrgRoles,
|
||||||
|
useGetProjectRoleBySlug,
|
||||||
useGetProjectRoles,
|
useGetProjectRoles,
|
||||||
useGetUserOrgPermissions,
|
useGetUserOrgPermissions,
|
||||||
useGetUserProjectPermissions
|
useGetUserProjectPermissions
|
||||||
|
@@ -17,13 +17,10 @@ export const useCreateProjectRole = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ projectId, permissions, ...dto }: TCreateProjectRoleDTO) =>
|
mutationFn: ({ projectSlug, ...dto }: TCreateProjectRoleDTO) =>
|
||||||
apiRequest.post(`/api/v1/workspace/${projectId}/roles`, {
|
apiRequest.post(`/api/v1/workspace/${projectSlug}/roles`, dto),
|
||||||
...dto,
|
onSuccess: (_, { projectSlug }) => {
|
||||||
permissions: permissions.length ? packRules(permissions) : []
|
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||||
}),
|
|
||||||
onSuccess: (_, { projectId }) => {
|
|
||||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -32,13 +29,10 @@ export const useUpdateProjectRole = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, projectId, permissions, ...dto }: TUpdateProjectRoleDTO) =>
|
mutationFn: ({ id, projectSlug, ...dto }: TUpdateProjectRoleDTO) =>
|
||||||
apiRequest.patch(`/api/v1/workspace/${projectId}/roles/${id}`, {
|
apiRequest.patch(`/api/v1/workspace/${projectSlug}/roles/${id}`, dto),
|
||||||
...dto,
|
onSuccess: (_, { projectSlug }) => {
|
||||||
permissions: permissions?.length ? packRules(permissions) : []
|
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||||
}),
|
|
||||||
onSuccess: (_, { projectId }) => {
|
|
||||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -47,12 +41,10 @@ export const useDeleteProjectRole = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ projectId, id }: TDeleteProjectRoleDTO) =>
|
mutationFn: ({ projectSlug, id }: TDeleteProjectRoleDTO) =>
|
||||||
apiRequest.delete(`/api/v1/workspace/${projectId}/roles/${id}`, {
|
apiRequest.delete(`/api/v1/workspace/${projectSlug}/roles/${id}`),
|
||||||
data: { projectId }
|
onSuccess: (_, { projectSlug }) => {
|
||||||
}),
|
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectSlug));
|
||||||
onSuccess: (_, { projectId }) => {
|
|
||||||
queryClient.invalidateQueries(roleQueryKeys.getProjectRoles(projectId));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -14,7 +14,6 @@ import {
|
|||||||
TGetUserProjectPermissionDTO,
|
TGetUserProjectPermissionDTO,
|
||||||
TOrgRole,
|
TOrgRole,
|
||||||
TPermission,
|
TPermission,
|
||||||
TProjectPermission,
|
|
||||||
TProjectRole
|
TProjectRole
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -37,7 +36,9 @@ const glob: JsInterpreter<FieldCondition<string>> = (node, object, context) => {
|
|||||||
const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
const conditionsMatcher = buildMongoQueryMatcher({ $glob }, { glob });
|
||||||
|
|
||||||
export const roleQueryKeys = {
|
export const roleQueryKeys = {
|
||||||
getProjectRoles: (projectId: string) => ["roles", { projectId }] as const,
|
getProjectRoles: (projectSlug: string) => ["roles", { projectSlug }] as const,
|
||||||
|
getProjectRoleBySlug: (projectSlug: string, roleSlug: string) =>
|
||||||
|
["roles", { projectSlug, roleSlug }] as const,
|
||||||
getOrgRoles: (orgId: string) => ["org-roles", { orgId }] as const,
|
getOrgRoles: (orgId: string) => ["org-roles", { orgId }] as const,
|
||||||
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
|
getUserOrgPermissions: ({ orgId }: TGetUserOrgPermissionsDTO) =>
|
||||||
["user-permissions", { orgId }] as const,
|
["user-permissions", { orgId }] as const,
|
||||||
@@ -46,20 +47,29 @@ export const roleQueryKeys = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getProjectRoles = async (projectId: string) => {
|
const getProjectRoles = async (projectId: string) => {
|
||||||
const { data } = await apiRequest.get<{
|
const { data } = await apiRequest.get<{ roles: Array<Omit<TProjectRole, "permissions">> }>(
|
||||||
data: { roles: Array<Omit<TProjectRole, "permissions"> & { permissions: unknown }> };
|
`/api/v1/workspace/${projectId}/roles`
|
||||||
}>(`/api/v1/workspace/${projectId}/roles`);
|
);
|
||||||
return data.data.roles.map(({ permissions, ...el }) => ({
|
return data.roles;
|
||||||
...el,
|
|
||||||
permissions: unpackRules(permissions as PackRule<TProjectPermission>[])
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetProjectRoles = (projectId: string) =>
|
export const useGetProjectRoles = (projectSlug: string) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: roleQueryKeys.getProjectRoles(projectId),
|
queryKey: roleQueryKeys.getProjectRoles(projectSlug),
|
||||||
queryFn: () => getProjectRoles(projectId),
|
queryFn: () => getProjectRoles(projectSlug),
|
||||||
enabled: Boolean(projectId)
|
enabled: Boolean(projectSlug)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useGetProjectRoleBySlug = (projectSlug: string, roleSlug: string) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: roleQueryKeys.getProjectRoleBySlug(projectSlug, roleSlug),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{ role: TProjectRole }>(
|
||||||
|
`/api/v1/workspace/${projectSlug}/roles/slug/${roleSlug}`
|
||||||
|
);
|
||||||
|
return data.role;
|
||||||
|
},
|
||||||
|
enabled: Boolean(projectSlug && roleSlug)
|
||||||
});
|
});
|
||||||
|
|
||||||
const getOrgRoles = async (orgId: string) => {
|
const getOrgRoles = async (orgId: string) => {
|
||||||
|
@@ -71,7 +71,7 @@ export type TDeleteOrgRoleDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TCreateProjectRoleDTO = {
|
export type TCreateProjectRoleDTO = {
|
||||||
projectId: string;
|
projectSlug: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -79,11 +79,11 @@ export type TCreateProjectRoleDTO = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TUpdateProjectRoleDTO = {
|
export type TUpdateProjectRoleDTO = {
|
||||||
projectId: string;
|
projectSlug: string;
|
||||||
id: string;
|
id: string;
|
||||||
} & Partial<Omit<TCreateProjectRoleDTO, "orgId">>;
|
} & Partial<Omit<TCreateProjectRoleDTO, "orgId">>;
|
||||||
|
|
||||||
export type TDeleteProjectRoleDTO = {
|
export type TDeleteProjectRoleDTO = {
|
||||||
projectId: string;
|
projectSlug: string;
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
3
frontend/src/hooks/api/secretSharing/index.ts
Normal file
3
frontend/src/hooks/api/secretSharing/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./mutations";
|
||||||
|
export * from "./queries";
|
||||||
|
export * from "./types";
|
35
frontend/src/hooks/api/secretSharing/mutations.ts
Normal file
35
frontend/src/hooks/api/secretSharing/mutations.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TCreateSharedSecretRequest, TDeleteSharedSecretRequest, TSharedSecret } from "./types";
|
||||||
|
|
||||||
|
export const useCreateSharedSecret = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||||
|
const { data } = await apiRequest.post<TSharedSecret>("/api/v1/secret-sharing", inputData);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => queryClient.invalidateQueries(["sharedSecrets"])
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteSharedSecret = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<
|
||||||
|
TSharedSecret,
|
||||||
|
{ message: string },
|
||||||
|
{ sharedSecretId: string }
|
||||||
|
>({
|
||||||
|
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => {
|
||||||
|
const { data } = await apiRequest.delete<TSharedSecret>(
|
||||||
|
`/api/v1/secret-sharing/${sharedSecretId}`
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries(["sharedSecrets"]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
34
frontend/src/hooks/api/secretSharing/queries.ts
Normal file
34
frontend/src/hooks/api/secretSharing/queries.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TSharedSecret, TViewSharedSecretResponse } from "./types";
|
||||||
|
|
||||||
|
export const useGetSharedSecrets = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["sharedSecrets"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TSharedSecret[]>(
|
||||||
|
"/api/v1/secret-sharing/"
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetActiveSharedSecretByIdAndHashedHex = (id: string, hashedHex: string) => {
|
||||||
|
return useQuery<TViewSharedSecretResponse, [string]>({
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TViewSharedSecretResponse>(
|
||||||
|
`/api/v1/secret-sharing/public/${id}?hashedHex=${hashedHex}`
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
name: data.name,
|
||||||
|
encryptedValue: data.encryptedValue,
|
||||||
|
iv: data.iv,
|
||||||
|
tag: data.tag,
|
||||||
|
expiresAt: data.expiresAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
33
frontend/src/hooks/api/secretSharing/types.ts
Normal file
33
frontend/src/hooks/api/secretSharing/types.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export type TSharedSecret = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
encryptedValue: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
hashedHex: string;
|
||||||
|
userId: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateSharedSecretRequest = {
|
||||||
|
name: string;
|
||||||
|
encryptedValue: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
hashedHex: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TViewSharedSecretResponse = {
|
||||||
|
name: string;
|
||||||
|
encryptedValue: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteSharedSecretRequest = {
|
||||||
|
sharedSecretId: string;
|
||||||
|
};
|
@@ -630,6 +630,18 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link href={`/org/${currentOrg?.id}/secret-sharing`} passHref>
|
||||||
|
<a>
|
||||||
|
<MenuItem
|
||||||
|
isSelected={
|
||||||
|
router.asPath === `/org/${currentOrg?.id}/secret-sharing`
|
||||||
|
}
|
||||||
|
icon="system-outline-90-lock-closed"
|
||||||
|
>
|
||||||
|
Secret Sharing
|
||||||
|
</MenuItem>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
{(window.location.origin.includes("https://app.infisical.com") ||
|
{(window.location.origin.includes("https://app.infisical.com") ||
|
||||||
window.location.origin.includes("https://gamma.infisical.com")) && (
|
window.location.origin.includes("https://gamma.infisical.com")) && (
|
||||||
<Link href={`/org/${currentOrg?.id}/billing`} passHref>
|
<Link href={`/org/${currentOrg?.id}/billing`} passHref>
|
||||||
|
@@ -169,12 +169,12 @@ export default function AWSSecretManagerCreateIntegrationPage() {
|
|||||||
mappingBehavior: selectedMappingBehavior
|
mappingBehavior: selectedMappingBehavior
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setTargetSecretNameErrorText("");
|
setTargetSecretNameErrorText("");
|
||||||
|
|
||||||
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
router.push(`/integrations/${localStorage.getItem("projectData.id")}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
setIsLoading(false);
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
27
frontend/src/pages/org/[id]/secret-sharing/index.tsx
Normal file
27
frontend/src/pages/org/[id]/secret-sharing/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
|
import { ShareSecretPage } from "@app/views/ShareSecretPage";
|
||||||
|
|
||||||
|
const SecretApproval = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{t("common.head-title", { title: t("approval.title") })}</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
|
<meta property="og:title" content={String(t("approval.og-title"))} />
|
||||||
|
<meta name="og:description" content={String(t("approval.og-description"))} />
|
||||||
|
</Head>
|
||||||
|
<div className="h-full">
|
||||||
|
<ShareSecretPage />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecretApproval;
|
||||||
|
|
||||||
|
SecretApproval.requireAuth = true;
|
24
frontend/src/pages/shared/secret/[id]/index.tsx
Normal file
24
frontend/src/pages/shared/secret/[id]/index.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import Head from "next/head";
|
||||||
|
|
||||||
|
import { ShareSecretPublicPage } from "@app/views/ShareSecretPublicPage";
|
||||||
|
|
||||||
|
const SecretApproval = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Securely Share Secrets | Infisical</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
|
<meta property="og:title" content="" />
|
||||||
|
<meta name="og:description" content="" />
|
||||||
|
</Head>
|
||||||
|
<div className="h-full">
|
||||||
|
<ShareSecretPublicPage />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SecretApproval;
|
||||||
|
|
||||||
|
SecretApproval.requireAuth = false;
|
@@ -105,8 +105,18 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
if (err.response.data.error === "User Locked") {
|
||||||
|
createNotification({
|
||||||
|
title: err.response.data.error,
|
||||||
|
text: err.response.data.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setLoginError(true);
|
setLoginError(true);
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Login unsuccessful. Double-check your credentials and try again.",
|
text: "Login unsuccessful. Double-check your credentials and try again.",
|
||||||
|
@@ -5,7 +5,7 @@ import { useRouter } from "next/router";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import jwt_decode from "jwt-decode";
|
import jwt_decode from "jwt-decode";
|
||||||
|
|
||||||
import Error from "@app/components/basic/Error"; // which to notification
|
import Error from "@app/components/basic/Error";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa";
|
import attemptCliLoginMfa from "@app/components/utilities/attemptCliLoginMfa";
|
||||||
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
|
import attemptLoginMfa from "@app/components/utilities/attemptLoginMfa";
|
||||||
@@ -46,20 +46,7 @@ type Props = {
|
|||||||
callbackPort?: string | null;
|
callbackPort?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface VerifyMfaTokenError {
|
|
||||||
response: {
|
|
||||||
data: {
|
|
||||||
context: {
|
|
||||||
code: string;
|
|
||||||
triesLeft: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
status: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
const [isLoadingResend, setIsLoadingResend] = useState(false);
|
||||||
@@ -178,20 +165,31 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
const error = err as VerifyMfaTokenError;
|
if (err.response.data.error === "User Locked") {
|
||||||
|
createNotification({
|
||||||
|
title: err.response.data.error,
|
||||||
|
text: err.response.data.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Failed to log in",
|
text: "Failed to log in",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error?.response?.status === 500) {
|
if (triesLeft) {
|
||||||
window.location.reload();
|
setTriesLeft((left) => {
|
||||||
} else if (error?.response?.data?.context?.triesLeft) {
|
if (triesLeft === 1) {
|
||||||
setTriesLeft(error?.response?.data?.context?.triesLeft);
|
router.push("/");
|
||||||
if (error.response.data.context.triesLeft === 0) {
|
}
|
||||||
window.location.reload();
|
return (left as number) - 1;
|
||||||
}
|
});
|
||||||
|
} else {
|
||||||
|
setTriesLeft(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -236,7 +234,7 @@ export const MFAStep = ({ email, password, providerAuthToken }: Props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{typeof triesLeft === "number" && (
|
{typeof triesLeft === "number" && (
|
||||||
<Error text={`${t("mfa.step2-code-error")} ${triesLeft}`} />
|
<Error text={`Invalid code. You have ${triesLeft} attempt(s) remaining.`} />
|
||||||
)}
|
)}
|
||||||
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
<div className="mx-auto mt-2 flex w-1/4 min-w-[20rem] max-w-xs flex-col items-center justify-center text-center text-sm md:max-w-md md:text-left lg:w-[19%]">
|
||||||
<div className="text-l w-full py-1 text-lg">
|
<div className="text-l w-full py-1 text-lg">
|
||||||
|
@@ -31,7 +31,6 @@ export const PasswordStep = ({
|
|||||||
setPassword,
|
setPassword,
|
||||||
setStep
|
setStep
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -146,13 +145,23 @@ export const PasswordStep = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
if (err.response.data.error === "User Locked") {
|
||||||
|
createNotification({
|
||||||
|
title: err.response.data.error,
|
||||||
|
text: err.response.data.message,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
createNotification({
|
createNotification({
|
||||||
text: "Login unsuccessful. Double-check your master password and try again.",
|
text: "Login unsuccessful. Double-check your master password and try again.",
|
||||||
type: "error"
|
type: "error"
|
||||||
});
|
});
|
||||||
console.error(err);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -5,25 +5,19 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import {
|
import { Button, FormControl, Modal, ModalContent, Select, SelectItem } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
FormControl,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
|
||||||
SelectItem} from "@app/components/v2";
|
|
||||||
import { useOrganization, useWorkspace } from "@app/context";
|
import { useOrganization, useWorkspace } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddGroupToWorkspace,
|
useAddGroupToWorkspace,
|
||||||
useGetOrganizationGroups,
|
useGetOrganizationGroups,
|
||||||
useGetProjectRoles,
|
useGetProjectRoles,
|
||||||
useListWorkspaceGroups,
|
useListWorkspaceGroups
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
slug: z.string(),
|
slug: z.string(),
|
||||||
role: z.string()
|
role: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
@@ -33,150 +27,146 @@ type Props = {
|
|||||||
handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void;
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["group"]>, state?: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GroupModal = ({
|
export const GroupModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
popUp,
|
const { currentOrg } = useOrganization();
|
||||||
handlePopUpToggle
|
const { currentWorkspace } = useWorkspace();
|
||||||
}: Props) => {
|
|
||||||
const { currentOrg } = useOrganization();
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
|
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
|
|
||||||
const { data: groups } = useGetOrganizationGroups(orgId);
|
|
||||||
const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || "");
|
|
||||||
|
|
||||||
const { data: roles } = useGetProjectRoles(workspaceId);
|
|
||||||
|
|
||||||
const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace();
|
|
||||||
|
|
||||||
const filteredGroupMembershipOrgs = useMemo(() => {
|
|
||||||
const wsGroupIds = new Map();
|
|
||||||
|
|
||||||
groupMemberships?.forEach((groupMembership) => {
|
const { data: groups } = useGetOrganizationGroups(orgId);
|
||||||
wsGroupIds.set(groupMembership.group.id, true);
|
const { data: groupMemberships } = useListWorkspaceGroups(currentWorkspace?.slug || "");
|
||||||
});
|
|
||||||
|
|
||||||
return (groups || []).filter(({ id }) => !wsGroupIds.has(id));
|
const { data: roles } = useGetProjectRoles(projectSlug);
|
||||||
}, [groups, groupMemberships]);
|
|
||||||
|
const { mutateAsync: addGroupToWorkspaceMutateAsync } = useAddGroupToWorkspace();
|
||||||
const {
|
|
||||||
control,
|
const filteredGroupMembershipOrgs = useMemo(() => {
|
||||||
handleSubmit,
|
const wsGroupIds = new Map();
|
||||||
reset,
|
|
||||||
formState: { isSubmitting }
|
groupMemberships?.forEach((groupMembership) => {
|
||||||
} = useForm<FormData>({
|
wsGroupIds.set(groupMembership.group.id, true);
|
||||||
resolver: zodResolver(schema)
|
});
|
||||||
|
|
||||||
|
return (groups || []).filter(({ id }) => !wsGroupIds.has(id));
|
||||||
|
}, [groups, groupMemberships]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ slug, role }: FormData) => {
|
||||||
|
try {
|
||||||
|
await addGroupToWorkspaceMutateAsync({
|
||||||
|
projectSlug: currentWorkspace?.slug || "",
|
||||||
|
groupSlug: slug,
|
||||||
|
role: role || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const onFormSubmit = async ({ slug, role }: FormData) => {
|
reset();
|
||||||
try {
|
handlePopUpToggle("group", false);
|
||||||
await addGroupToWorkspaceMutateAsync({
|
|
||||||
projectSlug: currentWorkspace?.slug || "",
|
createNotification({
|
||||||
groupSlug: slug,
|
text: "Successfully added group to project",
|
||||||
role: role || undefined
|
type: "success"
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
reset();
|
createNotification({
|
||||||
handlePopUpToggle("group", false);
|
text: "Failed to add group to project",
|
||||||
|
type: "error"
|
||||||
createNotification({
|
});
|
||||||
text: "Successfully added group to project",
|
|
||||||
type: "success"
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
createNotification({
|
|
||||||
text: "Failed to add group to project",
|
|
||||||
type: "error"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
return (
|
|
||||||
<Modal
|
return (
|
||||||
isOpen={popUp?.group?.isOpen}
|
<Modal
|
||||||
onOpenChange={(isOpen) => {
|
isOpen={popUp?.group?.isOpen}
|
||||||
handlePopUpToggle("group", isOpen);
|
onOpenChange={(isOpen) => {
|
||||||
reset();
|
handlePopUpToggle("group", isOpen);
|
||||||
}}
|
reset();
|
||||||
>
|
}}
|
||||||
<ModalContent title="Add Group to Project">
|
>
|
||||||
{filteredGroupMembershipOrgs.length ? (
|
<ModalContent title="Add Group to Project">
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
{filteredGroupMembershipOrgs.length ? (
|
||||||
<Controller
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
control={control}
|
<Controller
|
||||||
name="slug"
|
control={control}
|
||||||
defaultValue={filteredGroupMembershipOrgs?.[0]?.id}
|
name="slug"
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
defaultValue={filteredGroupMembershipOrgs?.[0]?.id}
|
||||||
<FormControl label="Group" errorText={error?.message} isError={Boolean(error)}>
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
<Select
|
<FormControl label="Group" errorText={error?.message} isError={Boolean(error)}>
|
||||||
defaultValue={field.value}
|
<Select
|
||||||
{...field}
|
defaultValue={field.value}
|
||||||
onValueChange={(e) => onChange(e)}
|
{...field}
|
||||||
className="w-full"
|
onValueChange={(e) => onChange(e)}
|
||||||
>
|
className="w-full"
|
||||||
{filteredGroupMembershipOrgs.map(({ name, slug, id }) => (
|
>
|
||||||
<SelectItem value={slug} key={`org-group-${id}`}>
|
{filteredGroupMembershipOrgs.map(({ name, slug, id }) => (
|
||||||
{name}
|
<SelectItem value={slug} key={`org-group-${id}`}>
|
||||||
</SelectItem>
|
{name}
|
||||||
))}
|
</SelectItem>
|
||||||
</Select>
|
))}
|
||||||
</FormControl>
|
</Select>
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
)}
|
||||||
<Controller
|
/>
|
||||||
control={control}
|
<Controller
|
||||||
name="role"
|
control={control}
|
||||||
defaultValue=""
|
name="role"
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
defaultValue=""
|
||||||
<FormControl
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
label="Role"
|
<FormControl
|
||||||
errorText={error?.message}
|
label="Role"
|
||||||
isError={Boolean(error)}
|
errorText={error?.message}
|
||||||
className="mt-4"
|
isError={Boolean(error)}
|
||||||
>
|
className="mt-4"
|
||||||
<Select
|
>
|
||||||
defaultValue={field.value}
|
<Select
|
||||||
{...field}
|
defaultValue={field.value}
|
||||||
onValueChange={(e) => onChange(e)}
|
{...field}
|
||||||
className="w-full"
|
onValueChange={(e) => onChange(e)}
|
||||||
>
|
className="w-full"
|
||||||
{(roles || []).map(({ name, slug }) => (
|
>
|
||||||
<SelectItem value={slug} key={`st-role-${slug}`}>
|
{(roles || []).map(({ name, slug }) => (
|
||||||
{name}
|
<SelectItem value={slug} key={`st-role-${slug}`}>
|
||||||
</SelectItem>
|
{name}
|
||||||
))}
|
</SelectItem>
|
||||||
</Select>
|
))}
|
||||||
</FormControl>
|
</Select>
|
||||||
)}
|
</FormControl>
|
||||||
/>
|
)}
|
||||||
<div className="flex items-center">
|
/>
|
||||||
<Button
|
<div className="flex items-center">
|
||||||
className="mr-4"
|
<Button
|
||||||
size="sm"
|
className="mr-4"
|
||||||
type="submit"
|
size="sm"
|
||||||
isLoading={isSubmitting}
|
type="submit"
|
||||||
isDisabled={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
>
|
isDisabled={isSubmitting}
|
||||||
{popUp?.group?.data ? "Update" : "Create"}
|
>
|
||||||
</Button>
|
{popUp?.group?.data ? "Update" : "Create"}
|
||||||
<Button colorSchema="secondary" variant="plain">
|
</Button>
|
||||||
Cancel
|
<Button colorSchema="secondary" variant="plain">
|
||||||
</Button>
|
Cancel
|
||||||
</div>
|
</Button>
|
||||||
</form>
|
</div>
|
||||||
) : (
|
</form>
|
||||||
<div className="flex flex-col space-y-4">
|
) : (
|
||||||
<div className="text-sm">
|
<div className="flex flex-col space-y-4">
|
||||||
All groups in your organization have already been added to this project.
|
<div className="text-sm">
|
||||||
</div>
|
All groups in your organization have already been added to this project.
|
||||||
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
|
</div>
|
||||||
<Button variant="outline_bg">Create a new group</Button>
|
<Link href={`/org/${currentWorkspace?.orgId}/members`}>
|
||||||
</Link>
|
<Button variant="outline_bg">Create a new group</Button>
|
||||||
</div>
|
</Link>
|
||||||
)}
|
</div>
|
||||||
</ModalContent>
|
)}
|
||||||
</Modal>
|
</ModalContent>
|
||||||
);
|
</Modal>
|
||||||
}
|
);
|
||||||
|
};
|
||||||
|
@@ -201,11 +201,7 @@ export type TMemberRolesProp = {
|
|||||||
|
|
||||||
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
const MAX_ROLES_TO_BE_SHOWN_IN_TABLE = 2;
|
||||||
|
|
||||||
export const GroupRoles = ({
|
export const GroupRoles = ({ roles = [], disableEdit = false, groupSlug }: TMemberRolesProp) => {
|
||||||
roles = [],
|
|
||||||
disableEdit = false,
|
|
||||||
groupSlug
|
|
||||||
}: TMemberRolesProp) => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const);
|
const { popUp, handlePopUpToggle } = usePopUp(["editRole"] as const);
|
||||||
const [searchRoles, setSearchRoles] = useState("");
|
const [searchRoles, setSearchRoles] = useState("");
|
||||||
@@ -220,9 +216,9 @@ export const GroupRoles = ({
|
|||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
|
|
||||||
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
|
||||||
const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role);
|
const userRolesGroupBySlug = groupBy(roles, ({ customRoleSlug, role }) => customRoleSlug || role);
|
||||||
|
|
||||||
const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole();
|
const updateGroupWorkspaceRole = useUpdateGroupWorkspaceRole();
|
||||||
@@ -317,7 +313,7 @@ export const GroupRoles = ({
|
|||||||
icon={faClock}
|
icon={faClock}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
new Date() > new Date(temporaryAccessEndTime as string) &&
|
new Date() > new Date(temporaryAccessEndTime as string) &&
|
||||||
"text-red-600"
|
"text-red-600"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -390,14 +386,14 @@ export const GroupRoles = ({
|
|||||||
defaultValue={
|
defaultValue={
|
||||||
userProjectRoleDetails?.isTemporary
|
userProjectRoleDetails?.isTemporary
|
||||||
? {
|
? {
|
||||||
isTemporary: true,
|
isTemporary: true,
|
||||||
temporaryAccessStartTime:
|
temporaryAccessStartTime:
|
||||||
userProjectRoleDetails.temporaryAccessStartTime as string,
|
userProjectRoleDetails.temporaryAccessStartTime as string,
|
||||||
temporaryRange:
|
temporaryRange:
|
||||||
userProjectRoleDetails.temporaryRange as string,
|
userProjectRoleDetails.temporaryRange as string,
|
||||||
temporaryAccessEndTime:
|
temporaryAccessEndTime:
|
||||||
userProjectRoleDetails.temporaryAccessEndTime
|
userProjectRoleDetails.temporaryAccessEndTime
|
||||||
}
|
}
|
||||||
: false
|
: false
|
||||||
}
|
}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
|
@@ -30,17 +30,17 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
|
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
|
|
||||||
const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId);
|
const { data: identityMembershipOrgs } = useGetIdentityMembershipOrgs(orgId);
|
||||||
const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId);
|
const { data: identityMemberships } = useGetWorkspaceIdentityMemberships(workspaceId);
|
||||||
|
|
||||||
const { data: roles } = useGetProjectRoles(workspaceId);
|
const { data: roles } = useGetProjectRoles(projectSlug);
|
||||||
|
|
||||||
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
|
const { mutateAsync: addIdentityToWorkspaceMutateAsync } = useAddIdentityToWorkspace();
|
||||||
|
|
||||||
|
@@ -65,7 +65,8 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
|
|||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
|
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const isMemberEditDisabled = permission.cannot(
|
const isMemberEditDisabled = permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionActions.Edit,
|
||||||
@@ -79,14 +80,14 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
|
|||||||
slug: customRoleSlug || role,
|
slug: customRoleSlug || role,
|
||||||
temporaryAccess: dto.isTemporary
|
temporaryAccess: dto.isTemporary
|
||||||
? {
|
? {
|
||||||
isTemporary: true,
|
isTemporary: true,
|
||||||
temporaryRange: dto.temporaryRange,
|
temporaryRange: dto.temporaryRange,
|
||||||
temporaryAccessEndTime: dto.temporaryAccessEndTime,
|
temporaryAccessEndTime: dto.temporaryAccessEndTime,
|
||||||
temporaryAccessStartTime: dto.temporaryAccessStartTime
|
temporaryAccessStartTime: dto.temporaryAccessStartTime
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
isTemporary: dto.isTemporary
|
isTemporary: dto.isTemporary
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -191,9 +192,9 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
|
|||||||
? isExpired
|
? isExpired
|
||||||
? "Timed Access Expired"
|
? "Timed Access Expired"
|
||||||
: `Until ${format(
|
: `Until ${format(
|
||||||
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
||||||
"yyyy-MM-dd HH:mm:ss"
|
"yyyy-MM-dd HH:mm:ss"
|
||||||
)}`
|
)}`
|
||||||
: "Non expiry access"
|
: "Non expiry access"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -212,9 +213,9 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
|
|||||||
? isExpired
|
? isExpired
|
||||||
? "Access Expired"
|
? "Access Expired"
|
||||||
: formatDistance(
|
: formatDistance(
|
||||||
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
||||||
new Date()
|
new Date()
|
||||||
)
|
)
|
||||||
: "Permanent"}
|
: "Permanent"}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -338,7 +339,7 @@ export const IdentityRbacSection = ({ identityProjectMember, onOpenUpgradeModal
|
|||||||
type="submit"
|
type="submit"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"transition-all",
|
"transition-all",
|
||||||
"opacity-0 cursor-default",
|
"cursor-default opacity-0",
|
||||||
roleForm.formState.isDirty && "cursor-pointer opacity-100"
|
roleForm.formState.isDirty && "cursor-pointer opacity-100"
|
||||||
)}
|
)}
|
||||||
isDisabled={!roleForm.formState.isDirty}
|
isDisabled={!roleForm.formState.isDirty}
|
||||||
|
@@ -131,20 +131,17 @@ const SpecificPrivilegeSecretForm = ({
|
|||||||
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
|
{ action: ProjectPermissionActions.Delete, allowed: data.delete },
|
||||||
{ action: ProjectPermissionActions.Edit, allowed: data.edit }
|
{ action: ProjectPermissionActions.Edit, allowed: data.edit }
|
||||||
];
|
];
|
||||||
const conditions: Record<string, any> = { environment: data.environmentSlug };
|
|
||||||
if (data.secretPath) {
|
|
||||||
conditions.secretPath = { $glob: data.secretPath };
|
|
||||||
}
|
|
||||||
await updateIdentityPrivilege.mutateAsync({
|
await updateIdentityPrivilege.mutateAsync({
|
||||||
privilegeDetails: {
|
privilegeDetails: {
|
||||||
...data.temporaryAccess,
|
...data.temporaryAccess,
|
||||||
permissions: actions
|
privilegePermission: {
|
||||||
.filter(({ allowed }) => allowed)
|
actions: actions.filter(({ allowed }) => allowed).map(({ action }) => action),
|
||||||
.map(({ action }) => ({
|
subject: ProjectPermissionSub.Secrets,
|
||||||
action,
|
conditions: {
|
||||||
subject: ProjectPermissionSub.Secrets,
|
environment: data.environmentSlug,
|
||||||
conditions
|
...(data.secretPath ? { secretPath: { $glob: data.secretPath } } : {})
|
||||||
}))
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
privilegeSlug: privilege.slug,
|
privilegeSlug: privilege.slug,
|
||||||
identityId,
|
identityId,
|
||||||
@@ -474,15 +471,13 @@ export const SpecificPrivilegeSection = ({ identityId }: Props) => {
|
|||||||
if (createIdentityPrivilege.isLoading) return;
|
if (createIdentityPrivilege.isLoading) return;
|
||||||
try {
|
try {
|
||||||
await createIdentityPrivilege.mutateAsync({
|
await createIdentityPrivilege.mutateAsync({
|
||||||
permissions: [
|
privilegePermission: {
|
||||||
{
|
actions: [ProjectPermissionActions.Read],
|
||||||
action: ProjectPermissionActions.Read,
|
subject: ProjectPermissionSub.Secrets,
|
||||||
subject: ProjectPermissionSub.Secrets,
|
conditions: {
|
||||||
conditions: {
|
environment: currentWorkspace?.environments?.[0].slug as string
|
||||||
environment: currentWorkspace?.environments?.[0].slug
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
},
|
||||||
identityId,
|
identityId,
|
||||||
projectSlug
|
projectSlug
|
||||||
});
|
});
|
||||||
|
@@ -65,7 +65,8 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
|
|||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
|
const { data: projectRoles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const isMemberEditDisabled = permission.cannot(
|
const isMemberEditDisabled = permission.cannot(
|
||||||
ProjectPermissionActions.Edit,
|
ProjectPermissionActions.Edit,
|
||||||
@@ -79,14 +80,14 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
|
|||||||
slug: customRoleSlug || role,
|
slug: customRoleSlug || role,
|
||||||
temporaryAccess: dto.isTemporary
|
temporaryAccess: dto.isTemporary
|
||||||
? {
|
? {
|
||||||
isTemporary: true,
|
isTemporary: true,
|
||||||
temporaryRange: dto.temporaryRange,
|
temporaryRange: dto.temporaryRange,
|
||||||
temporaryAccessEndTime: dto.temporaryAccessEndTime,
|
temporaryAccessEndTime: dto.temporaryAccessEndTime,
|
||||||
temporaryAccessStartTime: dto.temporaryAccessStartTime
|
temporaryAccessStartTime: dto.temporaryAccessStartTime
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
isTemporary: dto.isTemporary
|
isTemporary: dto.isTemporary
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -191,9 +192,9 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
|
|||||||
? isExpired
|
? isExpired
|
||||||
? "Timed Access Expired"
|
? "Timed Access Expired"
|
||||||
: `Until ${format(
|
: `Until ${format(
|
||||||
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
||||||
"yyyy-MM-dd HH:mm:ss"
|
"yyyy-MM-dd HH:mm:ss"
|
||||||
)}`
|
)}`
|
||||||
: "Non expiry access"
|
: "Non expiry access"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -212,9 +213,9 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
|
|||||||
? isExpired
|
? isExpired
|
||||||
? "Access Expired"
|
? "Access Expired"
|
||||||
: formatDistance(
|
: formatDistance(
|
||||||
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
new Date(temporaryAccess.temporaryAccessEndTime || ""),
|
||||||
new Date()
|
new Date()
|
||||||
)
|
)
|
||||||
: "Permanent"}
|
: "Permanent"}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -335,7 +336,7 @@ export const MemberRbacSection = ({ projectMember, onOpenUpgradeModal }: Props)
|
|||||||
type="submit"
|
type="submit"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"transition-all",
|
"transition-all",
|
||||||
"opacity-0 cursor-default",
|
"cursor-default opacity-0",
|
||||||
roleForm.formState.isDirty && "cursor-pointer opacity-100"
|
roleForm.formState.isDirty && "cursor-pointer opacity-100"
|
||||||
)}
|
)}
|
||||||
isDisabled={!roleForm.formState.isDirty}
|
isDisabled={!roleForm.formState.isDirty}
|
||||||
|
@@ -3,7 +3,6 @@ import { motion } from "framer-motion";
|
|||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { withProjectPermission } from "@app/hoc";
|
import { withProjectPermission } from "@app/hoc";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
|
||||||
|
|
||||||
import { ProjectRoleList } from "./components/ProjectRoleList";
|
import { ProjectRoleList } from "./components/ProjectRoleList";
|
||||||
import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection";
|
import { ProjectRoleModifySection } from "./components/ProjectRoleModifySection";
|
||||||
@@ -21,7 +20,7 @@ export const ProjectRoleListTab = withProjectPermission(
|
|||||||
exit={{ opacity: 0, translateX: 30 }}
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
>
|
>
|
||||||
<ProjectRoleModifySection
|
<ProjectRoleModifySection
|
||||||
role={popUp.editRole.data as TProjectRole}
|
roleSlug={popUp.editRole.data as string}
|
||||||
onGoBack={() => handlePopUpClose("editRole")}
|
onGoBack={() => handlePopUpClose("editRole")}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -33,7 +32,7 @@ export const ProjectRoleListTab = withProjectPermission(
|
|||||||
animate={{ opacity: 1, translateX: 0 }}
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
exit={{ opacity: 0, translateX: -30 }}
|
exit={{ opacity: 0, translateX: -30 }}
|
||||||
>
|
>
|
||||||
<ProjectRoleList onSelectRole={(role) => handlePopUpOpen("editRole", role)} />
|
<ProjectRoleList onSelectRole={(slug) => handlePopUpOpen("editRole", slug)} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@@ -24,7 +24,7 @@ import { useDeleteProjectRole, useGetProjectRoles } from "@app/hooks/api";
|
|||||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSelectRole: (role?: TProjectRole) => void;
|
onSelectRole: (slug?: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
||||||
@@ -32,10 +32,9 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
|||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
|
const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp(["deleteRole"] as const);
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
|
|
||||||
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(workspaceId);
|
const { data: roles, isLoading: isRolesLoading } = useGetProjectRoles(projectSlug);
|
||||||
console.log(roles);
|
|
||||||
|
|
||||||
const { mutateAsync: deleteRole } = useDeleteProjectRole();
|
const { mutateAsync: deleteRole } = useDeleteProjectRole();
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
|||||||
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
const { id } = popUp?.deleteRole?.data as TProjectRole;
|
||||||
try {
|
try {
|
||||||
await deleteRole({
|
await deleteRole({
|
||||||
projectId: workspaceId,
|
projectSlug,
|
||||||
id
|
id
|
||||||
});
|
});
|
||||||
createNotification({ type: "success", text: "Successfully removed the role" });
|
createNotification({ type: "success", text: "Successfully removed the role" });
|
||||||
@@ -109,7 +108,7 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
isDisabled={!isAllowed}
|
isDisabled={!isAllowed}
|
||||||
ariaLabel="edit"
|
ariaLabel="edit"
|
||||||
onClick={() => onSelectRole(role)}
|
onClick={() => onSelectRole(role.slug)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faEdit} />
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
@@ -146,9 +145,8 @@ export const ProjectRoleList = ({ onSelectRole }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp.deleteRole.isOpen}
|
isOpen={popUp.deleteRole.isOpen}
|
||||||
title={`Are you sure want to delete ${
|
title={`Are you sure want to delete ${(popUp?.deleteRole?.data as TProjectRole)?.name || " "
|
||||||
(popUp?.deleteRole?.data as TProjectRole)?.name || " "
|
} role?`}
|
||||||
} role?`}
|
|
||||||
deleteKey={(popUp?.deleteRole?.data as TProjectRole)?.slug || ""}
|
deleteKey={(popUp?.deleteRole?.data as TProjectRole)?.slug || ""}
|
||||||
onClose={() => handlePopUpClose("deleteRole")}
|
onClose={() => handlePopUpClose("deleteRole")}
|
||||||
onDeleteApproved={handleRoleDelete}
|
onDeleteApproved={handleRoleDelete}
|
||||||
|
@@ -19,9 +19,13 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
import { Button, FormControl, Input, Spinner } from "@app/components/v2";
|
||||||
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { useCreateProjectRole, useUpdateProjectRole } from "@app/hooks/api";
|
import {
|
||||||
|
useCreateProjectRole,
|
||||||
|
useGetProjectRoleBySlug,
|
||||||
|
useUpdateProjectRole
|
||||||
|
} from "@app/hooks/api";
|
||||||
import { TProjectRole } from "@app/hooks/api/roles/types";
|
import { TProjectRole } from "@app/hooks/api/roles/types";
|
||||||
|
|
||||||
import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission";
|
import { MultiEnvProjectPermission } from "./MultiEnvProjectPermission";
|
||||||
@@ -117,17 +121,20 @@ const SINGLE_PERMISSION_LIST = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
role?: TProjectRole;
|
roleSlug?: string;
|
||||||
onGoBack: VoidFunction;
|
onGoBack: VoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
export const ProjectRoleModifySection = ({ roleSlug, onGoBack }: Props) => {
|
||||||
const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(role?.slug || "");
|
const isNonEditable = ["admin", "member", "viewer", "no-access"].includes(roleSlug || "");
|
||||||
const isNewRole = !role?.slug;
|
const isNewRole = !roleSlug;
|
||||||
|
|
||||||
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
|
const { data: roleDetails, isLoading: isRoleDetailsLoading } = useGetProjectRoleBySlug(
|
||||||
|
currentWorkspace?.slug || "",
|
||||||
|
roleSlug as string
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -137,19 +144,21 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
|||||||
getValues,
|
getValues,
|
||||||
control
|
control
|
||||||
} = useForm<TFormSchema>({
|
} = useForm<TFormSchema>({
|
||||||
defaultValues: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : {},
|
values: roleDetails
|
||||||
|
? { ...roleDetails, permissions: rolePermission2Form(roleDetails.permissions) }
|
||||||
|
: ({} as TProjectRole),
|
||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
});
|
});
|
||||||
const { mutateAsync: createRole } = useCreateProjectRole();
|
const { mutateAsync: createRole } = useCreateProjectRole();
|
||||||
const { mutateAsync: updateRole } = useUpdateProjectRole();
|
const { mutateAsync: updateRole } = useUpdateProjectRole();
|
||||||
|
|
||||||
const handleRoleUpdate = async (el: TFormSchema) => {
|
const handleRoleUpdate = async (el: TFormSchema) => {
|
||||||
if (!role?.id) return;
|
if (!roleDetails?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateRole({
|
await updateRole({
|
||||||
id: role?.id,
|
id: roleDetails?.id as string,
|
||||||
projectId: workspaceId,
|
projectSlug,
|
||||||
...el,
|
...el,
|
||||||
permissions: formRolePermission2API(el.permissions)
|
permissions: formRolePermission2API(el.permissions)
|
||||||
});
|
});
|
||||||
@@ -169,7 +178,7 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await createRole({
|
await createRole({
|
||||||
projectId: workspaceId,
|
projectSlug,
|
||||||
...el,
|
...el,
|
||||||
permissions: formRolePermission2API(el.permissions)
|
permissions: formRolePermission2API(el.permissions)
|
||||||
});
|
});
|
||||||
@@ -181,6 +190,14 @@ export const ProjectRoleModifySection = ({ role, onGoBack }: Props) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isNewRole && isRoleDetailsLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center justify-center p-8">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
|
@@ -95,10 +95,8 @@ export const rolePermission2Form = (permissions: TProjectPermission[] = []) => {
|
|||||||
const formVal: Record<string, any> = {};
|
const formVal: Record<string, any> = {};
|
||||||
|
|
||||||
permissions.forEach((permission) => {
|
permissions.forEach((permission) => {
|
||||||
const {
|
const { subject: caslSub, action } = permission;
|
||||||
subject: [subject],
|
const subject = typeof caslSub === "string" ? caslSub : caslSub[0];
|
||||||
action
|
|
||||||
} = permission;
|
|
||||||
if (!formVal?.[subject]) formVal[subject] = {};
|
if (!formVal?.[subject]) formVal[subject] = {};
|
||||||
|
|
||||||
if (subject === "secrets") {
|
if (subject === "secrets") {
|
||||||
@@ -123,7 +121,7 @@ const multiEnvForm2Api = (
|
|||||||
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
const isFullAccess = PERMISSION_ACTIONS.every((action) => formVal?.all?.[action]);
|
||||||
// if any of them is set in all push it without any condition
|
// if any of them is set in all push it without any condition
|
||||||
PERMISSION_ACTIONS.forEach((action) => {
|
PERMISSION_ACTIONS.forEach((action) => {
|
||||||
if (formVal?.all?.[action]) permissions.push({ action, subject: [subject] });
|
if (formVal?.all?.[action]) permissions.push({ action, subject });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isFullAccess) {
|
if (!isFullAccess) {
|
||||||
@@ -144,7 +142,7 @@ const multiEnvForm2Api = (
|
|||||||
if (formVal[slug]?.secretPath)
|
if (formVal[slug]?.secretPath)
|
||||||
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
|
conditions.secretPath = { $glob: formVal?.[slug]?.secretPath };
|
||||||
|
|
||||||
permissions.push({ action, subject: [subject], conditions });
|
permissions.push({ action, subject, conditions });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -161,7 +159,7 @@ export const formRolePermission2API = (formVal: TFormSchema["permissions"]) => {
|
|||||||
} else {
|
} else {
|
||||||
Object.entries(actions).forEach(([action, isAllowed]) => {
|
Object.entries(actions).forEach(([action, isAllowed]) => {
|
||||||
if (isAllowed) {
|
if (isAllowed) {
|
||||||
permissions.push({ subject: [rule], action });
|
permissions.push({ subject: rule, action });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
30
frontend/src/views/ShareSecretPage/ShareSecretPage.tsx
Normal file
30
frontend/src/views/ShareSecretPage/ShareSecretPage.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { ShareSecretSection } from "./components";
|
||||||
|
|
||||||
|
export const ShareSecretPage = () => {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto h-full w-full max-w-7xl bg-bunker-800 px-6 text-white">
|
||||||
|
<div className="flex items-center justify-between py-6">
|
||||||
|
<div className="flex w-full flex-col">
|
||||||
|
<h2 className="text-3xl font-semibold text-gray-200">Secret Sharing</h2>
|
||||||
|
<p className="text-bunker-300">Share secrets with anybody securely and efficiently</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-max justify-center">
|
||||||
|
<Link href="https://infisical.com/docs/documentation/platform/secret-sharing">
|
||||||
|
<span className="w-max cursor-pointer rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||||
|
Documentation{" "}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.06rem] ml-1 text-xs"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ShareSecretSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1,289 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import * as yup from "yup";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
encryptSymmetric,
|
||||||
|
} from "@app/components/utilities/cryptography/crypto";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalClose,
|
||||||
|
ModalContent,
|
||||||
|
SecretInput,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import { useTimedReset } from "@app/hooks";
|
||||||
|
import { useCreateSharedSecret } from "@app/hooks/api/secretSharing";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const expirationUnitsAndActions = [
|
||||||
|
{
|
||||||
|
unit: "Minutes",
|
||||||
|
action: (expiresAt: Date, expiresInValue: number) =>
|
||||||
|
expiresAt.setMinutes(expiresAt.getMinutes() + expiresInValue)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unit: "Hours",
|
||||||
|
action: (expiresAt: Date, expiresInValue: number) =>
|
||||||
|
expiresAt.setHours(expiresAt.getHours() + expiresInValue)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unit: "Days",
|
||||||
|
action: (expiresAt: Date, expiresInValue: number) =>
|
||||||
|
expiresAt.setDate(expiresAt.getDate() + expiresInValue)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unit: "Weeks",
|
||||||
|
action: (expiresAt: Date, expiresInValue: number) =>
|
||||||
|
expiresAt.setDate(expiresAt.getDate() + expiresInValue * 7)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unit: "Months",
|
||||||
|
action: (expiresAt: Date, expiresInValue: number) =>
|
||||||
|
expiresAt.setMonth(expiresAt.getMonth() + expiresInValue)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
unit: "Years",
|
||||||
|
action: (expiresAt: Date, expiresInValue: number) =>
|
||||||
|
expiresAt.setFullYear(expiresAt.getFullYear() + expiresInValue)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
name: yup.string().max(100).required().label("Shared Secret Name"),
|
||||||
|
value: yup.string().max(1000).required().label("Shared Secret Value"),
|
||||||
|
expiresInValue: yup.number().min(1).required().label("Expiration Value"),
|
||||||
|
expiresInUnit: yup.string().required().label("Expiration Unit")
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["createSharedSecret"]>;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<["createSharedSecret"]>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddShareSecretModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: yupResolver(schema)
|
||||||
|
});
|
||||||
|
const createSharedSecret = useCreateSharedSecret();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const [newSharedSecret, setnewSharedSecret] = useState("");
|
||||||
|
const hasSharedSecret = Boolean(newSharedSecret);
|
||||||
|
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||||
|
initialState: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const copyUrlToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(newSharedSecret);
|
||||||
|
setIsUrlCopied(true);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (isUrlCopied) {
|
||||||
|
setTimeout(() => setIsUrlCopied(false), 2000);
|
||||||
|
}
|
||||||
|
}, [isUrlCopied]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name, value, expiresInValue, expiresInUnit }: FormData) => {
|
||||||
|
try {
|
||||||
|
if (!currentOrg?.id) return;
|
||||||
|
|
||||||
|
const key = crypto.randomBytes(16).toString("hex");
|
||||||
|
const hashedHex = crypto.createHash("sha256").update(key).digest("hex");
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext: value,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const expiresAt = new Date();
|
||||||
|
const updateExpiresAt = expirationUnitsAndActions.find(
|
||||||
|
(item) => item.unit === expiresInUnit
|
||||||
|
)?.action;
|
||||||
|
if (updateExpiresAt) {
|
||||||
|
updateExpiresAt(expiresAt, expiresInValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await createSharedSecret.mutateAsync({
|
||||||
|
name,
|
||||||
|
encryptedValue: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
hashedHex,
|
||||||
|
expiresAt,
|
||||||
|
});
|
||||||
|
setnewSharedSecret(
|
||||||
|
`${window.location.origin}/shared/secret/${id}?key=${encodeURIComponent(hashedHex)}-${encodeURIComponent(key)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created a shared secret",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const axiosError = err as AxiosError;
|
||||||
|
if (axiosError?.response?.status === 401) {
|
||||||
|
createNotification({
|
||||||
|
text: "You do not have access to create shared secrets",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create a shared secret",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.createSharedSecret?.isOpen}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
handlePopUpToggle("createSharedSecret", open);
|
||||||
|
reset();
|
||||||
|
setnewSharedSecret("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Share a secret with anybody on the internet"
|
||||||
|
subTitle="When a secret is shared, you will only see the public share URL once before it disappears. Make sure to save it somewhere."
|
||||||
|
>
|
||||||
|
{!hasSharedSecret ? (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Shared Secret Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="Type your secret identifier" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="value"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Shared Secret Value"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<SecretInput
|
||||||
|
isVisible
|
||||||
|
{...field}
|
||||||
|
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2 text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex w-full flex-row justify-end">
|
||||||
|
<div className="w-3/5">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresInValue"
|
||||||
|
defaultValue={1}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Expiration Value"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} type="number" min={0} />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-2/5 pl-4">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresInUnit"
|
||||||
|
defaultValue={expirationUnitsAndActions[0].unit}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Expiration Unit"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{expirationUnitsAndActions.map(({ unit }) => (
|
||||||
|
<SelectItem value={unit} key={unit}>
|
||||||
|
{unit}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
type="submit"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button variant="plain" colorSchema="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{newSharedSecret}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={copyUrlToClipboard}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
||||||
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
Click to Copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1,84 @@
|
|||||||
|
import Head from "next/head";
|
||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
|
import { AddShareSecretModal } from "./AddShareSecretModal";
|
||||||
|
import { ShareSecretsTable } from "./ShareSecretsTable";
|
||||||
|
|
||||||
|
type DeleteModalData = { name: string; id: string };
|
||||||
|
|
||||||
|
export const ShareSecretSection = () => {
|
||||||
|
const deleteSharedSecret = useDeleteSharedSecret();
|
||||||
|
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||||
|
"createSharedSecret",
|
||||||
|
"deleteSharedSecretConfirmation"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const onDeleteApproved = async () => {
|
||||||
|
try {
|
||||||
|
deleteSharedSecret.mutateAsync({
|
||||||
|
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id,
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully deleted shared secret",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("deleteSharedSecretConfirmation");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to delete shared secret",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<Head>
|
||||||
|
<title>Secret Sharing</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
|
</Head>
|
||||||
|
<div className="mb-2 flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Shared Secrets</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("createSharedSecret");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Share Secret
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mb-8 flex items-center justify-between">
|
||||||
|
<p className="flex-grow text-gray-400">
|
||||||
|
Every secret shared can be accessed with the URL (shown during creation) before its
|
||||||
|
expiry.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ShareSecretsTable
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
/>
|
||||||
|
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteSharedSecretConfirmation.isOpen}
|
||||||
|
title={`Delete ${(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name || " "
|
||||||
|
} shared secret?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteSharedSecretConfirmation", isOpen)}
|
||||||
|
deleteKey={(popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.name}
|
||||||
|
onClose={() => handlePopUpClose("deleteSharedSecretConfirmation")}
|
||||||
|
onDeleteApproved={onDeleteApproved}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1,119 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { faTrashCan } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { IconButton, Td, Tr } from "@app/components/v2";
|
||||||
|
import { TSharedSecret } from "@app/hooks/api/secretSharing";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const formatDate = (date: Date): string => (date ? new Date(date).toUTCString() : "");
|
||||||
|
|
||||||
|
const isExpired = (expiresAt: Date): boolean => new Date(expiresAt) < new Date();
|
||||||
|
|
||||||
|
const getValidityStatusText = (expiresAt: Date): string =>
|
||||||
|
isExpired(expiresAt) ? "Expired " : "Valid for ";
|
||||||
|
|
||||||
|
const timeAgo = (inputDate: Date, currentDate: Date): string => {
|
||||||
|
const now = new Date(currentDate).getTime();
|
||||||
|
const date = new Date(inputDate).getTime();
|
||||||
|
const elapsedMilliseconds = now - date;
|
||||||
|
const elapsedSeconds = Math.abs(Math.floor(elapsedMilliseconds / 1000));
|
||||||
|
const elapsedMinutes = Math.abs(Math.floor(elapsedSeconds / 60));
|
||||||
|
const elapsedHours = Math.abs(Math.floor(elapsedMinutes / 60));
|
||||||
|
const elapsedDays = Math.abs(Math.floor(elapsedHours / 24));
|
||||||
|
const elapsedWeeks = Math.abs(Math.floor(elapsedDays / 7));
|
||||||
|
const elapsedMonths = Math.abs(Math.floor(elapsedDays / 30));
|
||||||
|
const elapsedYears = Math.abs(Math.floor(elapsedDays / 365));
|
||||||
|
|
||||||
|
if (elapsedYears > 0) {
|
||||||
|
return `${elapsedYears} year${elapsedYears === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
if (elapsedMonths > 0) {
|
||||||
|
return `${elapsedMonths} month${elapsedMonths === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
if (elapsedWeeks > 0) {
|
||||||
|
return `${elapsedWeeks} week${elapsedWeeks === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
if (elapsedDays > 0) {
|
||||||
|
return `${elapsedDays} day${elapsedDays === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
if (elapsedHours > 0) {
|
||||||
|
return `${elapsedHours} hour${elapsedHours === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
if (elapsedMinutes > 0) {
|
||||||
|
return `${elapsedMinutes} minute${elapsedMinutes === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
return `${elapsedSeconds} second${elapsedSeconds === 1 ? "" : "s"} ${elapsedMilliseconds >= 0 ? "ago" : "from now"
|
||||||
|
}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShareSecretsRow = ({
|
||||||
|
row,
|
||||||
|
handlePopUpOpen,
|
||||||
|
onSecretExpiration
|
||||||
|
}: {
|
||||||
|
row: TSharedSecret;
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["deleteSharedSecretConfirmation"]>,
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
id
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
onSecretExpiration: (expiredSecretId: string) => void;
|
||||||
|
}) => {
|
||||||
|
const [currentTime, setCurrentTime] = useState(new Date());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
setCurrentTime(new Date());
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isExpired(row.expiresAt)) {
|
||||||
|
onSecretExpiration(row.id);
|
||||||
|
}
|
||||||
|
}, [isExpired(row.expiresAt)]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={row.id}>
|
||||||
|
<Td>{row.name}</Td>
|
||||||
|
<Td>
|
||||||
|
<p className="text-sm text-yellow-400">{timeAgo(row.createdAt, currentTime)}</p>
|
||||||
|
<p className="text-xs text-gray-500">{formatDate(row.createdAt)}</p>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<p className={`text-sm ${isExpired(row.expiresAt) ? "text-red-500" : "text-green-500"}`}>
|
||||||
|
{getValidityStatusText(row.expiresAt) + timeAgo(row.expiresAt, currentTime)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-500">{formatDate(row.expiresAt)}</p>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("deleteSharedSecretConfirmation", {
|
||||||
|
name: row.name,
|
||||||
|
id: row.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
colorSchema="danger"
|
||||||
|
ariaLabel="delete"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrashCan} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1,72 @@
|
|||||||
|
import { faKey } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useGetSharedSecrets } from "@app/hooks/api/secretSharing";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { ShareSecretsRow } from "./ShareSecretsRow";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["deleteSharedSecretConfirmation"]>,
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
id
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ShareSecretsTable = ({ handlePopUpOpen }: Props) => {
|
||||||
|
const { isLoading, data = [] } = useGetSharedSecrets();
|
||||||
|
|
||||||
|
let tableData = data.filter((secret) => !secret.expiresAt || new Date(secret.expiresAt) > new Date())
|
||||||
|
const handleSecretExpiration = () => {
|
||||||
|
tableData = data.filter((secret) => !secret.expiresAt || new Date(secret.expiresAt) > new Date());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Secret Name</Th> <Th>Created</Th> <Th>Valid Until</Th>
|
||||||
|
<Th aria-label="button" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={4} innerKey="shared-secrets" />}
|
||||||
|
{!isLoading &&
|
||||||
|
tableData &&
|
||||||
|
tableData.map((row) => (
|
||||||
|
<ShareSecretsRow
|
||||||
|
key={row.id}
|
||||||
|
row={row}
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
onSecretExpiration={handleSecretExpiration}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!isLoading && tableData && tableData?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
||||||
|
<EmptyState title="No secrets shared currently" icon={faKey} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
1
frontend/src/views/ShareSecretPage/components/index.tsx
Normal file
1
frontend/src/views/ShareSecretPage/components/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ShareSecretSection } from "./ShareSecretSection";
|
1
frontend/src/views/ShareSecretPage/index.tsx
Normal file
1
frontend/src/views/ShareSecretPage/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ShareSecretPage } from "./ShareSecretPage";
|
@@ -0,0 +1,120 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import Head from "next/head";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { decryptSymmetric } from "@app/components/utilities/cryptography/crypto";
|
||||||
|
import { useTimedReset } from "@app/hooks";
|
||||||
|
import { useGetActiveSharedSecretByIdAndHashedHex } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
|
import { DragonMainImage, SecretTable } from "./components";
|
||||||
|
|
||||||
|
export const ShareSecretPublicPage = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { id, key: urlEncodedPublicKey } = router.query;
|
||||||
|
const [hashedHex, key] = urlEncodedPublicKey!.toString().split("-");
|
||||||
|
|
||||||
|
const publicKey = decodeURIComponent(urlEncodedPublicKey as string);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!id || !publicKey) {
|
||||||
|
router.push("/404");
|
||||||
|
}
|
||||||
|
}, [id, publicKey]);
|
||||||
|
|
||||||
|
const { isLoading, data } = useGetActiveSharedSecretByIdAndHashedHex(id as string, hashedHex as string );
|
||||||
|
const decryptedSecret = useMemo(() => {
|
||||||
|
if (data && data.encryptedValue && publicKey) {
|
||||||
|
const res = decryptSymmetric({
|
||||||
|
ciphertext: data.encryptedValue,
|
||||||
|
iv: data.iv,
|
||||||
|
tag: data.tag,
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}, [data, publicKey]);
|
||||||
|
|
||||||
|
const [timeLeft, setTimeLeft] = useState("");
|
||||||
|
const [isUrlCopied, , setIsUrlCopied] = useTimedReset<boolean>({
|
||||||
|
initialState: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const millisecondsPerDay = 1000 * 60 * 60 * 24;
|
||||||
|
const millisecondsPerHour = 1000 * 60 * 60;
|
||||||
|
const millisecondsPerMinute = 1000 * 60;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateTimer = () => {
|
||||||
|
if (data && data.expiresAt) {
|
||||||
|
const expirationTime = new Date(data.expiresAt).getTime();
|
||||||
|
const currentTime = new Date().getTime();
|
||||||
|
const timeDifference = expirationTime - currentTime;
|
||||||
|
|
||||||
|
if (timeDifference < 0) {
|
||||||
|
setTimeLeft("Expired");
|
||||||
|
} else {
|
||||||
|
const hoursRemaining = Math.floor((timeDifference % millisecondsPerDay) / millisecondsPerHour);
|
||||||
|
const minutesRemaining = Math.floor((timeDifference % millisecondsPerHour) / millisecondsPerMinute);
|
||||||
|
const secondsRemaining = Math.floor((timeDifference % millisecondsPerMinute) / 1000);
|
||||||
|
setTimeLeft(`${hoursRemaining}h ${minutesRemaining}m ${secondsRemaining}s`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const timer = setInterval(updateTimer, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [data?.expiresAt]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isUrlCopied) {
|
||||||
|
setTimeout(() => setIsUrlCopied(false), 2000);
|
||||||
|
}
|
||||||
|
}, [isUrlCopied]);
|
||||||
|
|
||||||
|
|
||||||
|
const copyUrlToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(decryptedSecret);
|
||||||
|
setIsUrlCopied(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-between bg-bunker-800 text-gray-200 md:h-screen">
|
||||||
|
<Head>
|
||||||
|
<title>Secret Shared | Infisical</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
</Head>
|
||||||
|
|
||||||
|
<div className="my-4 flex justify-center md:my-8">
|
||||||
|
<Image src="/images/biglogo.png" height={180} width={240} alt="Infisical logo" />
|
||||||
|
</div>
|
||||||
|
<p className="mb-6 px-8 text-center text-xl md:px-0 md:text-3xl">
|
||||||
|
You’ve been shared a secret securely with Infisical.
|
||||||
|
</p>
|
||||||
|
<div className="flex min-h-screen w-full flex-col md:flex-row">
|
||||||
|
<DragonMainImage />
|
||||||
|
<div className="m-4 flex flex-1 flex-col items-center justify-start md:m-0">
|
||||||
|
<p className="mt-8 mb-2 text-xl font-semibold text-mineshaft-100 md:mt-20">
|
||||||
|
Secret Details
|
||||||
|
</p>
|
||||||
|
<div className="mb-16 rounded-lg border border-mineshaft-600 bg-mineshaft-900 md:p-8">
|
||||||
|
<SecretTable
|
||||||
|
isLoading={isLoading}
|
||||||
|
sharedSecret={data}
|
||||||
|
decryptedSecret={decryptedSecret}
|
||||||
|
timeLeft={timeLeft}
|
||||||
|
isUrlCopied={isUrlCopied}
|
||||||
|
copyUrlToClipboard={copyUrlToClipboard}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Link href="/">
|
||||||
|
<a className="mt-4 cursor-pointer rounded-md bg-mineshaft-500 py-2 px-4 text-lg font-semibold duration-200 hover:bg-primary hover:text-black">
|
||||||
|
Check Infisical out now!
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1,14 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export const DragonMainImage = () => {
|
||||||
|
return (
|
||||||
|
<div className="hidden flex-1 flex-col items-center justify-center md:block md:items-start md:p-4">
|
||||||
|
<Image
|
||||||
|
src="/images/dragon-book.svg"
|
||||||
|
height={1000}
|
||||||
|
width={1413}
|
||||||
|
alt="Infisical Dragon - Came to send you a secret!"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1,96 @@
|
|||||||
|
import { faCheck, faCopy, faKey } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
SecretInput,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isLoading: boolean;
|
||||||
|
sharedSecret?: TViewSharedSecretResponse;
|
||||||
|
decryptedSecret: string;
|
||||||
|
timeLeft: string;
|
||||||
|
isUrlCopied: boolean;
|
||||||
|
copyUrlToClipboard: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretTable = ({
|
||||||
|
isLoading,
|
||||||
|
sharedSecret,
|
||||||
|
decryptedSecret,
|
||||||
|
timeLeft,
|
||||||
|
isUrlCopied,
|
||||||
|
copyUrlToClipboard
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Value</Th>
|
||||||
|
<Th>Valid Until</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{!isLoading && sharedSecret && decryptedSecret && (
|
||||||
|
<Tr key={sharedSecret.name}>
|
||||||
|
<Td>{sharedSecret.name}</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex items-center md:space-x-2">
|
||||||
|
<div className="max-w-[20rem] flex-1 break-words">
|
||||||
|
<SecretInput
|
||||||
|
isVisible
|
||||||
|
value={decryptedSecret}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy to clipboard"
|
||||||
|
onClick={copyUrlToClipboard}
|
||||||
|
className="rounded p-2 hover:bg-gray-700"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isUrlCopied ? faCheck : faCopy} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
<Td>{timeLeft}</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{isLoading && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
||||||
|
Loading...
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{!isLoading && !sharedSecret && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
||||||
|
<EmptyState title="No such secret is shared yet!" icon={faKey} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
{!isLoading && sharedSecret && !decryptedSecret && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={4} className="bg-mineshaft-800 text-center text-bunker-400">
|
||||||
|
<EmptyState title="Invalid URL to fetch the Secret!" icon={faKey} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
@@ -0,0 +1,2 @@
|
|||||||
|
export { DragonMainImage } from "./MainImage";
|
||||||
|
export { SecretTable } from "./SecretTable";
|
1
frontend/src/views/ShareSecretPublicPage/index.tsx
Normal file
1
frontend/src/views/ShareSecretPublicPage/index.tsx
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ShareSecretPublicPage } from "./ShareSecretPublicPage";
|
@@ -1,5 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
|
update-ca-certificates
|
||||||
|
|
||||||
cd frontend-build
|
cd frontend-build
|
||||||
scripts/initialize-standalone-build.sh
|
scripts/initialize-standalone-build.sh
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user