mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 20:27:12 +00:00
feat(secret-sharing): secret requests
This commit is contained in:
@@ -0,0 +1,25 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
|
||||||
|
if (!hasSharingTypeColumn) {
|
||||||
|
table.string("type", 32).defaultTo(SecretSharingType.Share).notNullable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
const hasSharingTypeColumn = await knex.schema.hasColumn(TableName.SecretSharing, "type");
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.SecretSharing, (table) => {
|
||||||
|
if (hasSharingTypeColumn) {
|
||||||
|
table.dropColumn("type");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import { TImmutableDBKeys } from "./models";
|
|||||||
export const SecretSharingSchema = z.object({
|
export const SecretSharingSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
encryptedValue: z.string().nullable().optional(),
|
encryptedValue: z.string().nullable().optional(),
|
||||||
|
type: z.string(),
|
||||||
iv: z.string().nullable().optional(),
|
iv: z.string().nullable().optional(),
|
||||||
tag: z.string().nullable().optional(),
|
tag: z.string().nullable().optional(),
|
||||||
hashedHex: z.string().nullable().optional(),
|
hashedHex: z.string().nullable().optional(),
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ export enum EventType {
|
|||||||
UPDATE_APP_CONNECTION = "update-app-connection",
|
UPDATE_APP_CONNECTION = "update-app-connection",
|
||||||
DELETE_APP_CONNECTION = "delete-app-connection",
|
DELETE_APP_CONNECTION = "delete-app-connection",
|
||||||
CREATE_SHARED_SECRET = "create-shared-secret",
|
CREATE_SHARED_SECRET = "create-shared-secret",
|
||||||
|
CREATE_SECRET_REQUEST = "create-secret-request",
|
||||||
DELETE_SHARED_SECRET = "delete-shared-secret",
|
DELETE_SHARED_SECRET = "delete-shared-secret",
|
||||||
READ_SHARED_SECRET = "read-shared-secret",
|
READ_SHARED_SECRET = "read-shared-secret",
|
||||||
GET_SECRET_SYNCS = "get-secret-syncs",
|
GET_SECRET_SYNCS = "get-secret-syncs",
|
||||||
@@ -2020,6 +2021,15 @@ interface CreateSharedSecretEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CreateSecretRequestEvent {
|
||||||
|
type: EventType.CREATE_SECRET_REQUEST;
|
||||||
|
metadata: {
|
||||||
|
id: string;
|
||||||
|
accessType: string;
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface DeleteSharedSecretEvent {
|
interface DeleteSharedSecretEvent {
|
||||||
type: EventType.DELETE_SHARED_SECRET;
|
type: EventType.DELETE_SHARED_SECRET;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -2470,4 +2480,5 @@ export type Event =
|
|||||||
| KmipOperationActivateEvent
|
| KmipOperationActivateEvent
|
||||||
| KmipOperationRevokeEvent
|
| KmipOperationRevokeEvent
|
||||||
| KmipOperationLocateEvent
|
| KmipOperationLocateEvent
|
||||||
| KmipOperationRegisterEvent;
|
| KmipOperationRegisterEvent
|
||||||
|
| CreateSecretRequestEvent;
|
||||||
|
|||||||
@@ -1096,7 +1096,9 @@ export const registerRoutes = async (
|
|||||||
permissionService,
|
permissionService,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
smtpService,
|
||||||
|
userDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
const accessApprovalPolicyService = accessApprovalPolicyServiceFactory({
|
||||||
|
|||||||
@@ -37,6 +37,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 { registerSecretRequestsRouter } from "./secret-requests-router";
|
||||||
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
import { registerSecretSharingRouter } from "./secret-sharing-router";
|
||||||
import { registerSecretTagRouter } from "./secret-tag-router";
|
import { registerSecretTagRouter } from "./secret-tag-router";
|
||||||
import { registerSlackRouter } from "./slack-router";
|
import { registerSlackRouter } from "./slack-router";
|
||||||
@@ -110,7 +111,15 @@ 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" });
|
|
||||||
|
await server.register(
|
||||||
|
async (secretSharingRouter) => {
|
||||||
|
await secretSharingRouter.register(registerSecretSharingRouter, { prefix: "/shared" });
|
||||||
|
await secretSharingRouter.register(registerSecretRequestsRouter, { prefix: "/requests" });
|
||||||
|
},
|
||||||
|
{ prefix: "/secret-sharing" }
|
||||||
|
);
|
||||||
|
|
||||||
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
|
await server.register(registerUserEngagementRouter, { prefix: "/user-engagement" });
|
||||||
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
await server.register(registerDashboardRouter, { prefix: "/dashboard" });
|
||||||
await server.register(registerCmekRouter, { prefix: "/kms" });
|
await server.register(registerCmekRouter, { prefix: "/kms" });
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { SecretSharingSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
|
||||||
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
|
export const registerSecretRequestsRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretRequest: SecretSharingSchema.omit({
|
||||||
|
encryptedSecret: true,
|
||||||
|
tag: true,
|
||||||
|
iv: true,
|
||||||
|
encryptedValue: true
|
||||||
|
}).extend({
|
||||||
|
isSecretValueSet: z.boolean(),
|
||||||
|
requester: z.object({
|
||||||
|
organizationName: z.string(),
|
||||||
|
firstName: z.string().nullish(),
|
||||||
|
lastName: z.string().nullish(),
|
||||||
|
username: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretRequest = await req.server.services.secretSharing.getSecretRequestById({
|
||||||
|
id: req.params.id,
|
||||||
|
actorOrgId: req.permission?.orgId,
|
||||||
|
orgId: req.permission?.orgId,
|
||||||
|
actor: req.permission?.type,
|
||||||
|
actorId: req.permission?.id,
|
||||||
|
actorAuthMethod: req.permission?.authMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRequest };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:id/set-value",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
secretValue: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretRequest: SecretSharingSchema.omit({
|
||||||
|
encryptedSecret: true,
|
||||||
|
tag: true,
|
||||||
|
iv: true,
|
||||||
|
encryptedValue: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretRequest = await req.server.services.secretSharing.setSecretRequestValue({
|
||||||
|
id: req.params.id,
|
||||||
|
actorOrgId: req.permission?.orgId,
|
||||||
|
orgId: req.permission?.orgId,
|
||||||
|
actor: req.permission?.type,
|
||||||
|
actorId: req.permission?.id,
|
||||||
|
actorAuthMethod: req.permission?.authMethod,
|
||||||
|
secretValue: req.body.secretValue
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRequest };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/:id/reveal-value",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretRequest: SecretSharingSchema.omit({
|
||||||
|
encryptedSecret: true,
|
||||||
|
tag: true,
|
||||||
|
iv: true,
|
||||||
|
encryptedValue: true
|
||||||
|
}).extend({
|
||||||
|
secretValue: z.string()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretRequest = await req.server.services.secretSharing.revealSecretRequestValue({
|
||||||
|
id: req.params.id,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretRequest };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretRequest: SecretSharingSchema.omit({
|
||||||
|
encryptedSecret: true,
|
||||||
|
tag: true,
|
||||||
|
iv: true,
|
||||||
|
encryptedValue: true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretRequest = await req.server.services.secretSharing.deleteSharedSecretById({
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
sharedSecretId: req.params.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actor: req.permission.type,
|
||||||
|
type: SecretSharingType.Request
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.SecretRequestDeleted,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
secretRequestId: req.params.id,
|
||||||
|
organizationId: req.permission.orgId,
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { secretRequest };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
offset: z.coerce.number().min(0).max(100).default(0),
|
||||||
|
limit: z.coerce.number().min(1).max(100).default(25)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secrets: z.array(SecretSharingSchema),
|
||||||
|
totalCount: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { secrets, totalCount } = await req.server.services.secretSharing.getSharedSecrets({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
type: SecretSharingType.Request,
|
||||||
|
...req.query
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
secrets,
|
||||||
|
totalCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().max(50).optional(),
|
||||||
|
expiresAt: z.string(),
|
||||||
|
accessType: z.nativeEnum(SecretSharingAccessType).default(SecretSharingAccessType.Organization)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
id: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const shareRequest = await req.server.services.secretSharing.createSecretRequest({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
...req.auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_SECRET_REQUEST,
|
||||||
|
metadata: {
|
||||||
|
accessType: req.body.accessType,
|
||||||
|
name: req.body.name,
|
||||||
|
id: shareRequest.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.telemetry.sendPostHogEvents({
|
||||||
|
event: PostHogEventTypes.SecretRequestCreated,
|
||||||
|
distinctId: getTelemetryDistinctId(req),
|
||||||
|
properties: {
|
||||||
|
secretRequestId: shareRequest.id,
|
||||||
|
organizationId: req.permission.orgId,
|
||||||
|
secretRequestName: req.body.name,
|
||||||
|
...req.auditLogInfo
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { id: shareRequest.id };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@app/server/config/rateLimiter";
|
} 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";
|
||||||
|
import { SecretSharingType } from "@app/services/secret-sharing/secret-sharing-types";
|
||||||
|
|
||||||
export const registerSecretSharingRouter = async (server: FastifyZodProvider) => {
|
export const registerSecretSharingRouter = async (server: FastifyZodProvider) => {
|
||||||
server.route({
|
server.route({
|
||||||
@@ -38,6 +39,7 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
|
type: SecretSharingType.Share,
|
||||||
...req.query
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -211,7 +213,8 @@ export const registerSecretSharingRouter = async (server: FastifyZodProvider) =>
|
|||||||
orgId: req.permission.orgId,
|
orgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
sharedSecretId
|
sharedSecretId,
|
||||||
|
type: SecretSharingType.Share
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = {
|
|||||||
secretDAL: Pick<TSecretDALFactory, "pruneSecretReminders">;
|
secretDAL: Pick<TSecretDALFactory, "pruneSecretReminders">;
|
||||||
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
secretFolderVersionDAL: Pick<TSecretFolderVersionDALFactory, "pruneExcessVersions">;
|
||||||
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
snapshotDAL: Pick<TSnapshotDALFactory, "pruneExcessSnapshots">;
|
||||||
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets">;
|
secretSharingDAL: Pick<TSecretSharingDALFactory, "pruneExpiredSharedSecrets" | "pruneExpiredSecretRequests">;
|
||||||
queueService: TQueueServiceFactory;
|
queueService: TQueueServiceFactory;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,6 +45,7 @@ export const dailyResourceCleanUpQueueServiceFactory = ({
|
|||||||
await identityAccessTokenDAL.removeExpiredTokens();
|
await identityAccessTokenDAL.removeExpiredTokens();
|
||||||
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
await identityUniversalAuthClientSecretDAL.removeExpiredClientSecrets();
|
||||||
await secretSharingDAL.pruneExpiredSharedSecrets();
|
await secretSharingDAL.pruneExpiredSharedSecrets();
|
||||||
|
await secretSharingDAL.pruneExpiredSecretRequests();
|
||||||
await snapshotDAL.pruneExcessSnapshots();
|
await snapshotDAL.pruneExcessSnapshots();
|
||||||
await secretVersionDAL.pruneExcessVersions();
|
await secretVersionDAL.pruneExcessVersions();
|
||||||
await secretVersionV2DAL.pruneExcessVersions();
|
await secretVersionV2DAL.pruneExcessVersions();
|
||||||
|
|||||||
@@ -7,12 +7,58 @@ import { ormify, selectAllTableCols } from "@app/lib/knex";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { QueueName } from "@app/queue";
|
import { QueueName } from "@app/queue";
|
||||||
|
|
||||||
|
import { SecretSharingType } from "./secret-sharing-types";
|
||||||
|
|
||||||
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
|
export type TSecretSharingDALFactory = ReturnType<typeof secretSharingDALFactory>;
|
||||||
|
|
||||||
export const secretSharingDALFactory = (db: TDbClient) => {
|
export const secretSharingDALFactory = (db: TDbClient) => {
|
||||||
const sharedSecretOrm = ormify(db, TableName.SecretSharing);
|
const sharedSecretOrm = ormify(db, TableName.SecretSharing);
|
||||||
|
|
||||||
const countAllUserOrgSharedSecrets = async ({ orgId, userId }: { orgId: string; userId: string }) => {
|
const getSecretRequestById = async (id: string) => {
|
||||||
|
const repDb = db.replicaNode();
|
||||||
|
|
||||||
|
const secretRequest = await repDb(TableName.SecretSharing)
|
||||||
|
.leftJoin(TableName.Organization, `${TableName.Organization}.id`, `${TableName.SecretSharing}.orgId`)
|
||||||
|
.leftJoin(TableName.Users, `${TableName.Users}.id`, `${TableName.SecretSharing}.userId`)
|
||||||
|
.where(`${TableName.SecretSharing}.id`, id)
|
||||||
|
.where(`${TableName.SecretSharing}.type`, SecretSharingType.Request)
|
||||||
|
.select(
|
||||||
|
repDb.ref("name").withSchema(TableName.Organization).as("orgName"),
|
||||||
|
repDb.ref("firstName").withSchema(TableName.Users).as("requesterFirstName"),
|
||||||
|
repDb.ref("lastName").withSchema(TableName.Users).as("requesterLastName"),
|
||||||
|
repDb.ref("username").withSchema(TableName.Users).as("requesterUsername")
|
||||||
|
)
|
||||||
|
.select(selectAllTableCols(TableName.SecretSharing))
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!secretRequest) {
|
||||||
|
throw new DatabaseError({
|
||||||
|
error: new Error("Get Secret Request By Id, Not found"),
|
||||||
|
message: "Get Secret Request By Id, Not found",
|
||||||
|
name: "GetSecretRequestById"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...secretRequest,
|
||||||
|
requester: {
|
||||||
|
organizationName: secretRequest.orgName,
|
||||||
|
firstName: secretRequest.requesterFirstName,
|
||||||
|
lastName: secretRequest.requesterLastName,
|
||||||
|
username: secretRequest.requesterUsername
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const countAllUserOrgSharedSecrets = async ({
|
||||||
|
orgId,
|
||||||
|
userId,
|
||||||
|
type
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
userId: string;
|
||||||
|
type: SecretSharingType;
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
interface CountResult {
|
interface CountResult {
|
||||||
count: string;
|
count: string;
|
||||||
@@ -22,6 +68,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
|||||||
.replicaNode()(TableName.SecretSharing)
|
.replicaNode()(TableName.SecretSharing)
|
||||||
.where(`${TableName.SecretSharing}.orgId`, orgId)
|
.where(`${TableName.SecretSharing}.orgId`, orgId)
|
||||||
.where(`${TableName.SecretSharing}.userId`, userId)
|
.where(`${TableName.SecretSharing}.userId`, userId)
|
||||||
|
.where(`${TableName.SecretSharing}.type`, type)
|
||||||
.count("*")
|
.count("*")
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
@@ -38,6 +85,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
|||||||
const docs = await (tx || db)(TableName.SecretSharing)
|
const docs = await (tx || db)(TableName.SecretSharing)
|
||||||
.where("expiresAt", "<", today)
|
.where("expiresAt", "<", today)
|
||||||
.andWhere("encryptedValue", "<>", "")
|
.andWhere("encryptedValue", "<>", "")
|
||||||
|
.andWhere("type", SecretSharingType.Share)
|
||||||
.update({
|
.update({
|
||||||
encryptedValue: "",
|
encryptedValue: "",
|
||||||
tag: "",
|
tag: "",
|
||||||
@@ -50,6 +98,26 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pruneExpiredSecretRequests = async (tx?: Knex) => {
|
||||||
|
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests started`);
|
||||||
|
try {
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
const docs = await (tx || db)(TableName.SecretSharing)
|
||||||
|
.whereNotNull("expiresAt")
|
||||||
|
.andWhere("expiresAt", "<", today)
|
||||||
|
.andWhere("encryptedSecret", null)
|
||||||
|
.andWhere("type", SecretSharingType.Request)
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
logger.info(`${QueueName.DailyResourceCleanUp}: pruning expired secret requests completed`);
|
||||||
|
|
||||||
|
return docs;
|
||||||
|
} catch (error) {
|
||||||
|
throw new DatabaseError({ error, name: "pruneExpiredSecretRequests" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const findActiveSharedSecrets = async (filters: Partial<TSecretSharing>, tx?: Knex) => {
|
const findActiveSharedSecrets = async (filters: Partial<TSecretSharing>, tx?: Knex) => {
|
||||||
try {
|
try {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -57,6 +125,7 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
|||||||
.where(filters)
|
.where(filters)
|
||||||
.andWhere("expiresAt", ">", now)
|
.andWhere("expiresAt", ">", now)
|
||||||
.andWhere("encryptedValue", "<>", "")
|
.andWhere("encryptedValue", "<>", "")
|
||||||
|
.andWhere("type", SecretSharingType.Share)
|
||||||
.select(selectAllTableCols(TableName.SecretSharing))
|
.select(selectAllTableCols(TableName.SecretSharing))
|
||||||
.orderBy("expiresAt", "asc");
|
.orderBy("expiresAt", "asc");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -86,7 +155,9 @@ export const secretSharingDALFactory = (db: TDbClient) => {
|
|||||||
...sharedSecretOrm,
|
...sharedSecretOrm,
|
||||||
countAllUserOrgSharedSecrets,
|
countAllUserOrgSharedSecrets,
|
||||||
pruneExpiredSharedSecrets,
|
pruneExpiredSharedSecrets,
|
||||||
|
pruneExpiredSecretRequests,
|
||||||
softDeleteById,
|
softDeleteById,
|
||||||
findActiveSharedSecrets
|
findActiveSharedSecrets,
|
||||||
|
getSecretRequestById
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,26 +4,36 @@ import bcrypt from "bcrypt";
|
|||||||
|
|
||||||
import { TSecretSharing } from "@app/db/schemas";
|
import { TSecretSharing } from "@app/db/schemas";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { SecretSharingAccessType } from "@app/lib/types";
|
import { SecretSharingAccessType } from "@app/lib/types";
|
||||||
import { isUuidV4 } from "@app/lib/validator";
|
import { isUuidV4 } from "@app/lib/validator";
|
||||||
|
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { TOrgDALFactory } from "../org/org-dal";
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
|
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
|
||||||
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
import { TSecretSharingDALFactory } from "./secret-sharing-dal";
|
||||||
import {
|
import {
|
||||||
|
SecretSharingType,
|
||||||
TCreatePublicSharedSecretDTO,
|
TCreatePublicSharedSecretDTO,
|
||||||
|
TCreateSecretRequestDTO,
|
||||||
TCreateSharedSecretDTO,
|
TCreateSharedSecretDTO,
|
||||||
TDeleteSharedSecretDTO,
|
TDeleteSharedSecretDTO,
|
||||||
TGetActiveSharedSecretByIdDTO,
|
TGetActiveSharedSecretByIdDTO,
|
||||||
TGetSharedSecretsDTO
|
TGetSecretRequestByIdDTO,
|
||||||
|
TGetSharedSecretsDTO,
|
||||||
|
TRevealSecretRequestValueDTO,
|
||||||
|
TSetSecretRequestValueDTO
|
||||||
} from "./secret-sharing-types";
|
} from "./secret-sharing-types";
|
||||||
|
|
||||||
type TSecretSharingServiceFactoryDep = {
|
type TSecretSharingServiceFactoryDep = {
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
secretSharingDAL: TSecretSharingDALFactory;
|
secretSharingDAL: TSecretSharingDALFactory;
|
||||||
orgDAL: TOrgDALFactory;
|
orgDAL: TOrgDALFactory;
|
||||||
|
userDAL: TUserDALFactory;
|
||||||
kmsService: TKmsServiceFactory;
|
kmsService: TKmsServiceFactory;
|
||||||
|
smtpService: TSmtpService;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
export type TSecretSharingServiceFactory = ReturnType<typeof secretSharingServiceFactory>;
|
||||||
@@ -32,7 +42,9 @@ export const secretSharingServiceFactory = ({
|
|||||||
permissionService,
|
permissionService,
|
||||||
secretSharingDAL,
|
secretSharingDAL,
|
||||||
orgDAL,
|
orgDAL,
|
||||||
kmsService
|
kmsService,
|
||||||
|
smtpService,
|
||||||
|
userDAL
|
||||||
}: TSecretSharingServiceFactoryDep) => {
|
}: TSecretSharingServiceFactoryDep) => {
|
||||||
const $validateSharedSecretExpiry = (expiresAt: string) => {
|
const $validateSharedSecretExpiry = (expiresAt: string) => {
|
||||||
if (new Date(expiresAt) < new Date()) {
|
if (new Date(expiresAt) < new Date()) {
|
||||||
@@ -75,7 +87,6 @@ export const secretSharingServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const encryptWithRoot = kmsService.encryptWithRootKey();
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
|
|
||||||
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||||
|
|
||||||
const id = crypto.randomBytes(32).toString("hex");
|
const id = crypto.randomBytes(32).toString("hex");
|
||||||
@@ -88,6 +99,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
encryptedValue: null,
|
encryptedValue: null,
|
||||||
encryptedSecret,
|
encryptedSecret,
|
||||||
name,
|
name,
|
||||||
|
type: SecretSharingType.Share,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
expiresAfterViews,
|
expiresAfterViews,
|
||||||
@@ -101,6 +113,193 @@ export const secretSharingServiceFactory = ({
|
|||||||
return { id: idToReturn };
|
return { id: idToReturn };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createSecretRequest = async ({
|
||||||
|
actor,
|
||||||
|
accessType,
|
||||||
|
expiresAt,
|
||||||
|
name,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TCreateSecretRequestDTO) => {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
||||||
|
|
||||||
|
$validateSharedSecretExpiry(expiresAt);
|
||||||
|
|
||||||
|
const newSecretRequest = await secretSharingDAL.create({
|
||||||
|
type: SecretSharingType.Request,
|
||||||
|
userId: actorId,
|
||||||
|
orgId,
|
||||||
|
name,
|
||||||
|
encryptedSecret: null,
|
||||||
|
accessType,
|
||||||
|
expiresAt: new Date(expiresAt)
|
||||||
|
});
|
||||||
|
|
||||||
|
return { id: newSecretRequest.id };
|
||||||
|
};
|
||||||
|
|
||||||
|
const revealSecretRequestValue = async ({
|
||||||
|
id,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
actorOrgId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: TRevealSecretRequestValueDTO) => {
|
||||||
|
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
|
||||||
|
|
||||||
|
if (!secretRequest) {
|
||||||
|
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
||||||
|
|
||||||
|
if (secretRequest.userId !== actorId || secretRequest.orgId !== orgId) {
|
||||||
|
throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!secretRequest.encryptedSecret) {
|
||||||
|
throw new BadRequestError({ message: "Secret request has no value set" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const decryptWithRoot = kmsService.decryptWithRootKey();
|
||||||
|
const decryptedSecret = decryptWithRoot(secretRequest.encryptedSecret);
|
||||||
|
|
||||||
|
return { ...secretRequest, secretValue: decryptedSecret.toString() };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSecretRequestById = async ({
|
||||||
|
id,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
}: TGetSecretRequestByIdDTO) => {
|
||||||
|
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
|
||||||
|
|
||||||
|
if (!secretRequest) {
|
||||||
|
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secretRequest.accessType === SecretSharingAccessType.Organization) {
|
||||||
|
if (orgId === undefined) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
||||||
|
|
||||||
|
if (secretRequest.orgId !== orgId) {
|
||||||
|
throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Access denied: Secret request has expired"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...secretRequest,
|
||||||
|
isSecretValueSet: Boolean(secretRequest.encryptedSecret)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const setSecretRequestValue = async ({
|
||||||
|
id,
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId,
|
||||||
|
secretValue
|
||||||
|
}: TSetSecretRequestValueDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
const secretRequest = await secretSharingDAL.getSecretRequestById(id);
|
||||||
|
|
||||||
|
if (!secretRequest) {
|
||||||
|
throw new NotFoundError({ message: `Secret request with ID '${id}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
let respondentUsername: string | undefined;
|
||||||
|
|
||||||
|
if (secretRequest.accessType === SecretSharingAccessType.Organization) {
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
if (!permission) throw new ForbiddenRequestError({ name: "User is not a part of the specified organization" });
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
throw new UnauthorizedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secretRequest.orgId !== orgId) {
|
||||||
|
throw new ForbiddenRequestError({ name: "User does not have permission to access this secret request" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await userDAL.findById(actorId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundError({ message: `User with ID '${actorId}' not found` });
|
||||||
|
}
|
||||||
|
|
||||||
|
respondentUsername = user.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secretRequest.encryptedSecret) {
|
||||||
|
throw new BadRequestError({ message: "Secret request already has a value set" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secretValue.length > 10_000) {
|
||||||
|
throw new BadRequestError({ message: "Shared secret value too long" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secretRequest.expiresAt && secretRequest.expiresAt < new Date()) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Access denied: Secret request has expired"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptWithRoot = kmsService.encryptWithRootKey();
|
||||||
|
const encryptedSecret = encryptWithRoot(Buffer.from(secretValue));
|
||||||
|
|
||||||
|
const request = await secretSharingDAL.transaction(async (tx) => {
|
||||||
|
const updatedRequest = await secretSharingDAL.updateById(id, { encryptedSecret }, tx);
|
||||||
|
|
||||||
|
await smtpService.sendMail({
|
||||||
|
recipients: [secretRequest.requesterUsername],
|
||||||
|
subjectLine: "Secret Request Completed",
|
||||||
|
substitutions: {
|
||||||
|
name: secretRequest.name,
|
||||||
|
respondentUsername,
|
||||||
|
secretRequestUrl: `${appCfg.SITE_URL}/organization/secret-sharing?selectedTab=request-secret`
|
||||||
|
},
|
||||||
|
template: SmtpTemplates.SecretRequestCompleted
|
||||||
|
});
|
||||||
|
|
||||||
|
return updatedRequest;
|
||||||
|
});
|
||||||
|
|
||||||
|
return request;
|
||||||
|
};
|
||||||
|
|
||||||
const createPublicSharedSecret = async ({
|
const createPublicSharedSecret = async ({
|
||||||
password,
|
password,
|
||||||
secretValue,
|
secretValue,
|
||||||
@@ -121,6 +320,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
encryptedValue: null,
|
encryptedValue: null,
|
||||||
iv: null,
|
iv: null,
|
||||||
tag: null,
|
tag: null,
|
||||||
|
type: SecretSharingType.Share,
|
||||||
encryptedSecret,
|
encryptedSecret,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
expiresAt: new Date(expiresAt),
|
expiresAt: new Date(expiresAt),
|
||||||
@@ -137,7 +337,8 @@ export const secretSharingServiceFactory = ({
|
|||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
offset,
|
offset,
|
||||||
limit
|
limit,
|
||||||
|
type
|
||||||
}: TGetSharedSecretsDTO) => {
|
}: TGetSharedSecretsDTO) => {
|
||||||
if (!actorOrgId) throw new ForbiddenRequestError();
|
if (!actorOrgId) throw new ForbiddenRequestError();
|
||||||
|
|
||||||
@@ -153,14 +354,16 @@ export const secretSharingServiceFactory = ({
|
|||||||
const secrets = await secretSharingDAL.find(
|
const secrets = await secretSharingDAL.find(
|
||||||
{
|
{
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
orgId: actorOrgId
|
orgId: actorOrgId,
|
||||||
|
type
|
||||||
},
|
},
|
||||||
{ offset, limit, sort: [["createdAt", "desc"]] }
|
{ offset, limit, sort: [["createdAt", "desc"]] }
|
||||||
);
|
);
|
||||||
|
|
||||||
const count = await secretSharingDAL.countAllUserOrgSharedSecrets({
|
const count = await secretSharingDAL.countAllUserOrgSharedSecrets({
|
||||||
orgId: actorOrgId,
|
orgId: actorOrgId,
|
||||||
userId: actorId
|
userId: actorId,
|
||||||
|
type
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -187,9 +390,11 @@ export const secretSharingServiceFactory = ({
|
|||||||
const sharedSecret = isUuidV4(sharedSecretId)
|
const sharedSecret = isUuidV4(sharedSecretId)
|
||||||
? await secretSharingDAL.findOne({
|
? await secretSharingDAL.findOne({
|
||||||
id: sharedSecretId,
|
id: sharedSecretId,
|
||||||
|
type: SecretSharingType.Share,
|
||||||
hashedHex
|
hashedHex
|
||||||
})
|
})
|
||||||
: await secretSharingDAL.findOne({
|
: await secretSharingDAL.findOne({
|
||||||
|
type: SecretSharingType.Share,
|
||||||
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
|
identifier: Buffer.from(sharedSecretId, "base64url").toString("hex")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -254,7 +459,7 @@ export const secretSharingServiceFactory = ({
|
|||||||
secret: {
|
secret: {
|
||||||
...sharedSecret,
|
...sharedSecret,
|
||||||
...(decryptedSecretValue && {
|
...(decryptedSecretValue && {
|
||||||
secretValue: Buffer.from(decryptedSecretValue).toString()
|
secretValue: decryptedSecretValue.toString()
|
||||||
}),
|
}),
|
||||||
orgName:
|
orgName:
|
||||||
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
sharedSecret.accessType === SecretSharingAccessType.Organization && orgId === sharedSecret.orgId
|
||||||
@@ -270,8 +475,8 @@ export const secretSharingServiceFactory = ({
|
|||||||
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
|
if (!permission) throw new ForbiddenRequestError({ name: "User does not belong to the specified organization" });
|
||||||
|
|
||||||
const sharedSecret = isUuidV4(sharedSecretId)
|
const sharedSecret = isUuidV4(sharedSecretId)
|
||||||
? await secretSharingDAL.findById(sharedSecretId)
|
? await secretSharingDAL.findOne({ id: sharedSecretId, type: deleteSharedSecretInput.type })
|
||||||
: await secretSharingDAL.findOne({ identifier: sharedSecretId });
|
: await secretSharingDAL.findOne({ identifier: sharedSecretId, type: deleteSharedSecretInput.type });
|
||||||
|
|
||||||
if (sharedSecret.orgId && sharedSecret.orgId !== orgId)
|
if (sharedSecret.orgId && sharedSecret.orgId !== orgId)
|
||||||
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
|
throw new ForbiddenRequestError({ message: "User does not have permission to delete shared secret" });
|
||||||
@@ -286,6 +491,11 @@ export const secretSharingServiceFactory = ({
|
|||||||
createPublicSharedSecret,
|
createPublicSharedSecret,
|
||||||
getSharedSecrets,
|
getSharedSecrets,
|
||||||
deleteSharedSecretById,
|
deleteSharedSecretById,
|
||||||
getSharedSecretById
|
getSharedSecretById,
|
||||||
|
|
||||||
|
createSecretRequest,
|
||||||
|
getSecretRequestById,
|
||||||
|
setSecretRequestValue,
|
||||||
|
revealSecretRequestValue
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { SecretSharingAccessType, TGenericPermission } from "@app/lib/types";
|
import { SecretSharingAccessType, TGenericPermission, TOrgPermission } from "@app/lib/types";
|
||||||
|
|
||||||
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
|
||||||
|
|
||||||
|
export enum SecretSharingType {
|
||||||
|
Share = "share",
|
||||||
|
Request = "request"
|
||||||
|
}
|
||||||
|
|
||||||
export type TGetSharedSecretsDTO = {
|
export type TGetSharedSecretsDTO = {
|
||||||
|
type: SecretSharingType;
|
||||||
offset: number;
|
offset: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
} & TGenericPermission;
|
} & TGenericPermission;
|
||||||
@@ -39,6 +45,26 @@ export type TValidateActiveSharedSecretDTO = TGetActiveSharedSecretByIdDTO & {
|
|||||||
|
|
||||||
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
export type TCreateSharedSecretDTO = TSharedSecretPermission & TCreatePublicSharedSecretDTO;
|
||||||
|
|
||||||
|
export type TCreateSecretRequestDTO = {
|
||||||
|
name?: string;
|
||||||
|
accessType: SecretSharingAccessType;
|
||||||
|
expiresAt: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TRevealSecretRequestValueDTO = {
|
||||||
|
id: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TGetSecretRequestByIdDTO = {
|
||||||
|
id: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
|
export type TSetSecretRequestValueDTO = {
|
||||||
|
id: string;
|
||||||
|
secretValue: string;
|
||||||
|
} & TOrgPermission;
|
||||||
|
|
||||||
export type TDeleteSharedSecretDTO = {
|
export type TDeleteSharedSecretDTO = {
|
||||||
sharedSecretId: string;
|
sharedSecretId: string;
|
||||||
|
type: SecretSharingType;
|
||||||
} & TSharedSecretPermission;
|
} & TSharedSecretPermission;
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ export enum SmtpTemplates {
|
|||||||
SecretSyncFailed = "secretSyncFailed.handlebars",
|
SecretSyncFailed = "secretSyncFailed.handlebars",
|
||||||
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
|
ExternalImportSuccessful = "externalImportSuccessful.handlebars",
|
||||||
ExternalImportFailed = "externalImportFailed.handlebars",
|
ExternalImportFailed = "externalImportFailed.handlebars",
|
||||||
ExternalImportStarted = "externalImportStarted.handlebars"
|
ExternalImportStarted = "externalImportStarted.handlebars",
|
||||||
|
SecretRequestCompleted = "secretRequestCompleted.handlebars"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SmtpHost {
|
export enum SmtpHost {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<title>Secret Request Completed</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h2>Infisical</h2>
|
||||||
|
<h2>A secret has been shared with you</h2>
|
||||||
|
|
||||||
|
{{#if name}}
|
||||||
|
<p>Secret request name: {{name}}</p>
|
||||||
|
{{/if}}
|
||||||
|
{{#if respondentUsername}}
|
||||||
|
<p>Shared by: {{respondentUsername}}</p>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You can access the secret by clicking the link below.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="{{secretRequestUrl}}">Access Secret</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{emailFooter}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -13,7 +13,9 @@ export enum PostHogEventTypes {
|
|||||||
IntegrationCreated = "Integration Created",
|
IntegrationCreated = "Integration Created",
|
||||||
MachineIdentityCreated = "Machine Identity Created",
|
MachineIdentityCreated = "Machine Identity Created",
|
||||||
UserOrgInvitation = "User Org Invitation",
|
UserOrgInvitation = "User Org Invitation",
|
||||||
TelemetryInstanceStats = "Self Hosted Instance Stats"
|
TelemetryInstanceStats = "Self Hosted Instance Stats",
|
||||||
|
SecretRequestCreated = "Secret Request Created",
|
||||||
|
SecretRequestDeleted = "Secret Request Deleted"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TSecretModifiedEvent = {
|
export type TSecretModifiedEvent = {
|
||||||
@@ -120,6 +122,23 @@ export type TTelemetryInstanceStatsEvent = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TSecretRequestCreatedEvent = {
|
||||||
|
event: PostHogEventTypes.SecretRequestCreated;
|
||||||
|
properties: {
|
||||||
|
secretRequestId: string;
|
||||||
|
organizationId: string;
|
||||||
|
secretRequestName?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSecretRequestDeletedEvent = {
|
||||||
|
event: PostHogEventTypes.SecretRequestDeleted;
|
||||||
|
properties: {
|
||||||
|
secretRequestId: string;
|
||||||
|
organizationId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type TPostHogEvent = { distinctId: string } & (
|
export type TPostHogEvent = { distinctId: string } & (
|
||||||
| TSecretModifiedEvent
|
| TSecretModifiedEvent
|
||||||
| TAdminInitEvent
|
| TAdminInitEvent
|
||||||
@@ -130,4 +149,6 @@ export type TPostHogEvent = { distinctId: string } & (
|
|||||||
| TIntegrationCreatedEvent
|
| TIntegrationCreatedEvent
|
||||||
| TProjectCreateEvent
|
| TProjectCreateEvent
|
||||||
| TTelemetryInstanceStatsEvent
|
| TTelemetryInstanceStatsEvent
|
||||||
|
| TSecretRequestCreatedEvent
|
||||||
|
| TSecretRequestDeletedEvent
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export const publicPaths = [
|
|||||||
"/login/sso",
|
"/login/sso",
|
||||||
"/admin/signup",
|
"/admin/signup",
|
||||||
"/shared/secret/[id]",
|
"/shared/secret/[id]",
|
||||||
|
"/secret-request/secret/[id]",
|
||||||
"/share-secret"
|
"/share-secret"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
"/organization/secret-scanning",
|
"/organization/secret-scanning",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
|
"/_authenticate/_inject-org-details/_org-layout/organization/secret-scanning"
|
||||||
),
|
),
|
||||||
|
SecretSharing: setRoute(
|
||||||
|
"/organization/secret-sharing",
|
||||||
|
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
|
||||||
|
),
|
||||||
SettingsPage: setRoute(
|
SettingsPage: setRoute(
|
||||||
"/organization/settings",
|
"/organization/settings",
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
|
"/_authenticate/_inject-org-details/_org-layout/organization/settings"
|
||||||
@@ -285,6 +289,10 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
Public: {
|
Public: {
|
||||||
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId")
|
ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId"),
|
||||||
|
ViewSecretRequestByIDPage: setRoute(
|
||||||
|
"/secret-request/secret/$secretRequestId",
|
||||||
|
"/secret-request/secret/$secretRequestId"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,8 +5,13 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import { secretSharingKeys } from "./queries";
|
import { secretSharingKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
TCreatedSharedSecret,
|
TCreatedSharedSecret,
|
||||||
|
TCreateSecretRequestRequest,
|
||||||
TCreateSharedSecretRequest,
|
TCreateSharedSecretRequest,
|
||||||
TDeleteSharedSecretRequest,
|
TDeleteSecretRequestDTO,
|
||||||
|
TDeleteSharedSecretRequestDTO,
|
||||||
|
TRevealedSecretRequest,
|
||||||
|
TRevealSecretRequestValueRequest,
|
||||||
|
TSetSecretRequestValueRequest,
|
||||||
TSharedSecret
|
TSharedSecret
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -15,7 +20,7 @@ export const useCreateSharedSecret = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||||
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
||||||
"/api/v1/secret-sharing",
|
"/api/v1/secret-sharing/shared",
|
||||||
inputData
|
inputData
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -30,7 +35,7 @@ export const useCreatePublicSharedSecret = () => {
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
mutationFn: async (inputData: TCreateSharedSecretRequest) => {
|
||||||
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
||||||
"/api/v1/secret-sharing/public",
|
"/api/v1/secret-sharing/shared/public",
|
||||||
inputData
|
inputData
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
@@ -40,12 +45,50 @@ export const useCreatePublicSharedSecret = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useCreateSecretRequest = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (inputData: TCreateSecretRequestRequest) => {
|
||||||
|
const { data } = await apiRequest.post<TCreatedSharedSecret>(
|
||||||
|
"/api/v1/secret-sharing/requests",
|
||||||
|
inputData
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () =>
|
||||||
|
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSecretRequests() })
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSetSecretRequestValue = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (inputData: TSetSecretRequestValueRequest) => {
|
||||||
|
const { data } = await apiRequest.post<TSharedSecret>(
|
||||||
|
`/api/v1/secret-sharing/requests/${inputData.id}/set-value`,
|
||||||
|
inputData
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRevealSecretRequestValue = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (inputData: TRevealSecretRequestValueRequest) => {
|
||||||
|
const { data } = await apiRequest.post<TRevealedSecretRequest>(
|
||||||
|
`/api/v1/secret-sharing/requests/${inputData.id}/reveal-value`,
|
||||||
|
inputData
|
||||||
|
);
|
||||||
|
return data.secretRequest;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
export const useDeleteSharedSecret = () => {
|
export const useDeleteSharedSecret = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<TSharedSecret, { message: string }, { sharedSecretId: string }>({
|
return useMutation<TSharedSecret, { message: string }, { sharedSecretId: string }>({
|
||||||
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequest) => {
|
mutationFn: async ({ sharedSecretId }: TDeleteSharedSecretRequestDTO) => {
|
||||||
const { data } = await apiRequest.delete<TSharedSecret>(
|
const { data } = await apiRequest.delete<TSharedSecret>(
|
||||||
`/api/v1/secret-sharing/${sharedSecretId}`
|
`/api/v1/secret-sharing/shared/${sharedSecretId}`
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
@@ -53,3 +96,19 @@ export const useDeleteSharedSecret = () => {
|
|||||||
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSharedSecrets() })
|
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSharedSecrets() })
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useDeleteSecretRequest = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TSharedSecret, unknown, TDeleteSecretRequestDTO>({
|
||||||
|
mutationFn: async ({ secretRequestId }: TDeleteSecretRequestDTO) => {
|
||||||
|
const { data } = await apiRequest.delete<TSharedSecret>(
|
||||||
|
`/api/v1/secret-sharing/requests/${secretRequestId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: secretSharingKeys.allSecretRequests() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,16 +2,20 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { TSharedSecret, TViewSharedSecretResponse } from "./types";
|
import { TGetSecretRequestByIdResponse, TSharedSecret, TViewSharedSecretResponse } from "./types";
|
||||||
|
|
||||||
export const secretSharingKeys = {
|
export const secretSharingKeys = {
|
||||||
allSharedSecrets: () => ["sharedSecrets"] as const,
|
allSharedSecrets: () => ["sharedSecrets"] as const,
|
||||||
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
|
specificSharedSecrets: ({ offset, limit }: { offset: number; limit: number }) =>
|
||||||
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const,
|
[...secretSharingKeys.allSharedSecrets(), { offset, limit }] as const,
|
||||||
|
allSecretRequests: () => ["secretRequests"] as const,
|
||||||
|
specificSecretRequests: ({ offset, limit }: { offset: number; limit: number }) =>
|
||||||
|
[...secretSharingKeys.allSecretRequests(), { offset, limit }] as const,
|
||||||
getSecretById: (arg: { id: string; hashedHex: string | null; password?: string }) => [
|
getSecretById: (arg: { id: string; hashedHex: string | null; password?: string }) => [
|
||||||
"shared-secret",
|
"shared-secret",
|
||||||
arg
|
arg
|
||||||
]
|
],
|
||||||
|
getSecretRequestById: (arg: { id: string }) => ["secret-request", arg] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetSharedSecrets = ({
|
export const useGetSharedSecrets = ({
|
||||||
@@ -30,7 +34,7 @@ export const useGetSharedSecrets = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>(
|
const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>(
|
||||||
"/api/v1/secret-sharing/",
|
"/api/v1/secret-sharing/shared",
|
||||||
{
|
{
|
||||||
params
|
params
|
||||||
}
|
}
|
||||||
@@ -40,6 +44,29 @@ export const useGetSharedSecrets = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetSecretRequests = ({
|
||||||
|
offset = 0,
|
||||||
|
limit = 25
|
||||||
|
}: {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: secretSharingKeys.specificSecretRequests({ offset, limit }),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{ secrets: TSharedSecret[]; totalCount: number }>(
|
||||||
|
"/api/v1/secret-sharing/requests",
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
offset: String(offset),
|
||||||
|
limit: String(limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
export const useGetActiveSharedSecretById = ({
|
export const useGetActiveSharedSecretById = ({
|
||||||
sharedSecretId,
|
sharedSecretId,
|
||||||
hashedHex,
|
hashedHex,
|
||||||
@@ -53,7 +80,7 @@ export const useGetActiveSharedSecretById = ({
|
|||||||
queryKey: secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }),
|
queryKey: secretSharingKeys.getSecretById({ id: sharedSecretId, hashedHex, password }),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.post<TViewSharedSecretResponse>(
|
const { data } = await apiRequest.post<TViewSharedSecretResponse>(
|
||||||
`/api/v1/secret-sharing/public/${sharedSecretId}`,
|
`/api/v1/secret-sharing/shared/public/${sharedSecretId}`,
|
||||||
{
|
{
|
||||||
...(hashedHex && { hashedHex }),
|
...(hashedHex && { hashedHex }),
|
||||||
password
|
password
|
||||||
@@ -65,3 +92,16 @@ export const useGetActiveSharedSecretById = ({
|
|||||||
enabled: Boolean(sharedSecretId)
|
enabled: Boolean(sharedSecretId)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetSecretRequestById = ({ secretRequestId }: { secretRequestId: string }) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: secretSharingKeys.getSecretRequestById({ id: secretRequestId }),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TGetSecretRequestByIdResponse>(
|
||||||
|
`/api/v1/secret-sharing/requests/${secretRequestId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.secretRequest;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -9,10 +9,17 @@ export type TSharedSecret = {
|
|||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
expiresAfterViews: number | null;
|
expiresAfterViews: number | null;
|
||||||
encryptedValue: string;
|
encryptedValue: string;
|
||||||
|
encryptedSecret: string;
|
||||||
iv: string;
|
iv: string;
|
||||||
tag: string;
|
tag: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TRevealedSecretRequest = {
|
||||||
|
secretRequest: {
|
||||||
|
secretValue: string;
|
||||||
|
} & TSharedSecret;
|
||||||
|
};
|
||||||
|
|
||||||
export type TCreatedSharedSecret = {
|
export type TCreatedSharedSecret = {
|
||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
@@ -26,6 +33,21 @@ export type TCreateSharedSecretRequest = {
|
|||||||
accessType?: SecretSharingAccessType;
|
accessType?: SecretSharingAccessType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TCreateSecretRequestRequest = {
|
||||||
|
name?: string;
|
||||||
|
accessType?: SecretSharingAccessType;
|
||||||
|
expiresAt: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSetSecretRequestValueRequest = {
|
||||||
|
secretValue: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TRevealSecretRequestValueRequest = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type TViewSharedSecretResponse = {
|
export type TViewSharedSecretResponse = {
|
||||||
isPasswordProtected: boolean;
|
isPasswordProtected: boolean;
|
||||||
secret: {
|
secret: {
|
||||||
@@ -38,10 +60,27 @@ export type TViewSharedSecretResponse = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TDeleteSharedSecretRequest = {
|
export type TGetSecretRequestByIdResponse = {
|
||||||
|
secretRequest: {
|
||||||
|
isSecretValueSet: boolean;
|
||||||
|
accessType: SecretSharingAccessType;
|
||||||
|
requester: {
|
||||||
|
organizationName: string;
|
||||||
|
username: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteSharedSecretRequestDTO = {
|
||||||
sharedSecretId: string;
|
sharedSecretId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TDeleteSecretRequestDTO = {
|
||||||
|
secretRequestId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum SecretSharingAccessType {
|
export enum SecretSharingAccessType {
|
||||||
Anyone = "anyone",
|
Anyone = "anyone",
|
||||||
Organization = "organization"
|
Organization = "organization"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
|
|
||||||
import { PageHeader } from "@app/components/v2";
|
import { PageHeader } from "@app/components/v2";
|
||||||
|
|
||||||
import { ShareSecretSection } from "./components";
|
import { ShareSecretSection } from "./ShareSecretSection";
|
||||||
|
|
||||||
export const SecretSharingPage = () => {
|
export const SecretSharingPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
|
import { RequestSecretTab } from "./components/RequestSecret/RequestSecretTab";
|
||||||
|
import { ShareSecretTab } from "./components/ShareSecret/ShareSecretTab";
|
||||||
|
|
||||||
|
type DeleteModalData = { name: string; id: string };
|
||||||
|
|
||||||
|
enum SecretSharingPageTabs {
|
||||||
|
ShareSecret = "share-secret",
|
||||||
|
RequestSecret = "request-secret"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShareSecretSection = () => {
|
||||||
|
const deleteSharedSecret = useDeleteSharedSecret();
|
||||||
|
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
||||||
|
"createSharedSecret",
|
||||||
|
"deleteSharedSecretConfirmation",
|
||||||
|
"createSecretRequest",
|
||||||
|
"deleteSecretRequestConfirmation",
|
||||||
|
"revealSecretRequestValue"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { selectedTab } = useSearch({
|
||||||
|
from: ROUTE_PATHS.Organization.SecretSharing.id
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateSelectedTab = (tab: string) => {
|
||||||
|
navigate({
|
||||||
|
to: ROUTE_PATHS.Organization.SecretSharing.path,
|
||||||
|
search: (prev) => ({ ...prev, selectedTab: tab as SecretSharingPageTabs })
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Helmet>
|
||||||
|
<title>Secret Sharing</title>
|
||||||
|
<link rel="icon" href="/infisical.ico" />
|
||||||
|
<meta property="og:image" content="/images/message.png" />
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<Tabs value={selectedTab} onValueChange={updateSelectedTab}>
|
||||||
|
<TabList>
|
||||||
|
<Tab value={SecretSharingPageTabs.ShareSecret}>Share Secrets</Tab>
|
||||||
|
<Tab value={SecretSharingPageTabs.RequestSecret}>Request Secrets</Tab>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel value={SecretSharingPageTabs.ShareSecret}>
|
||||||
|
<ShareSecretTab
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
handlePopUpClose={handlePopUpClose}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={SecretSharingPageTabs.RequestSecret}>
|
||||||
|
<RequestSecretTab
|
||||||
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
|
popUp={popUp}
|
||||||
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
handlePopUpClose={handlePopUpClose}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { RequestSecretForm } from "./RequestSecretForm";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["createSecretRequest"]>;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<["createSecretRequest"]>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddSecretRequestModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.createSecretRequest?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("createSecretRequest", isOpen);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent
|
||||||
|
title="Request a Secret"
|
||||||
|
subTitle="Securely request one off secrets from your team or people outside your organization."
|
||||||
|
>
|
||||||
|
<RequestSecretForm />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { faCheck, faCopy, faRedo } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
|
import { useTimedReset } from "@app/hooks";
|
||||||
|
import { SecretSharingAccessType, useCreateSecretRequest } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
accessType: z
|
||||||
|
.nativeEnum(SecretSharingAccessType)
|
||||||
|
.default(SecretSharingAccessType.Anyone)
|
||||||
|
.optional(),
|
||||||
|
expiresIn: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
const expiresInOptions = [
|
||||||
|
{ label: "5 min", value: 5 * 60 * 1000 },
|
||||||
|
{ label: "30 min", value: 30 * 60 * 1000 },
|
||||||
|
{ label: "1 hour", value: 60 * 60 * 1000 },
|
||||||
|
{ label: "1 day", value: 24 * 60 * 60 * 1000 },
|
||||||
|
{ label: "7 days", value: 7 * 24 * 60 * 60 * 1000 },
|
||||||
|
{ label: "14 days", value: 14 * 24 * 60 * 60 * 1000 },
|
||||||
|
{ label: "30 days", value: 30 * 24 * 60 * 60 * 1000 }
|
||||||
|
];
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
export const RequestSecretForm = () => {
|
||||||
|
const [secretLink, setSecretLink] = useState("");
|
||||||
|
const [, isCopyingSecret, setCopyTextSecret] = useTimedReset<string>({
|
||||||
|
initialState: "Copy to clipboard"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: createSecretRequest } = useCreateSecretRequest();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ name, accessType, expiresIn }: FormData) => {
|
||||||
|
const expiresAt = new Date(new Date().getTime() + Number(expiresIn));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { id } = await createSecretRequest({
|
||||||
|
name,
|
||||||
|
accessType,
|
||||||
|
expiresAt
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = `${window.location.origin}/secret-request/secret/${id}`;
|
||||||
|
|
||||||
|
setSecretLink(link);
|
||||||
|
reset();
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(link);
|
||||||
|
setCopyTextSecret("secret");
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Shared secret link copied to clipboard.",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to create a shared secret.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasSecretLink = Boolean(secretLink);
|
||||||
|
|
||||||
|
return !hasSecretLink ? (
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Name (Optional)" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} placeholder="API Key" type="text" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresIn"
|
||||||
|
defaultValue="3600000"
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Expires In" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{expiresInOptions.map(({ label, value: expiresInValue }) => (
|
||||||
|
<SelectItem value={String(expiresInValue || "")} key={label}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="accessType"
|
||||||
|
defaultValue={SecretSharingAccessType.Organization}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
tooltipText="Select who is able to input the secret"
|
||||||
|
label="General Access"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectItem value={SecretSharingAccessType.Anyone}>Anyone</SelectItem>
|
||||||
|
<SelectItem value={SecretSharingAccessType.Organization}>
|
||||||
|
People within your organization
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="mt-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Create Request Link
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="mr-2 flex items-center justify-end rounded-md bg-white/[0.05] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{secretLink}</p>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative ml-2"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(secretLink);
|
||||||
|
setCopyTextSecret("Copied");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isCopyingSecret ? faCheck : faCopy} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
className="mt-4 w-full bg-mineshaft-700 py-3 text-bunker-200"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setSecretLink("")}
|
||||||
|
rightIcon={<FontAwesomeIcon icon={faRedo} className="pl-2" />}
|
||||||
|
>
|
||||||
|
Request Another Secret
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
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 { useDeleteSecretRequest } from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { AddSecretRequestModal } from "./AddSecretRequestModal";
|
||||||
|
import { RequestedSecretsTable } from "./RequestedSecretsTable";
|
||||||
|
import { RevealSecretValueModal } from "./RevealSecretValueModal";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<
|
||||||
|
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
|
||||||
|
>,
|
||||||
|
data?: any
|
||||||
|
) => void;
|
||||||
|
popUp: UsePopUpState<
|
||||||
|
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
|
||||||
|
>;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<
|
||||||
|
["createSecretRequest", "deleteSecretRequestConfirmation", "revealSecretRequestValue"]
|
||||||
|
>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
handlePopUpClose: (
|
||||||
|
popUpName: keyof UsePopUpState<["deleteSecretRequestConfirmation", "revealSecretRequestValue"]>
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeleteModalData = { name: string; id: string };
|
||||||
|
|
||||||
|
export const RequestSecretTab = ({
|
||||||
|
handlePopUpOpen,
|
||||||
|
popUp,
|
||||||
|
handlePopUpToggle,
|
||||||
|
handlePopUpClose
|
||||||
|
}: Props) => {
|
||||||
|
const { mutateAsync: deleteSecretRequest } = useDeleteSecretRequest();
|
||||||
|
|
||||||
|
const onDeleteApproved = async () => {
|
||||||
|
try {
|
||||||
|
await deleteSecretRequest({
|
||||||
|
secretRequestId: popUp.deleteSecretRequestConfirmation.data?.id
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully deleted secret request",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("deleteSecretRequestConfirmation");
|
||||||
|
} 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">
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Secret Requests</p>
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("createSecretRequest");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Request Secret
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<RequestedSecretsTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
<AddSecretRequestModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<RevealSecretValueModal
|
||||||
|
isOpen={popUp.revealSecretRequestValue.isOpen}
|
||||||
|
popUp={popUp}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("revealSecretRequestValue", isOpen)}
|
||||||
|
/>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteSecretRequestConfirmation.isOpen}
|
||||||
|
title={`Delete ${
|
||||||
|
(popUp?.deleteSecretRequestConfirmation?.data as DeleteModalData)?.name || " "
|
||||||
|
} secret request?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteSecretRequestConfirmation", isOpen)}
|
||||||
|
deleteKey={(popUp?.deleteSecretRequestConfirmation?.data as DeleteModalData)?.name}
|
||||||
|
onClose={() => handlePopUpClose("deleteSecretRequestConfirmation")}
|
||||||
|
onDeleteApproved={onDeleteApproved}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
/* eslint-disable no-nested-ternary */
|
||||||
|
/* eslint-disable no-extra-boolean-cast */
|
||||||
|
import { faCopy, faEye, faSpinner, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Badge, IconButton, Td, Tooltip, Tr } from "@app/components/v2";
|
||||||
|
import { TSharedSecret, useRevealSecretRequestValue } from "@app/hooks/api/secretSharing";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
export const RequestedSecretsRow = ({
|
||||||
|
row,
|
||||||
|
handlePopUpOpen
|
||||||
|
}: {
|
||||||
|
row: TSharedSecret;
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["deleteSecretRequestConfirmation", "revealSecretRequestValue"]>,
|
||||||
|
data: unknown
|
||||||
|
) => void;
|
||||||
|
}) => {
|
||||||
|
const { mutateAsync: revealSecretValue, isPending } = useRevealSecretRequestValue();
|
||||||
|
|
||||||
|
let isExpired = false;
|
||||||
|
if (row.expiresAt !== null && new Date(row.expiresAt) < new Date()) {
|
||||||
|
isExpired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={row.id}>
|
||||||
|
<Td>{row.name ? `${row.name}` : "-"}</Td>
|
||||||
|
<Td>
|
||||||
|
{isExpired && !row.encryptedSecret ? (
|
||||||
|
<Badge variant="danger">Expired</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant={row.encryptedSecret ? "success" : "primary"}>
|
||||||
|
{row.encryptedSecret ? "Secret Provided" : "Pending Secret"}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
<Td>{`${format(new Date(row.createdAt), "yyyy-MM-dd - HH:mm a")}`}</Td>
|
||||||
|
<Td>{row.expiresAt ? format(new Date(row.expiresAt), "yyyy-MM-dd - HH:mm a") : "-"}</Td>
|
||||||
|
<Td>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
row.encryptedSecret
|
||||||
|
? "Reveal shared secret"
|
||||||
|
: "Secret value must be provided before it can be viewed."
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
isDisabled={!row.encryptedSecret}
|
||||||
|
className={row.encryptedSecret ? "" : "opacity-50"}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const secretRequest = await revealSecretValue({
|
||||||
|
id: row.id
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("revealSecretRequestValue", {
|
||||||
|
secretValue: secretRequest.secretValue,
|
||||||
|
secretRequestName: secretRequest.name
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpOpen("revealSecretRequestValue", {
|
||||||
|
secretValue: secretRequest.secretValue,
|
||||||
|
secretRequestName: secretRequest.name
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="reveal"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className={isPending ? "animate-spin" : ""}
|
||||||
|
icon={!isPending ? faEye : faSpinner}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
isDisabled={Boolean(row.encryptedSecret) || isExpired}
|
||||||
|
className={Boolean(row.encryptedSecret) || isExpired ? "opacity-50" : ""}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
`${window.location.origin}/secret-request/secret/${row.id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Shared secret link copied to clipboard.",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="copy link"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCopy} />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<Tooltip content="Delete Secret Request">
|
||||||
|
<IconButton
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handlePopUpOpen("deleteSecretRequestConfirmation", {
|
||||||
|
name: "delete",
|
||||||
|
id: row.id
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="delete"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { faKey } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import {
|
||||||
|
EmptyState,
|
||||||
|
Pagination,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useGetSecretRequests } from "@app/hooks/api/secretSharing";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { RequestedSecretsRow } from "./RequestedSecretsRow";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["deleteSecretRequestConfirmation", "revealSecretRequestValue"]>,
|
||||||
|
data: unknown
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RequestedSecretsTable = ({ handlePopUpOpen }: Props) => {
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [perPage, setPerPage] = useState(10);
|
||||||
|
const { isPending, data } = useGetSecretRequests({
|
||||||
|
offset: (page - 1) * perPage,
|
||||||
|
limit: perPage
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Status</Th>
|
||||||
|
<Th>Created At</Th>
|
||||||
|
<Th>Valid Until</Th>
|
||||||
|
<Th aria-label="button" className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isPending && <TableSkeleton columns={7} innerKey="shared-secrets" />}
|
||||||
|
{!isPending &&
|
||||||
|
data?.secrets?.map((row) => (
|
||||||
|
<RequestedSecretsRow key={row.id} row={row} handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isPending &&
|
||||||
|
data?.secrets &&
|
||||||
|
data?.totalCount >= perPage &&
|
||||||
|
data?.totalCount !== undefined && (
|
||||||
|
<Pagination
|
||||||
|
count={data.totalCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isPending && !data?.secrets?.length && (
|
||||||
|
<EmptyState title="No secrets shared yet" icon={faKey} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Button, IconButton, Modal, ModalClose, ModalContent, Tooltip } from "@app/components/v2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
|
popUp: UsePopUpState<["revealSecretRequestValue"]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContentProps = {
|
||||||
|
secretValue: string;
|
||||||
|
secretRequestName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Content = ({ secretValue, secretRequestName }: ContentProps) => {
|
||||||
|
const [isSecretValueCopied, setIsSecretValueCopied] = useToggle(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{secretRequestName && (
|
||||||
|
<p className="mb-8 text-sm text-mineshaft-200">
|
||||||
|
Shared secret value for <strong>{secretRequestName}</strong>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
|
<p className="mr-4 break-all">{secretValue}</p>
|
||||||
|
<Tooltip content="Click to copy">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="copy icon"
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="group relative"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(secretValue);
|
||||||
|
setIsSecretValueCopied.on();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isSecretValueCopied ? faCheck : faCopy} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 flex w-full items-center justify-between gap-2">
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="primary">Close</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RevealSecretValueModal = ({ isOpen, onOpenChange, popUp }: Props) => {
|
||||||
|
const data = popUp.revealSecretRequestValue.data as {
|
||||||
|
secretValue: string;
|
||||||
|
secretRequestName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const title = data?.secretRequestName
|
||||||
|
? `Shared secret value for secret request ${data.secretRequestName}`
|
||||||
|
: "Shared secret value";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent title={title}>
|
||||||
|
<Content secretRequestName={data?.secretRequestName} secretValue={data?.secretValue} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+23
-15
@@ -1,27 +1,40 @@
|
|||||||
import { Helmet } from "react-helmet";
|
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, DeleteActionModal } from "@app/components/v2";
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { useDeleteSharedSecret } from "@app/hooks/api";
|
||||||
import { useDeleteSharedSecret } from "@app/hooks/api/secretSharing";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { AddShareSecretModal } from "./AddShareSecretModal";
|
import { AddShareSecretModal } from "./AddShareSecretModal";
|
||||||
import { ShareSecretsTable } from "./ShareSecretsTable";
|
import { ShareSecretsTable } from "./ShareSecretsTable";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>,
|
||||||
|
data?: any
|
||||||
|
) => void;
|
||||||
|
popUp: UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<["createSharedSecret", "deleteSharedSecretConfirmation"]>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
handlePopUpClose: (popUpName: keyof UsePopUpState<["deleteSharedSecretConfirmation"]>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
type DeleteModalData = { name: string; id: string };
|
type DeleteModalData = { name: string; id: string };
|
||||||
|
|
||||||
export const ShareSecretSection = () => {
|
export const ShareSecretTab = ({
|
||||||
const deleteSharedSecret = useDeleteSharedSecret();
|
handlePopUpOpen,
|
||||||
const { popUp, handlePopUpToggle, handlePopUpClose, handlePopUpOpen } = usePopUp([
|
popUp,
|
||||||
"createSharedSecret",
|
handlePopUpToggle,
|
||||||
"deleteSharedSecretConfirmation"
|
handlePopUpClose
|
||||||
] as const);
|
}: Props) => {
|
||||||
|
const deleteSecretShare = useDeleteSharedSecret();
|
||||||
|
|
||||||
const onDeleteApproved = async () => {
|
const onDeleteApproved = async () => {
|
||||||
try {
|
try {
|
||||||
deleteSharedSecret.mutateAsync({
|
deleteSecretShare.mutateAsync({
|
||||||
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id
|
sharedSecretId: (popUp?.deleteSharedSecretConfirmation?.data as DeleteModalData)?.id
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -41,11 +54,6 @@ export const ShareSecretSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<Helmet>
|
|
||||||
<title>Secret Sharing</title>
|
|
||||||
<link rel="icon" href="/infisical.ico" />
|
|
||||||
<meta property="og:image" content="/images/message.png" />
|
|
||||||
</Helmet>
|
|
||||||
<div className="mb-4 flex justify-between">
|
<div className="mb-4 flex justify-between">
|
||||||
<p className="text-xl font-semibold text-mineshaft-100">Shared Secrets</p>
|
<p className="text-xl font-semibold text-mineshaft-100">Shared Secrets</p>
|
||||||
<Button
|
<Button
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
import { faHome } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { createFileRoute, linkOptions } from "@tanstack/react-router";
|
import { createFileRoute, linkOptions, stripSearchParams } from "@tanstack/react-router";
|
||||||
|
import { zodValidator } from "@tanstack/zod-adapter";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { SecretSharingPage } from "./SecretSharingPage";
|
import { SecretSharingPage } from "./SecretSharingPage";
|
||||||
|
|
||||||
|
const SecretSharingQueryParams = z.object({
|
||||||
|
selectedTab: z.string().catch("")
|
||||||
|
});
|
||||||
|
|
||||||
export const Route = createFileRoute(
|
export const Route = createFileRoute(
|
||||||
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
|
"/_authenticate/_inject-org-details/_org-layout/organization/secret-sharing"
|
||||||
)({
|
)({
|
||||||
component: SecretSharingPage,
|
component: SecretSharingPage,
|
||||||
|
|
||||||
|
validateSearch: zodValidator(SecretSharingQueryParams),
|
||||||
|
search: {
|
||||||
|
middlewares: [stripSearchParams({ selectedTab: "" })]
|
||||||
|
},
|
||||||
context: () => ({
|
context: () => ({
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{
|
{
|
||||||
@@ -16,7 +27,7 @@ export const Route = createFileRoute(
|
|||||||
link: linkOptions({ to: "/" })
|
link: linkOptions({ to: "/" })
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "secret sharing"
|
label: "Secret Sharing"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Helmet } from "react-helmet";
|
||||||
|
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
import { addSeconds, formatISO } from "date-fns";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { SessionStorageKeys } from "@app/const";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
|
import { useGetSecretRequestById } from "@app/hooks/api/secretSharing";
|
||||||
|
|
||||||
|
import { SecretRequestErrorContainer } from "./components/SecretErrorContainer";
|
||||||
|
import { SecretRequestContainer } from "./components/SecretRequestContainer";
|
||||||
|
import { SecretRequestSuccessContainer } from "./components/SecretRequestSuccessContainer";
|
||||||
|
import { SecretValueAlreadySharedContainer } from "./components/SecretValueAlreadySharedContainer";
|
||||||
|
|
||||||
|
export const ViewSecretRequestByIDPage = () => {
|
||||||
|
const id = useParams({
|
||||||
|
from: ROUTE_PATHS.Public.ViewSecretRequestByIDPage.id,
|
||||||
|
select: (el) => el.secretRequestId
|
||||||
|
});
|
||||||
|
|
||||||
|
const [step, setStep] = useState<"set-value" | "success">("set-value");
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: secretRequest,
|
||||||
|
error,
|
||||||
|
isPending
|
||||||
|
} = useGetSecretRequestById({
|
||||||
|
secretRequestId: id
|
||||||
|
});
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const statusCode = ((error as AxiosError)?.response?.data as { statusCode: number })?.statusCode;
|
||||||
|
const message = ((error as AxiosError)?.response?.data as { message: string })?.message;
|
||||||
|
|
||||||
|
const isUnauthorized = statusCode === 401;
|
||||||
|
const isForbidden = statusCode === 403;
|
||||||
|
const isInvalidCredential = message === "Invalid credentials";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isUnauthorized && !isInvalidCredential) {
|
||||||
|
// persist current URL in session storage so that we can come back to this after successful login
|
||||||
|
sessionStorage.setItem(
|
||||||
|
SessionStorageKeys.ORG_LOGIN_SUCCESS_REDIRECT_URL,
|
||||||
|
JSON.stringify({
|
||||||
|
expiry: formatISO(addSeconds(new Date(), 60)),
|
||||||
|
data: window.location.href
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "info",
|
||||||
|
text: "Login is required in order to access the shared secret."
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate({
|
||||||
|
to: "/login"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isForbidden) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "You do not have access to this shared secret."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<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="" />
|
||||||
|
</Helmet>
|
||||||
|
<div className="flex h-screen flex-col justify-between overflow-auto bg-gradient-to-tr from-mineshaft-700 to-bunker-800 text-gray-200 dark:[color-scheme:dark]">
|
||||||
|
<div />
|
||||||
|
<div className="mx-auto w-full max-w-xl px-4 py-4 md:px-0">
|
||||||
|
<div className="mb-8 text-center">
|
||||||
|
<div className="mb-4 flex justify-center pt-8">
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com">
|
||||||
|
<img
|
||||||
|
src="/images/gradientLogo.svg"
|
||||||
|
height={90}
|
||||||
|
width={120}
|
||||||
|
alt="Infisical logo"
|
||||||
|
className="cursor-pointer"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1 className="bg-gradient-to-b from-white to-bunker-200 bg-clip-text text-center text-4xl font-medium text-transparent">
|
||||||
|
{step === "set-value" ? "Secret Request" : "Secret request shared"}
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-mineshaft-300">
|
||||||
|
Secret requested by {secretRequest?.requester.username} from the{" "}
|
||||||
|
{secretRequest?.requester.organizationName} organization
|
||||||
|
</p>
|
||||||
|
<p className="text-md mt-2">
|
||||||
|
Powered by{" "}
|
||||||
|
<a
|
||||||
|
href="https://github.com/infisical/infisical"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||||
|
>
|
||||||
|
Infisical →
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{!isPending && (
|
||||||
|
<>
|
||||||
|
{!error &&
|
||||||
|
secretRequest &&
|
||||||
|
step === "set-value" &&
|
||||||
|
!secretRequest.isSecretValueSet && (
|
||||||
|
<SecretRequestContainer
|
||||||
|
onSuccess={() => setStep("success")}
|
||||||
|
secretRequestId={id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{secretRequest?.isSecretValueSet && <SecretValueAlreadySharedContainer />}
|
||||||
|
{error && !isInvalidCredential && !isUnauthorized && <SecretRequestErrorContainer />}
|
||||||
|
{step === "success" && (
|
||||||
|
<SecretRequestSuccessContainer
|
||||||
|
requesterUsername={secretRequest!.requester.username}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="m-auto my-8 flex w-full">
|
||||||
|
<div className="w-full border-t border-mineshaft-600" />
|
||||||
|
</div>
|
||||||
|
<div className="m-auto flex w-full flex-col rounded-md border border-primary-500/30 bg-primary/5 p-6 pt-5">
|
||||||
|
<p className="w-full pb-2 text-lg font-semibold text-mineshaft-100 md:pb-3 md:text-xl">
|
||||||
|
Open source{" "}
|
||||||
|
<span className="bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent">
|
||||||
|
secret management
|
||||||
|
</span>{" "}
|
||||||
|
for developers
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col items-start sm:flex-row sm:items-center">
|
||||||
|
<p className="md:text-md text-md mr-4">
|
||||||
|
<a
|
||||||
|
href="https://github.com/infisical/infisical"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-bold bg-gradient-to-tr from-yellow-500 to-primary-500 bg-clip-text text-transparent"
|
||||||
|
>
|
||||||
|
Infisical
|
||||||
|
</a>{" "}
|
||||||
|
is the all-in-one secret management platform to securely manage secrets, configs,
|
||||||
|
and certificates across your team and infrastructure.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 cursor-pointer sm:mt-0">
|
||||||
|
<a target="_blank" rel="noopener noreferrer" href="https://infisical.com">
|
||||||
|
<div className="flex items-center justify-between rounded-md border border-mineshaft-400/40 bg-mineshaft-600 px-3 py-2 duration-200 hover:border-primary/60 hover:bg-primary/20 hover:text-white">
|
||||||
|
<p className="mr-4 whitespace-nowrap">Try Infisical</p>
|
||||||
|
<FontAwesomeIcon icon={faArrowRight} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-mineshaft-600 p-2">
|
||||||
|
<p className="text-center text-sm text-mineshaft-300">
|
||||||
|
Made with ❤️ by{" "}
|
||||||
|
<a className="text-primary" href="https://infisical.com">
|
||||||
|
Infisical
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
156 2nd st, 3rd Floor, San Francisco, California, 94105, United States. 🇺🇸
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
import { faKey } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
export const SecretRequestErrorContainer = () => {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<FontAwesomeIcon icon={faKey} size="2x" />
|
||||||
|
<p className="mt-4">The secret request you are looking is missing or has expired.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { faArrowRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Button, FormControl, TextArea } from "@app/components/v2";
|
||||||
|
import { useSetSecretRequestValue } from "@app/hooks/api";
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
secretValue: z.string().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onSuccess: () => void;
|
||||||
|
secretRequestId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretRequestContainer = ({ onSuccess, secretRequestId }: Props) => {
|
||||||
|
const form = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema)
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: setSecretValue, isPending } = useSetSecretRequestValue();
|
||||||
|
|
||||||
|
const onSubmit = async (data: FormData) => {
|
||||||
|
await setSecretValue({
|
||||||
|
id: secretRequestId,
|
||||||
|
secretValue: data.secretValue
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
title: "Secret request value shared",
|
||||||
|
text: "The secret request value has been shared",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
onSuccess();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-4">
|
||||||
|
<div className="flex items-center justify-between rounded-md bg-white/[0.05] p-2 text-base text-gray-400">
|
||||||
|
<div className="w-full">
|
||||||
|
<Controller
|
||||||
|
control={form.control}
|
||||||
|
name="secretValue"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
label="Secret Value"
|
||||||
|
>
|
||||||
|
<TextArea {...field} rows={10} reSize="none" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
isLoading={isPending}
|
||||||
|
isDisabled={isPending}
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="w-full"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Share Secret
|
||||||
|
<FontAwesomeIcon className="ml-2" icon={faArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { faCheck } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
requesterUsername: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretRequestSuccessContainer = ({ requesterUsername }: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mx-auto w-min rounded-md border border-mineshaft-800 bg-mineshaft-600 p-3">
|
||||||
|
<FontAwesomeIcon icon={faCheck} size="2x" className="text-primary-500" />
|
||||||
|
</div>
|
||||||
|
<p className="text-md mt-2 font-semibold">Secret Shared</p>
|
||||||
|
<p className="mt-2 text-sm text-mineshaft-300">
|
||||||
|
<strong>{requesterUsername}</strong> has now been notified of your shared secret, and will
|
||||||
|
be able to access it shortly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import { faKey } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
export const SecretValueAlreadySharedContainer = () => {
|
||||||
|
return (
|
||||||
|
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-800 p-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="mx-auto w-min rounded-md border border-mineshaft-800 bg-mineshaft-600 p-3">
|
||||||
|
<FontAwesomeIcon icon={faKey} size="2x" className="text-primary-500" />
|
||||||
|
</div>
|
||||||
|
<p className="text-md mt-2 font-semibold">Secret Already Shared</p>
|
||||||
|
<p className="mt-2 text-sm text-mineshaft-300">
|
||||||
|
A secret value has already been shared for this secret request.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
|
||||||
|
|
||||||
|
import { ViewSecretRequestByIDPage } from "./ViewSecretRequestByIDPage";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/secret-request/secret/$secretRequestId")({
|
||||||
|
component: ViewSecretRequestByIDPage,
|
||||||
|
beforeLoad: async ({ context }) => {
|
||||||
|
await context.queryClient
|
||||||
|
.ensureQueryData({
|
||||||
|
queryKey: authKeys.getAuthToken,
|
||||||
|
queryFn: fetchAuthToken
|
||||||
|
})
|
||||||
|
.catch(() => undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -28,6 +28,7 @@ import { Route as authPasswordSetupPageRouteImport } from './pages/auth/Password
|
|||||||
import { Route as userLayoutImport } from './pages/user/layout'
|
import { Route as userLayoutImport } from './pages/user/layout'
|
||||||
import { Route as organizationLayoutImport } from './pages/organization/layout'
|
import { Route as organizationLayoutImport } from './pages/organization/layout'
|
||||||
import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route'
|
import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route'
|
||||||
|
import { Route as publicViewSecretRequestByIDPageRouteImport } from './pages/public/ViewSecretRequestByIDPage/route'
|
||||||
import { Route as authSignUpSsoPageRouteImport } from './pages/auth/SignUpSsoPage/route'
|
import { Route as authSignUpSsoPageRouteImport } from './pages/auth/SignUpSsoPage/route'
|
||||||
import { Route as authLoginSsoPageRouteImport } from './pages/auth/LoginSsoPage/route'
|
import { Route as authLoginSsoPageRouteImport } from './pages/auth/LoginSsoPage/route'
|
||||||
import { Route as authSelectOrgPageRouteImport } from './pages/auth/SelectOrgPage/route'
|
import { Route as authSelectOrgPageRouteImport } from './pages/auth/SelectOrgPage/route'
|
||||||
@@ -354,6 +355,13 @@ const publicViewSharedSecretByIDPageRouteRoute =
|
|||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const publicViewSecretRequestByIDPageRouteRoute =
|
||||||
|
publicViewSecretRequestByIDPageRouteImport.update({
|
||||||
|
id: '/secret-request/secret/$secretRequestId',
|
||||||
|
path: '/secret-request/secret/$secretRequestId',
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
const authSignUpSsoPageRouteRoute = authSignUpSsoPageRouteImport.update({
|
const authSignUpSsoPageRouteRoute = authSignUpSsoPageRouteImport.update({
|
||||||
id: '/sso',
|
id: '/sso',
|
||||||
path: '/sso',
|
path: '/sso',
|
||||||
@@ -1760,6 +1768,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof authSignUpSsoPageRouteImport
|
preLoaderRoute: typeof authSignUpSsoPageRouteImport
|
||||||
parentRoute: typeof RestrictLoginSignupSignupImport
|
parentRoute: typeof RestrictLoginSignupSignupImport
|
||||||
}
|
}
|
||||||
|
'/secret-request/secret/$secretRequestId': {
|
||||||
|
id: '/secret-request/secret/$secretRequestId'
|
||||||
|
path: '/secret-request/secret/$secretRequestId'
|
||||||
|
fullPath: '/secret-request/secret/$secretRequestId'
|
||||||
|
preLoaderRoute: typeof publicViewSecretRequestByIDPageRouteImport
|
||||||
|
parentRoute: typeof rootRoute
|
||||||
|
}
|
||||||
'/shared/secret/$secretId': {
|
'/shared/secret/$secretId': {
|
||||||
id: '/shared/secret/$secretId'
|
id: '/shared/secret/$secretId'
|
||||||
path: '/shared/secret/$secretId'
|
path: '/shared/secret/$secretId'
|
||||||
@@ -3643,6 +3658,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/login/select-organization': typeof authSelectOrgPageRouteRoute
|
'/login/select-organization': typeof authSelectOrgPageRouteRoute
|
||||||
'/login/sso': typeof authLoginSsoPageRouteRoute
|
'/login/sso': typeof authLoginSsoPageRouteRoute
|
||||||
'/signup/sso': typeof authSignUpSsoPageRouteRoute
|
'/signup/sso': typeof authSignUpSsoPageRouteRoute
|
||||||
|
'/secret-request/secret/$secretRequestId': typeof publicViewSecretRequestByIDPageRouteRoute
|
||||||
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
|
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
|
||||||
'/admin': typeof adminLayoutRouteWithChildren
|
'/admin': typeof adminLayoutRouteWithChildren
|
||||||
'/personal-settings/': typeof userPersonalSettingsPageRouteRoute
|
'/personal-settings/': typeof userPersonalSettingsPageRouteRoute
|
||||||
@@ -3817,6 +3833,7 @@ export interface FileRoutesByTo {
|
|||||||
'/login/select-organization': typeof authSelectOrgPageRouteRoute
|
'/login/select-organization': typeof authSelectOrgPageRouteRoute
|
||||||
'/login/sso': typeof authLoginSsoPageRouteRoute
|
'/login/sso': typeof authLoginSsoPageRouteRoute
|
||||||
'/signup/sso': typeof authSignUpSsoPageRouteRoute
|
'/signup/sso': typeof authSignUpSsoPageRouteRoute
|
||||||
|
'/secret-request/secret/$secretRequestId': typeof publicViewSecretRequestByIDPageRouteRoute
|
||||||
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
|
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
|
||||||
'/admin': typeof adminOverviewPageRouteRoute
|
'/admin': typeof adminOverviewPageRouteRoute
|
||||||
'/login/provider/error': typeof authProviderErrorPageRouteRoute
|
'/login/provider/error': typeof authProviderErrorPageRouteRoute
|
||||||
@@ -3991,6 +4008,7 @@ export interface FileRoutesById {
|
|||||||
'/_restrict-login-signup/login/select-organization': typeof authSelectOrgPageRouteRoute
|
'/_restrict-login-signup/login/select-organization': typeof authSelectOrgPageRouteRoute
|
||||||
'/_restrict-login-signup/login/sso': typeof authLoginSsoPageRouteRoute
|
'/_restrict-login-signup/login/sso': typeof authLoginSsoPageRouteRoute
|
||||||
'/_restrict-login-signup/signup/sso': typeof authSignUpSsoPageRouteRoute
|
'/_restrict-login-signup/signup/sso': typeof authSignUpSsoPageRouteRoute
|
||||||
|
'/secret-request/secret/$secretRequestId': typeof publicViewSecretRequestByIDPageRouteRoute
|
||||||
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
|
'/shared/secret/$secretId': typeof publicViewSharedSecretByIDPageRouteRoute
|
||||||
'/_authenticate/_inject-org-details/_org-layout': typeof organizationLayoutRouteWithChildren
|
'/_authenticate/_inject-org-details/_org-layout': typeof organizationLayoutRouteWithChildren
|
||||||
'/_authenticate/_inject-org-details/admin': typeof AuthenticateInjectOrgDetailsAdminRouteWithChildren
|
'/_authenticate/_inject-org-details/admin': typeof AuthenticateInjectOrgDetailsAdminRouteWithChildren
|
||||||
@@ -4176,6 +4194,7 @@ export interface FileRouteTypes {
|
|||||||
| '/login/select-organization'
|
| '/login/select-organization'
|
||||||
| '/login/sso'
|
| '/login/sso'
|
||||||
| '/signup/sso'
|
| '/signup/sso'
|
||||||
|
| '/secret-request/secret/$secretRequestId'
|
||||||
| '/shared/secret/$secretId'
|
| '/shared/secret/$secretId'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
| '/personal-settings/'
|
| '/personal-settings/'
|
||||||
@@ -4349,6 +4368,7 @@ export interface FileRouteTypes {
|
|||||||
| '/login/select-organization'
|
| '/login/select-organization'
|
||||||
| '/login/sso'
|
| '/login/sso'
|
||||||
| '/signup/sso'
|
| '/signup/sso'
|
||||||
|
| '/secret-request/secret/$secretRequestId'
|
||||||
| '/shared/secret/$secretId'
|
| '/shared/secret/$secretId'
|
||||||
| '/admin'
|
| '/admin'
|
||||||
| '/login/provider/error'
|
| '/login/provider/error'
|
||||||
@@ -4521,6 +4541,7 @@ export interface FileRouteTypes {
|
|||||||
| '/_restrict-login-signup/login/select-organization'
|
| '/_restrict-login-signup/login/select-organization'
|
||||||
| '/_restrict-login-signup/login/sso'
|
| '/_restrict-login-signup/login/sso'
|
||||||
| '/_restrict-login-signup/signup/sso'
|
| '/_restrict-login-signup/signup/sso'
|
||||||
|
| '/secret-request/secret/$secretRequestId'
|
||||||
| '/shared/secret/$secretId'
|
| '/shared/secret/$secretId'
|
||||||
| '/_authenticate/_inject-org-details/_org-layout'
|
| '/_authenticate/_inject-org-details/_org-layout'
|
||||||
| '/_authenticate/_inject-org-details/admin'
|
| '/_authenticate/_inject-org-details/admin'
|
||||||
@@ -4689,6 +4710,7 @@ export interface RootRouteChildren {
|
|||||||
publicShareSecretPageRouteRoute: typeof publicShareSecretPageRouteRoute
|
publicShareSecretPageRouteRoute: typeof publicShareSecretPageRouteRoute
|
||||||
middlewaresAuthenticateRoute: typeof middlewaresAuthenticateRouteWithChildren
|
middlewaresAuthenticateRoute: typeof middlewaresAuthenticateRouteWithChildren
|
||||||
middlewaresRestrictLoginSignupRoute: typeof middlewaresRestrictLoginSignupRouteWithChildren
|
middlewaresRestrictLoginSignupRoute: typeof middlewaresRestrictLoginSignupRouteWithChildren
|
||||||
|
publicViewSecretRequestByIDPageRouteRoute: typeof publicViewSecretRequestByIDPageRouteRoute
|
||||||
publicViewSharedSecretByIDPageRouteRoute: typeof publicViewSharedSecretByIDPageRouteRoute
|
publicViewSharedSecretByIDPageRouteRoute: typeof publicViewSharedSecretByIDPageRouteRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4699,6 +4721,8 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
middlewaresAuthenticateRoute: middlewaresAuthenticateRouteWithChildren,
|
middlewaresAuthenticateRoute: middlewaresAuthenticateRouteWithChildren,
|
||||||
middlewaresRestrictLoginSignupRoute:
|
middlewaresRestrictLoginSignupRoute:
|
||||||
middlewaresRestrictLoginSignupRouteWithChildren,
|
middlewaresRestrictLoginSignupRouteWithChildren,
|
||||||
|
publicViewSecretRequestByIDPageRouteRoute:
|
||||||
|
publicViewSecretRequestByIDPageRouteRoute,
|
||||||
publicViewSharedSecretByIDPageRouteRoute:
|
publicViewSharedSecretByIDPageRouteRoute:
|
||||||
publicViewSharedSecretByIDPageRouteRoute,
|
publicViewSharedSecretByIDPageRouteRoute,
|
||||||
}
|
}
|
||||||
@@ -4718,6 +4742,7 @@ export const routeTree = rootRoute
|
|||||||
"/share-secret",
|
"/share-secret",
|
||||||
"/_authenticate",
|
"/_authenticate",
|
||||||
"/_restrict-login-signup",
|
"/_restrict-login-signup",
|
||||||
|
"/secret-request/secret/$secretRequestId",
|
||||||
"/shared/secret/$secretId"
|
"/shared/secret/$secretId"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -4843,6 +4868,9 @@ export const routeTree = rootRoute
|
|||||||
"filePath": "auth/SignUpSsoPage/route.tsx",
|
"filePath": "auth/SignUpSsoPage/route.tsx",
|
||||||
"parent": "/_restrict-login-signup/signup"
|
"parent": "/_restrict-login-signup/signup"
|
||||||
},
|
},
|
||||||
|
"/secret-request/secret/$secretRequestId": {
|
||||||
|
"filePath": "public/ViewSecretRequestByIDPage/route.tsx"
|
||||||
|
},
|
||||||
"/shared/secret/$secretId": {
|
"/shared/secret/$secretId": {
|
||||||
"filePath": "public/ViewSharedSecretByIDPage/route.tsx"
|
"filePath": "public/ViewSharedSecretByIDPage/route.tsx"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ const sshRoutes = route("/ssh/$projectId", [
|
|||||||
export const routes = rootRoute("root.tsx", [
|
export const routes = rootRoute("root.tsx", [
|
||||||
index("index.tsx"),
|
index("index.tsx"),
|
||||||
route("/shared/secret/$secretId", "public/ViewSharedSecretByIDPage/route.tsx"),
|
route("/shared/secret/$secretId", "public/ViewSharedSecretByIDPage/route.tsx"),
|
||||||
|
route("/secret-request/secret/$secretRequestId", "public/ViewSecretRequestByIDPage/route.tsx"),
|
||||||
route("/share-secret", "public/ShareSecretPage/route.tsx"),
|
route("/share-secret", "public/ShareSecretPage/route.tsx"),
|
||||||
route("/cli-redirect", "auth/CliRedirectPage/route.tsx"),
|
route("/cli-redirect", "auth/CliRedirectPage/route.tsx"),
|
||||||
middleware("restrict-login-signup.tsx", [
|
middleware("restrict-login-signup.tsx", [
|
||||||
|
|||||||
Reference in New Issue
Block a user