mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-12 13:29:11 +00:00
Merge remote-tracking branch 'origin/main' into ENG-3546
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
import { Knex } from "knex";
|
||||||
|
|
||||||
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.IdentityUniversalAuth)) {
|
||||||
|
const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutEnabled");
|
||||||
|
const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutThreshold");
|
||||||
|
const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutDurationSeconds");
|
||||||
|
const hasLockoutCounterReset = await knex.schema.hasColumn(
|
||||||
|
TableName.IdentityUniversalAuth,
|
||||||
|
"lockoutCounterResetSeconds"
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
||||||
|
if (!hasLockoutEnabled) {
|
||||||
|
t.boolean("lockoutEnabled").notNullable().defaultTo(true);
|
||||||
|
}
|
||||||
|
if (!hasLockoutThreshold) {
|
||||||
|
t.integer("lockoutThreshold").notNullable().defaultTo(3);
|
||||||
|
}
|
||||||
|
if (!hasLockoutDuration) {
|
||||||
|
t.integer("lockoutDurationSeconds").notNullable().defaultTo(300); // 5 minutes
|
||||||
|
}
|
||||||
|
if (!hasLockoutCounterReset) {
|
||||||
|
t.integer("lockoutCounterResetSeconds").notNullable().defaultTo(30); // 30 seconds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(knex: Knex): Promise<void> {
|
||||||
|
if (await knex.schema.hasTable(TableName.IdentityUniversalAuth)) {
|
||||||
|
const hasLockoutEnabled = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutEnabled");
|
||||||
|
const hasLockoutThreshold = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutThreshold");
|
||||||
|
const hasLockoutDuration = await knex.schema.hasColumn(TableName.IdentityUniversalAuth, "lockoutDurationSeconds");
|
||||||
|
const hasLockoutCounterReset = await knex.schema.hasColumn(
|
||||||
|
TableName.IdentityUniversalAuth,
|
||||||
|
"lockoutCounterResetSeconds"
|
||||||
|
);
|
||||||
|
|
||||||
|
await knex.schema.alterTable(TableName.IdentityUniversalAuth, (t) => {
|
||||||
|
if (hasLockoutEnabled) {
|
||||||
|
t.dropColumn("lockoutEnabled");
|
||||||
|
}
|
||||||
|
if (hasLockoutThreshold) {
|
||||||
|
t.dropColumn("lockoutThreshold");
|
||||||
|
}
|
||||||
|
if (hasLockoutDuration) {
|
||||||
|
t.dropColumn("lockoutDurationSeconds");
|
||||||
|
}
|
||||||
|
if (hasLockoutCounterReset) {
|
||||||
|
t.dropColumn("lockoutCounterResetSeconds");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,11 @@ export const IdentityUniversalAuthsSchema = z.object({
|
|||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
identityId: z.string().uuid(),
|
identityId: z.string().uuid(),
|
||||||
accessTokenPeriod: z.coerce.number().default(0)
|
accessTokenPeriod: z.coerce.number().default(0),
|
||||||
|
lockoutEnabled: z.boolean().default(true),
|
||||||
|
lockoutThreshold: z.number().default(3),
|
||||||
|
lockoutDurationSeconds: z.number().default(300),
|
||||||
|
lockoutCounterResetSeconds: z.number().default(30)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TIdentityUniversalAuths = z.infer<typeof IdentityUniversalAuthsSchema>;
|
export type TIdentityUniversalAuths = z.infer<typeof IdentityUniversalAuthsSchema>;
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { getConfig } from "@app/lib/config/env";
|
|||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { ActorType } from "@app/services/auth/auth-type";
|
import { ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
import { OrgPermissionAuditLogsActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
import { ProjectPermissionAuditLogsActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||||
import { TAuditLogDALFactory } from "./audit-log-dal";
|
import { TAuditLogDALFactory } from "./audit-log-dal";
|
||||||
import { TAuditLogQueueServiceFactory } from "./audit-log-queue";
|
import { TAuditLogQueueServiceFactory } from "./audit-log-queue";
|
||||||
import { EventType, TAuditLogServiceFactory } from "./audit-log-types";
|
import { EventType, TAuditLogServiceFactory } from "./audit-log-types";
|
||||||
@@ -41,7 +41,10 @@ export const auditLogServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actionProjectType: ActionProjectType.Any
|
actionProjectType: ActionProjectType.Any
|
||||||
});
|
});
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
ProjectPermissionAuditLogsActions.Read,
|
||||||
|
ProjectPermissionSub.AuditLogs
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Organization-wide logs
|
// Organization-wide logs
|
||||||
const { permission } = await permissionService.getOrgPermission(
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
@@ -52,7 +55,10 @@ export const auditLogServiceFactory = ({
|
|||||||
actorOrgId
|
actorOrgId
|
||||||
);
|
);
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
ForbiddenError.from(permission).throwUnlessCan(
|
||||||
|
OrgPermissionAuditLogsActions.Read,
|
||||||
|
OrgPermissionSubjects.AuditLogs
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
// If project ID is not provided, then we need to return all the audit logs for the organization itself.
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ export enum EventType {
|
|||||||
|
|
||||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
||||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
||||||
|
CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS = "clear-identity-universal-auth-lockouts",
|
||||||
|
|
||||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
||||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
|
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
|
||||||
@@ -867,6 +868,10 @@ interface AddIdentityUniversalAuthEvent {
|
|||||||
accessTokenMaxTTL: number;
|
accessTokenMaxTTL: number;
|
||||||
accessTokenNumUsesLimit: number;
|
accessTokenNumUsesLimit: number;
|
||||||
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
accessTokenTrustedIps: Array<TIdentityTrustedIp>;
|
||||||
|
lockoutEnabled: boolean;
|
||||||
|
lockoutThreshold: number;
|
||||||
|
lockoutDurationSeconds: number;
|
||||||
|
lockoutCounterResetSeconds: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,6 +884,10 @@ interface UpdateIdentityUniversalAuthEvent {
|
|||||||
accessTokenMaxTTL?: number;
|
accessTokenMaxTTL?: number;
|
||||||
accessTokenNumUsesLimit?: number;
|
accessTokenNumUsesLimit?: number;
|
||||||
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
|
||||||
|
lockoutEnabled?: boolean;
|
||||||
|
lockoutThreshold?: number;
|
||||||
|
lockoutDurationSeconds?: number;
|
||||||
|
lockoutCounterResetSeconds?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1038,6 +1047,13 @@ interface RevokeIdentityUniversalAuthClientSecretEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ClearIdentityUniversalAuthLockoutsEvent {
|
||||||
|
type: EventType.CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface LoginIdentityGcpAuthEvent {
|
interface LoginIdentityGcpAuthEvent {
|
||||||
type: EventType.LOGIN_IDENTITY_GCP_AUTH;
|
type: EventType.LOGIN_IDENTITY_GCP_AUTH;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -3500,6 +3516,7 @@ export type Event =
|
|||||||
| GetIdentityUniversalAuthClientSecretsEvent
|
| GetIdentityUniversalAuthClientSecretsEvent
|
||||||
| GetIdentityUniversalAuthClientSecretByIdEvent
|
| GetIdentityUniversalAuthClientSecretByIdEvent
|
||||||
| RevokeIdentityUniversalAuthClientSecretEvent
|
| RevokeIdentityUniversalAuthClientSecretEvent
|
||||||
|
| ClearIdentityUniversalAuthLockoutsEvent
|
||||||
| LoginIdentityGcpAuthEvent
|
| LoginIdentityGcpAuthEvent
|
||||||
| AddIdentityGcpAuthEvent
|
| AddIdentityGcpAuthEvent
|
||||||
| DeleteIdentityGcpAuthEvent
|
| DeleteIdentityGcpAuthEvent
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability"
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionAuditLogsActions,
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionCommitsActions,
|
ProjectPermissionCommitsActions,
|
||||||
@@ -394,7 +395,7 @@ const buildMemberPermissionRules = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.Role);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.AuditLogs);
|
can([ProjectPermissionAuditLogsActions.Read], ProjectPermissionSub.AuditLogs);
|
||||||
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
|
can([ProjectPermissionActions.Read], ProjectPermissionSub.IpAllowList);
|
||||||
|
|
||||||
// double check if all CRUD are needed for CA and Certificates
|
// double check if all CRUD are needed for CA and Certificates
|
||||||
@@ -502,7 +503,7 @@ const buildViewerPermissionRules = () => {
|
|||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Settings);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Environments);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.Tags);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.AuditLogs);
|
can(ProjectPermissionAuditLogsActions.Read, ProjectPermissionSub.AuditLogs);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.IpAllowList);
|
||||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
can(ProjectPermissionActions.Read, ProjectPermissionSub.CertificateAuthorities);
|
||||||
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
|
can(ProjectPermissionCertificateActions.Read, ProjectPermissionSub.Certificates);
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ export enum OrgPermissionAppConnectionActions {
|
|||||||
Connect = "connect"
|
Connect = "connect"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionAuditLogsActions {
|
||||||
|
Read = "read"
|
||||||
|
}
|
||||||
|
|
||||||
export enum OrgPermissionKmipActions {
|
export enum OrgPermissionKmipActions {
|
||||||
Proxy = "proxy",
|
Proxy = "proxy",
|
||||||
Setup = "setup"
|
Setup = "setup"
|
||||||
@@ -125,7 +129,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionBillingActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionBillingActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
|
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
|
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
|
||||||
| [
|
| [
|
||||||
@@ -214,7 +218,9 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(OrgPermissionSubjects.AuditLogs).describe("The entity this permission pertains to."),
|
subject: z.literal(OrgPermissionSubjects.AuditLogs).describe("The entity this permission pertains to."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionAuditLogsActions).describe(
|
||||||
|
"Describe what action an entity can take."
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(OrgPermissionSubjects.ProjectTemplates).describe("The entity this permission pertains to."),
|
subject: z.literal(OrgPermissionSubjects.ProjectTemplates).describe("The entity this permission pertains to."),
|
||||||
@@ -340,10 +346,7 @@ const buildAdminPermission = () => {
|
|||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Kms);
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Kms);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
can(OrgPermissionAuditLogsActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.AuditLogs);
|
|
||||||
can(OrgPermissionActions.Edit, OrgPermissionSubjects.AuditLogs);
|
|
||||||
can(OrgPermissionActions.Delete, OrgPermissionSubjects.AuditLogs);
|
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Read, OrgPermissionSubjects.ProjectTemplates);
|
||||||
can(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
|
can(OrgPermissionActions.Create, OrgPermissionSubjects.ProjectTemplates);
|
||||||
@@ -416,7 +419,7 @@ const buildMemberPermission = () => {
|
|||||||
can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
can(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||||
can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
|
can(OrgPermissionIdentityActions.Delete, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
can(OrgPermissionActions.Read, OrgPermissionSubjects.AuditLogs);
|
can(OrgPermissionAuditLogsActions.Read, OrgPermissionSubjects.AuditLogs);
|
||||||
|
|
||||||
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
can(OrgPermissionAppConnectionActions.Connect, OrgPermissionSubjects.AppConnections);
|
||||||
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
can(OrgPermissionGatewayActions.ListGateways, OrgPermissionSubjects.Gateway);
|
||||||
|
|||||||
@@ -164,6 +164,10 @@ export enum ProjectPermissionSecretEventActions {
|
|||||||
SubscribeImportMutations = "subscribe-on-import-mutations"
|
SubscribeImportMutations = "subscribe-on-import-mutations"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionAuditLogsActions {
|
||||||
|
Read = "read"
|
||||||
|
}
|
||||||
|
|
||||||
export enum ProjectPermissionSub {
|
export enum ProjectPermissionSub {
|
||||||
Role = "role",
|
Role = "role",
|
||||||
Member = "member",
|
Member = "member",
|
||||||
@@ -304,7 +308,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionGroupActions, ProjectPermissionSub.Groups]
|
| [ProjectPermissionGroupActions, ProjectPermissionSub.Groups]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
|
| [ProjectPermissionAuditLogsActions, ProjectPermissionSub.AuditLogs]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
|
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||||
@@ -645,7 +649,7 @@ const GeneralPermissionSchema = [
|
|||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
subject: z.literal(ProjectPermissionSub.AuditLogs).describe("The entity this permission pertains to."),
|
subject: z.literal(ProjectPermissionSub.AuditLogs).describe("The entity this permission pertains to."),
|
||||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionActions).describe(
|
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionAuditLogsActions).describe(
|
||||||
"Describe what action an entity can take."
|
"Describe what action an entity can take."
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ export const PgSqlLock = {
|
|||||||
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`),
|
SecretRotationV2Creation: (folderId: string) => pgAdvisoryLockHashText(`secret-rotation-v2-creation:${folderId}`),
|
||||||
CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`),
|
CreateProject: (orgId: string) => pgAdvisoryLockHashText(`create-project:${orgId}`),
|
||||||
CreateFolder: (envId: string, projectId: string) => pgAdvisoryLockHashText(`create-folder:${envId}-${projectId}`),
|
CreateFolder: (envId: string, projectId: string) => pgAdvisoryLockHashText(`create-folder:${envId}-${projectId}`),
|
||||||
SshInit: (projectId: string) => pgAdvisoryLockHashText(`ssh-bootstrap:${projectId}`)
|
SshInit: (projectId: string) => pgAdvisoryLockHashText(`ssh-bootstrap:${projectId}`),
|
||||||
|
IdentityLogin: (identityId: string, nonce: string) => pgAdvisoryLockHashText(`identity-login:${identityId}:${nonce}`)
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// all the key prefixes used must be set here to avoid conflict
|
// all the key prefixes used must be set here to avoid conflict
|
||||||
|
|||||||
@@ -166,7 +166,12 @@ export const UNIVERSAL_AUTH = {
|
|||||||
accessTokenNumUsesLimit:
|
accessTokenNumUsesLimit:
|
||||||
"The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses.",
|
"The maximum number of times that an access token can be used; a value of 0 implies infinite number of uses.",
|
||||||
accessTokenPeriod:
|
accessTokenPeriod:
|
||||||
"The period for an access token in seconds. This value will be referenced at renewal time. Default value is 0."
|
"The period for an access token in seconds. This value will be referenced at renewal time. Default value is 0.",
|
||||||
|
lockoutEnabled: "Whether the lockout feature is enabled.",
|
||||||
|
lockoutThreshold: "The amount of times login must fail before locking the identity auth method.",
|
||||||
|
lockoutDurationSeconds: "How long an identity auth method lockout lasts.",
|
||||||
|
lockoutCounterResetSeconds:
|
||||||
|
"How long to wait from the most recent failed login until resetting the lockout counter."
|
||||||
},
|
},
|
||||||
RETRIEVE: {
|
RETRIEVE: {
|
||||||
identityId: "The ID of the identity to retrieve the auth method for."
|
identityId: "The ID of the identity to retrieve the auth method for."
|
||||||
@@ -181,7 +186,12 @@ export const UNIVERSAL_AUTH = {
|
|||||||
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
accessTokenTTL: "The new lifetime for an access token in seconds.",
|
||||||
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
|
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
|
||||||
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
|
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
|
||||||
accessTokenPeriod: "The new period for an access token in seconds."
|
accessTokenPeriod: "The new period for an access token in seconds.",
|
||||||
|
lockoutEnabled: "Whether the lockout feature is enabled.",
|
||||||
|
lockoutThreshold: "The amount of times login must fail before locking the identity auth method.",
|
||||||
|
lockoutDurationSeconds: "How long an identity auth method lockout lasts.",
|
||||||
|
lockoutCounterResetSeconds:
|
||||||
|
"How long to wait from the most recent failed login until resetting the lockout counter."
|
||||||
},
|
},
|
||||||
CREATE_CLIENT_SECRET: {
|
CREATE_CLIENT_SECRET: {
|
||||||
identityId: "The ID of the identity to create a client secret for.",
|
identityId: "The ID of the identity to create a client secret for.",
|
||||||
@@ -201,6 +211,9 @@ export const UNIVERSAL_AUTH = {
|
|||||||
identityId: "The ID of the identity to revoke the client secret from.",
|
identityId: "The ID of the identity to revoke the client secret from.",
|
||||||
clientSecretId: "The ID of the client secret to revoke."
|
clientSecretId: "The ID of the client secret to revoke."
|
||||||
},
|
},
|
||||||
|
CLEAR_CLIENT_LOCKOUTS: {
|
||||||
|
identityId: "The ID of the identity to clear the client lockouts from."
|
||||||
|
},
|
||||||
RENEW_ACCESS_TOKEN: {
|
RENEW_ACCESS_TOKEN: {
|
||||||
accessToken: "The access token to renew."
|
accessToken: "The access token to renew."
|
||||||
},
|
},
|
||||||
@@ -2148,7 +2161,9 @@ export const CertificateAuthorities = {
|
|||||||
directoryUrl: `The directory URL for the ACME Certificate Authority.`,
|
directoryUrl: `The directory URL for the ACME Certificate Authority.`,
|
||||||
accountEmail: `The email address for the ACME Certificate Authority.`,
|
accountEmail: `The email address for the ACME Certificate Authority.`,
|
||||||
provider: `The DNS provider for the ACME Certificate Authority.`,
|
provider: `The DNS provider for the ACME Certificate Authority.`,
|
||||||
hostedZoneId: `The hosted zone ID for the ACME Certificate Authority.`
|
hostedZoneId: `The hosted zone ID for the ACME Certificate Authority.`,
|
||||||
|
eabKid: `The External Account Binding (EAB) Key ID for the ACME Certificate Authority. Required if the ACME provider uses EAB.`,
|
||||||
|
eabHmacKey: `The External Account Binding (EAB) HMAC key for the ACME Certificate Authority. Required if the ACME provider uses EAB.`
|
||||||
},
|
},
|
||||||
INTERNAL: {
|
INTERNAL: {
|
||||||
type: "The type of CA to create.",
|
type: "The type of CA to create.",
|
||||||
|
|||||||
@@ -1464,7 +1464,8 @@ export const registerRoutes = async (
|
|||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
identityMetadataDAL
|
identityMetadataDAL,
|
||||||
|
keyStore
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityAuthTemplateService = identityAuthTemplateServiceFactory({
|
const identityAuthTemplateService = identityAuthTemplateServiceFactory({
|
||||||
@@ -1518,7 +1519,8 @@ export const registerRoutes = async (
|
|||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
identityUaClientSecretDAL,
|
identityUaClientSecretDAL,
|
||||||
identityUaDAL,
|
identityUaDAL,
|
||||||
licenseService
|
licenseService,
|
||||||
|
keyStore
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
|
const identityKubernetesAuthService = identityKubernetesAuthServiceFactory({
|
||||||
@@ -1752,7 +1754,8 @@ export const registerRoutes = async (
|
|||||||
const migrationService = externalMigrationServiceFactory({
|
const migrationService = externalMigrationServiceFactory({
|
||||||
externalMigrationQueue,
|
externalMigrationQueue,
|
||||||
userDAL,
|
userDAL,
|
||||||
permissionService
|
permissionService,
|
||||||
|
gatewayService
|
||||||
});
|
});
|
||||||
|
|
||||||
const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({
|
const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({
|
||||||
|
|||||||
@@ -703,6 +703,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
|||||||
// prevent older projects from accessing endpoint
|
// prevent older projects from accessing endpoint
|
||||||
if (!shouldUseSecretV2Bridge) throw new BadRequestError({ message: "Project version not supported" });
|
if (!shouldUseSecretV2Bridge) throw new BadRequestError({ message: "Project version not supported" });
|
||||||
|
|
||||||
|
// verify folder exists and user has project permission
|
||||||
|
await server.services.folder.getFolderByPath({ projectId, environment, secretPath }, req.permission);
|
||||||
|
|
||||||
const tags = req.query.tags?.split(",") ?? [];
|
const tags = req.query.tags?.split(",") ?? [];
|
||||||
|
|
||||||
let remainingLimit = limit;
|
let remainingLimit = limit;
|
||||||
|
|||||||
@@ -250,7 +250,8 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
|
|||||||
description: true
|
description: true
|
||||||
}).optional(),
|
}).optional(),
|
||||||
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
|
identity: IdentitiesSchema.pick({ name: true, id: true, hasDeleteProtection: true }).extend({
|
||||||
authMethods: z.array(z.string())
|
authMethods: z.array(z.string()),
|
||||||
|
activeLockoutAuthMethods: z.array(z.string())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -137,7 +137,21 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
.min(0)
|
.min(0)
|
||||||
.default(0)
|
.default(0)
|
||||||
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit),
|
.describe(UNIVERSAL_AUTH.ATTACH.accessTokenNumUsesLimit),
|
||||||
accessTokenPeriod: z.number().int().min(0).default(0).describe(UNIVERSAL_AUTH.ATTACH.accessTokenPeriod)
|
accessTokenPeriod: z.number().int().min(0).default(0).describe(UNIVERSAL_AUTH.ATTACH.accessTokenPeriod),
|
||||||
|
lockoutEnabled: z.boolean().default(true).describe(UNIVERSAL_AUTH.ATTACH.lockoutEnabled),
|
||||||
|
lockoutThreshold: z.number().min(1).max(30).default(3).describe(UNIVERSAL_AUTH.ATTACH.lockoutThreshold),
|
||||||
|
lockoutDurationSeconds: z
|
||||||
|
.number()
|
||||||
|
.min(30)
|
||||||
|
.max(86400)
|
||||||
|
.default(300)
|
||||||
|
.describe(UNIVERSAL_AUTH.ATTACH.lockoutDurationSeconds),
|
||||||
|
lockoutCounterResetSeconds: z
|
||||||
|
.number()
|
||||||
|
.min(5)
|
||||||
|
.max(3600)
|
||||||
|
.default(30)
|
||||||
|
.describe(UNIVERSAL_AUTH.ATTACH.lockoutCounterResetSeconds)
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
|
||||||
@@ -171,7 +185,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL,
|
accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL,
|
||||||
accessTokenTrustedIps: identityUniversalAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
accessTokenTrustedIps: identityUniversalAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||||
clientSecretTrustedIps: identityUniversalAuth.clientSecretTrustedIps as TIdentityTrustedIp[],
|
clientSecretTrustedIps: identityUniversalAuth.clientSecretTrustedIps as TIdentityTrustedIp[],
|
||||||
accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit
|
accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit,
|
||||||
|
lockoutEnabled: identityUniversalAuth.lockoutEnabled,
|
||||||
|
lockoutThreshold: identityUniversalAuth.lockoutThreshold,
|
||||||
|
lockoutDurationSeconds: identityUniversalAuth.lockoutDurationSeconds,
|
||||||
|
lockoutCounterResetSeconds: identityUniversalAuth.lockoutCounterResetSeconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -243,7 +261,21 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
.min(0)
|
.min(0)
|
||||||
.max(315360000)
|
.max(315360000)
|
||||||
.optional()
|
.optional()
|
||||||
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenPeriod)
|
.describe(UNIVERSAL_AUTH.UPDATE.accessTokenPeriod),
|
||||||
|
lockoutEnabled: z.boolean().optional().describe(UNIVERSAL_AUTH.UPDATE.lockoutEnabled),
|
||||||
|
lockoutThreshold: z.number().min(1).max(30).optional().describe(UNIVERSAL_AUTH.UPDATE.lockoutThreshold),
|
||||||
|
lockoutDurationSeconds: z
|
||||||
|
.number()
|
||||||
|
.min(30)
|
||||||
|
.max(86400)
|
||||||
|
.optional()
|
||||||
|
.describe(UNIVERSAL_AUTH.UPDATE.lockoutDurationSeconds),
|
||||||
|
lockoutCounterResetSeconds: z
|
||||||
|
.number()
|
||||||
|
.min(5)
|
||||||
|
.max(3600)
|
||||||
|
.optional()
|
||||||
|
.describe(UNIVERSAL_AUTH.UPDATE.lockoutCounterResetSeconds)
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
(val) => (val.accessTokenMaxTTL && val.accessTokenTTL ? val.accessTokenTTL <= val.accessTokenMaxTTL : true),
|
||||||
@@ -276,7 +308,11 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL,
|
accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL,
|
||||||
accessTokenTrustedIps: identityUniversalAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
accessTokenTrustedIps: identityUniversalAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
|
||||||
clientSecretTrustedIps: identityUniversalAuth.clientSecretTrustedIps as TIdentityTrustedIp[],
|
clientSecretTrustedIps: identityUniversalAuth.clientSecretTrustedIps as TIdentityTrustedIp[],
|
||||||
accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit
|
accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit,
|
||||||
|
lockoutEnabled: identityUniversalAuth.lockoutEnabled,
|
||||||
|
lockoutThreshold: identityUniversalAuth.lockoutThreshold,
|
||||||
|
lockoutDurationSeconds: identityUniversalAuth.lockoutDurationSeconds,
|
||||||
|
lockoutCounterResetSeconds: identityUniversalAuth.lockoutCounterResetSeconds
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -594,4 +630,53 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { clientSecretData };
|
return { clientSecretData };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/universal-auth/identities/:identityId/clear-lockouts",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
schema: {
|
||||||
|
hide: false,
|
||||||
|
tags: [ApiDocsTags.UniversalAuth],
|
||||||
|
description: "Clear Universal Auth Lockouts for identity",
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
bearerAuth: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
params: z.object({
|
||||||
|
identityId: z.string().describe(UNIVERSAL_AUTH.CLEAR_CLIENT_LOCKOUTS.identityId)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
deleted: z.number()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const clearLockoutsData = await server.services.identityUa.clearUniversalAuthLockouts({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
identityId: req.params.identityId
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: clearLockoutsData.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS,
|
||||||
|
metadata: {
|
||||||
|
identityId: clearLockoutsData.identityId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return clearLockoutsData;
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
|||||||
vaultAccessToken: z.string(),
|
vaultAccessToken: z.string(),
|
||||||
vaultNamespace: z.string().trim().optional(),
|
vaultNamespace: z.string().trim().optional(),
|
||||||
vaultUrl: z.string(),
|
vaultUrl: z.string(),
|
||||||
mappingType: z.nativeEnum(VaultMappingType)
|
mappingType: z.nativeEnum(VaultMappingType),
|
||||||
|
gatewayId: z.string().optional()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
|||||||
@@ -419,6 +419,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
|||||||
200: z.object({
|
200: z.object({
|
||||||
secret: secretRawSchema.extend({
|
secret: secretRawSchema.extend({
|
||||||
secretValueHidden: z.boolean(),
|
secretValueHidden: z.boolean(),
|
||||||
|
secretPath: z.string(),
|
||||||
tags: SanitizedTagSchema.array().optional(),
|
tags: SanitizedTagSchema.array().optional(),
|
||||||
secretMetadata: ResourceMetadataSchema.optional()
|
secretMetadata: ResourceMetadataSchema.optional()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -453,10 +453,14 @@ export const authLoginServiceFactory = ({
|
|||||||
|
|
||||||
const selectedOrg = await orgDAL.findById(organizationId);
|
const selectedOrg = await orgDAL.findById(organizationId);
|
||||||
|
|
||||||
// Check if authEnforced is true, if that's the case, throw an error
|
// Check if authEnforced is true and the current auth method is not an enforced method
|
||||||
if (selectedOrg.authEnforced) {
|
if (
|
||||||
|
selectedOrg.authEnforced &&
|
||||||
|
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||||
|
decodedToken.authMethod !== AuthMethod.OIDC
|
||||||
|
) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Authentication is required by your organization before you can log in."
|
message: "Login with the auth method required by your organization."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ type DBConfigurationColumn = {
|
|||||||
directoryUrl: string;
|
directoryUrl: string;
|
||||||
accountEmail: string;
|
accountEmail: string;
|
||||||
hostedZoneId: string;
|
hostedZoneId: string;
|
||||||
|
eabKid?: string;
|
||||||
|
eabHmacKey?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const castDbEntryToAcmeCertificateAuthority = (
|
export const castDbEntryToAcmeCertificateAuthority = (
|
||||||
@@ -89,7 +91,9 @@ export const castDbEntryToAcmeCertificateAuthority = (
|
|||||||
hostedZoneId: dbConfigurationCol.hostedZoneId
|
hostedZoneId: dbConfigurationCol.hostedZoneId
|
||||||
},
|
},
|
||||||
directoryUrl: dbConfigurationCol.directoryUrl,
|
directoryUrl: dbConfigurationCol.directoryUrl,
|
||||||
accountEmail: dbConfigurationCol.accountEmail
|
accountEmail: dbConfigurationCol.accountEmail,
|
||||||
|
eabKid: dbConfigurationCol.eabKid,
|
||||||
|
eabHmacKey: dbConfigurationCol.eabHmacKey
|
||||||
},
|
},
|
||||||
status: ca.status as CaStatus
|
status: ca.status as CaStatus
|
||||||
};
|
};
|
||||||
@@ -128,7 +132,7 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig } = configuration;
|
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig, eabKid, eabHmacKey } = configuration;
|
||||||
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
|
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
|
||||||
|
|
||||||
if (!appConnection) {
|
if (!appConnection) {
|
||||||
@@ -171,7 +175,9 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
directoryUrl,
|
directoryUrl,
|
||||||
accountEmail,
|
accountEmail,
|
||||||
dnsProvider: dnsProviderConfig.provider,
|
dnsProvider: dnsProviderConfig.provider,
|
||||||
hostedZoneId: dnsProviderConfig.hostedZoneId
|
hostedZoneId: dnsProviderConfig.hostedZoneId,
|
||||||
|
eabKid,
|
||||||
|
eabHmacKey
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
@@ -214,7 +220,7 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => {
|
const updatedCa = await certificateAuthorityDAL.transaction(async (tx) => {
|
||||||
if (configuration) {
|
if (configuration) {
|
||||||
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig } = configuration;
|
const { dnsAppConnectionId, directoryUrl, accountEmail, dnsProviderConfig, eabKid, eabHmacKey } = configuration;
|
||||||
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
|
const appConnection = await appConnectionDAL.findById(dnsAppConnectionId);
|
||||||
|
|
||||||
if (!appConnection) {
|
if (!appConnection) {
|
||||||
@@ -254,7 +260,9 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
directoryUrl,
|
directoryUrl,
|
||||||
accountEmail,
|
accountEmail,
|
||||||
dnsProvider: dnsProviderConfig.provider,
|
dnsProvider: dnsProviderConfig.provider,
|
||||||
hostedZoneId: dnsProviderConfig.hostedZoneId
|
hostedZoneId: dnsProviderConfig.hostedZoneId,
|
||||||
|
eabKid,
|
||||||
|
eabHmacKey
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
@@ -354,10 +362,19 @@ export const AcmeCertificateAuthorityFns = ({
|
|||||||
|
|
||||||
await blockLocalAndPrivateIpAddresses(acmeCa.configuration.directoryUrl);
|
await blockLocalAndPrivateIpAddresses(acmeCa.configuration.directoryUrl);
|
||||||
|
|
||||||
const acmeClient = new acme.Client({
|
const acmeClientOptions: acme.ClientOptions = {
|
||||||
directoryUrl: acmeCa.configuration.directoryUrl,
|
directoryUrl: acmeCa.configuration.directoryUrl,
|
||||||
accountKey
|
accountKey
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (acmeCa.configuration.eabKid && acmeCa.configuration.eabHmacKey) {
|
||||||
|
acmeClientOptions.externalAccountBinding = {
|
||||||
|
kid: acmeCa.configuration.eabKid,
|
||||||
|
hmacKey: acmeCa.configuration.eabHmacKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const acmeClient = new acme.Client(acmeClientOptions);
|
||||||
|
|
||||||
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
const alg = keyAlgorithmToAlgCfg(CertKeyAlgorithm.RSA_2048);
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -18,7 +18,9 @@ export const AcmeCertificateAuthorityConfigurationSchema = z.object({
|
|||||||
hostedZoneId: z.string().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.hostedZoneId)
|
hostedZoneId: z.string().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.hostedZoneId)
|
||||||
}),
|
}),
|
||||||
directoryUrl: z.string().url().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.directoryUrl),
|
directoryUrl: z.string().url().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.directoryUrl),
|
||||||
accountEmail: z.string().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.accountEmail)
|
accountEmail: z.string().trim().min(1).describe(CertificateAuthorities.CONFIGURATIONS.ACME.accountEmail),
|
||||||
|
eabKid: z.string().trim().max(64).optional().describe(CertificateAuthorities.CONFIGURATIONS.ACME.eabKid),
|
||||||
|
eabHmacKey: z.string().trim().max(512).optional().describe(CertificateAuthorities.CONFIGURATIONS.ACME.eabHmacKey)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AcmeCertificateAuthorityCredentialsSchema = z.object({
|
export const AcmeCertificateAuthorityCredentialsSchema = z.object({
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
|
import https from "node:https";
|
||||||
|
|
||||||
import axios, { AxiosInstance } from "axios";
|
import axios, { AxiosInstance } from "axios";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
|
|
||||||
import { InfisicalImportData, VaultMappingType } from "../external-migration-types";
|
import { InfisicalImportData, VaultMappingType } from "../external-migration-types";
|
||||||
|
|
||||||
|
enum KvVersion {
|
||||||
|
V1 = "1",
|
||||||
|
V2 = "2"
|
||||||
|
}
|
||||||
|
|
||||||
type VaultData = {
|
type VaultData = {
|
||||||
namespace: string;
|
namespace: string;
|
||||||
mount: string;
|
mount: string;
|
||||||
@@ -14,7 +23,42 @@ type VaultData = {
|
|||||||
secretData: Record<string, string>;
|
secretData: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const vaultFactory = () => {
|
const vaultFactory = (gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">) => {
|
||||||
|
const $gatewayProxyWrapper = async <T>(
|
||||||
|
inputs: {
|
||||||
|
gatewayId: string;
|
||||||
|
targetHost?: string;
|
||||||
|
targetPort?: number;
|
||||||
|
},
|
||||||
|
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
|
||||||
|
): Promise<T> => {
|
||||||
|
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
|
||||||
|
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||||
|
|
||||||
|
const callbackResult = await withGatewayProxy(
|
||||||
|
async (port, httpsAgent) => {
|
||||||
|
const res = await gatewayCallback("http://localhost", port, httpsAgent);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: GatewayProxyProtocol.Http,
|
||||||
|
targetHost: inputs.targetHost,
|
||||||
|
targetPort: inputs.targetPort,
|
||||||
|
relayHost,
|
||||||
|
relayPort: Number(relayPort),
|
||||||
|
identityId: relayDetails.identityId,
|
||||||
|
orgId: relayDetails.orgId,
|
||||||
|
tlsOptions: {
|
||||||
|
ca: relayDetails.certChain,
|
||||||
|
cert: relayDetails.certificate,
|
||||||
|
key: relayDetails.privateKey.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return callbackResult;
|
||||||
|
};
|
||||||
|
|
||||||
const getMounts = async (request: AxiosInstance) => {
|
const getMounts = async (request: AxiosInstance) => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get<{
|
.get<{
|
||||||
@@ -31,9 +75,11 @@ const vaultFactory = () => {
|
|||||||
|
|
||||||
const getPaths = async (
|
const getPaths = async (
|
||||||
request: AxiosInstance,
|
request: AxiosInstance,
|
||||||
{ mountPath, secretPath = "" }: { mountPath: string; secretPath?: string }
|
{ mountPath, secretPath = "" }: { mountPath: string; secretPath?: string },
|
||||||
|
kvVersion: KvVersion
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
if (kvVersion === KvVersion.V2) {
|
||||||
// For KV v2: /v1/{mount}/metadata/{path}?list=true
|
// For KV v2: /v1/{mount}/metadata/{path}?list=true
|
||||||
const path = secretPath ? `${mountPath}/metadata/${secretPath}` : `${mountPath}/metadata`;
|
const path = secretPath ? `${mountPath}/metadata/${secretPath}` : `${mountPath}/metadata`;
|
||||||
const response = await request.get<{
|
const response = await request.get<{
|
||||||
@@ -42,6 +88,17 @@ const vaultFactory = () => {
|
|||||||
};
|
};
|
||||||
}>(`/v1/${path}?list=true`);
|
}>(`/v1/${path}?list=true`);
|
||||||
|
|
||||||
|
return response.data.data.keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv version v1: /v1/{mount}?list=true
|
||||||
|
const path = secretPath ? `${mountPath}/${secretPath}` : mountPath;
|
||||||
|
const response = await request.get<{
|
||||||
|
data: {
|
||||||
|
keys: string[];
|
||||||
|
};
|
||||||
|
}>(`/v1/${path}?list=true`);
|
||||||
|
|
||||||
return response.data.data.keys;
|
return response.data.data.keys;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (axios.isAxiosError(err)) {
|
if (axios.isAxiosError(err)) {
|
||||||
@@ -56,8 +113,10 @@ const vaultFactory = () => {
|
|||||||
|
|
||||||
const getSecrets = async (
|
const getSecrets = async (
|
||||||
request: AxiosInstance,
|
request: AxiosInstance,
|
||||||
{ mountPath, secretPath }: { mountPath: string; secretPath: string }
|
{ mountPath, secretPath }: { mountPath: string; secretPath: string },
|
||||||
|
kvVersion: KvVersion
|
||||||
) => {
|
) => {
|
||||||
|
if (kvVersion === KvVersion.V2) {
|
||||||
// For KV v2: /v1/{mount}/data/{path}
|
// For KV v2: /v1/{mount}/data/{path}
|
||||||
const response = await request
|
const response = await request
|
||||||
.get<{
|
.get<{
|
||||||
@@ -79,6 +138,25 @@ const vaultFactory = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return response.data.data.data;
|
return response.data.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv version v1
|
||||||
|
|
||||||
|
const response = await request
|
||||||
|
.get<{
|
||||||
|
data: Record<string, string>; // KV v1 has flat data structure
|
||||||
|
lease_duration: number;
|
||||||
|
lease_id: string;
|
||||||
|
renewable: boolean;
|
||||||
|
}>(`/v1/${mountPath}/${secretPath}`)
|
||||||
|
.catch((err) => {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
logger.error(err.response?.data, "External migration: Failed to get Vault secret");
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
// helper function to check if a mount is KV v2 (will be useful if we add support for Vault KV v1)
|
// helper function to check if a mount is KV v2 (will be useful if we add support for Vault KV v1)
|
||||||
@@ -89,9 +167,10 @@ const vaultFactory = () => {
|
|||||||
const recursivelyGetAllPaths = async (
|
const recursivelyGetAllPaths = async (
|
||||||
request: AxiosInstance,
|
request: AxiosInstance,
|
||||||
mountPath: string,
|
mountPath: string,
|
||||||
|
kvVersion: KvVersion,
|
||||||
currentPath: string = ""
|
currentPath: string = ""
|
||||||
): Promise<string[]> => {
|
): Promise<string[]> => {
|
||||||
const paths = await getPaths(request, { mountPath, secretPath: currentPath });
|
const paths = await getPaths(request, { mountPath, secretPath: currentPath }, kvVersion);
|
||||||
|
|
||||||
if (paths === null || paths.length === 0) {
|
if (paths === null || paths.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@@ -105,7 +184,7 @@ const vaultFactory = () => {
|
|||||||
|
|
||||||
if (path.endsWith("/")) {
|
if (path.endsWith("/")) {
|
||||||
// it's a folder so we recurse into it
|
// it's a folder so we recurse into it
|
||||||
const subSecrets = await recursivelyGetAllPaths(request, mountPath, fullItemPath);
|
const subSecrets = await recursivelyGetAllPaths(request, mountPath, kvVersion, fullItemPath);
|
||||||
allSecrets.push(...subSecrets);
|
allSecrets.push(...subSecrets);
|
||||||
} else {
|
} else {
|
||||||
// it's a secret so we add it to our results
|
// it's a secret so we add it to our results
|
||||||
@@ -119,22 +198,26 @@ const vaultFactory = () => {
|
|||||||
async function collectVaultData({
|
async function collectVaultData({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
namespace,
|
namespace,
|
||||||
accessToken
|
accessToken,
|
||||||
|
gatewayId
|
||||||
}: {
|
}: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
gatewayId?: string;
|
||||||
}): Promise<VaultData[]> {
|
}): Promise<VaultData[]> {
|
||||||
|
const getData = async (host: string, port?: number, httpsAgent?: https.Agent) => {
|
||||||
|
const allData: VaultData[] = [];
|
||||||
|
|
||||||
const request = axios.create({
|
const request = axios.create({
|
||||||
baseURL: baseUrl,
|
baseURL: port ? `${host}:${port}` : host,
|
||||||
headers: {
|
headers: {
|
||||||
"X-Vault-Token": accessToken,
|
"X-Vault-Token": accessToken,
|
||||||
...(namespace ? { "X-Vault-Namespace": namespace } : {})
|
...(namespace ? { "X-Vault-Namespace": namespace } : {})
|
||||||
}
|
},
|
||||||
|
httpsAgent
|
||||||
});
|
});
|
||||||
|
|
||||||
const allData: VaultData[] = [];
|
|
||||||
|
|
||||||
// Get all mounts in this namespace
|
// Get all mounts in this namespace
|
||||||
const mounts = await getMounts(request);
|
const mounts = await getMounts(request);
|
||||||
|
|
||||||
@@ -151,17 +234,23 @@ const vaultFactory = () => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const kvVersion = mountInfo.options?.version === "2" ? KvVersion.V2 : KvVersion.V1;
|
||||||
|
|
||||||
// get all paths in this mount
|
// get all paths in this mount
|
||||||
const paths = await recursivelyGetAllPaths(request, `${mountPath.replace(/\/$/, "")}`);
|
const paths = await recursivelyGetAllPaths(request, `${mountPath.replace(/\/$/, "")}`, kvVersion);
|
||||||
|
|
||||||
const cleanMountPath = mountPath.replace(/\/$/, "");
|
const cleanMountPath = mountPath.replace(/\/$/, "");
|
||||||
|
|
||||||
for await (const secretPath of paths) {
|
for await (const secretPath of paths) {
|
||||||
// get the actual secret data
|
// get the actual secret data
|
||||||
const secretData = await getSecrets(request, {
|
const secretData = await getSecrets(
|
||||||
|
request,
|
||||||
|
{
|
||||||
mountPath: cleanMountPath,
|
mountPath: cleanMountPath,
|
||||||
secretPath: secretPath.replace(`${cleanMountPath}/`, "")
|
secretPath: secretPath.replace(`${cleanMountPath}/`, "")
|
||||||
});
|
},
|
||||||
|
kvVersion
|
||||||
|
);
|
||||||
|
|
||||||
allData.push({
|
allData.push({
|
||||||
namespace: namespace || "",
|
namespace: namespace || "",
|
||||||
@@ -173,6 +262,29 @@ const vaultFactory = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return allData;
|
return allData;
|
||||||
|
};
|
||||||
|
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (gatewayId) {
|
||||||
|
const url = new URL(baseUrl);
|
||||||
|
|
||||||
|
const { port, protocol, hostname } = url;
|
||||||
|
const cleanedProtocol = protocol.slice(0, -1);
|
||||||
|
|
||||||
|
data = await $gatewayProxyWrapper(
|
||||||
|
{
|
||||||
|
gatewayId,
|
||||||
|
targetHost: `${cleanedProtocol}://${hostname}`,
|
||||||
|
targetPort: port ? Number(port) : 8200 // 8200, default port for Vault self-hosted/dedicated
|
||||||
|
},
|
||||||
|
getData
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
data = await getData(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -296,17 +408,22 @@ export const transformToInfisicalFormatNamespaceToProjects = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const importVaultDataFn = async ({
|
export const importVaultDataFn = async (
|
||||||
|
{
|
||||||
vaultAccessToken,
|
vaultAccessToken,
|
||||||
vaultNamespace,
|
vaultNamespace,
|
||||||
vaultUrl,
|
vaultUrl,
|
||||||
mappingType
|
mappingType,
|
||||||
}: {
|
gatewayId
|
||||||
|
}: {
|
||||||
vaultAccessToken: string;
|
vaultAccessToken: string;
|
||||||
vaultNamespace?: string;
|
vaultNamespace?: string;
|
||||||
vaultUrl: string;
|
vaultUrl: string;
|
||||||
mappingType: VaultMappingType;
|
mappingType: VaultMappingType;
|
||||||
}) => {
|
gatewayId?: string;
|
||||||
|
},
|
||||||
|
{ gatewayService }: { gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId"> }
|
||||||
|
) => {
|
||||||
await blockLocalAndPrivateIpAddresses(vaultUrl);
|
await blockLocalAndPrivateIpAddresses(vaultUrl);
|
||||||
|
|
||||||
if (mappingType === VaultMappingType.Namespace && !vaultNamespace) {
|
if (mappingType === VaultMappingType.Namespace && !vaultNamespace) {
|
||||||
@@ -315,12 +432,13 @@ export const importVaultDataFn = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaultApi = vaultFactory();
|
const vaultApi = vaultFactory(gatewayService);
|
||||||
|
|
||||||
const vaultData = await vaultApi.collectVaultData({
|
const vaultData = await vaultApi.collectVaultData({
|
||||||
accessToken: vaultAccessToken,
|
accessToken: vaultAccessToken,
|
||||||
baseUrl: vaultUrl,
|
baseUrl: vaultUrl,
|
||||||
namespace: vaultNamespace
|
namespace: vaultNamespace,
|
||||||
|
gatewayId
|
||||||
});
|
});
|
||||||
|
|
||||||
const infisicalData = transformToInfisicalFormatNamespaceToProjects(vaultData, mappingType);
|
const infisicalData = transformToInfisicalFormatNamespaceToProjects(vaultData, mappingType);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OrgMembershipRole } from "@app/db/schemas";
|
import { OrgMembershipRole } from "@app/db/schemas";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
@@ -12,6 +13,7 @@ type TExternalMigrationServiceFactoryDep = {
|
|||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
externalMigrationQueue: TExternalMigrationQueueFactory;
|
externalMigrationQueue: TExternalMigrationQueueFactory;
|
||||||
userDAL: Pick<TUserDALFactory, "findById">;
|
userDAL: Pick<TUserDALFactory, "findById">;
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
|
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
|
||||||
@@ -19,7 +21,8 @@ export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrati
|
|||||||
export const externalMigrationServiceFactory = ({
|
export const externalMigrationServiceFactory = ({
|
||||||
permissionService,
|
permissionService,
|
||||||
externalMigrationQueue,
|
externalMigrationQueue,
|
||||||
userDAL
|
userDAL,
|
||||||
|
gatewayService
|
||||||
}: TExternalMigrationServiceFactoryDep) => {
|
}: TExternalMigrationServiceFactoryDep) => {
|
||||||
const importEnvKeyData = async ({
|
const importEnvKeyData = async ({
|
||||||
decryptionKey,
|
decryptionKey,
|
||||||
@@ -72,6 +75,7 @@ export const externalMigrationServiceFactory = ({
|
|||||||
vaultNamespace,
|
vaultNamespace,
|
||||||
mappingType,
|
mappingType,
|
||||||
vaultUrl,
|
vaultUrl,
|
||||||
|
gatewayId,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
@@ -91,12 +95,18 @@ export const externalMigrationServiceFactory = ({
|
|||||||
|
|
||||||
const user = await userDAL.findById(actorId);
|
const user = await userDAL.findById(actorId);
|
||||||
|
|
||||||
const vaultData = await importVaultDataFn({
|
const vaultData = await importVaultDataFn(
|
||||||
|
{
|
||||||
vaultAccessToken,
|
vaultAccessToken,
|
||||||
vaultNamespace,
|
vaultNamespace,
|
||||||
vaultUrl,
|
vaultUrl,
|
||||||
mappingType
|
mappingType,
|
||||||
});
|
gatewayId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gatewayService
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const stringifiedJson = JSON.stringify({
|
const stringifiedJson = JSON.stringify({
|
||||||
data: vaultData,
|
data: vaultData,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export type TImportVaultDataDTO = {
|
|||||||
vaultNamespace?: string;
|
vaultNamespace?: string;
|
||||||
mappingType: VaultMappingType;
|
mappingType: VaultMappingType;
|
||||||
vaultUrl: string;
|
vaultUrl: string;
|
||||||
|
gatewayId?: string;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TImportInfisicalDataCreate = {
|
export type TImportInfisicalDataCreate = {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
validatePrivilegeChangeOperation
|
validatePrivilegeChangeOperation
|
||||||
} from "@app/ee/services/permission/permission-fns";
|
} from "@app/ee/services/permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, PermissionBoundaryError, UnauthorizedError } from "@app/lib/errors";
|
||||||
@@ -22,6 +23,7 @@ import { TIdentityUaClientSecretDALFactory } from "./identity-ua-client-secret-d
|
|||||||
import { TIdentityUaDALFactory } from "./identity-ua-dal";
|
import { TIdentityUaDALFactory } from "./identity-ua-dal";
|
||||||
import {
|
import {
|
||||||
TAttachUaDTO,
|
TAttachUaDTO,
|
||||||
|
TClearUaLockoutsDTO,
|
||||||
TCreateUaClientSecretDTO,
|
TCreateUaClientSecretDTO,
|
||||||
TGetUaClientSecretsDTO,
|
TGetUaClientSecretsDTO,
|
||||||
TGetUaDTO,
|
TGetUaDTO,
|
||||||
@@ -38,17 +40,24 @@ type TIdentityUaServiceFactoryDep = {
|
|||||||
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIdentityUaServiceFactory = ReturnType<typeof identityUaServiceFactory>;
|
export type TIdentityUaServiceFactory = ReturnType<typeof identityUaServiceFactory>;
|
||||||
|
|
||||||
|
// type LockoutObject = {
|
||||||
|
// lockedOut: boolean;
|
||||||
|
// failedAttempts: number;
|
||||||
|
// };
|
||||||
|
|
||||||
export const identityUaServiceFactory = ({
|
export const identityUaServiceFactory = ({
|
||||||
identityUaDAL,
|
identityUaDAL,
|
||||||
identityUaClientSecretDAL,
|
identityUaClientSecretDAL,
|
||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
keyStore
|
||||||
}: TIdentityUaServiceFactoryDep) => {
|
}: TIdentityUaServiceFactoryDep) => {
|
||||||
const login = async (clientId: string, clientSecret: string, ip: string) => {
|
const login = async (clientId: string, clientSecret: string, ip: string) => {
|
||||||
const identityUa = await identityUaDAL.findOne({ clientId });
|
const identityUa = await identityUaDAL.findOne({ clientId });
|
||||||
@@ -196,7 +205,11 @@ export const identityUaServiceFactory = ({
|
|||||||
actor,
|
actor,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
isActorSuperAdmin,
|
isActorSuperAdmin,
|
||||||
accessTokenPeriod
|
accessTokenPeriod,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDurationSeconds,
|
||||||
|
lockoutCounterResetSeconds
|
||||||
}: TAttachUaDTO) => {
|
}: TAttachUaDTO) => {
|
||||||
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
|
await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin);
|
||||||
|
|
||||||
@@ -266,7 +279,11 @@ export const identityUaServiceFactory = ({
|
|||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
|
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
|
||||||
accessTokenPeriod
|
accessTokenPeriod,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDurationSeconds,
|
||||||
|
lockoutCounterResetSeconds
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
@@ -286,7 +303,11 @@ export const identityUaServiceFactory = ({
|
|||||||
actorId,
|
actorId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
actor,
|
actor,
|
||||||
actorOrgId
|
actorOrgId,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDurationSeconds,
|
||||||
|
lockoutCounterResetSeconds
|
||||||
}: TUpdateUaDTO) => {
|
}: TUpdateUaDTO) => {
|
||||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||||
@@ -362,7 +383,11 @@ export const identityUaServiceFactory = ({
|
|||||||
accessTokenPeriod,
|
accessTokenPeriod,
|
||||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps
|
||||||
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
? JSON.stringify(reformattedAccessTokenTrustedIps)
|
||||||
: undefined
|
: undefined,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDurationSeconds,
|
||||||
|
lockoutCounterResetSeconds
|
||||||
});
|
});
|
||||||
return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId };
|
return { ...updatedUaAuth, orgId: identityMembershipOrg.orgId };
|
||||||
};
|
};
|
||||||
@@ -713,6 +738,38 @@ export const identityUaServiceFactory = ({
|
|||||||
return { ...updatedClientSecret, identityId, orgId: identityMembershipOrg.orgId };
|
return { ...updatedClientSecret, identityId, orgId: identityMembershipOrg.orgId };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearUniversalAuthLockouts = async ({
|
||||||
|
identityId,
|
||||||
|
actorId,
|
||||||
|
actor,
|
||||||
|
actorOrgId,
|
||||||
|
actorAuthMethod
|
||||||
|
}: TClearUaLockoutsDTO) => {
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId });
|
||||||
|
if (!identityMembershipOrg) throw new NotFoundError({ message: `Failed to find identity with ID ${identityId}` });
|
||||||
|
|
||||||
|
if (!identityMembershipOrg.identity.authMethods.includes(IdentityAuthMethod.UNIVERSAL_AUTH)) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "The identity does not have universal auth"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { permission } = await permissionService.getOrgPermission(
|
||||||
|
actor,
|
||||||
|
actorId,
|
||||||
|
identityMembershipOrg.orgId,
|
||||||
|
actorAuthMethod,
|
||||||
|
actorOrgId
|
||||||
|
);
|
||||||
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
|
const deleted = await keyStore.deleteItems({
|
||||||
|
pattern: `lockout:identity:${identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:*`
|
||||||
|
});
|
||||||
|
|
||||||
|
return { deleted, identityId, orgId: identityMembershipOrg.orgId };
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
login,
|
login,
|
||||||
attachUniversalAuth,
|
attachUniversalAuth,
|
||||||
@@ -722,6 +779,7 @@ export const identityUaServiceFactory = ({
|
|||||||
createUniversalAuthClientSecret,
|
createUniversalAuthClientSecret,
|
||||||
getUniversalAuthClientSecrets,
|
getUniversalAuthClientSecrets,
|
||||||
revokeUniversalAuthClientSecret,
|
revokeUniversalAuthClientSecret,
|
||||||
getUniversalAuthClientSecretById
|
getUniversalAuthClientSecretById,
|
||||||
|
clearUniversalAuthLockouts
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export type TAttachUaDTO = {
|
|||||||
clientSecretTrustedIps: { ipAddress: string }[];
|
clientSecretTrustedIps: { ipAddress: string }[];
|
||||||
accessTokenTrustedIps: { ipAddress: string }[];
|
accessTokenTrustedIps: { ipAddress: string }[];
|
||||||
isActorSuperAdmin?: boolean;
|
isActorSuperAdmin?: boolean;
|
||||||
|
lockoutEnabled: boolean;
|
||||||
|
lockoutThreshold: number;
|
||||||
|
lockoutDurationSeconds: number;
|
||||||
|
lockoutCounterResetSeconds: number;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TUpdateUaDTO = {
|
export type TUpdateUaDTO = {
|
||||||
@@ -19,6 +23,10 @@ export type TUpdateUaDTO = {
|
|||||||
accessTokenPeriod?: number;
|
accessTokenPeriod?: number;
|
||||||
clientSecretTrustedIps?: { ipAddress: string }[];
|
clientSecretTrustedIps?: { ipAddress: string }[];
|
||||||
accessTokenTrustedIps?: { ipAddress: string }[];
|
accessTokenTrustedIps?: { ipAddress: string }[];
|
||||||
|
lockoutEnabled?: boolean;
|
||||||
|
lockoutThreshold?: number;
|
||||||
|
lockoutDurationSeconds?: number;
|
||||||
|
lockoutCounterResetSeconds?: number;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TGetUaDTO = {
|
export type TGetUaDTO = {
|
||||||
@@ -45,6 +53,10 @@ export type TRevokeUaClientSecretDTO = {
|
|||||||
clientSecretId: string;
|
clientSecretId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TClearUaLockoutsDTO = {
|
||||||
|
identityId: string;
|
||||||
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
export type TGetUniversalAuthClientSecretByIdDTO = {
|
export type TGetUniversalAuthClientSecretByIdDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
clientSecretId: string;
|
clientSecretId: string;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
validatePrivilegeChangeOperation
|
validatePrivilegeChangeOperation
|
||||||
} from "@app/ee/services/permission/permission-fns";
|
} from "@app/ee/services/permission/permission-fns";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
|
import { TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError, PermissionBoundaryError } from "@app/lib/errors";
|
||||||
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
import { TIdentityProjectDALFactory } from "@app/services/identity-project/identity-project-dal";
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ type TIdentityServiceFactoryDep = {
|
|||||||
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
|
identityProjectDAL: Pick<TIdentityProjectDALFactory, "findByIdentityId">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "updateSubscriptionOrgMemberCount">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "getKeysByPattern">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
|
export type TIdentityServiceFactory = ReturnType<typeof identityServiceFactory>;
|
||||||
@@ -42,7 +44,8 @@ export const identityServiceFactory = ({
|
|||||||
identityOrgMembershipDAL,
|
identityOrgMembershipDAL,
|
||||||
identityProjectDAL,
|
identityProjectDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService
|
licenseService,
|
||||||
|
keyStore
|
||||||
}: TIdentityServiceFactoryDep) => {
|
}: TIdentityServiceFactoryDep) => {
|
||||||
const createIdentity = async ({
|
const createIdentity = async ({
|
||||||
name,
|
name,
|
||||||
@@ -255,7 +258,20 @@ export const identityServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Read, OrgPermissionSubjects.Identity);
|
||||||
|
|
||||||
return identity;
|
const activeLockouts = await keyStore.getKeysByPattern(`lockout:identity:${id}:*`);
|
||||||
|
|
||||||
|
const activeLockoutAuthMethods = new Set<string>();
|
||||||
|
activeLockouts.forEach((key) => {
|
||||||
|
const parts = key.split(":");
|
||||||
|
if (parts.length > 3) {
|
||||||
|
activeLockoutAuthMethods.add(parts[3]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...identity,
|
||||||
|
identity: { ...identity.identity, activeLockoutAuthMethods: Array.from(activeLockoutAuthMethods) }
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteIdentity = async ({
|
const deleteIdentity = async ({
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
TDeleteFolderDTO,
|
TDeleteFolderDTO,
|
||||||
TDeleteManyFoldersDTO,
|
TDeleteManyFoldersDTO,
|
||||||
TGetFolderByIdDTO,
|
TGetFolderByIdDTO,
|
||||||
|
TGetFolderByPathDTO,
|
||||||
TGetFolderDTO,
|
TGetFolderDTO,
|
||||||
TGetFoldersDeepByEnvsDTO,
|
TGetFoldersDeepByEnvsDTO,
|
||||||
TUpdateFolderDTO,
|
TUpdateFolderDTO,
|
||||||
@@ -1398,6 +1399,31 @@ export const secretFolderServiceFactory = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFolderByPath = async (
|
||||||
|
{ projectId, environment, secretPath }: TGetFolderByPathDTO,
|
||||||
|
actor: OrgServiceActor
|
||||||
|
) => {
|
||||||
|
// folder check is allowed to be read by anyone
|
||||||
|
// permission is to check if user has access
|
||||||
|
await permissionService.getProjectPermission({
|
||||||
|
actor: actor.type,
|
||||||
|
actorId: actor.id,
|
||||||
|
projectId,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
actionProjectType: ActionProjectType.SecretManager
|
||||||
|
});
|
||||||
|
|
||||||
|
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||||
|
|
||||||
|
if (!folder)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Could not find folder with path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
||||||
|
});
|
||||||
|
|
||||||
|
return folder;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createFolder,
|
createFolder,
|
||||||
updateFolder,
|
updateFolder,
|
||||||
@@ -1405,6 +1431,7 @@ export const secretFolderServiceFactory = ({
|
|||||||
deleteFolder,
|
deleteFolder,
|
||||||
getFolders,
|
getFolders,
|
||||||
getFolderById,
|
getFolderById,
|
||||||
|
getFolderByPath,
|
||||||
getProjectFolderCount,
|
getProjectFolderCount,
|
||||||
getFoldersMultiEnv,
|
getFoldersMultiEnv,
|
||||||
getFoldersDeepByEnvs,
|
getFoldersDeepByEnvs,
|
||||||
|
|||||||
@@ -91,3 +91,9 @@ export type TDeleteManyFoldersDTO = {
|
|||||||
idOrName: string;
|
idOrName: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TGetFolderByPathDTO = {
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ The changelog below reflects new product developments and updates on a monthly b
|
|||||||
- Revamped UI for Access Controls, Access Tree, Policies, and Approval Workflows.
|
- Revamped UI for Access Controls, Access Tree, Policies, and Approval Workflows.
|
||||||
- Released [TLS Certificate Authentication method](https://infisical.com/docs/documentation/platform/identities/tls-cert-auth).
|
- Released [TLS Certificate Authentication method](https://infisical.com/docs/documentation/platform/identities/tls-cert-auth).
|
||||||
- Added ability to copy session tokens in the Infisical Dashboard.
|
- Added ability to copy session tokens in the Infisical Dashboard.
|
||||||
- Expanded resource support for [Infisical Terraform Provider](https://infisical.com/docs/integrations/frameworks/terraform).
|
- Expanded resource support for [Infisical Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs).
|
||||||
|
|
||||||
|
|
||||||
## May 2025
|
## May 2025
|
||||||
@@ -62,7 +62,7 @@ The changelog below reflects new product developments and updates on a monthly b
|
|||||||
## March 2025
|
## March 2025
|
||||||
|
|
||||||
- Released [Infisical Gateway](https://infisical.com/docs/documentation/platform/gateways/overview) for secure access to private resources without needing direct inbound connections to private networks.
|
- Released [Infisical Gateway](https://infisical.com/docs/documentation/platform/gateways/overview) for secure access to private resources without needing direct inbound connections to private networks.
|
||||||
- Enhanced [Terraform](https://infisical.com/docs/integrations/frameworks/terraform#terraform) capabilities with token authentication, ability to import existing Infisical secrets as resources, and support for project templates.
|
- Enhanced [Terraform](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) capabilities with token authentication, ability to import existing Infisical secrets as resources, and support for project templates.
|
||||||
- Self-hosted improvements: Usage and billing visibility for enabled features, ability to delete users, and support for multiple super admins.
|
- Self-hosted improvements: Usage and billing visibility for enabled features, ability to delete users, and support for multiple super admins.
|
||||||
- UI and UX updates: Improved secret import interface on the overview page, password reset without backup PDF.
|
- UI and UX updates: Improved secret import interface on the overview page, password reset without backup PDF.
|
||||||
- CLI enhancements: Various improvements including multiline secret support and ability to pass headers.
|
- CLI enhancements: Various improvements including multiline secret support and ability to pass headers.
|
||||||
@@ -93,7 +93,7 @@ The changelog below reflects new product developments and updates on a monthly b
|
|||||||
- Added support for OIDC group mapping in [Keycloak](https://infisical.com/docs/documentation/platform/sso/keycloak-oidc/overview), enabling automatic mapping of Keycloak groups to Infisical for role-based access control.
|
- Added support for OIDC group mapping in [Keycloak](https://infisical.com/docs/documentation/platform/sso/keycloak-oidc/overview), enabling automatic mapping of Keycloak groups to Infisical for role-based access control.
|
||||||
- Enhanced [Kubernetes operator](https://infisical.com/docs/integrations/platforms/kubernetes/overview#kubernetes-operator) with namespaced group support, bi-directional secret sync (push to Infisical), [dynamic secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview#dynamic-secrets) capabilities, and support for multiple operator instances.
|
- Enhanced [Kubernetes operator](https://infisical.com/docs/integrations/platforms/kubernetes/overview#kubernetes-operator) with namespaced group support, bi-directional secret sync (push to Infisical), [dynamic secrets](https://infisical.com/docs/documentation/platform/dynamic-secrets/overview#dynamic-secrets) capabilities, and support for multiple operator instances.
|
||||||
- Restructured navigation with dedicated sections for Secrets Management, [Certificate Management (PKI)](https://infisical.com/docs/documentation/platform/pki/overview), [Key Management (KMS)](https://infisical.com/docs/documentation/platform/kms/overview#key-management-service-kms), and [SSH Key Management](https://infisical.com/docs/documentation/platform/ssh).
|
- Restructured navigation with dedicated sections for Secrets Management, [Certificate Management (PKI)](https://infisical.com/docs/documentation/platform/pki/overview), [Key Management (KMS)](https://infisical.com/docs/documentation/platform/kms/overview#key-management-service-kms), and [SSH Key Management](https://infisical.com/docs/documentation/platform/ssh).
|
||||||
- Added [ephemeral Terraform resource](https://infisical.com/docs/integrations/frameworks/terraform#terraform-provider) support and improved secret sync architecture.
|
- Added [ephemeral Terraform resource](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) support and improved secret sync architecture.
|
||||||
- Released [.NET provider](https://github.com/Infisical/infisical-dotnet-configuration) with first-party Azure authentication support and Azure CLI integration.
|
- Released [.NET provider](https://github.com/Infisical/infisical-dotnet-configuration) with first-party Azure authentication support and Azure CLI integration.
|
||||||
- Implemented secret Access Visibility allowing users to view all entities with access to specific secrets in the secret side panel.
|
- Implemented secret Access Visibility allowing users to view all entities with access to specific secrets in the secret side panel.
|
||||||
- Added secret filtering by metadata and SSH assigned certificates (Version 1).
|
- Added secret filtering by metadata and SSH assigned certificates (Version 1).
|
||||||
@@ -212,7 +212,7 @@ The changelog below reflects new product developments and updates on a monthly b
|
|||||||
- Completed Postgres migration initiative with restructed Fastify-based backend.
|
- Completed Postgres migration initiative with restructed Fastify-based backend.
|
||||||
- Reduced size of Infisical Node.js SDK by ≈90%.
|
- Reduced size of Infisical Node.js SDK by ≈90%.
|
||||||
- Added secret fallback support to all SDK's.
|
- Added secret fallback support to all SDK's.
|
||||||
- Added Machine Identity support to [Terraform Provider](https://github.com/Infisical/terraform-provider-infisical).
|
- Added Machine Identity support to [Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs).
|
||||||
- Released [.NET SDK](https://infisical.com/docs/sdks/languages/csharp).
|
- Released [.NET SDK](https://infisical.com/docs/sdks/languages/csharp).
|
||||||
- Added symmetric encryption support to all SDK's.
|
- Added symmetric encryption support to all SDK's.
|
||||||
- Fixed secret reminders bug, where reminders were not being updated correctly.
|
- Fixed secret reminders bug, where reminders were not being updated correctly.
|
||||||
@@ -276,7 +276,7 @@ The changelog below reflects new product developments and updates on a monthly b
|
|||||||
|
|
||||||
## June 2023
|
## June 2023
|
||||||
|
|
||||||
- Released the [Terraform Provider](https://infisical.com/docs/integrations/frameworks/terraform#5-run-terraform).
|
- Released the [Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs).
|
||||||
- Updated the usage and billing page. Added the free trial for the professional tier.
|
- Updated the usage and billing page. Added the free trial for the professional tier.
|
||||||
- Added native integrations with [Checkly](https://infisical.com/docs/integrations/cloud/checkly), [Hashicorp Vault](https://infisical.com/docs/integrations/cloud/hashicorp-vault), and [Cloudflare Pages](https://infisical.com/docs/integrations/cloud/cloudflare-pages).
|
- Added native integrations with [Checkly](https://infisical.com/docs/integrations/cloud/checkly), [Hashicorp Vault](https://infisical.com/docs/integrations/cloud/hashicorp-vault), and [Cloudflare Pages](https://infisical.com/docs/integrations/cloud/cloudflare-pages).
|
||||||
- Completed a penetration test with a `very good` result.
|
- Completed a penetration test with a `very good` result.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ should approach the development and contribution process.
|
|||||||
Infisical has two major code-bases. One for the platform code, and one for SDKs. The contribution process has some key differences between the two, so we've split the documentation into two sections:
|
Infisical has two major code-bases. One for the platform code, and one for SDKs. The contribution process has some key differences between the two, so we've split the documentation into two sections:
|
||||||
|
|
||||||
- The [Infisical Platform](https://github.com/Infisical/infisical), the Infisical platform itself.
|
- The [Infisical Platform](https://github.com/Infisical/infisical), the Infisical platform itself.
|
||||||
- The [Infisical SDK](https://github.com/Infisical/sdk), the official Infisical client SDKs.
|
- The [Infisical SDK](https://infisical.com/docs/sdks/overview), the official Infisical client SDKs.
|
||||||
|
|
||||||
|
|
||||||
<CardGroup cols={2}>
|
<CardGroup cols={2}>
|
||||||
|
|||||||
@@ -1,408 +0,0 @@
|
|||||||
---
|
|
||||||
title: "Local development"
|
|
||||||
description: "This guide will help you contribute to the Infisical SDK."
|
|
||||||
---
|
|
||||||
|
|
||||||
## Fork and clone the repo
|
|
||||||
|
|
||||||
[Fork](https://docs.github.com/en/get-started/quickstart/fork-a-repo) the [repository](https://github.com/Infisical/sdk) to your own GitHub account and then [clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) it to your local device.
|
|
||||||
|
|
||||||
Once, you've done that, create a new branch:
|
|
||||||
|
|
||||||
```console
|
|
||||||
git checkout -b MY_BRANCH_NAME
|
|
||||||
```
|
|
||||||
|
|
||||||
## Set up environment variables
|
|
||||||
|
|
||||||
Start by creating a .env file at the root of the Infisical directory then copy the contents of the file below into the .env file.
|
|
||||||
|
|
||||||
<Accordion title=".env file content">
|
|
||||||
```env
|
|
||||||
# This is required for running tests locally.
|
|
||||||
# Rename this file to ".env" and fill in the values below.
|
|
||||||
|
|
||||||
# Please make sure that the machine identity has access to the project you are testing in.
|
|
||||||
# https://infisical.com/docs/documentation/platform/identities/universal-auth
|
|
||||||
INFISICAL_UNIVERSAL_CLIENT_ID=MACHINE_IDENTITY_CLIENT_ID
|
|
||||||
INFISICAL_UNIVERSAL_CLIENT_SECRET=MACHINE_IDENTITY_CLIENT_SECRET
|
|
||||||
|
|
||||||
# The ID of the Infisical project where we will create the test secrets.
|
|
||||||
# NOTE: The project must have a dev environment. (This is created by default when you create a project.)
|
|
||||||
INFISICAL_PROJECT_ID=INFISICAL_TEST_PROJECT_ID
|
|
||||||
|
|
||||||
# The Infisical site URL. If you are testing with a local Infisical instance, then this should be set to "http://localhost:8080".
|
|
||||||
INFISICAL_SITE_URL=https://app.infisical.com
|
|
||||||
|
|
||||||
````
|
|
||||||
</Accordion>
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
The above values are required for running tests locally. Before opening a pull request, make sure to run `cargo test` to ensure that all tests pass.
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
|
|
||||||
## Guidelines
|
|
||||||
|
|
||||||
### Predictable and consistent
|
|
||||||
When adding new functionality (such as new functions), it's very important that the functionality is added to _all_ the SDK's. This is to ensure that the SDK's are predictable and consistent across all languages. If you are adding new functionality, please make sure to add it to all the SDK's.
|
|
||||||
|
|
||||||
### Handling errors
|
|
||||||
Error handling is very important when writing SDK's. We want to make sure that the SDK's are easy to use, and that the user gets a good understanding of what went wrong when something fails. When adding new functionality, please make sure to add proper error handling. [Read more about error handling here](#error-handling).
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
If you add new functionality or modify existing functionality, please write tests thats properly cover the new functionality. You can run tests locally by running `cargo test` from the root directory. You must always run tests before opening a pull request.
|
|
||||||
|
|
||||||
### Code style
|
|
||||||
Please follow the default rust styling guide when writing code for the base SDK. [Read more about rust code style here](https://doc.rust-lang.org/nightly/style-guide/#the-default-rust-style).
|
|
||||||
|
|
||||||
|
|
||||||
## Prerequisites for contributing
|
|
||||||
|
|
||||||
### Understanding the terms
|
|
||||||
|
|
||||||
In the guide we use some terms that might be unfamiliar to you. Here's a quick explanation of the terms we use:
|
|
||||||
- **Base SDK**: The base SDK is the SDK that all other SDK's are built on top of. The base SDK is written in Rust, and is responsible for executing commands and parsing the input and output to and from JSON.
|
|
||||||
- **Commands**: Commands are what's being sent from the target language to the command handler. The command handler uses the command to execute the corresponding function in the base SDK. Commands are in reality just a JSON string that tells the command handler what function to execute, and what input to use.
|
|
||||||
- **Command handler**: The command handler is the part of the base SDK that takes care of executing commands. It also takes care of parsing the input and output to and from JSON.
|
|
||||||
- **Target language**: The target language refers to the actual SDK code. For example, the [Node.js SDK](https://www.npmjs.com/package/@infisical/sdk) is a "target language", and so is the [Python SDK](https://pypi.org/project/infisical-python/).
|
|
||||||
|
|
||||||
|
|
||||||
### Understanding the execution flow
|
|
||||||
After the target language SDK is initiated, it uses language-specific bindings to interact with the base SDK.
|
|
||||||
These bindings are instantiated, setting up the interface for command execution. A client within the command handler is created, which issues commands to the base SDK.
|
|
||||||
When a command is executed, it is first validated. If valid, the command handler locates the corresponding command to perform. If the command executes successfully, the command handler returns the output to the target language SDK, where it is parsed and returned to the user.
|
|
||||||
If the command handler fails to validate the input, an error will be returned to the target language SDK.
|
|
||||||
|
|
||||||
|
|
||||||
<Frame caption="Execution flow diagram for the SDK from the target language to the base SDK. The execution flow is the same for all target languages.">
|
|
||||||
<img height="640" width="520" src="/images/sdk-flow.png" />
|
|
||||||
</Frame>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Rust knowledge
|
|
||||||
|
|
||||||
Contributing to the SDK requires intermediate to advanced knowledge of Rust concepts such as lifetimes, traits, generics, and async/await _(futures)_, and more.
|
|
||||||
|
|
||||||
### Rust setup
|
|
||||||
The base SDK is written in rust. Therefore you must have rustc and cargo installed. You can install rustc and cargo by following the instructions [here](https://www.rust-lang.org/tools/install).
|
|
||||||
|
|
||||||
You shouldn't have to use the rust cross compilation toolchain, as all compilation is done through a collection of Github Actions. However. If you need to test cross compilation, please do so with Github Actions.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
If you add new functionality or modify existing functionality, please write tests thats properly cover the new functionality. You can run tests locally by running `cargo test` from the root directory.
|
|
||||||
|
|
||||||
### Language-specific crates
|
|
||||||
The language-specific crates should ideally never have to be modified, as they are simply a wrapper for the `infisical-json` crate, which executes "commands" from the base SDK. If you need to create a new target-language specific crate, please try to create native bindings for the target language. Some languages don't have direct support for native bindings (Java as an example). In those cases we can use the C bindings (`crates/infisical-c`) in the target language.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Generate types
|
|
||||||
Having almost seemless type safety from the base SDK to the target language is critical, as writing types for each language has a lot of drawbacks such as duplicated code, and lots of overhead trying to keep the types up-to-date and in sync across a large collection of languages. Therefore we decided to use [QuickType](https://quicktype.io/) and [Serde](https://serde.rs/) to help us generate types for each language. In our Rust base SDK (`crates/infisical`), we define all the inputs/outputs.
|
|
||||||
|
|
||||||
If you are interested in reading about QuickType works under the hood, you can [read more here](http://blog.quicktype.io/under-the-hood/).
|
|
||||||
|
|
||||||
This is an example of a type defined in Rust (both input and output). For this to become a generated type, you'll need to add it to our schema generator. More on that further down.
|
|
||||||
```rust
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
// Input:
|
|
||||||
pub struct CreateSecretOptions {
|
|
||||||
pub environment: String, // environment
|
|
||||||
pub secret_comment: Option<String>, // secretComment
|
|
||||||
pub path: Option<String>, // secretPath
|
|
||||||
pub secret_value: String, // secretValue
|
|
||||||
pub skip_multiline_encoding: Option<bool>, // skipMultilineEncoding
|
|
||||||
pub r#type: Option<String>, // shared / personal
|
|
||||||
pub project_id: String, // workspaceId
|
|
||||||
pub secret_name: String, // secretName (PASSED AS PARAMETER IN REQUEST)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output:
|
|
||||||
#[derive(Serialize, Deserialize, Debug, JsonSchema)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct CreateSecretResponse {
|
|
||||||
pub secret: Secret, // "Secret" is defined elsewhere.
|
|
||||||
}
|
|
||||||
````
|
|
||||||
|
|
||||||
### Adding input types to the schema generator
|
|
||||||
|
|
||||||
You will _only_ have to define outputs in our schema generator, then QuickType will take care of the rest behind the scenes. You can find the Rust crate that takes care of type generation here: `crates/sdk-schemas/src/main.rs`.
|
|
||||||
|
|
||||||
Simply add the output _(also called response)_, to the `write_schema_for_response!` macro. This will let QuickType know that it should generate types for the given structs. The main function will look something like this:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
// Input types for new Client
|
|
||||||
write_schema_for!(infisical_json::client::ClientSettings);
|
|
||||||
// Input types for Client::run_command
|
|
||||||
write_schema_for!(infisical_json::command::Command);
|
|
||||||
|
|
||||||
// Output types for Client::run_command
|
|
||||||
// Only add structs which are direct results of SDK commands.
|
|
||||||
write_schema_for_response! {
|
|
||||||
infisical::manager::secrets::GetSecretResponse,
|
|
||||||
infisical::manager::secrets::ListSecretsResponse,
|
|
||||||
infisical::manager::secrets::UpdateSecretResponse,
|
|
||||||
infisical::manager::secrets::DeleteSecretResponse,
|
|
||||||
infisical::manager::secrets::CreateSecretResponse, // <-- This is the output from the above example!
|
|
||||||
infisical::auth::AccessTokenSuccessResponse
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Generating the types for the target language
|
|
||||||
|
|
||||||
Once you've added the output to the schema generator, you can generate the types for the target language by running the following command from the root directory:
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ npm install
|
|
||||||
$ npm run schemas
|
|
||||||
```
|
|
||||||
|
|
||||||
<Warning>If you change any of the structs defined in the base SDK, you will need to run this script to re-generate the types.</Warning>
|
|
||||||
|
|
||||||
This command will run the `schemas.ts` file found in the `support/scripts` folder. If you are adding a new language, it's important that you add the language to the code.
|
|
||||||
|
|
||||||
This is an example of how how we generate types for Node.js:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const ts = await quicktype({
|
|
||||||
inputData,
|
|
||||||
lang: "typescript",
|
|
||||||
rendererOptions: {}
|
|
||||||
});
|
|
||||||
await ensureDir("./languages/node/src/infisical_client");
|
|
||||||
writeToFile("./languages/node/src/infisical_client/schemas.ts", ts.lines);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building bindings
|
|
||||||
We've tried to streamline the building process as much as possible. So you shouldn't have to worry much about building bindings, as it should just be a few commands.
|
|
||||||
|
|
||||||
### Node.js
|
|
||||||
Building bindings for Node.js is very straight foward. The command below will generate NAPI bindings for Node.js, and move the bindings to the correct folder. We use [NAPI-RS](https://napi.rs/) to generate the bindings.
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ cd languages/node
|
|
||||||
$ npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Python
|
|
||||||
To generate and use python bindings you will need to run the following commands.
|
|
||||||
The Python SDK is located inside the crates folder. This is a limitation of the maturin tool, forcing us to structure the project in this way.
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ pip install -U pip maturin
|
|
||||||
$ cd crates/infisical-py
|
|
||||||
$ python3 -m venv .venv
|
|
||||||
$ source .venv/bin/activate
|
|
||||||
$ maturin develop
|
|
||||||
```
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
After running the commands above, it's very important that you rename the generated .so file to `infisical_py.so`. After renaming it you also need to move it into the root of the `crates/infisical-py` folder.
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
### Java
|
|
||||||
Java uses the C bindings to interact with the base SDK. To build and use the C bindings in Java, please follow the instructions below.
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ cd crates/infisical-c
|
|
||||||
$ cargo build --release
|
|
||||||
$ cd ../../languages/java
|
|
||||||
```
|
|
||||||
<Warning>
|
|
||||||
After generating the C bindings, the generated .so or .dll has been created in the `/target` directory at the root of the project.
|
|
||||||
You have to manually move the generated file into the `languages/java/src/main/resources` directory.
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
## Error handling
|
|
||||||
|
|
||||||
### Error handling in the base SDK
|
|
||||||
|
|
||||||
The base SDK should never panic. If an error occurs, we should return a `Result` with an error message. We have a custom Result type defined in the `error.rs` file in the base SDK.
|
|
||||||
|
|
||||||
All our errors are defined in an enum called `Error`. The `Error` enum is defined in the `error.rs` file in the base SDK. The `Error` enum is used in the `Result` type, which is used as the return type for all functions in the base SDK.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum Error {
|
|
||||||
// Secret not found
|
|
||||||
#[error("Secret with name '{}' not found.", .secret_name)]
|
|
||||||
SecretNotFound { secret_name: String },
|
|
||||||
|
|
||||||
// .. other errors
|
|
||||||
|
|
||||||
// Errors that are not specific to the base SDK.
|
|
||||||
#[error(transparent)]
|
|
||||||
Reqwest(#[from] reqwest::Error),
|
|
||||||
#[error(transparent)]
|
|
||||||
Serde(#[from] serde_json::Error),
|
|
||||||
#[error(transparent)]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Returning an error
|
|
||||||
|
|
||||||
You can find many examples of how we return errors in the SDK code. A relevant example is for creating secrets, which can be found in `crates/infisical/src/api/secrets/create_secret.rs`. When the error happened due to a request error to our API, we have an API error handler. This prevents duplicate code and keeps error handling consistent across the SDK. You can find the api error handler in the `error.rs` file.
|
|
||||||
|
|
||||||
### Error handling in the target language SDK's.
|
|
||||||
|
|
||||||
All data sent to the target language SDK has the same format. The format is an object with 3 fields: `success (boolean)`, `data (could be anything or nothing)`, and `errorMessage (string or null)`.
|
|
||||||
|
|
||||||
The `success` field is used to determine if the request was successful or not. The `data` field is used to return data from the SDK. The `errorMessage` field is used to return an error message if the request was not successful.
|
|
||||||
|
|
||||||
This means that if the success if false or if the error message is not null, something went wrong and we should throw an error on the target-language level, with the error message.
|
|
||||||
|
|
||||||
## Command handler
|
|
||||||
|
|
||||||
### What is the command handler
|
|
||||||
|
|
||||||
The command handler (the `infisical-json` crate), takes care of executing commands sent from the target language. It also takes care of parsing the input and output to and from JSON. The command handler is the only part of the base SDK that should be aware of JSON. The rest of the base SDK should be completely unaware of JSON, and only work with the Rust structs defined in the base SDK.
|
|
||||||
|
|
||||||
The command handler exposes a function called `run_command`, which is what we use in the target language to execute commands. The function takes a json string as input, and returns a json string as output. We use helper functions generated by QuickType to convert the input and output to and from JSON.
|
|
||||||
|
|
||||||
### Creating new SDK methods
|
|
||||||
|
|
||||||
Creating new commands is necessary when adding new methods to the SDK's. Defining a new command is a 3-step process in most cases.
|
|
||||||
|
|
||||||
#### 1. Define the input and output structs
|
|
||||||
|
|
||||||
Earlier in this guide, we defined the input and output structs for the `CreateSecret` command. We will use that as an example here as well.
|
|
||||||
|
|
||||||
#### 2. Creating the method in the base SDK
|
|
||||||
|
|
||||||
The first step is to create the method in the base SDK. This step will be different depending on what method you are adding. In this example we're going to assume you're adding a function for creating a new secret.
|
|
||||||
|
|
||||||
After you created the function for creating the secret, you'll need need to add it to the ClientSecrets implementation. We do it this way to keep the code organized and easy to read. The ClientSecrets struct is located in the `crates/infisical/src/manager/secrets.rs` file.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct ClientSecrets<'a> {
|
|
||||||
pub(crate) client: &'a mut crate::Client,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> ClientSecrets<'a> {
|
|
||||||
pub async fn create(&mut self, input: &CreateSecretOptions) -> Result<CreateSecretResponse> {
|
|
||||||
create_secret(self.client, input).await // <-- This is the function you created!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Client {
|
|
||||||
pub fn secrets(&'a mut self) -> ClientSecrets<'a> {
|
|
||||||
ClientSecrets { client: self }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Define a new command
|
|
||||||
|
|
||||||
We define new commands in the `crates/infisical-json/src/command.rs` file. The `Command` enum is what we use to define new commands.
|
|
||||||
|
|
||||||
In the codesnippet below we define a new command called `CreateSecret`. The `CreateSecret` command takes a `CreateSecretOptions` struct as input. We don't have to define the output, because QuickType's converter helps us with figuring out the return type for each command.
|
|
||||||
|
|
||||||
````rust
|
|
||||||
```rust
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, JsonSchema, Debug)]
|
|
||||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
||||||
pub enum Command {
|
|
||||||
GetSecret(GetSecretOptions),
|
|
||||||
ListSecrets(ListSecretsOptions),
|
|
||||||
CreateSecret(CreateSecretOptions), // <-- The new command!
|
|
||||||
UpdateSecret(UpdateSecretOptions),
|
|
||||||
DeleteSecret(DeleteSecretOptions),
|
|
||||||
}
|
|
||||||
````
|
|
||||||
|
|
||||||
#### 4. Add the command to the command handler
|
|
||||||
|
|
||||||
After defining the command, we need to add it to the command handler itself. This takes place in the `crates/infisical-json/src/client.rs` file. The `run_command` function is what we use to execute commands.
|
|
||||||
|
|
||||||
In the Client implementation we try to parse the JSON string into a `Command` enum. If the parsing is successful, we match the command and execute the corresponding function.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
match cmd {
|
|
||||||
Command::GetSecret(req) => self.0.secrets().get(&req).await.into_string(),
|
|
||||||
Command::ListSecrets(req) => self.0.secrets().list(&req).await.into_string(),
|
|
||||||
Command::UpdateSecret(req) => self.0.secrets().update(&req).await.into_string(),
|
|
||||||
Command::DeleteSecret(req) => self.0.secrets().delete(&req).await.into_string(),
|
|
||||||
|
|
||||||
// This is the new command:
|
|
||||||
Command::CreateSecret(req) => self.0.secrets().create(&req).await.into_string(),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 5. Implementing the new command in the target language SDK's
|
|
||||||
|
|
||||||
We did it! We've now added a new command to the base SDK. The last step is to implement the new command in the target language SDK's. The process is a little different from language to language, but in this example we're going to assume that we're adding a new command to the Node.js SDK.
|
|
||||||
|
|
||||||
First you'll need to generate the new type schemas, we added a new command, input struct, and output struct. [Read more about generating types here](#generating-the-types-for-the-target-language).
|
|
||||||
|
|
||||||
Secondly you need to build the new node bindings so we can use the new functionality in the Node.js SDK. You can do this by running the following command from the `languages/node` directory:
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ npm install
|
|
||||||
$ npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
The build command will execute a build script in the `infisical-napi` crate, and move the generated bindings to the appropriate folder.
|
|
||||||
|
|
||||||
After building the new bindings, you can access the new functionality in the Node.js SDK source.
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// 'binding' is a js file that makes it easier to access the methods in the bindings. (it's auto generated when running npm run build)
|
|
||||||
import * as rust from "../../binding";
|
|
||||||
// We can import the newly generated types from the schemas.ts file. (Generated with QuickType!)
|
|
||||||
import type { CreateSecretOptions, CreateSecretResponse } from "./schemas";
|
|
||||||
// This is the QuickType converter that we use to create commands with! It takes care of all JSON parsing and serialization.
|
|
||||||
import { Convert, ClientSettings } from "./schemas";
|
|
||||||
|
|
||||||
export class InfisicalClient {
|
|
||||||
#client: rust.Client;
|
|
||||||
|
|
||||||
constructor(settings: ClientSettings) {
|
|
||||||
const settingsJson = settings == null ? null : Convert.clientSettingsToJson(settings);
|
|
||||||
this.#client = new rust.InfisicalClient(settingsJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... getSecret
|
|
||||||
// ... listSecrets
|
|
||||||
// ... updateSecret
|
|
||||||
// ... deleteSecret
|
|
||||||
|
|
||||||
async createSecret(options: CreateSecretOptions): Promise<CreateSecretResponse["secret"]> {
|
|
||||||
// The runCommand will return a JSON string, which we can parse into a CreateSecretResponse.
|
|
||||||
const command = await this.#client.runCommand(
|
|
||||||
Convert.commandToJson({
|
|
||||||
createSecret: options
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const response = Convert.toResponseForCreateSecretResponse(command); // <-- This is the QuickType converter in action!
|
|
||||||
|
|
||||||
// If the response is not successful or the data is null, we throw an error.
|
|
||||||
if (!response.success || response.data == null) {
|
|
||||||
throw new Error(response.errorMessage ?? "Something went wrong");
|
|
||||||
}
|
|
||||||
|
|
||||||
// To make it easier to work with the response, we return the secret directly.
|
|
||||||
return response.data.secret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And that's it! We've now added a new command to the base SDK, and implemented it in the Node.js SDK. The process is very similar for all other languages, but the code will look a little different.
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
The SDK has a lot of moving parts, and it can be a little overwhelming at first. But once you get the hang of it, it's actually quite simple. If you have any questions, feel free to reach out to us on [Slack](https://infisical.com/slack), or [open an issue](https://github.com/Infisical/sdk/issues) on GitHub.
|
|
||||||
+1
-5
@@ -368,10 +368,6 @@
|
|||||||
"contributing/platform/backend/how-to-create-a-feature",
|
"contributing/platform/backend/how-to-create-a-feature",
|
||||||
"contributing/platform/backend/folder-structure"
|
"contributing/platform/backend/folder-structure"
|
||||||
]
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"group": "Contributing to SDK",
|
|
||||||
"pages": ["contributing/sdk/developing"]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -2564,7 +2560,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Terraform",
|
"label": "Terraform",
|
||||||
"href": "https://infisical.com/docs/integrations/frameworks/terraform"
|
"href": "https://registry.terraform.io/providers/Infisical/infisical/latest/docs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Ansible",
|
"label": "Ansible",
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ description: "Learn how to migrate secrets from Vault to Infisical."
|
|||||||
|
|
||||||
Migrating from Vault Self-Hosted or Dedicated Vault is a straight forward process with our inbuilt migration option. In order to migrate from Vault, you'll need to provide Infisical an access token to your Vault instance.
|
Migrating from Vault Self-Hosted or Dedicated Vault is a straight forward process with our inbuilt migration option. In order to migrate from Vault, you'll need to provide Infisical an access token to your Vault instance.
|
||||||
|
|
||||||
Currently the Vault migration only supports migrating secrets from the KV v2 secrets engine. If you're using a different secrets engine, please open an issue on our [GitHub repository](https://github.com/infisical/infisical/issues).
|
Currently the Vault migration only supports migrating secrets from the KV V2 and V1 secrets engine. If you're using a different secrets engine, please open an issue on our [GitHub repository](https://github.com/infisical/infisical/issues).
|
||||||
|
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- A Vault instance with the KV v2 secrets engine enabled.
|
- A Vault instance with the KV secret engine enabled.
|
||||||
- An access token to your Vault instance.
|
- An access token to your Vault instance.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ In the following steps, we explore how to set up ACME Certificate Authority inte
|
|||||||
- **Directory URL**: Enter the ACME v2 directory URL for your chosen CA provider (e.g., `https://acme-v02.api.letsencrypt.org/directory` for Let's Encrypt).
|
- **Directory URL**: Enter the ACME v2 directory URL for your chosen CA provider (e.g., `https://acme-v02.api.letsencrypt.org/directory` for Let's Encrypt).
|
||||||
- **Account Email**: Email address to associate with your ACME account. This email will receive important notifications about your certificates.
|
- **Account Email**: Email address to associate with your ACME account. This email will receive important notifications about your certificates.
|
||||||
- **Enable Direct Issuance**: Toggle on to allow direct certificate issuance without requiring subscribers.
|
- **Enable Direct Issuance**: Toggle on to allow direct certificate issuance without requiring subscribers.
|
||||||
|
- **EAB Key Identifier (KID)**: (Optional) The Key Identifier (KID) provided by your ACME CA for External Account Binding (EAB). This is required by some ACME providers (e.g., ZeroSSL, DigiCert) to link your ACME account to an external account you've pre-registered with them.
|
||||||
|
- **EAB HMAC Key**: (Optional) The HMAC Key provided by your ACME CA for External Account Binding (EAB). This key is used in conjunction with the KID to prove ownership of the external account during ACME account registration.
|
||||||
|
|
||||||
Finally, press **Create** to register the ACME CA with Infisical.
|
Finally, press **Create** to register the ACME CA with Infisical.
|
||||||
</Step>
|
</Step>
|
||||||
@@ -277,6 +279,19 @@ Let's Encrypt is a free, automated, and open Certificate Authority that provides
|
|||||||
Always test your ACME integration using Let's Encrypt's staging environment first. This allows you to verify your DNS configuration and certificate issuance process without consuming your production rate limits.
|
Always test your ACME integration using Let's Encrypt's staging environment first. This allows you to verify your DNS configuration and certificate issuance process without consuming your production rate limits.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
## Example: DigiCert Integration
|
||||||
|
|
||||||
|
DigiCert is a leading commercial Certificate Authority providing a wide range of trusted SSL/TLS certificates. Infisical can integrate with [DigiCert's ACME](https://docs.digicert.com/en/certcentral/certificate-tools/certificate-lifecycle-automation-guides/third-party-acme-integration/request-and-manage-certificates-with-acme.html) service to automate the provisioning and management of these certificates.
|
||||||
|
|
||||||
|
- **Directory URL**: `https://acme.digicert.com/v2/acme/directory`
|
||||||
|
- **External Account Binding (EAB)**: Required. You will need a Key Identifier (KID) and HMAC Key from your DigiCert account to register the ACME CA in Infisical.
|
||||||
|
- **Certificate Validity**: Typically 90 days, with automatic renewal through Infisical.
|
||||||
|
- **Trusted By**: All major browsers and operating systems.
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
When integrating with DigiCert ACME, ensure you have obtained the necessary External Account Binding (EAB) Key Identifier (KID) and HMAC Key from your DigiCert account.
|
||||||
|
</Note>
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ The table below provides a quick overview of which delivery method may be suitab
|
|||||||
| Kubernetes (file-based, with rotation) | [Kubernetes CSI Provider](/integrations/platforms/kubernetes-csi) | Mounted files | Uses CSI driver to mount secrets as files with automatic rotation |
|
| Kubernetes (file-based, with rotation) | [Kubernetes CSI Provider](/integrations/platforms/kubernetes-csi) | Mounted files | Uses CSI driver to mount secrets as files with automatic rotation |
|
||||||
| Image builds (VMs or containers) | [Packer Plugin](/integrations/frameworks/packer) | Env vars or files | Inject secrets at image build time |
|
| Image builds (VMs or containers) | [Packer Plugin](/integrations/frameworks/packer) | Env vars or files | Inject secrets at image build time |
|
||||||
| Ansible automation | [Ansible Collection](/integrations/platforms/ansible) | Variables | Runtime secret fetching in playbooks using lookup plugin |
|
| Ansible automation | [Ansible Collection](/integrations/platforms/ansible) | Variables | Runtime secret fetching in playbooks using lookup plugin |
|
||||||
| Terraform / Pulumi | [Terraform Provider](/integrations/frameworks/terraform), [Pulumi](/integrations/frameworks/pulumi) | Inputs / ephemeral resources | Use ephemeral for security; avoids storing secrets in state |
|
| Terraform / Pulumi | [Terraform Provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs), [Pulumi](/integrations/frameworks/pulumi) | Inputs / ephemeral resources | Use ephemeral for security; avoids storing secrets in state |
|
||||||
| Third-party platforms (GitHub, AWS, etc.) | [Secret Syncs](/integrations/secret-syncs/overview) | Preloaded secrets | Push secrets to platforms that can't fetch directly from Infisical |
|
| Third-party platforms (GitHub, AWS, etc.) | [Secret Syncs](/integrations/secret-syncs/overview) | Preloaded secrets | Push secrets to platforms that can't fetch directly from Infisical |
|
||||||
|
|
||||||
From here, you can explore the delivery method that best matches your environment:
|
From here, you can explore the delivery method that best matches your environment:
|
||||||
@@ -90,7 +90,7 @@ This is useful when external systems require secrets to be available ahead of ti
|
|||||||
|
|
||||||
Infisical integrates with common IaC and automation tools to help you securely inject secrets into your infrastructure provisioning workflows:
|
Infisical integrates with common IaC and automation tools to help you securely inject secrets into your infrastructure provisioning workflows:
|
||||||
|
|
||||||
- [Terraform](/integrations/frameworks/terraform): Use the official Infisical Terraform provider to fetch secrets either as ephemeral resources (never written to state files) or as traditional data sources. Ideal for managing cloud infrastructure while keeping secrets secure and version-safe.
|
- [Terraform](https://registry.terraform.io/providers/Infisical/infisical/latest/docs): Use the official Infisical Terraform provider to fetch secrets either as ephemeral resources (never written to state files) or as traditional data sources. Ideal for managing cloud infrastructure while keeping secrets secure and version-safe.
|
||||||
- [Pulumi](/integrations/frameworks/pulumi): Integrate Infisical into Pulumi projects using the Terraform Bridge, allowing you to fetch and manage secrets in TypeScript, Go, Python, or C# — without changing your existing workflows.
|
- [Pulumi](/integrations/frameworks/pulumi): Integrate Infisical into Pulumi projects using the Terraform Bridge, allowing you to fetch and manage secrets in TypeScript, Go, Python, or C# — without changing your existing workflows.
|
||||||
- [Ansible](/integrations/platforms/ansible): Retrieve secrets from Infisical at runtime using the official Ansible Collection and lookup plugin. Works well for dynamic configuration during playbook execution.
|
- [Ansible](/integrations/platforms/ansible): Retrieve secrets from Infisical at runtime using the official Ansible Collection and lookup plugin. Works well for dynamic configuration during playbook execution.
|
||||||
- [Packer](/integrations/frameworks/packer): Inject secrets into VM or container images at build time using the Infisical Packer Plugin — useful for provisioning base images that require secure configuration values.
|
- [Packer](/integrations/frameworks/packer): Inject secrets into VM or container images at build time using the Infisical Packer Plugin — useful for provisioning base images that require secure configuration values.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 539 KiB After Width: | Height: | Size: 587 KiB |
@@ -7,7 +7,7 @@ Infisical can be integrated with Pulumi by leveraging Pulumi’s [Terraform Brid
|
|||||||
which allows Terraform providers to be used seamlessly within Pulumi projects. This enables infrastructure and platform teams to manage Infisical secrets and resources
|
which allows Terraform providers to be used seamlessly within Pulumi projects. This enables infrastructure and platform teams to manage Infisical secrets and resources
|
||||||
using Pulumi’s familiar programming languages (including TypeScript, Python, Go, and C#), without any change to existing workflows.
|
using Pulumi’s familiar programming languages (including TypeScript, Python, Go, and C#), without any change to existing workflows.
|
||||||
|
|
||||||
The Terraform Bridge wraps the [Infisical Terraform provider](/integrations/frameworks/terraform) and exposes its resources (such as `infisical_secret`, `infisical_project`, and `infisical_service_token`)
|
The Terraform Bridge wraps the [Infisical Terraform provider](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) and exposes its resources (such as `infisical_secret`, `infisical_project`, and `infisical_service_token`)
|
||||||
in a Pulumi-compatible interface. This makes it easy to integrate secret management directly into Pulumi-based IaC pipelines, ensuring secrets stay in sync with
|
in a Pulumi-compatible interface. This makes it easy to integrate secret management directly into Pulumi-based IaC pipelines, ensuring secrets stay in sync with
|
||||||
the rest of your cloud infrastructure. Authentication is handled through the same methods as Terraform: using environment variables such as `INFISICAL_TOKEN` and `INFISICAL_SITE_URL`.
|
the rest of your cloud infrastructure. Authentication is handled through the same methods as Terraform: using environment variables such as `INFISICAL_TOKEN` and `INFISICAL_SITE_URL`.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: "Terraform"
|
title: "Terraform"
|
||||||
description: "Learn how to fetch secrets from Infisical with Terraform using both traditional data sources and ephemeral resources"
|
description: "Learn how to fetch secrets from Infisical with Terraform using both traditional data sources and ephemeral resources"
|
||||||
|
url: "https://registry.terraform.io/providers/Infisical/infisical/latest/docs"
|
||||||
---
|
---
|
||||||
|
{/*
|
||||||
This guide demonstrates how to use Infisical to manage secrets in your Terraform infrastructure code, supporting both traditional data sources and ephemeral resources for enhanced security. It uses:
|
This guide demonstrates how to use Infisical to manage secrets in your Terraform infrastructure code, supporting both traditional data sources and ephemeral resources for enhanced security. It uses:
|
||||||
|
|
||||||
- Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets
|
- Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets
|
||||||
@@ -234,4 +235,4 @@ For detailed instructions on setting up OIDC authentication with GitHub Actions,
|
|||||||
See also:
|
See also:
|
||||||
- [Machine Identity setup guide](/documentation/platform/identities/machine-identities)
|
- [Machine Identity setup guide](/documentation/platform/identities/machine-identities)
|
||||||
- [Terraform Provider Registry](https://registry.terraform.io/providers/Infisical/infisical/latest/docs)
|
- [Terraform Provider Registry](https://registry.terraform.io/providers/Infisical/infisical/latest/docs)
|
||||||
- [GitOps Best Practices](https://www.infisical.com/blog/gitops-best-practices)
|
- [GitOps Best Practices](https://www.infisical.com/blog/gitops-best-practices) */}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ Integrations allow environment variables to be synced from Infisical into your l
|
|||||||
Missing an integration? [Throw in a request](https://github.com/Infisical/infisical/issues).
|
Missing an integration? [Throw in a request](https://github.com/Infisical/infisical/issues).
|
||||||
|
|
||||||
| Integration | Type | Status |
|
| Integration | Type | Status |
|
||||||
| -------------------------------------------------------------- | ---------------------- | ----------- |
|
| ------------------------------------------------------------------------------------- | ---------------------- | ---------------------------------- |
|
||||||
| [Docker](/integrations/platforms/docker) | Platform | Available |
|
| [Docker](/integrations/platforms/docker) | Platform | Available |
|
||||||
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
|
| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available |
|
||||||
| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available |
|
| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available |
|
||||||
| [Terraform](/integrations/frameworks/terraform) | Infrastructure as code | Available |
|
| [Terraform](https://registry.terraform.io/providers/Infisical/infisical/latest/docs) | Infrastructure as code | Available |
|
||||||
| [PM2](/integrations/platforms/pm2) | Platform | Available |
|
| [PM2](/integrations/platforms/pm2) | Platform | Available |
|
||||||
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
|
| [Heroku](/integrations/cloud/heroku) | Cloud | Available |
|
||||||
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
|
| [Vercel](/integrations/cloud/vercel) | Cloud | Available |
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -153,7 +153,7 @@ const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
|||||||
reset();
|
reset();
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
navigate({
|
navigate({
|
||||||
to: getProjectHomePage(project.type),
|
to: getProjectHomePage(project.type, project.environments),
|
||||||
params: { projectId: project.id }
|
params: { projectId: project.id }
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
{leftIcon && (
|
{leftIcon && (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"inline-flex shrink-0 cursor-pointer items-center justify-center transition-all",
|
"pointer-events-none inline-flex shrink-0 items-center justify-center transition-all",
|
||||||
loadingToggleClass,
|
loadingToggleClass,
|
||||||
size === "xs" ? "mr-1" : "mr-2"
|
size === "xs" ? "mr-1" : "mr-2"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type Props = {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
icon?: IconDefinition;
|
icon?: IconDefinition;
|
||||||
iconSize?: SizeProp;
|
iconSize?: SizeProp;
|
||||||
|
titleClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmptyState = ({
|
export const EmptyState = ({
|
||||||
@@ -17,7 +18,8 @@ export const EmptyState = ({
|
|||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
icon = faCubesStacked,
|
icon = faCubesStacked,
|
||||||
iconSize = "2x"
|
iconSize = "2x",
|
||||||
|
titleClassName
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
@@ -27,7 +29,7 @@ export const EmptyState = ({
|
|||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={icon} size={iconSize} />
|
<FontAwesomeIcon icon={icon} size={iconSize} />
|
||||||
<div className="flex flex-col items-center py-4">
|
<div className="flex flex-col items-center py-4">
|
||||||
<div className="text-sm text-bunker-300">{title}</div>
|
<div className={twMerge("text-sm text-bunker-300", titleClassName)}>{title}</div>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const FilterableSelect = <T,>({
|
|||||||
}),
|
}),
|
||||||
menuPortal: (provided) => ({
|
menuPortal: (provided) => ({
|
||||||
...provided,
|
...provided,
|
||||||
zIndex: 9999
|
zIndex: 99999
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
tabSelectsValue={tabSelectsValue}
|
tabSelectsValue={tabSelectsValue}
|
||||||
|
|||||||
@@ -6,20 +6,12 @@ import { useToggle } from "@app/hooks";
|
|||||||
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
import { HIDDEN_SECRET_VALUE } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretItem";
|
||||||
|
|
||||||
const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g;
|
const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g;
|
||||||
const replaceContentWithDot = (str: string) => {
|
|
||||||
let finalStr = "";
|
|
||||||
for (let i = 0; i < str.length; i += 1) {
|
|
||||||
const char = str.at(i);
|
|
||||||
finalStr += char === "\n" ? "\n" : "*";
|
|
||||||
}
|
|
||||||
return finalStr;
|
|
||||||
};
|
|
||||||
|
|
||||||
const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => {
|
const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => {
|
||||||
if (isImport && !content) return "IMPORTED";
|
if (isImport && !content) return "IMPORTED";
|
||||||
if (content === "") return "EMPTY";
|
if (content === "") return "EMPTY";
|
||||||
if (!content) return "EMPTY";
|
if (!content) return "EMPTY";
|
||||||
if (!isVisible) return replaceContentWithDot(content);
|
if (!isVisible) return HIDDEN_SECRET_VALUE;
|
||||||
|
|
||||||
let skipNext = false;
|
let skipNext = false;
|
||||||
const formattedContent = content.split(REGEX).flatMap((el, i) => {
|
const formattedContent = content.split(REGEX).flatMap((el, i) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DetailedHTMLProps, HTMLAttributes, ReactNode, TdHTMLAttributes } from "react";
|
import { DetailedHTMLProps, forwardRef, HTMLAttributes, ReactNode, TdHTMLAttributes } from "react";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { Skeleton } from "../Skeleton";
|
import { Skeleton } from "../Skeleton";
|
||||||
@@ -9,13 +9,10 @@ export type TableContainerProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
||||||
|
|
||||||
export const TableContainer = ({
|
export const TableContainer = forwardRef<HTMLDivElement, TableContainerProps>(
|
||||||
children,
|
({ children, className, isRounded = true, ...props }, ref): JSX.Element => (
|
||||||
className,
|
|
||||||
isRounded = true,
|
|
||||||
...props
|
|
||||||
}: TableContainerProps): JSX.Element => (
|
|
||||||
<div
|
<div
|
||||||
|
ref={ref}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"relative w-full overflow-x-auto border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter",
|
"relative w-full overflow-x-auto border border-solid border-mineshaft-700 bg-mineshaft-800 font-inter",
|
||||||
isRounded && "rounded-lg",
|
isRounded && "rounded-lg",
|
||||||
@@ -25,6 +22,7 @@ export const TableContainer = ({
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// main parent table
|
// main parent table
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { useOrgPermission } from "./OrgPermissionContext";
|
|||||||
export type { TOrgPermission } from "./types";
|
export type { TOrgPermission } from "./types";
|
||||||
export {
|
export {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
|
OrgPermissionAuditLogsActions,
|
||||||
OrgPermissionBillingActions,
|
OrgPermissionBillingActions,
|
||||||
OrgPermissionGroupActions,
|
OrgPermissionGroupActions,
|
||||||
OrgPermissionIdentityActions,
|
OrgPermissionIdentityActions,
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ export enum OrgPermissionAppConnectionActions {
|
|||||||
Connect = "connect"
|
Connect = "connect"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum OrgPermissionAuditLogsActions {
|
||||||
|
Read = "read"
|
||||||
|
}
|
||||||
|
|
||||||
export enum OrgPermissionKmipActions {
|
export enum OrgPermissionKmipActions {
|
||||||
Proxy = "proxy",
|
Proxy = "proxy",
|
||||||
Setup = "setup"
|
Setup = "setup"
|
||||||
@@ -118,7 +122,7 @@ export type OrgPermissionSet =
|
|||||||
| [OrgPermissionBillingActions, OrgPermissionSubjects.Billing]
|
| [OrgPermissionBillingActions, OrgPermissionSubjects.Billing]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
| [OrgPermissionActions, OrgPermissionSubjects.Kms]
|
||||||
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.AuditLogs]
|
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
|
||||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||||
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
|
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
|
||||||
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
|
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { useProjectPermission } from "./ProjectPermissionContext";
|
|||||||
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
export type { ProjectPermissionSet, TProjectPermission } from "./types";
|
||||||
export {
|
export {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionAuditLogsActions,
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
|
|||||||
@@ -150,6 +150,10 @@ export enum ProjectPermissionSecretEventActions {
|
|||||||
SubscribeImportMutations = "subscribe-on-import-mutations"
|
SubscribeImportMutations = "subscribe-on-import-mutations"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ProjectPermissionAuditLogsActions {
|
||||||
|
Read = "read"
|
||||||
|
}
|
||||||
|
|
||||||
export enum PermissionConditionOperators {
|
export enum PermissionConditionOperators {
|
||||||
$IN = "$in",
|
$IN = "$in",
|
||||||
$ALL = "$all",
|
$ALL = "$all",
|
||||||
@@ -365,7 +369,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
|
| [ProjectPermissionActions, ProjectPermissionSub.Groups]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
| [ProjectPermissionActions, ProjectPermissionSub.Integrations]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
| [ProjectPermissionActions, ProjectPermissionSub.Webhooks]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.AuditLogs]
|
| [ProjectPermissionAuditLogsActions, ProjectPermissionSub.AuditLogs]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
|
| [ProjectPermissionActions, ProjectPermissionSub.Environments]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export { useOrganization } from "./OrganizationContext";
|
|||||||
export type { TOrgPermission } from "./OrgPermissionContext";
|
export type { TOrgPermission } from "./OrgPermissionContext";
|
||||||
export {
|
export {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
|
OrgPermissionAuditLogsActions,
|
||||||
OrgPermissionBillingActions,
|
OrgPermissionBillingActions,
|
||||||
OrgPermissionGroupActions,
|
OrgPermissionGroupActions,
|
||||||
OrgPermissionIdentityActions,
|
OrgPermissionIdentityActions,
|
||||||
@@ -11,6 +12,7 @@ export {
|
|||||||
export type { TProjectPermission } from "./ProjectPermissionContext";
|
export type { TProjectPermission } from "./ProjectPermissionContext";
|
||||||
export {
|
export {
|
||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
|
ProjectPermissionAuditLogsActions,
|
||||||
ProjectPermissionCertificateActions,
|
ProjectPermissionCertificateActions,
|
||||||
ProjectPermissionCmekActions,
|
ProjectPermissionCmekActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
|
|||||||
@@ -22,3 +22,62 @@ export const formatDateTime = ({
|
|||||||
}
|
}
|
||||||
return format(date, dateFormat);
|
return format(date, dateFormat);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to convert seconds to value and unit
|
||||||
|
export const getObjectFromSeconds = (
|
||||||
|
totalSeconds: number,
|
||||||
|
activeUnits?: Array<"s" | "m" | "h" | "d" | "w" | "y">
|
||||||
|
): { value: number; unit: "s" | "m" | "h" | "d" | "w" | "y" } => {
|
||||||
|
const SECONDS_IN_MINUTE = 60;
|
||||||
|
const SECONDS_IN_HOUR = SECONDS_IN_MINUTE * 60;
|
||||||
|
const SECONDS_IN_DAY = SECONDS_IN_HOUR * 24;
|
||||||
|
const SECONDS_IN_WEEK = SECONDS_IN_DAY * 7;
|
||||||
|
const SECONDS_IN_YEAR = SECONDS_IN_DAY * 365;
|
||||||
|
|
||||||
|
const activeUnitsSet = activeUnits ? new Set(activeUnits) : null;
|
||||||
|
|
||||||
|
const isUnitActive = (unit: "s" | "m" | "h" | "d" | "w" | "y"): boolean => {
|
||||||
|
return activeUnitsSet ? activeUnitsSet.has(unit) : true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
isUnitActive("y") &&
|
||||||
|
totalSeconds >= SECONDS_IN_YEAR &&
|
||||||
|
totalSeconds % SECONDS_IN_YEAR === 0
|
||||||
|
) {
|
||||||
|
return { value: totalSeconds / SECONDS_IN_YEAR, unit: "y" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isUnitActive("w") &&
|
||||||
|
totalSeconds >= SECONDS_IN_WEEK &&
|
||||||
|
totalSeconds % SECONDS_IN_WEEK === 0
|
||||||
|
) {
|
||||||
|
return { value: totalSeconds / SECONDS_IN_WEEK, unit: "w" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUnitActive("d") && totalSeconds >= SECONDS_IN_DAY && totalSeconds % SECONDS_IN_DAY === 0) {
|
||||||
|
return { value: totalSeconds / SECONDS_IN_DAY, unit: "d" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isUnitActive("h") &&
|
||||||
|
totalSeconds >= SECONDS_IN_HOUR &&
|
||||||
|
totalSeconds % SECONDS_IN_HOUR === 0
|
||||||
|
) {
|
||||||
|
return { value: totalSeconds / SECONDS_IN_HOUR, unit: "h" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isUnitActive("m") &&
|
||||||
|
totalSeconds >= SECONDS_IN_MINUTE &&
|
||||||
|
totalSeconds % SECONDS_IN_MINUTE === 0
|
||||||
|
) {
|
||||||
|
return { value: totalSeconds / SECONDS_IN_MINUTE, unit: "m" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: totalSeconds,
|
||||||
|
unit: "s"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import { createWorkspace } from "@app/hooks/api/workspace/queries";
|
import { createWorkspace } from "@app/hooks/api/workspace/queries";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType, WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||||
|
|
||||||
const secretsToBeAdded = [
|
const secretsToBeAdded = [
|
||||||
{
|
{
|
||||||
@@ -72,12 +72,14 @@ export const getProjectBaseURL = (type: ProjectType) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProjectHomePage = (type: ProjectType) => {
|
// @ts-expect-error akhilmhdh: will remove this later
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export const getProjectHomePage = (type: ProjectType, environments: WorkspaceEnv[]) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ProjectType.SecretManager:
|
case ProjectType.SecretManager:
|
||||||
return "/projects/secret-management/$projectId/overview";
|
return "/projects/secret-management/$projectId/overview" as const;
|
||||||
case ProjectType.CertificateManager:
|
case ProjectType.CertificateManager:
|
||||||
return "/projects/cert-management/$projectId/subscribers";
|
return "/projects/cert-management/$projectId/subscribers" as const;
|
||||||
case ProjectType.SecretScanning:
|
case ProjectType.SecretScanning:
|
||||||
return `/projects/${type}/$projectId/data-sources` as const;
|
return `/projects/${type}/$projectId/data-sources` as const;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const eventToNameMap: { [K in EventType]: string } = {
|
|||||||
[EventType.GET_IDENTITY_UNIVERSAL_AUTH]: "Get universal auth",
|
[EventType.GET_IDENTITY_UNIVERSAL_AUTH]: "Get universal auth",
|
||||||
[EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Create universal auth client secret",
|
[EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Create universal auth client secret",
|
||||||
[EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Revoke universal auth client secret",
|
[EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Revoke universal auth client secret",
|
||||||
|
[EventType.CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS]: "Clear universal auth lockouts",
|
||||||
[EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS]: "Get universal auth client secrets",
|
[EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS]: "Get universal auth client secrets",
|
||||||
[EventType.CREATE_ENVIRONMENT]: "Create environment",
|
[EventType.CREATE_ENVIRONMENT]: "Create environment",
|
||||||
[EventType.UPDATE_ENVIRONMENT]: "Update environment",
|
[EventType.UPDATE_ENVIRONMENT]: "Update environment",
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export enum EventType {
|
|||||||
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
|
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
|
||||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
||||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
||||||
|
CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS = "clear-identity-universal-auth-lockouts",
|
||||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
||||||
|
|
||||||
LOGIN_IDENTITY_LDAP_AUTH = "login-identity-ldap-auth",
|
LOGIN_IDENTITY_LDAP_AUTH = "login-identity-ldap-auth",
|
||||||
|
|||||||
@@ -326,6 +326,14 @@ interface RevokeIdentityUniversalAuthClientSecretEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ClearIdentityUniversalAuthLockoutsEvent {
|
||||||
|
type: EventType.CLEAR_IDENTITY_UNIVERSAL_AUTH_LOCKOUTS;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
clientSecretId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface CreateEnvironmentEvent {
|
interface CreateEnvironmentEvent {
|
||||||
type: EventType.CREATE_ENVIRONMENT;
|
type: EventType.CREATE_ENVIRONMENT;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -892,6 +900,7 @@ export type Event =
|
|||||||
| CreateIdentityUniversalAuthClientSecretEvent
|
| CreateIdentityUniversalAuthClientSecretEvent
|
||||||
| GetIdentityUniversalAuthClientSecretsEvent
|
| GetIdentityUniversalAuthClientSecretsEvent
|
||||||
| RevokeIdentityUniversalAuthClientSecretEvent
|
| RevokeIdentityUniversalAuthClientSecretEvent
|
||||||
|
| ClearIdentityUniversalAuthLockoutsEvent
|
||||||
| CreateEnvironmentEvent
|
| CreateEnvironmentEvent
|
||||||
| UpdateEnvironmentEvent
|
| UpdateEnvironmentEvent
|
||||||
| DeleteEnvironmentEvent
|
| DeleteEnvironmentEvent
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export type TAcmeCertificateAuthority = {
|
|||||||
};
|
};
|
||||||
directoryUrl: string;
|
directoryUrl: string;
|
||||||
accountEmail: string;
|
accountEmail: string;
|
||||||
|
eabKid?: string;
|
||||||
|
eabHmacKey?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
import {
|
import {
|
||||||
@@ -273,6 +274,12 @@ export const useGetProjectSecretsDetails = (
|
|||||||
...options,
|
...options,
|
||||||
// wait for all values to be available
|
// wait for all values to be available
|
||||||
enabled: Boolean(projectId) && (options?.enabled ?? true),
|
enabled: Boolean(projectId) && (options?.enabled ?? true),
|
||||||
|
retry: (count, error) => {
|
||||||
|
// don't retry 404s
|
||||||
|
if (error instanceof AxiosError && error.status === 404) return false;
|
||||||
|
|
||||||
|
return count <= 5;
|
||||||
|
},
|
||||||
queryKey: dashboardKeys.getProjectSecretsDetails({
|
queryKey: dashboardKeys.getProjectSecretsDetails({
|
||||||
secretPath,
|
secretPath,
|
||||||
search,
|
search,
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export type DashboardProjectSecretsOverview = Omit<
|
|||||||
DashboardProjectSecretsOverviewResponse,
|
DashboardProjectSecretsOverviewResponse,
|
||||||
"secrets" | "secretRotations"
|
"secrets" | "secretRotations"
|
||||||
> & {
|
> & {
|
||||||
secrets?: SecretV3RawSanitized[];
|
secrets?: (SecretV3RawSanitized & { sourceEnv?: string })[];
|
||||||
secretRotations?: (TSecretRotationV2 & {
|
secretRotations?: (TSecretRotationV2 & {
|
||||||
secrets: (SecretV3RawSanitized | null)[];
|
secrets: (SecretV3RawSanitized | null)[];
|
||||||
})[];
|
})[];
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
AddIdentityTlsCertAuthDTO,
|
AddIdentityTlsCertAuthDTO,
|
||||||
AddIdentityTokenAuthDTO,
|
AddIdentityTokenAuthDTO,
|
||||||
AddIdentityUniversalAuthDTO,
|
AddIdentityUniversalAuthDTO,
|
||||||
|
ClearIdentityUniversalAuthLockoutsDTO,
|
||||||
ClientSecretData,
|
ClientSecretData,
|
||||||
CreateIdentityDTO,
|
CreateIdentityDTO,
|
||||||
CreateIdentityUniversalAuthClientSecretDTO,
|
CreateIdentityUniversalAuthClientSecretDTO,
|
||||||
@@ -148,7 +149,11 @@ export const useAddIdentityUniversalAuth = () => {
|
|||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps
|
accessTokenTrustedIps,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDurationSeconds,
|
||||||
|
lockoutCounterResetSeconds
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
data: { identityUniversalAuth }
|
data: { identityUniversalAuth }
|
||||||
@@ -157,7 +162,11 @@ export const useAddIdentityUniversalAuth = () => {
|
|||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps
|
accessTokenTrustedIps,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDurationSeconds,
|
||||||
|
lockoutCounterResetSeconds
|
||||||
});
|
});
|
||||||
return identityUniversalAuth;
|
return identityUniversalAuth;
|
||||||
},
|
},
|
||||||
@@ -183,7 +192,11 @@ export const useUpdateIdentityUniversalAuth = () => {
|
|||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps,
|
accessTokenTrustedIps,
|
||||||
accessTokenPeriod
|
accessTokenPeriod,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDurationSeconds,
|
||||||
|
lockoutCounterResetSeconds
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
data: { identityUniversalAuth }
|
data: { identityUniversalAuth }
|
||||||
@@ -193,7 +206,11 @@ export const useUpdateIdentityUniversalAuth = () => {
|
|||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
accessTokenTrustedIps,
|
accessTokenTrustedIps,
|
||||||
accessTokenPeriod
|
accessTokenPeriod,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDurationSeconds,
|
||||||
|
lockoutCounterResetSeconds
|
||||||
});
|
});
|
||||||
return identityUniversalAuth;
|
return identityUniversalAuth;
|
||||||
},
|
},
|
||||||
@@ -275,6 +292,25 @@ export const useRevokeIdentityUniversalAuthClientSecret = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useClearIdentityUniversalAuthLockouts = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<number, object, ClearIdentityUniversalAuthLockoutsDTO>({
|
||||||
|
mutationFn: async ({ identityId }) => {
|
||||||
|
const {
|
||||||
|
data: { deleted }
|
||||||
|
} = await apiRequest.post<{ deleted: number }>(
|
||||||
|
`/api/v1/auth/universal-auth/identities/${identityId}/clear-lockouts`
|
||||||
|
);
|
||||||
|
return deleted;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { identityId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: identitiesKeys.getIdentityUniversalAuth(identityId)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useAddIdentityGcpAuth = () => {
|
export const useAddIdentityGcpAuth = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<IdentityGcpAuth, object, AddIdentityGcpAuthDTO>({
|
return useMutation<IdentityGcpAuth, object, AddIdentityGcpAuthDTO>({
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type Identity = {
|
|||||||
name: string;
|
name: string;
|
||||||
hasDeleteProtection: boolean;
|
hasDeleteProtection: boolean;
|
||||||
authMethods: IdentityAuthMethod[];
|
authMethods: IdentityAuthMethod[];
|
||||||
|
activeLockoutAuthMethods: IdentityAuthMethod[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
isInstanceAdmin?: boolean;
|
isInstanceAdmin?: boolean;
|
||||||
@@ -113,6 +114,10 @@ export type IdentityUniversalAuth = {
|
|||||||
accessTokenNumUsesLimit: number;
|
accessTokenNumUsesLimit: number;
|
||||||
accessTokenTrustedIps: IdentityTrustedIp[];
|
accessTokenTrustedIps: IdentityTrustedIp[];
|
||||||
accessTokenPeriod: number;
|
accessTokenPeriod: number;
|
||||||
|
lockoutEnabled: boolean;
|
||||||
|
lockoutThreshold: number;
|
||||||
|
lockoutDurationSeconds: number;
|
||||||
|
lockoutCounterResetSeconds: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AddIdentityUniversalAuthDTO = {
|
export type AddIdentityUniversalAuthDTO = {
|
||||||
@@ -128,6 +133,10 @@ export type AddIdentityUniversalAuthDTO = {
|
|||||||
accessTokenTrustedIps: {
|
accessTokenTrustedIps: {
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}[];
|
}[];
|
||||||
|
lockoutEnabled: boolean;
|
||||||
|
lockoutThreshold: number;
|
||||||
|
lockoutDurationSeconds: number;
|
||||||
|
lockoutCounterResetSeconds: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UpdateIdentityUniversalAuthDTO = {
|
export type UpdateIdentityUniversalAuthDTO = {
|
||||||
@@ -143,6 +152,10 @@ export type UpdateIdentityUniversalAuthDTO = {
|
|||||||
accessTokenTrustedIps?: {
|
accessTokenTrustedIps?: {
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}[];
|
}[];
|
||||||
|
lockoutEnabled?: boolean;
|
||||||
|
lockoutThreshold?: number;
|
||||||
|
lockoutDurationSeconds?: number;
|
||||||
|
lockoutCounterResetSeconds?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeleteIdentityUniversalAuthDTO = {
|
export type DeleteIdentityUniversalAuthDTO = {
|
||||||
@@ -558,6 +571,10 @@ export type DeleteIdentityUniversalAuthClientSecretDTO = {
|
|||||||
clientSecretId: string;
|
clientSecretId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ClearIdentityUniversalAuthLockoutsDTO = {
|
||||||
|
identityId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type IdentityTokenAuth = {
|
export type IdentityTokenAuth = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
accessTokenTTL: number;
|
accessTokenTTL: number;
|
||||||
|
|||||||
@@ -46,18 +46,21 @@ export const useImportVault = () => {
|
|||||||
vaultAccessToken,
|
vaultAccessToken,
|
||||||
vaultNamespace,
|
vaultNamespace,
|
||||||
vaultUrl,
|
vaultUrl,
|
||||||
mappingType
|
mappingType,
|
||||||
|
gatewayId
|
||||||
}: {
|
}: {
|
||||||
vaultAccessToken: string;
|
vaultAccessToken: string;
|
||||||
vaultNamespace?: string;
|
vaultNamespace?: string;
|
||||||
vaultUrl: string;
|
vaultUrl: string;
|
||||||
mappingType: string;
|
mappingType: string;
|
||||||
|
gatewayId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
await apiRequest.post("/api/v3/external-migration/vault/", {
|
await apiRequest.post("/api/v3/external-migration/vault/", {
|
||||||
vaultAccessToken,
|
vaultAccessToken,
|
||||||
vaultNamespace,
|
vaultNamespace,
|
||||||
vaultUrl,
|
vaultUrl,
|
||||||
mappingType
|
mappingType,
|
||||||
|
gatewayId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ import {
|
|||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
UpdatePitVersionLimitDTO,
|
UpdatePitVersionLimitDTO,
|
||||||
UpdateProjectDTO,
|
UpdateProjectDTO,
|
||||||
Workspace
|
Workspace,
|
||||||
|
WorkspaceEnv
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const fetchWorkspaceById = async (workspaceId: string) => {
|
export const fetchWorkspaceById = async (workspaceId: string) => {
|
||||||
@@ -396,12 +397,16 @@ export const useDeleteWorkspace = () => {
|
|||||||
export const useCreateWsEnvironment = () => {
|
export const useCreateWsEnvironment = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<object, object, CreateEnvironmentDTO>({
|
return useMutation<WorkspaceEnv, WorkspaceEnv, CreateEnvironmentDTO>({
|
||||||
mutationFn: ({ workspaceId, name, slug }) => {
|
mutationFn: async ({ workspaceId, name, slug }) => {
|
||||||
return apiRequest.post(`/api/v1/workspace/${workspaceId}/environments`, {
|
const { data } = await apiRequest.post<{ environment: WorkspaceEnv }>(
|
||||||
|
`/api/v1/workspace/${workspaceId}/environments`,
|
||||||
|
{
|
||||||
name,
|
name,
|
||||||
slug
|
slug
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
return data.environment;
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
|
import { MouseEvent, RefObject, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
type Params = {
|
type Params = {
|
||||||
minWidth: number;
|
minWidth: number;
|
||||||
maxWidth: number;
|
maxWidth: number;
|
||||||
initialWidth: number;
|
initialWidth: number;
|
||||||
|
ref: RefObject<HTMLTableElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth }: Params) => {
|
export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth, ref }: Params) => {
|
||||||
const [colWidth, setColWidth] = useState(initialWidth);
|
const [colWidth, setColWidth] = useState(initialWidth);
|
||||||
const [isResizing, setIsResizing] = useState(false);
|
const [isResizing, setIsResizing] = useState(false);
|
||||||
const startX = useRef(0);
|
const startX = useRef(0);
|
||||||
@@ -63,6 +64,28 @@ export const useResizableColWidth = ({ minWidth, maxWidth, initialWidth }: Param
|
|||||||
};
|
};
|
||||||
}, [isResizing, handleMouseMove, handleMouseUp]);
|
}, [isResizing, handleMouseMove, handleMouseUp]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = ref?.current;
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (colWidth > maxWidth) {
|
||||||
|
setColWidth(Math.max(maxWidth, minWidth));
|
||||||
|
} else if (ref.current?.clientWidth && colWidth > ref.current.clientWidth * 0.9) {
|
||||||
|
// this else is a fallback to ensure col is always visible
|
||||||
|
setColWidth(initialWidth);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(handleResize);
|
||||||
|
resizeObserver.observe(element);
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, [ref, maxWidth, colWidth]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
colWidth,
|
colWidth,
|
||||||
handleMouseDown,
|
handleMouseDown,
|
||||||
|
|||||||
@@ -130,7 +130,11 @@ export const useSecretOverview = (secrets: DashboardProjectSecretsOverview["secr
|
|||||||
|
|
||||||
const getEnvSecretKeyCount = useCallback(
|
const getEnvSecretKeyCount = useCallback(
|
||||||
(env: string) => {
|
(env: string) => {
|
||||||
return secrets?.filter((secret) => secret.env === env).length ?? 0;
|
return (
|
||||||
|
secrets?.filter((secret) =>
|
||||||
|
secret.sourceEnv ? secret.sourceEnv === env : secret.env === env
|
||||||
|
).length ?? 0
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[secrets]
|
[secrets]
|
||||||
);
|
);
|
||||||
|
|||||||
+4
-1
@@ -36,7 +36,10 @@ export const AssumePrivilegeModeBanner = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
const url = getProjectHomePage(currentWorkspace.type);
|
const url = getProjectHomePage(
|
||||||
|
currentWorkspace.type,
|
||||||
|
currentWorkspace.environments
|
||||||
|
);
|
||||||
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const ProjectSelect = () => {
|
|||||||
<div className="-mr-2 flex w-full items-center gap-1">
|
<div className="-mr-2 flex w-full items-center gap-1">
|
||||||
<DropdownMenu modal={false}>
|
<DropdownMenu modal={false}>
|
||||||
<Link
|
<Link
|
||||||
to={getProjectHomePage(currentWorkspace.type)}
|
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
|
||||||
params={{
|
params={{
|
||||||
projectId: currentWorkspace.id
|
projectId: currentWorkspace.id
|
||||||
}}
|
}}
|
||||||
@@ -158,7 +158,7 @@ export const ProjectSelect = () => {
|
|||||||
// to reproduce change this back to router.push and switch between two projects with different env count
|
// to reproduce change this back to router.push and switch between two projects with different env count
|
||||||
// look into this on dashboard revamp
|
// look into this on dashboard revamp
|
||||||
const url = linkOptions({
|
const url = linkOptions({
|
||||||
to: getProjectHomePage(workspace.type),
|
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||||
params: {
|
params: {
|
||||||
projectId: workspace.id
|
projectId: workspace.id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
faVault
|
faVault
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Link, Outlet } from "@tanstack/react-router";
|
import { Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
|
||||||
@@ -31,6 +31,7 @@ export const SecretManagerLayout = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const workspaceId = currentWorkspace?.id || "";
|
const workspaceId = currentWorkspace?.id || "";
|
||||||
const projectSlug = currentWorkspace?.slug || "";
|
const projectSlug = currentWorkspace?.slug || "";
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
|
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
|
||||||
workspaceId
|
workspaceId
|
||||||
@@ -73,11 +74,21 @@ export const SecretManagerLayout = () => {
|
|||||||
<Link
|
<Link
|
||||||
to="/projects/secret-management/$projectId/overview"
|
to="/projects/secret-management/$projectId/overview"
|
||||||
params={{
|
params={{
|
||||||
projectId: currentWorkspace.id
|
projectId: currentWorkspace.id,
|
||||||
|
...(currentWorkspace.environments.length
|
||||||
|
? { envSlug: currentWorkspace.environments[0]?.slug }
|
||||||
|
: {})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<MenuItem isSelected={isActive}>
|
<MenuItem
|
||||||
|
isSelected={
|
||||||
|
isActive ||
|
||||||
|
location.pathname.startsWith(
|
||||||
|
`/projects/secret-management/${currentWorkspace.id}/overview`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="mx-1 flex gap-2">
|
<div className="mx-1 flex gap-2">
|
||||||
<div className="w-6">
|
<div className="w-6">
|
||||||
<FontAwesomeIcon icon={faVault} />
|
<FontAwesomeIcon icon={faVault} />
|
||||||
|
|||||||
+89
-11
@@ -42,6 +42,17 @@ import {
|
|||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
|
const REQUIRED_EAB_DIRECTORIES = [
|
||||||
|
"https://acme.digicert.com/v2/acme/directory",
|
||||||
|
"https://acme.zerossl.com/v2/DV90",
|
||||||
|
"https://acme.ssl.com/sslcom-dv-rsa",
|
||||||
|
"https://acme.ssl.com/sslcom-dv-ecc",
|
||||||
|
"https://dv.acme-v02.api.pki.goog/directory",
|
||||||
|
"https://acme.sectigo.com/v2/OV",
|
||||||
|
"https://acme.sectigo.com/v2/EV",
|
||||||
|
"https://acme.cisco.com/ACMEv2/directory"
|
||||||
|
];
|
||||||
|
|
||||||
const baseSchema = z.object({
|
const baseSchema = z.object({
|
||||||
type: z.nativeEnum(CaType),
|
type: z.nativeEnum(CaType),
|
||||||
name: slugSchema({
|
name: slugSchema({
|
||||||
@@ -51,7 +62,8 @@ const baseSchema = z.object({
|
|||||||
status: z.nativeEnum(CaStatus)
|
status: z.nativeEnum(CaStatus)
|
||||||
});
|
});
|
||||||
|
|
||||||
const acmeConfigurationSchema = z.object({
|
const acmeConfigurationSchema = z
|
||||||
|
.object({
|
||||||
dnsAppConnection: z.object({
|
dnsAppConnection: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string()
|
name: z.string()
|
||||||
@@ -61,8 +73,28 @@ const acmeConfigurationSchema = z.object({
|
|||||||
hostedZoneId: z.string()
|
hostedZoneId: z.string()
|
||||||
}),
|
}),
|
||||||
directoryUrl: z.string(),
|
directoryUrl: z.string(),
|
||||||
accountEmail: z.string()
|
accountEmail: z.string(),
|
||||||
});
|
eabKid: z.string().optional(),
|
||||||
|
eabHmacKey: z.string().optional()
|
||||||
|
})
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
if (REQUIRED_EAB_DIRECTORIES.includes(data.directoryUrl)) {
|
||||||
|
if (!data.eabKid || data.eabKid.trim() === "") {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "EAB Key Identifier (KID) is required for this directory URL",
|
||||||
|
path: ["eabKid"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!data.eabHmacKey || data.eabHmacKey.trim() === "") {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "EAB HMAC Key is required for this directory URL",
|
||||||
|
path: ["eabHmacKey"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const azureAdCsConfigurationSchema = z.object({
|
const azureAdCsConfigurationSchema = z.object({
|
||||||
azureAdcsConnection: z.object({
|
azureAdcsConnection: z.object({
|
||||||
@@ -122,6 +154,10 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
caType === CaType.ACME && configuration && "dnsProviderConfig" in configuration
|
caType === CaType.ACME && configuration && "dnsProviderConfig" in configuration
|
||||||
? configuration.dnsProviderConfig.provider
|
? configuration.dnsProviderConfig.provider
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const directoryUrl =
|
||||||
|
caType === CaType.ACME && configuration && "directoryUrl" in configuration
|
||||||
|
? configuration.directoryUrl
|
||||||
|
: undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialType = (popUp?.ca?.data as { type: CaType })?.type;
|
const initialType = (popUp?.ca?.data as { type: CaType })?.type;
|
||||||
@@ -155,7 +191,9 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
hostedZoneId: ""
|
hostedZoneId: ""
|
||||||
},
|
},
|
||||||
directoryUrl: "",
|
directoryUrl: "",
|
||||||
accountEmail: ""
|
accountEmail: "",
|
||||||
|
eabKid: "",
|
||||||
|
eabHmacKey: ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -178,13 +216,10 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const availableConnections: TAvailableAppConnection[] = useMemo(() => {
|
const availableConnections: TAvailableAppConnection[] = useMemo(() => {
|
||||||
if (caType === CaType.ACME) {
|
|
||||||
return [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])];
|
|
||||||
}
|
|
||||||
if (caType === CaType.AZURE_AD_CS) {
|
if (caType === CaType.AZURE_AD_CS) {
|
||||||
return availableAzureConnections || [];
|
return availableAzureConnections || [];
|
||||||
}
|
}
|
||||||
return [];
|
return [...(availableRoute53Connections || []), ...(availableCloudflareConnections || [])];
|
||||||
}, [
|
}, [
|
||||||
caType,
|
caType,
|
||||||
availableRoute53Connections,
|
availableRoute53Connections,
|
||||||
@@ -192,7 +227,8 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
availableAzureConnections
|
availableAzureConnections
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isPending = isRoute53Pending || isCloudflarePending || isAzurePending;
|
const isPending =
|
||||||
|
isRoute53Pending || isCloudflarePending || (isAzurePending && caType === CaType.AZURE_AD_CS);
|
||||||
|
|
||||||
const dnsAppConnection =
|
const dnsAppConnection =
|
||||||
caType === CaType.ACME && configuration && "dnsAppConnection" in configuration
|
caType === CaType.ACME && configuration && "dnsAppConnection" in configuration
|
||||||
@@ -227,7 +263,9 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
hostedZoneId: ca.configuration.dnsProviderConfig.hostedZoneId
|
hostedZoneId: ca.configuration.dnsProviderConfig.hostedZoneId
|
||||||
},
|
},
|
||||||
directoryUrl: ca.configuration.directoryUrl,
|
directoryUrl: ca.configuration.directoryUrl,
|
||||||
accountEmail: ca.configuration.accountEmail
|
accountEmail: ca.configuration.accountEmail,
|
||||||
|
eabKid: ca.configuration.eabKid,
|
||||||
|
eabHmacKey: ca.configuration.eabHmacKey
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (ca.type === CaType.AZURE_AD_CS && availableConnections?.length) {
|
} else if (ca.type === CaType.AZURE_AD_CS && availableConnections?.length) {
|
||||||
@@ -268,7 +306,9 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
dnsProviderConfig: formConfiguration.dnsProviderConfig,
|
dnsProviderConfig: formConfiguration.dnsProviderConfig,
|
||||||
directoryUrl: formConfiguration.directoryUrl,
|
directoryUrl: formConfiguration.directoryUrl,
|
||||||
accountEmail: formConfiguration.accountEmail,
|
accountEmail: formConfiguration.accountEmail,
|
||||||
dnsAppConnectionId: formConfiguration.dnsAppConnection.id
|
dnsAppConnectionId: formConfiguration.dnsAppConnection.id,
|
||||||
|
eabKid: formConfiguration.eabKid,
|
||||||
|
eabHmacKey: formConfiguration.eabHmacKey
|
||||||
};
|
};
|
||||||
} else if (type === CaType.AZURE_AD_CS && "azureAdcsConnection" in formConfiguration) {
|
} else if (type === CaType.AZURE_AD_CS && "azureAdcsConnection" in formConfiguration) {
|
||||||
configPayload = {
|
configPayload = {
|
||||||
@@ -499,6 +539,44 @@ export const ExternalCaModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="configuration.eabKid"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="EAB Key Identifier (KID)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isOptional={!REQUIRED_EAB_DIRECTORIES.includes(directoryUrl || "")}
|
||||||
|
isRequired={REQUIRED_EAB_DIRECTORIES.includes(directoryUrl || "")}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="abc123def456ghi789jkl012mno345pqr678stu901vwx234yz"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="configuration.eabHmacKey"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="EAB HMAC Key"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isOptional={!REQUIRED_EAB_DIRECTORIES.includes(directoryUrl || "")}
|
||||||
|
isRequired={REQUIRED_EAB_DIRECTORIES.includes(directoryUrl || "")}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="dGhpc2lzYW5leGFtcGxlaG1hY2tleWZvcmRpZ2ljZXJ0YWNtZXRlc3RpbmcxMjM0NTY3ODkw"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{caType === CaType.AZURE_AD_CS && (
|
{caType === CaType.AZURE_AD_CS && (
|
||||||
|
|||||||
+5
-1
@@ -148,7 +148,11 @@ export const IdentityModal = ({ popUp, handlePopUpToggle }: Props) => {
|
|||||||
accessTokenTTL: 2592000,
|
accessTokenTTL: 2592000,
|
||||||
accessTokenMaxTTL: 2592000,
|
accessTokenMaxTTL: 2592000,
|
||||||
accessTokenNumUsesLimit: 0,
|
accessTokenNumUsesLimit: 0,
|
||||||
accessTokenPeriod: 0
|
accessTokenPeriod: 0,
|
||||||
|
lockoutEnabled: true,
|
||||||
|
lockoutThreshold: 3,
|
||||||
|
lockoutDurationSeconds: 300,
|
||||||
|
lockoutCounterResetSeconds: 30
|
||||||
});
|
});
|
||||||
|
|
||||||
handlePopUpToggle("identity", false);
|
handlePopUpToggle("identity", false);
|
||||||
|
|||||||
+330
-13
@@ -3,6 +3,7 @@ import { Controller, useFieldArray, useForm } from "react-hook-form";
|
|||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
@@ -11,12 +12,16 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
Switch,
|
||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
Tabs
|
Tabs
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
|
import { getObjectFromSeconds } from "@app/helpers/datetime";
|
||||||
import {
|
import {
|
||||||
useAddIdentityUniversalAuth,
|
useAddIdentityUniversalAuth,
|
||||||
useGetIdentityUniversalAuth,
|
useGetIdentityUniversalAuth,
|
||||||
@@ -60,9 +65,79 @@ const schema = z
|
|||||||
ipAddress: z.string().max(50)
|
ipAddress: z.string().max(50)
|
||||||
})
|
})
|
||||||
.array()
|
.array()
|
||||||
.min(1)
|
.min(1),
|
||||||
|
lockoutEnabled: z.boolean().default(true),
|
||||||
|
lockoutThreshold: z
|
||||||
|
.string()
|
||||||
|
.refine(
|
||||||
|
(value) => Number(value) <= 30 && Number(value) >= 1,
|
||||||
|
"Lockout threshold must be between 1 and 30"
|
||||||
|
),
|
||||||
|
lockoutDurationValue: z.string(),
|
||||||
|
lockoutDurationUnit: z.enum(["s", "m", "h", "d"], {
|
||||||
|
invalid_type_error: "Please select a valid time unit"
|
||||||
|
}),
|
||||||
|
lockoutCounterResetValue: z.string(),
|
||||||
|
lockoutCounterResetUnit: z.enum(["s", "m", "h"], {
|
||||||
|
invalid_type_error: "Please select a valid time unit"
|
||||||
})
|
})
|
||||||
.required();
|
})
|
||||||
|
.required()
|
||||||
|
.superRefine((data, ctx) => {
|
||||||
|
const {
|
||||||
|
lockoutDurationValue,
|
||||||
|
lockoutCounterResetValue,
|
||||||
|
lockoutDurationUnit,
|
||||||
|
lockoutCounterResetUnit,
|
||||||
|
lockoutEnabled
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
if (!lockoutEnabled) return;
|
||||||
|
|
||||||
|
let isAnyParseError = false;
|
||||||
|
|
||||||
|
const parsedLockoutDuration = parseInt(lockoutDurationValue, 10);
|
||||||
|
if (Number.isNaN(parsedLockoutDuration)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Lockout duration must be a number",
|
||||||
|
path: ["lockoutDurationValue"]
|
||||||
|
});
|
||||||
|
isAnyParseError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedLockoutCounterReset = parseInt(lockoutCounterResetValue, 10);
|
||||||
|
if (Number.isNaN(parsedLockoutCounterReset)) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Lockout counter reset must be a number",
|
||||||
|
path: ["lockoutCounterResetValue"]
|
||||||
|
});
|
||||||
|
isAnyParseError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAnyParseError) return;
|
||||||
|
|
||||||
|
const lockoutDurationInSeconds = ms(`${parsedLockoutDuration}${lockoutDurationUnit}`) / 1000;
|
||||||
|
const lockoutCounterResetInSeconds =
|
||||||
|
ms(`${parsedLockoutCounterReset}${lockoutCounterResetUnit}`) / 1000;
|
||||||
|
|
||||||
|
if (lockoutDurationInSeconds > 86400 || lockoutDurationInSeconds < 30) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Lockout duration must be between 30 seconds and 1 day",
|
||||||
|
path: ["lockoutDurationValue"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lockoutCounterResetInSeconds > 3600 || lockoutCounterResetInSeconds < 5) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "Lockout counter reset must be between 5 seconds and 1 hour",
|
||||||
|
path: ["lockoutCounterResetValue"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
@@ -107,12 +182,25 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenNumUsesLimit: "0",
|
accessTokenNumUsesLimit: "0",
|
||||||
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||||
accessTokenPeriod: "0"
|
accessTokenPeriod: "0",
|
||||||
|
lockoutEnabled: true,
|
||||||
|
lockoutThreshold: "3",
|
||||||
|
lockoutDurationValue: "5",
|
||||||
|
lockoutDurationUnit: "m",
|
||||||
|
lockoutCounterResetValue: "30",
|
||||||
|
lockoutCounterResetUnit: "s"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const accessTokenPeriodValue = Number(watch("accessTokenPeriod"));
|
const accessTokenPeriodValue = Number(watch("accessTokenPeriod"));
|
||||||
|
|
||||||
|
const lockoutEnabledWatch = watch("lockoutEnabled");
|
||||||
|
const lockoutThresholdWatch = watch("lockoutThreshold");
|
||||||
|
const lockoutDurationValueWatch = watch("lockoutDurationValue");
|
||||||
|
const lockoutDurationUnitWatch = watch("lockoutDurationUnit");
|
||||||
|
const lockoutCounterResetValueWatch = watch("lockoutCounterResetValue");
|
||||||
|
const lockoutCounterResetUnitWatch = watch("lockoutCounterResetUnit");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fields: clientSecretTrustedIpsFields,
|
fields: clientSecretTrustedIpsFields,
|
||||||
append: appendClientSecretTrustedIp,
|
append: appendClientSecretTrustedIp,
|
||||||
@@ -126,6 +214,9 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
const lockoutDurationObj = getObjectFromSeconds(data.lockoutDurationSeconds);
|
||||||
|
const lockoutCounterResetObj = getObjectFromSeconds(data.lockoutCounterResetSeconds);
|
||||||
|
|
||||||
reset({
|
reset({
|
||||||
accessTokenTTL: String(data.accessTokenTTL),
|
accessTokenTTL: String(data.accessTokenTTL),
|
||||||
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
|
||||||
@@ -144,7 +235,13 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
lockoutEnabled: data.lockoutEnabled,
|
||||||
|
lockoutThreshold: String(data.lockoutThreshold),
|
||||||
|
lockoutDurationValue: String(lockoutDurationObj.value),
|
||||||
|
lockoutDurationUnit: lockoutDurationObj.unit as "s" | "m" | "h" | "d",
|
||||||
|
lockoutCounterResetValue: String(lockoutCounterResetObj.value),
|
||||||
|
lockoutCounterResetUnit: lockoutCounterResetObj.unit as "s" | "m" | "h"
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
@@ -153,7 +250,13 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenNumUsesLimit: "0",
|
accessTokenNumUsesLimit: "0",
|
||||||
accessTokenPeriod: "0",
|
accessTokenPeriod: "0",
|
||||||
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||||
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
|
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }],
|
||||||
|
lockoutEnabled: true,
|
||||||
|
lockoutThreshold: "3",
|
||||||
|
lockoutDurationValue: "5",
|
||||||
|
lockoutDurationUnit: "m",
|
||||||
|
lockoutCounterResetValue: "30",
|
||||||
|
lockoutCounterResetUnit: "s"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@@ -164,11 +267,21 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenNumUsesLimit,
|
accessTokenNumUsesLimit,
|
||||||
clientSecretTrustedIps,
|
clientSecretTrustedIps,
|
||||||
accessTokenTrustedIps,
|
accessTokenTrustedIps,
|
||||||
accessTokenPeriod
|
accessTokenPeriod,
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold,
|
||||||
|
lockoutDurationValue,
|
||||||
|
lockoutDurationUnit,
|
||||||
|
lockoutCounterResetValue,
|
||||||
|
lockoutCounterResetUnit
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityId) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
|
const lockoutDurationSeconds = ms(`${lockoutDurationValue}${lockoutDurationUnit}`) / 1000;
|
||||||
|
const lockoutCounterResetSeconds =
|
||||||
|
ms(`${lockoutCounterResetValue}${lockoutCounterResetUnit}`) / 1000;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// update universal auth configuration
|
// update universal auth configuration
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
@@ -179,7 +292,11 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
accessTokenTrustedIps,
|
accessTokenTrustedIps,
|
||||||
accessTokenPeriod: Number(accessTokenPeriod)
|
accessTokenPeriod: Number(accessTokenPeriod),
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold: Number(lockoutThreshold),
|
||||||
|
lockoutDurationSeconds,
|
||||||
|
lockoutCounterResetSeconds
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// create new universal auth configuration
|
// create new universal auth configuration
|
||||||
@@ -192,7 +309,11 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
accessTokenTrustedIps,
|
accessTokenTrustedIps,
|
||||||
accessTokenPeriod: Number(accessTokenPeriod)
|
accessTokenPeriod: Number(accessTokenPeriod),
|
||||||
|
lockoutEnabled,
|
||||||
|
lockoutThreshold: Number(lockoutThreshold),
|
||||||
|
lockoutDurationSeconds: Number(lockoutDurationSeconds),
|
||||||
|
lockoutCounterResetSeconds: Number(lockoutCounterResetSeconds)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,16 +338,31 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
onSubmit={handleSubmit(onFormSubmit, (fields) => {
|
||||||
setTabValue(
|
const firstErrorField = Object.keys(fields)[0];
|
||||||
["accessTokenTrustedIps", "clientSecretTrustedIps"].includes(Object.keys(fields)[0])
|
let tab = IdentityFormTab.Configuration;
|
||||||
? IdentityFormTab.Advanced
|
|
||||||
: IdentityFormTab.Configuration
|
if (["accessTokenTrustedIps", "clientSecretTrustedIps"].includes(firstErrorField)) {
|
||||||
);
|
tab = IdentityFormTab.Advanced;
|
||||||
|
} else if (
|
||||||
|
[
|
||||||
|
"lockoutEnabled",
|
||||||
|
"lockoutThreshold",
|
||||||
|
"lockoutDurationValue",
|
||||||
|
"lockoutDurationUnit",
|
||||||
|
"lockoutCounterResetValue",
|
||||||
|
"lockoutCounterResetUnit"
|
||||||
|
].includes(firstErrorField)
|
||||||
|
) {
|
||||||
|
tab = IdentityFormTab.Lockout;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTabValue(tab);
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
|
||||||
<TabList>
|
<TabList>
|
||||||
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
|
||||||
|
<Tab value={IdentityFormTab.Lockout}>Lockout</Tab>
|
||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
<TabPanel value={IdentityFormTab.Configuration}>
|
||||||
@@ -296,6 +432,187 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel value={IdentityFormTab.Lockout}>
|
||||||
|
<div className="mb-3 flex flex-col">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="lockoutEnabled"
|
||||||
|
defaultValue
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
helperText={`The lockout feature will prevent login attempts for ${lockoutDurationValueWatch}${lockoutDurationUnitWatch} after ${lockoutThresholdWatch} consecutive login failures. If ${lockoutCounterResetValueWatch}${lockoutCounterResetUnitWatch} pass after the most recent failure, the lockout counter resets.`}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
className="ml-0 mr-3 bg-mineshaft-400/80 shadow-inner data-[state=checked]:bg-green/80"
|
||||||
|
containerClassName="flex-row-reverse w-fit"
|
||||||
|
id="lockout-enabled"
|
||||||
|
thumbClassName="bg-mineshaft-800"
|
||||||
|
onCheckedChange={onChange}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
Lockout
|
||||||
|
</Switch>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="lockoutThreshold"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||||
|
label="Lockout Threshold"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
tooltipText="The amount of times login must fail before locking the identity auth method"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="Enter lockout threshold..."
|
||||||
|
isDisabled={!lockoutEnabledWatch}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex items-end gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="lockoutDurationValue"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||||
|
label="Lockout Duration"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
tooltipText="How long an identity auth method lockout lasts"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="Enter lockout duration..."
|
||||||
|
isDisabled={!lockoutEnabledWatch}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="lockoutDurationUnit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className={`mb-0 ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
isDisabled={!lockoutEnabledWatch}
|
||||||
|
value={field.value}
|
||||||
|
className="min-w-32 pr-2"
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value="s"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Seconds</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="m"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Minutes</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="h"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Hours</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="d"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Days</div>
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end gap-2">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="lockoutCounterResetValue"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className={`mb-0 flex-grow ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||||
|
label="Lockout Counter Reset"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
tooltipText="How long to wait from the most recent failed login until resetting the lockout counter"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="Enter lockout counter reset..."
|
||||||
|
isDisabled={!lockoutEnabledWatch}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="lockoutCounterResetUnit"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className={`mb-0 ${lockoutEnabledWatch ? "" : "opacity-70"}`}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
isDisabled={!lockoutEnabledWatch}
|
||||||
|
value={field.value}
|
||||||
|
className="min-w-32 pr-2"
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value="s"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Seconds</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="m"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Minutes</div>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem
|
||||||
|
value="h"
|
||||||
|
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
|
||||||
|
>
|
||||||
|
<div className="ml-3 font-medium">Hours</div>
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
<TabPanel value={IdentityFormTab.Advanced}>
|
<TabPanel value={IdentityFormTab.Advanced}>
|
||||||
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
|
|||||||
+1
@@ -1,4 +1,5 @@
|
|||||||
export enum IdentityFormTab {
|
export enum IdentityFormTab {
|
||||||
Advanced = "advanced",
|
Advanced = "advanced",
|
||||||
|
Lockout = "lockout",
|
||||||
Configuration = "configuration"
|
Configuration = "configuration"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
|
import {
|
||||||
|
OrgPermissionAuditLogsActions,
|
||||||
|
OrgPermissionSubjects,
|
||||||
|
ProjectPermissionAuditLogsActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useSubscription
|
||||||
|
} from "@app/context";
|
||||||
import { Timezone } from "@app/helpers/datetime";
|
import { Timezone } from "@app/helpers/datetime";
|
||||||
import { withPermission } from "@app/hoc";
|
import { withPermission, withProjectPermission } from "@app/hoc";
|
||||||
import { Workspace } from "@app/hooks/api/workspace/types";
|
import { Workspace } from "@app/hooks/api/workspace/types";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
@@ -28,10 +34,14 @@ type Props = {
|
|||||||
project?: Workspace;
|
project?: Workspace;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const LogsSection = withPermission(
|
const LogsSectionComponent = ({
|
||||||
({ presets, refetchInterval, showFilters = true, pageView = false, project }: Props) => {
|
presets,
|
||||||
|
refetchInterval,
|
||||||
|
showFilters = true,
|
||||||
|
pageView = false,
|
||||||
|
project
|
||||||
|
}: Props) => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const);
|
||||||
const [logFilter, setLogFilter] = useState<TAuditLogFilterFormData>({
|
const [logFilter, setLogFilter] = useState<TAuditLogFilterFormData>({
|
||||||
eventType: presets?.eventType || [],
|
eventType: presets?.eventType || [],
|
||||||
@@ -154,7 +164,7 @@ export const LogsSection = withPermission(
|
|||||||
secretPath: logFilter.secretPath || undefined,
|
secretPath: logFilter.secretPath || undefined,
|
||||||
secretKey: logFilter.secretKey || undefined,
|
secretKey: logFilter.secretKey || undefined,
|
||||||
eventMetadata: logFilter?.eventMetadata,
|
eventMetadata: logFilter?.eventMetadata,
|
||||||
projectId: logFilter?.project?.id,
|
projectId: project?.id || logFilter?.project?.id,
|
||||||
actorType: presets?.actorType,
|
actorType: presets?.actorType,
|
||||||
limit: 15,
|
limit: 15,
|
||||||
eventType: logFilter?.eventType,
|
eventType: logFilter?.eventType,
|
||||||
@@ -175,6 +185,22 @@ export const LogsSection = withPermission(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
};
|
||||||
{ action: OrgPermissionActions.Read, subject: OrgPermissionSubjects.AuditLogs }
|
|
||||||
);
|
export const LogsSection = (props: Props) => {
|
||||||
|
const { project } = props;
|
||||||
|
|
||||||
|
if (project) {
|
||||||
|
const ProjectLogsSectionWithPermission = withProjectPermission(LogsSectionComponent, {
|
||||||
|
action: ProjectPermissionAuditLogsActions.Read,
|
||||||
|
subject: ProjectPermissionSub.AuditLogs
|
||||||
|
});
|
||||||
|
return <ProjectLogsSectionWithPermission {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const OrgLogsSectionWithPermission = withPermission(LogsSectionComponent, {
|
||||||
|
action: OrgPermissionAuditLogsActions.Read,
|
||||||
|
subject: OrgPermissionSubjects.AuditLogs
|
||||||
|
});
|
||||||
|
return <OrgLogsSectionWithPermission {...props} />;
|
||||||
|
};
|
||||||
|
|||||||
@@ -115,8 +115,10 @@ const Page = () => {
|
|||||||
<ViewIdentityAuthModal
|
<ViewIdentityAuthModal
|
||||||
isOpen={popUp.viewAuthMethod.isOpen}
|
isOpen={popUp.viewAuthMethod.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("viewAuthMethod", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("viewAuthMethod", isOpen)}
|
||||||
authMethod={popUp.viewAuthMethod.data}
|
authMethod={popUp.viewAuthMethod.data?.authMethod}
|
||||||
|
lockedOut={popUp.viewAuthMethod.data?.lockedOut || false}
|
||||||
identityId={identityId}
|
identityId={identityId}
|
||||||
|
onResetAllLockouts={popUp.viewAuthMethod.data?.refetchIdentity}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+17
-4
@@ -1,8 +1,8 @@
|
|||||||
import { faCog, faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faCog, faLock, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button } from "@app/components/v2";
|
import { Button, Tooltip } from "@app/components/v2";
|
||||||
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api";
|
import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
@@ -16,7 +16,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => {
|
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => {
|
||||||
const { data } = useGetIdentityById(identityId);
|
const { data, refetch } = useGetIdentityById(identityId);
|
||||||
|
|
||||||
return data ? (
|
return data ? (
|
||||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
@@ -28,12 +28,25 @@ export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: P
|
|||||||
{data.identity.authMethods.map((authMethod) => (
|
{data.identity.authMethods.map((authMethod) => (
|
||||||
<button
|
<button
|
||||||
key={authMethod}
|
key={authMethod}
|
||||||
onClick={() => handlePopUpOpen("viewAuthMethod", authMethod)}
|
onClick={() =>
|
||||||
|
handlePopUpOpen("viewAuthMethod", {
|
||||||
|
authMethod,
|
||||||
|
lockedOut: data.identity.activeLockoutAuthMethods.includes(authMethod),
|
||||||
|
refetchIdentity: refetch
|
||||||
|
})
|
||||||
|
}
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center justify-between bg-mineshaft-900 px-4 py-2 text-sm hover:bg-mineshaft-700 data-[state=open]:bg-mineshaft-600"
|
className="flex w-full items-center justify-between bg-mineshaft-900 px-4 py-2 text-sm hover:bg-mineshaft-700 data-[state=open]:bg-mineshaft-600"
|
||||||
>
|
>
|
||||||
<span>{identityAuthToNameMap[authMethod]}</span>
|
<span>{identityAuthToNameMap[authMethod]}</span>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{data.identity.activeLockoutAuthMethods.includes(authMethod) && (
|
||||||
|
<Tooltip content="Auth method has active lockouts">
|
||||||
|
<FontAwesomeIcon icon={faLock} size="xs" className="text-red-400/50" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" />
|
<FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" />
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+16
-3
@@ -37,9 +37,11 @@ import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthCon
|
|||||||
type Props = {
|
type Props = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
authMethod?: IdentityAuthMethod;
|
authMethod?: IdentityAuthMethod;
|
||||||
|
lockedOut: boolean;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onOpenChange: (isOpen: boolean) => void;
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
onDeleteAuthMethod: () => void;
|
onDeleteAuthMethod: () => void;
|
||||||
|
onResetAllLockouts: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TRevokeOptions = {
|
type TRevokeOptions = {
|
||||||
@@ -50,8 +52,13 @@ type TRevokeOptions = {
|
|||||||
export const Content = ({
|
export const Content = ({
|
||||||
identityId,
|
identityId,
|
||||||
authMethod,
|
authMethod,
|
||||||
onDeleteAuthMethod
|
lockedOut,
|
||||||
}: Pick<Props, "authMethod" | "identityId" | "onDeleteAuthMethod">) => {
|
onDeleteAuthMethod,
|
||||||
|
onResetAllLockouts
|
||||||
|
}: Pick<
|
||||||
|
Props,
|
||||||
|
"authMethod" | "lockedOut" | "identityId" | "onDeleteAuthMethod" | "onResetAllLockouts"
|
||||||
|
>) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
|
|
||||||
@@ -159,9 +166,11 @@ export const Content = ({
|
|||||||
<Component
|
<Component
|
||||||
identityId={identityId}
|
identityId={identityId}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
onResetAllLockouts={onResetAllLockouts}
|
||||||
popUp={popUp}
|
popUp={popUp}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
lockedOut={lockedOut}
|
||||||
/>
|
/>
|
||||||
<DeleteActionModal
|
<DeleteActionModal
|
||||||
isOpen={popUp?.revokeAuthMethod?.isOpen}
|
isOpen={popUp?.revokeAuthMethod?.isOpen}
|
||||||
@@ -184,7 +193,9 @@ export const ViewIdentityAuthModal = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
authMethod,
|
authMethod,
|
||||||
identityId
|
identityId,
|
||||||
|
lockedOut,
|
||||||
|
onResetAllLockouts
|
||||||
}: Omit<Props, "onDeleteAuthMethod">) => {
|
}: Omit<Props, "onDeleteAuthMethod">) => {
|
||||||
if (!identityId || !authMethod) return null;
|
if (!identityId || !authMethod) return null;
|
||||||
|
|
||||||
@@ -194,7 +205,9 @@ export const ViewIdentityAuthModal = ({
|
|||||||
<Content
|
<Content
|
||||||
identityId={identityId}
|
identityId={identityId}
|
||||||
authMethod={authMethod}
|
authMethod={authMethod}
|
||||||
|
lockedOut={lockedOut}
|
||||||
onDeleteAuthMethod={() => onOpenChange(false)}
|
onDeleteAuthMethod={() => onOpenChange(false)}
|
||||||
|
onResetAllLockouts={() => onResetAllLockouts()}
|
||||||
/>
|
/>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
+67
-2
@@ -1,9 +1,15 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import ms from "ms";
|
||||||
|
|
||||||
import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
|
||||||
|
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { useTimedReset } from "@app/hooks";
|
import { useTimedReset } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
|
useClearIdentityUniversalAuthLockouts,
|
||||||
useGetIdentityUniversalAuth,
|
useGetIdentityUniversalAuth,
|
||||||
useGetIdentityUniversalAuthClientSecrets
|
useGetIdentityUniversalAuthClientSecrets
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
@@ -19,16 +25,40 @@ export const ViewIdentityUniversalAuthContent = ({
|
|||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
onDelete,
|
onDelete,
|
||||||
popUp
|
popUp,
|
||||||
|
lockedOut,
|
||||||
|
onResetAllLockouts
|
||||||
}: ViewAuthMethodProps) => {
|
}: ViewAuthMethodProps) => {
|
||||||
const { data, isPending } = useGetIdentityUniversalAuth(identityId);
|
const { data, isPending } = useGetIdentityUniversalAuth(identityId);
|
||||||
const { data: clientSecrets = [], isPending: clientSecretsPending } =
|
const { data: clientSecrets = [], isPending: clientSecretsPending } =
|
||||||
useGetIdentityUniversalAuthClientSecrets(identityId);
|
useGetIdentityUniversalAuthClientSecrets(identityId);
|
||||||
|
const { mutateAsync: clearLockoutsFn, isPending: isClearLockoutsPending } =
|
||||||
|
useClearIdentityUniversalAuthLockouts();
|
||||||
|
|
||||||
|
const [lockedOutState, setLockedOutState] = useState(lockedOut);
|
||||||
|
|
||||||
const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset<string>({
|
const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset<string>({
|
||||||
initialState: "Copy Client ID to clipboard"
|
initialState: "Copy Client ID to clipboard"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function clearLockouts() {
|
||||||
|
try {
|
||||||
|
const deleted = await clearLockoutsFn({ identityId });
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully cleared ${deleted} lockout${deleted === 1 ? "" : "s"}`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
setLockedOutState(false);
|
||||||
|
onResetAllLockouts();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to clear lockouts. Please try again.",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isPending || clientSecretsPending) {
|
if (isPending || clientSecretsPending) {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-center">
|
<div className="flex w-full items-center justify-center">
|
||||||
@@ -85,6 +115,41 @@ export const ViewIdentityUniversalAuthContent = ({
|
|||||||
<IdentityAuthFieldDisplay label="Client Secret Trusted IPs">
|
<IdentityAuthFieldDisplay label="Client Secret Trusted IPs">
|
||||||
{data.clientSecretTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
{data.clientSecretTrustedIps.map((ip) => ip.ipAddress).join(", ")}
|
||||||
</IdentityAuthFieldDisplay>
|
</IdentityAuthFieldDisplay>
|
||||||
|
<IdentityAuthFieldDisplay label="Lockout">
|
||||||
|
{data.lockoutEnabled ? "Enabled" : "Disabled"}
|
||||||
|
</IdentityAuthFieldDisplay>
|
||||||
|
{data.lockoutEnabled && (
|
||||||
|
<>
|
||||||
|
<div className="col-span-2 mt-3 flex justify-between border-b border-mineshaft-500 pb-2">
|
||||||
|
<span className="text-bunker-300">Lockout Options</span>
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgPermissionIdentityActions.Edit}
|
||||||
|
a={OrgPermissionSubjects.Identity}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
isDisabled={!isAllowed || !lockedOutState || isClearLockoutsPending}
|
||||||
|
size="xs"
|
||||||
|
onClick={() => clearLockouts()}
|
||||||
|
isLoading={isClearLockoutsPending}
|
||||||
|
colorSchema="secondary"
|
||||||
|
>
|
||||||
|
Reset All Lockouts
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
<IdentityAuthFieldDisplay label="Lockout Threshold">
|
||||||
|
{data.lockoutThreshold}
|
||||||
|
</IdentityAuthFieldDisplay>
|
||||||
|
<IdentityAuthFieldDisplay label="Lockout Duration">
|
||||||
|
{ms(data.lockoutDurationSeconds * 1000, { long: true })}
|
||||||
|
</IdentityAuthFieldDisplay>
|
||||||
|
<IdentityAuthFieldDisplay label="Lockout Counter Reset">
|
||||||
|
{ms(data.lockoutCounterResetSeconds * 1000, { long: true })}
|
||||||
|
</IdentityAuthFieldDisplay>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<div className="col-span-2 my-3">
|
<div className="col-span-2 my-3">
|
||||||
<div className="mb-3 border-b border-mineshaft-500 pb-2">
|
<div className="mb-3 border-b border-mineshaft-500 pb-2">
|
||||||
<span className="text-bunker-300">Client ID</span>
|
<span className="text-bunker-300">Client ID</span>
|
||||||
|
|||||||
+2
@@ -9,4 +9,6 @@ export type ViewAuthMethodProps = {
|
|||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
popUp: UsePopUpState<["revokeAuthMethod", "upgradePlan", "identityAuthMethod"]>;
|
popUp: UsePopUpState<["revokeAuthMethod", "upgradePlan", "identityAuthMethod"]>;
|
||||||
|
lockedOut: boolean;
|
||||||
|
onResetAllLockouts: () => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import {
|
|||||||
useRequestProjectAccess,
|
useRequestProjectAccess,
|
||||||
useSearchProjects
|
useSearchProjects
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { ProjectType, Workspace } from "@app/hooks/api/workspace/types";
|
import { ProjectType, Workspace, WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||||
import {
|
import {
|
||||||
ProjectListToggle,
|
ProjectListToggle,
|
||||||
ProjectListView
|
ProjectListView
|
||||||
@@ -152,13 +152,17 @@ export const AllProjectView = ({
|
|||||||
type: projectTypeFilter
|
type: projectTypeFilter
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAccessProject = async (type: ProjectType, projectId: string) => {
|
const handleAccessProject = async (
|
||||||
|
type: ProjectType,
|
||||||
|
projectId: string,
|
||||||
|
environments: WorkspaceEnv[]
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
await orgAdminAccessProject.mutateAsync({
|
await orgAdminAccessProject.mutateAsync({
|
||||||
projectId
|
projectId
|
||||||
});
|
});
|
||||||
await navigate({
|
await navigate({
|
||||||
to: getProjectHomePage(type),
|
to: getProjectHomePage(type, environments),
|
||||||
params: {
|
params: {
|
||||||
projectId
|
projectId
|
||||||
}
|
}
|
||||||
@@ -315,7 +319,7 @@ export const AllProjectView = ({
|
|||||||
onKeyDown={(evt) => {
|
onKeyDown={(evt) => {
|
||||||
if (evt.key === "Enter" && workspace.isMember) {
|
if (evt.key === "Enter" && workspace.isMember) {
|
||||||
navigate({
|
navigate({
|
||||||
to: getProjectHomePage(workspace.type),
|
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||||
params: {
|
params: {
|
||||||
projectId: workspace.id
|
projectId: workspace.id
|
||||||
}
|
}
|
||||||
@@ -325,7 +329,7 @@ export const AllProjectView = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (workspace.isMember) {
|
if (workspace.isMember) {
|
||||||
navigate({
|
navigate({
|
||||||
to: getProjectHomePage(workspace.type),
|
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||||
params: {
|
params: {
|
||||||
projectId: workspace.id
|
projectId: workspace.id
|
||||||
}
|
}
|
||||||
@@ -371,7 +375,7 @@ export const AllProjectView = ({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleAccessProject(workspace.type, workspace.id);
|
handleAccessProject(workspace.type, workspace.id, workspace.environments);
|
||||||
}}
|
}}
|
||||||
disabled={
|
disabled={
|
||||||
orgAdminAccessProject.variables?.projectId === workspace.id &&
|
orgAdminAccessProject.variables?.projectId === workspace.id &&
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export const MyProjectView = ({
|
|||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate({
|
navigate({
|
||||||
to: getProjectHomePage(workspace.type),
|
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||||
params: {
|
params: {
|
||||||
projectId: workspace.id
|
projectId: workspace.id
|
||||||
}
|
}
|
||||||
@@ -247,7 +247,7 @@ export const MyProjectView = ({
|
|||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate({
|
navigate({
|
||||||
to: getProjectHomePage(workspace.type),
|
to: getProjectHomePage(workspace.type, workspace.environments),
|
||||||
params: {
|
params: {
|
||||||
projectId: workspace.id
|
projectId: workspace.id
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-1
@@ -5,6 +5,7 @@ import { OrgPermissionSubjects } from "@app/context";
|
|||||||
import {
|
import {
|
||||||
OrgGatewayPermissionActions,
|
OrgGatewayPermissionActions,
|
||||||
OrgPermissionAppConnectionActions,
|
OrgPermissionAppConnectionActions,
|
||||||
|
OrgPermissionAuditLogsActions,
|
||||||
OrgPermissionBillingActions,
|
OrgPermissionBillingActions,
|
||||||
OrgPermissionGroupActions,
|
OrgPermissionGroupActions,
|
||||||
OrgPermissionIdentityActions,
|
OrgPermissionIdentityActions,
|
||||||
@@ -23,6 +24,12 @@ const generalPermissionSchema = z
|
|||||||
})
|
})
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
|
const auditLogsPermissionSchema = z
|
||||||
|
.object({
|
||||||
|
[OrgPermissionAuditLogsActions.Read]: z.boolean().optional()
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
const billingPermissionSchema = z
|
const billingPermissionSchema = z
|
||||||
.object({
|
.object({
|
||||||
[OrgPermissionBillingActions.Read]: z.boolean().optional(),
|
[OrgPermissionBillingActions.Read]: z.boolean().optional(),
|
||||||
@@ -121,7 +128,7 @@ export const formSchema = z.object({
|
|||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|
||||||
"audit-logs": generalPermissionSchema,
|
"audit-logs": auditLogsPermissionSchema,
|
||||||
member: generalPermissionSchema,
|
member: generalPermissionSchema,
|
||||||
groups: groupPermissionSchema,
|
groups: groupPermissionSchema,
|
||||||
role: generalPermissionSchema,
|
role: generalPermissionSchema,
|
||||||
|
|||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
|
||||||
|
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
|
||||||
|
import { OrgPermissionAuditLogsActions } from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
|
||||||
|
import { TFormSchema } from "../OrgRoleModifySection.utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isEditable: boolean;
|
||||||
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
|
control: Control<TFormSchema>;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Permission {
|
||||||
|
NoAccess = "no-access",
|
||||||
|
Custom = "custom"
|
||||||
|
}
|
||||||
|
|
||||||
|
const PERMISSION_ACTIONS = [
|
||||||
|
{
|
||||||
|
action: OrgPermissionAuditLogsActions.Read,
|
||||||
|
label: "Read"
|
||||||
|
}
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const OrgPermissionAuditLogsRow = ({ isEditable, control, setValue }: Props) => {
|
||||||
|
const [isRowExpanded, setIsRowExpanded] = useToggle();
|
||||||
|
const [isCustom, setIsCustom] = useToggle();
|
||||||
|
|
||||||
|
const rule = useWatch({
|
||||||
|
control,
|
||||||
|
name: "permissions.audit-logs"
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedPermissionCategory = useMemo(() => {
|
||||||
|
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
|
||||||
|
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
|
||||||
|
|
||||||
|
if (isCustom) return Permission.Custom;
|
||||||
|
if (score === 0) return Permission.NoAccess;
|
||||||
|
|
||||||
|
return Permission.Custom;
|
||||||
|
}, [rule, isCustom]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
|
||||||
|
else setIsCustom.off();
|
||||||
|
}, [selectedPermissionCategory]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isRowCustom = selectedPermissionCategory === Permission.Custom;
|
||||||
|
if (isRowCustom) {
|
||||||
|
setIsRowExpanded.on();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePermissionChange = (val: Permission) => {
|
||||||
|
if (!val) return;
|
||||||
|
if (val === Permission.Custom) {
|
||||||
|
setIsRowExpanded.on();
|
||||||
|
setIsCustom.on();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsCustom.off();
|
||||||
|
|
||||||
|
switch (val) {
|
||||||
|
case Permission.NoAccess:
|
||||||
|
default:
|
||||||
|
setValue(
|
||||||
|
"permissions.audit-logs",
|
||||||
|
{
|
||||||
|
[OrgPermissionAuditLogsActions.Read]: false
|
||||||
|
},
|
||||||
|
{ shouldDirty: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr
|
||||||
|
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
|
||||||
|
onClick={() => setIsRowExpanded.toggle()}
|
||||||
|
>
|
||||||
|
<Td className="w-4">
|
||||||
|
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
|
||||||
|
</Td>
|
||||||
|
<Td className="w-full select-none">Audit Logs</Td>
|
||||||
|
<Td>
|
||||||
|
<Select
|
||||||
|
value={selectedPermissionCategory}
|
||||||
|
className="h-8 w-40 bg-mineshaft-700"
|
||||||
|
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
|
||||||
|
onValueChange={handlePermissionChange}
|
||||||
|
isDisabled={!isEditable}
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
|
||||||
|
<SelectItem value={Permission.Custom}>Custom</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
{isRowExpanded && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
|
||||||
|
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
|
||||||
|
{PERMISSION_ACTIONS.map(({ action, label }) => {
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={`permissions.audit-logs.${action}`}
|
||||||
|
key={`permissions.audit-logs.${action}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Checkbox
|
||||||
|
isChecked={Boolean(field.value)}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
if (!isEditable) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to update default role"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.onChange(e);
|
||||||
|
}}
|
||||||
|
id={`permissions.audit-logs.${action}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -71,6 +71,7 @@ type Props = {
|
|||||||
| "gateway"
|
| "gateway"
|
||||||
| "secret-share"
|
| "secret-share"
|
||||||
| "billing"
|
| "billing"
|
||||||
|
| "audit-logs"
|
||||||
| "machine-identity-auth-template"
|
| "machine-identity-auth-template"
|
||||||
>;
|
>;
|
||||||
setValue: UseFormSetValue<TFormSchema>;
|
setValue: UseFormSetValue<TFormSchema>;
|
||||||
|
|||||||
+6
-4
@@ -16,6 +16,7 @@ import {
|
|||||||
TFormSchema
|
TFormSchema
|
||||||
} from "../OrgRoleModifySection.utils";
|
} from "../OrgRoleModifySection.utils";
|
||||||
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
|
import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow";
|
||||||
|
import { OrgPermissionAuditLogsRow } from "./OrgPermissionAuditLogsRow";
|
||||||
import { OrgPermissionBillingRow } from "./OrgPermissionBillingRow";
|
import { OrgPermissionBillingRow } from "./OrgPermissionBillingRow";
|
||||||
import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
|
import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
|
||||||
import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
|
import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
|
||||||
@@ -39,10 +40,6 @@ const SIMPLE_PERMISSION_OPTIONS = [
|
|||||||
title: "Incident Contacts",
|
title: "Incident Contacts",
|
||||||
formName: "incident-contact"
|
formName: "incident-contact"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: "Audit Logs",
|
|
||||||
formName: "audit-logs"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: "Organization Profile",
|
title: "Organization Profile",
|
||||||
formName: "settings"
|
formName: "settings"
|
||||||
@@ -166,6 +163,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<OrgPermissionAuditLogsRow
|
||||||
|
control={control}
|
||||||
|
setValue={setValue}
|
||||||
|
isEditable={isCustomRole}
|
||||||
|
/>
|
||||||
<OrgPermissionIdentityRow
|
<OrgPermissionIdentityRow
|
||||||
control={control}
|
control={control}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
|
|||||||
+14
-77
@@ -1,70 +1,19 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import ms from "ms";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
|
import { getObjectFromSeconds } from "@app/helpers/datetime";
|
||||||
import { useUpdateOrg } from "@app/hooks/api";
|
import { useUpdateOrg } from "@app/hooks/api";
|
||||||
|
|
||||||
const MAX_SHARED_SECRET_LIFETIME_SECONDS = 30 * 24 * 60 * 60; // 30 days in seconds
|
const MAX_SHARED_SECRET_LIFETIME_SECONDS = 30 * 24 * 60 * 60; // 30 days in seconds
|
||||||
const MIN_SHARED_SECRET_LIFETIME_SECONDS = 5 * 60; // 5 minutes in seconds
|
const MIN_SHARED_SECRET_LIFETIME_SECONDS = 5 * 60; // 5 minutes in seconds
|
||||||
|
|
||||||
// Helper function to convert duration to seconds
|
|
||||||
const durationToSeconds = (value: number, unit: "m" | "h" | "d"): number => {
|
|
||||||
switch (unit) {
|
|
||||||
case "m":
|
|
||||||
return value * 60;
|
|
||||||
case "h":
|
|
||||||
return value * 60 * 60;
|
|
||||||
case "d":
|
|
||||||
return value * 60 * 60 * 24;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to convert seconds to form lifetime value and unit
|
|
||||||
const getFormLifetimeFromSeconds = (
|
|
||||||
totalSeconds: number | null | undefined
|
|
||||||
): { maxLifetimeValue: number; maxLifetimeUnit: "m" | "h" | "d" } => {
|
|
||||||
const DEFAULT_LIFETIME_VALUE = 30;
|
|
||||||
const DEFAULT_LIFETIME_UNIT = "d" as "m" | "h" | "d";
|
|
||||||
|
|
||||||
if (totalSeconds == null || totalSeconds <= 0) {
|
|
||||||
return {
|
|
||||||
maxLifetimeValue: DEFAULT_LIFETIME_VALUE,
|
|
||||||
maxLifetimeUnit: DEFAULT_LIFETIME_UNIT
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const secondsInDay = 24 * 60 * 60;
|
|
||||||
const secondsInHour = 60 * 60;
|
|
||||||
const secondsInMinute = 60;
|
|
||||||
|
|
||||||
if (totalSeconds % secondsInDay === 0) {
|
|
||||||
const value = totalSeconds / secondsInDay;
|
|
||||||
if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "d" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalSeconds % secondsInHour === 0) {
|
|
||||||
const value = totalSeconds / secondsInHour;
|
|
||||||
if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "h" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalSeconds % secondsInMinute === 0) {
|
|
||||||
const value = totalSeconds / secondsInMinute;
|
|
||||||
if (value >= 1) return { maxLifetimeValue: value, maxLifetimeUnit: "m" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
maxLifetimeValue: DEFAULT_LIFETIME_VALUE,
|
|
||||||
maxLifetimeUnit: DEFAULT_LIFETIME_UNIT
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const formSchema = z
|
const formSchema = z
|
||||||
.object({
|
.object({
|
||||||
maxLifetimeValue: z.number().min(1, "Value must be at least 1"),
|
maxLifetimeValue: z.number().min(1, "Value must be at least 1"),
|
||||||
@@ -77,34 +26,20 @@ const formSchema = z
|
|||||||
.superRefine((data, ctx) => {
|
.superRefine((data, ctx) => {
|
||||||
const { maxLifetimeValue, maxLifetimeUnit } = data;
|
const { maxLifetimeValue, maxLifetimeUnit } = data;
|
||||||
|
|
||||||
const durationInSeconds = durationToSeconds(maxLifetimeValue, maxLifetimeUnit);
|
const durationInSeconds = ms(`${maxLifetimeValue}${maxLifetimeUnit}`) / 1000;
|
||||||
|
|
||||||
// Check max limit
|
|
||||||
if (durationInSeconds > MAX_SHARED_SECRET_LIFETIME_SECONDS) {
|
if (durationInSeconds > MAX_SHARED_SECRET_LIFETIME_SECONDS) {
|
||||||
let message = "Duration exceeds maximum allowed limit";
|
|
||||||
|
|
||||||
if (maxLifetimeUnit === "m") {
|
|
||||||
message = `Maximum allowed minutes is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / 60} (30 days)`;
|
|
||||||
} else if (maxLifetimeUnit === "h") {
|
|
||||||
message = `Maximum allowed hours is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / (60 * 60)} (30 days)`;
|
|
||||||
} else if (maxLifetimeUnit === "d") {
|
|
||||||
message = `Maximum allowed days is ${MAX_SHARED_SECRET_LIFETIME_SECONDS / (24 * 60 * 60)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message,
|
message: "Duration exceeds a maximum of 30 days",
|
||||||
path: ["maxLifetimeValue"]
|
path: ["maxLifetimeValue"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check min limit
|
|
||||||
if (durationInSeconds < MIN_SHARED_SECRET_LIFETIME_SECONDS) {
|
if (durationInSeconds < MIN_SHARED_SECRET_LIFETIME_SECONDS) {
|
||||||
const message = `Duration must be at least ${MIN_SHARED_SECRET_LIFETIME_SECONDS / 60} minutes`; // 5 minutes
|
|
||||||
|
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message,
|
message: "Duration must be at least 5 minutes",
|
||||||
path: ["maxLifetimeValue"]
|
path: ["maxLifetimeValue"]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -122,10 +57,14 @@ export const OrgSecretShareLimitSection = () => {
|
|||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
|
|
||||||
const getDefaultFormValues = () => {
|
const getDefaultFormValues = () => {
|
||||||
const initialLifetime = getFormLifetimeFromSeconds(currentOrg?.maxSharedSecretLifetime);
|
const initialLifetime = getObjectFromSeconds(currentOrg?.maxSharedSecretLifetime, [
|
||||||
|
"m",
|
||||||
|
"h",
|
||||||
|
"d"
|
||||||
|
]);
|
||||||
return {
|
return {
|
||||||
maxLifetimeValue: initialLifetime.maxLifetimeValue,
|
maxLifetimeValue: initialLifetime.value,
|
||||||
maxLifetimeUnit: initialLifetime.maxLifetimeUnit,
|
maxLifetimeUnit: initialLifetime.unit as "m" | "h" | "d",
|
||||||
maxViewLimit: currentOrg?.maxSharedSecretViewLimit?.toString() || "1",
|
maxViewLimit: currentOrg?.maxSharedSecretViewLimit?.toString() || "1",
|
||||||
shouldLimitView: Boolean(currentOrg?.maxSharedSecretViewLimit)
|
shouldLimitView: Boolean(currentOrg?.maxSharedSecretViewLimit)
|
||||||
};
|
};
|
||||||
@@ -152,10 +91,8 @@ export const OrgSecretShareLimitSection = () => {
|
|||||||
|
|
||||||
const handleFormSubmit = async (formData: TForm) => {
|
const handleFormSubmit = async (formData: TForm) => {
|
||||||
try {
|
try {
|
||||||
const maxSharedSecretLifetimeSeconds = durationToSeconds(
|
const maxSharedSecretLifetimeSeconds =
|
||||||
formData.maxLifetimeValue,
|
ms(`${formData.maxLifetimeValue}${formData.maxLifetimeUnit}`) / 1000;
|
||||||
formData.maxLifetimeUnit
|
|
||||||
);
|
|
||||||
|
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
orgId: currentOrg.id,
|
orgId: currentOrg.id,
|
||||||
|
|||||||
+12
-8
@@ -1,6 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { faKey, faVault } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
import { Modal, ModalContent } from "@app/components/v2";
|
import { Modal, ModalContent } from "@app/components/v2";
|
||||||
@@ -20,14 +18,16 @@ enum WizardSteps {
|
|||||||
|
|
||||||
const PLATFORM_LIST = [
|
const PLATFORM_LIST = [
|
||||||
{
|
{
|
||||||
icon: faKey,
|
image: "/images/integrations/EnvKey.png",
|
||||||
platform: "env-key",
|
platform: "env-key",
|
||||||
title: "Env Key"
|
title: "EnvKey",
|
||||||
|
size: 34
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: faVault,
|
image: "/images/integrations/Vault.png",
|
||||||
platform: "vault",
|
platform: "vault",
|
||||||
title: "Vault"
|
title: "HCP Vault",
|
||||||
|
size: 40
|
||||||
}
|
}
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ export const SelectImportFromPlatformModal = ({ isOpen, onToggle }: Props) => {
|
|||||||
{PLATFORM_LIST.map((platform, idx) => (
|
{PLATFORM_LIST.map((platform, idx) => (
|
||||||
<div
|
<div
|
||||||
key={`platform-${idx + 1}`}
|
key={`platform-${idx + 1}`}
|
||||||
className="flex h-28 w-32 cursor-pointer flex-col items-center space-y-4 rounded border border-mineshaft-500 bg-bunker-600 p-6 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
className="flex h-28 w-32 cursor-pointer flex-col items-center justify-between rounded border border-mineshaft-500 bg-mineshaft-700 p-6 py-5 transition-all hover:border-primary/70 hover:bg-primary/10 hover:text-white"
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -81,7 +81,11 @@ export const SelectImportFromPlatformModal = ({ isOpen, onToggle }: Props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={platform.icon} size="lg" />
|
<img
|
||||||
|
src={platform.image}
|
||||||
|
alt={`${platform.title} logo`}
|
||||||
|
style={{ width: platform.size }}
|
||||||
|
/>
|
||||||
<div className="whitespace-pre-wrap text-center text-sm">{platform.title}</div>
|
<div className="whitespace-pre-wrap text-center text-sm">{platform.title}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+73
-11
@@ -2,12 +2,19 @@ import { Controller, useForm } from "react-hook-form";
|
|||||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Input, Tooltip } from "@app/components/v2";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, FormControl, Input, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
|
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
|
||||||
|
import {
|
||||||
|
OrgGatewayPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { gatewaysQueryKeys } from "@app/hooks/api";
|
||||||
import { useImportVault } from "@app/hooks/api/migration/mutations";
|
import { useImportVault } from "@app/hooks/api/migration/mutations";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -62,36 +69,32 @@ const MAPPING_TYPE_MENU_ITEMS = [
|
|||||||
export const VaultPlatformModal = ({ onClose }: Props) => {
|
export const VaultPlatformModal = ({ onClose }: Props) => {
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
vaultUrl: z.string().min(1),
|
vaultUrl: z.string().min(1),
|
||||||
|
gatewayId: z.string().optional(),
|
||||||
vaultNamespace: z.string().trim().optional(),
|
vaultNamespace: z.string().trim().optional(),
|
||||||
vaultAccessToken: z.string().min(1),
|
vaultAccessToken: z.string().min(1),
|
||||||
mappingType: z.nativeEnum(VaultMappingType).default(VaultMappingType.KeyVault)
|
mappingType: z.nativeEnum(VaultMappingType).default(VaultMappingType.KeyVault)
|
||||||
});
|
});
|
||||||
type TFormData = z.infer<typeof formSchema>;
|
type TFormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
const { mutateAsync: importVault } = useImportVault();
|
const { mutateAsync: importVault } = useImportVault();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isLoading, isDirty, isSubmitting, isValid, errors }
|
formState: { isLoading, isDirty, isSubmitting, isValid }
|
||||||
} = useForm<TFormData>({
|
} = useForm<TFormData>({
|
||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log({
|
|
||||||
isSubmitting,
|
|
||||||
isLoading,
|
|
||||||
isValid,
|
|
||||||
errors
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (data: TFormData) => {
|
const onSubmit = async (data: TFormData) => {
|
||||||
await importVault({
|
await importVault({
|
||||||
vaultAccessToken: data.vaultAccessToken,
|
vaultAccessToken: data.vaultAccessToken,
|
||||||
vaultNamespace: data.vaultNamespace,
|
vaultNamespace: data.vaultNamespace,
|
||||||
vaultUrl: data.vaultUrl,
|
vaultUrl: data.vaultUrl,
|
||||||
mappingType: data.mappingType
|
mappingType: data.mappingType,
|
||||||
|
...(data.gatewayId && { gatewayId: data.gatewayId })
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
title: "Import started",
|
title: "Import started",
|
||||||
@@ -110,11 +113,70 @@ export const VaultPlatformModal = ({ onClose }: Props) => {
|
|||||||
The Vault migration currently supports importing static secrets from Vault
|
The Vault migration currently supports importing static secrets from Vault
|
||||||
Dedicated/Self-Hosted.
|
Dedicated/Self-Hosted.
|
||||||
<div className="mt-2 text-xs opacity-80">
|
<div className="mt-2 text-xs opacity-80">
|
||||||
Currently only KV Secret Engine V2 is supported for Vault migrations.
|
Currently only KV Secret Engine is supported for Vault migrations.
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</p>
|
||||||
</NoticeBannerV2>
|
</NoticeBannerV2>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||||
|
<div className="w-full flex-1">
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
|
a={OrgPermissionSubjects.Gateway}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="gatewayId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Gateway"
|
||||||
|
isOptional
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
isDisabled={isAllowed}
|
||||||
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
value={value as string}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (v !== "") {
|
||||||
|
onChange(v);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isLoading={isGatewayLoading}
|
||||||
|
placeholder="Default: Internet Gateway"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={undefined as unknown as string}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Internet Gateway
|
||||||
|
</SelectItem>
|
||||||
|
{gateways?.map((el) => (
|
||||||
|
<SelectItem value={el.id} key={el.id}>
|
||||||
|
{el.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="vaultUrl"
|
name="vaultUrl"
|
||||||
|
|||||||
+1
-1
@@ -136,7 +136,7 @@ export const GroupMembersTable = ({ groupMembership }: Props) => {
|
|||||||
text: "User privilege assumption has started"
|
text: "User privilege assumption has started"
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = getProjectHomePage(currentWorkspace.type);
|
const url = getProjectHomePage(currentWorkspace.type, currentWorkspace.environments);
|
||||||
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const Page = () => {
|
|||||||
type: "success",
|
type: "success",
|
||||||
text: "Identity privilege assumption has started"
|
text: "Identity privilege assumption has started"
|
||||||
});
|
});
|
||||||
const url = getProjectHomePage(currentWorkspace.type);
|
const url = getProjectHomePage(currentWorkspace.type, currentWorkspace.environments);
|
||||||
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const Page = () => {
|
|||||||
text: "User privilege assumption has started"
|
text: "User privilege assumption has started"
|
||||||
});
|
});
|
||||||
|
|
||||||
const url = getProjectHomePage(currentWorkspace.type);
|
const url = getProjectHomePage(currentWorkspace.type, currentWorkspace.environments);
|
||||||
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
window.location.href = url.replace("$projectId", currentWorkspace.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -12,6 +12,7 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import {
|
import {
|
||||||
PermissionConditionOperators,
|
PermissionConditionOperators,
|
||||||
|
ProjectPermissionAuditLogsActions,
|
||||||
ProjectPermissionCommitsActions,
|
ProjectPermissionCommitsActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionGroupActions,
|
ProjectPermissionGroupActions,
|
||||||
@@ -41,6 +42,10 @@ const GeneralPolicyActionSchema = z.object({
|
|||||||
create: z.boolean().optional()
|
create: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const AuditLogsPolicyActionSchema = z.object({
|
||||||
|
[ProjectPermissionAuditLogsActions.Read]: z.boolean().optional()
|
||||||
|
});
|
||||||
|
|
||||||
const CertificatePolicyActionSchema = z.object({
|
const CertificatePolicyActionSchema = z.object({
|
||||||
[ProjectPermissionCertificateActions.Create]: z.boolean().optional(),
|
[ProjectPermissionCertificateActions.Create]: z.boolean().optional(),
|
||||||
[ProjectPermissionCertificateActions.Delete]: z.boolean().optional(),
|
[ProjectPermissionCertificateActions.Delete]: z.boolean().optional(),
|
||||||
@@ -316,7 +321,7 @@ export const projectRoleFormSchema = z.object({
|
|||||||
[ProjectPermissionSub.ServiceTokens]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.ServiceTokens]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Settings]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Settings]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Environments]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Environments]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.AuditLogs]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.AuditLogs]: AuditLogsPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.IpAllowList]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.CertificateAuthorities]: GeneralPolicyActionSchema.array().default([]),
|
||||||
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]),
|
[ProjectPermissionSub.Certificates]: CertificatePolicyActionSchema.array().default([]),
|
||||||
@@ -1324,12 +1329,7 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = {
|
|||||||
},
|
},
|
||||||
[ProjectPermissionSub.AuditLogs]: {
|
[ProjectPermissionSub.AuditLogs]: {
|
||||||
title: "Audit Logs",
|
title: "Audit Logs",
|
||||||
actions: [
|
actions: [{ label: "Read", value: ProjectPermissionAuditLogsActions.Read }]
|
||||||
{ label: "Read", value: "read" },
|
|
||||||
{ label: "Create", value: "create" },
|
|
||||||
{ label: "Modify", value: "edit" },
|
|
||||||
{ label: "Remove", value: "delete" }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
[ProjectPermissionSub.IpAllowList]: {
|
[ProjectPermissionSub.IpAllowList]: {
|
||||||
title: "IP Allowlist",
|
title: "IP Allowlist",
|
||||||
@@ -1721,7 +1721,7 @@ const projectManagerTemplate = (
|
|||||||
permissions: [
|
permissions: [
|
||||||
{
|
{
|
||||||
subject: ProjectPermissionSub.AuditLogs,
|
subject: ProjectPermissionSub.AuditLogs,
|
||||||
actions: Object.values(ProjectPermissionActions)
|
actions: Object.values(ProjectPermissionAuditLogsActions)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
subject: ProjectPermissionSub.Groups,
|
subject: ProjectPermissionSub.Groups,
|
||||||
|
|||||||
+3
-1
@@ -1,7 +1,7 @@
|
|||||||
import { Link } from "@tanstack/react-router";
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { EmptyState } from "@app/components/v2";
|
import { EmptyState } from "@app/components/v2";
|
||||||
import { useSubscription } from "@app/context";
|
import { useSubscription, useWorkspace } from "@app/context";
|
||||||
import { EventType } from "@app/hooks/api/auditLogs/enums";
|
import { EventType } from "@app/hooks/api/auditLogs/enums";
|
||||||
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
import { TIntegrationWithEnv } from "@app/hooks/api/integrations/types";
|
||||||
import { LogsSection } from "@app/pages/organization/AuditLogsPage/components/LogsSection";
|
import { LogsSection } from "@app/pages/organization/AuditLogsPage/components/LogsSection";
|
||||||
@@ -15,6 +15,7 @@ type Props = {
|
|||||||
|
|
||||||
export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
const auditLogsRetentionDays = subscription?.auditLogsRetentionDays ?? 30;
|
const auditLogsRetentionDays = subscription?.auditLogsRetentionDays ?? 30;
|
||||||
|
|
||||||
@@ -30,6 +31,7 @@ export const IntegrationAuditLogsSection = ({ integration }: Props) => {
|
|||||||
<LogsSection
|
<LogsSection
|
||||||
refetchInterval={4000}
|
refetchInterval={4000}
|
||||||
showFilters={false}
|
showFilters={false}
|
||||||
|
project={currentWorkspace}
|
||||||
presets={{
|
presets={{
|
||||||
eventMetadata: { integrationId: integration.id },
|
eventMetadata: { integrationId: integration.id },
|
||||||
startDate: new Date(
|
startDate: new Date(
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import { OrderByDirection } from "@app/hooks/api/generic/types";
|
|||||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||||
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
|
import { useCreateCommit } from "@app/hooks/api/secrets/mutations";
|
||||||
import { SecretV3RawSanitized } from "@app/hooks/api/types";
|
import { SecretV3RawSanitized } from "@app/hooks/api/types";
|
||||||
|
import { ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||||
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
|
import { usePathAccessPolicies } from "@app/hooks/usePathAccessPolicies";
|
||||||
import { useResizableColWidth } from "@app/hooks/useResizableColWidth";
|
import { useResizableColWidth } from "@app/hooks/useResizableColWidth";
|
||||||
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
|
||||||
@@ -64,6 +65,8 @@ import { ActionBar } from "./components/ActionBar";
|
|||||||
import { CommitForm } from "./components/CommitForm";
|
import { CommitForm } from "./components/CommitForm";
|
||||||
import { CreateSecretForm } from "./components/CreateSecretForm";
|
import { CreateSecretForm } from "./components/CreateSecretForm";
|
||||||
import { DynamicSecretListView } from "./components/DynamicSecretListView";
|
import { DynamicSecretListView } from "./components/DynamicSecretListView";
|
||||||
|
import { EnvironmentTabs } from "./components/EnvironmentTabs";
|
||||||
|
import { FolderBreadCrumbs } from "./components/FolderBreadCrumbs";
|
||||||
import { FolderListView } from "./components/FolderListView";
|
import { FolderListView } from "./components/FolderListView";
|
||||||
import { PitDrawer } from "./components/PitDrawer";
|
import { PitDrawer } from "./components/PitDrawer";
|
||||||
import { SecretDropzone } from "./components/SecretDropzone";
|
import { SecretDropzone } from "./components/SecretDropzone";
|
||||||
@@ -105,7 +108,7 @@ const Page = () => {
|
|||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const { mutateAsync: createCommit } = useCreateCommit();
|
const { mutateAsync: createCommit } = useCreateCommit();
|
||||||
|
|
||||||
const tableRef = useRef<HTMLDivElement>(null);
|
const tableRef = useRef<HTMLTableElement>(null);
|
||||||
|
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const { isBatchMode, pendingChanges } = useBatchMode();
|
const { isBatchMode, pendingChanges } = useBatchMode();
|
||||||
@@ -249,7 +252,8 @@ const Page = () => {
|
|||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
isPending: isDetailsLoading,
|
isPending: isDetailsLoading,
|
||||||
isFetching: isDetailsFetching
|
isFetching: isDetailsFetching,
|
||||||
|
isFetched
|
||||||
} = useGetProjectSecretsDetails({
|
} = useGetProjectSecretsDetails({
|
||||||
environment,
|
environment,
|
||||||
projectId: workspaceId,
|
projectId: workspaceId,
|
||||||
@@ -270,6 +274,18 @@ const Page = () => {
|
|||||||
tags: filter.tags
|
tags: filter.tags
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// if switching tabs in a folder path that doesn't exist in a separate env we navigate to the root
|
||||||
|
if (!data && isFetched) {
|
||||||
|
navigate({
|
||||||
|
search: (prev) => ({
|
||||||
|
...prev,
|
||||||
|
secretPath: "/"
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, isFetched]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
imports,
|
imports,
|
||||||
folders,
|
folders,
|
||||||
@@ -491,7 +507,8 @@ const Page = () => {
|
|||||||
minWidth: 100,
|
minWidth: 100,
|
||||||
maxWidth: tableRef.current
|
maxWidth: tableRef.current
|
||||||
? tableRef.current.clientWidth - 148 // ensure value column can't collapse completely
|
? tableRef.current.clientWidth - 148 // ensure value column can't collapse completely
|
||||||
: 800
|
: 800,
|
||||||
|
ref: tableRef
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -710,12 +727,18 @@ const Page = () => {
|
|||||||
|
|
||||||
const mergedSecrets = getMergedSecretsWithPending();
|
const mergedSecrets = getMergedSecretsWithPending();
|
||||||
const mergedFolders = getMergedFoldersWithPending();
|
const mergedFolders = getMergedFoldersWithPending();
|
||||||
|
|
||||||
|
if (!(currentWorkspace?.version === ProjectVersion.V3))
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
|
<SecretV2MigrationSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto flex max-w-7xl flex-col text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={
|
title="Secrets Management"
|
||||||
currentWorkspace.environments.find((env) => env.slug === environment)?.name ?? environment
|
|
||||||
}
|
|
||||||
description={
|
description={
|
||||||
<p className="text-md text-bunker-300">
|
<p className="text-md text-bunker-300">
|
||||||
Inject your secrets using
|
Inject your secrets using
|
||||||
@@ -759,6 +782,8 @@ const Page = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<SecretV2MigrationSection />
|
<SecretV2MigrationSection />
|
||||||
|
<FolderBreadCrumbs secretPath={secretPath} />
|
||||||
|
<EnvironmentTabs secretPath={secretPath} />
|
||||||
{!isRollbackMode ? (
|
{!isRollbackMode ? (
|
||||||
<>
|
<>
|
||||||
<ActionBar
|
<ActionBar
|
||||||
@@ -938,6 +963,7 @@ const Page = () => {
|
|||||||
workspaceId={workspaceId}
|
workspaceId={workspaceId}
|
||||||
secretPath={secretPath}
|
secretPath={secretPath}
|
||||||
onNavigateToFolder={handleResetFilter}
|
onNavigateToFolder={handleResetFilter}
|
||||||
|
canNavigate={isFetched}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{canReadDynamicSecret && Boolean(dynamicSecrets?.length) && (
|
{canReadDynamicSecret && Boolean(dynamicSecrets?.length) && (
|
||||||
|
|||||||
+595
@@ -0,0 +1,595 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { MultiValue } from "react-select";
|
||||||
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faCheckCircle,
|
||||||
|
faFilter,
|
||||||
|
faFingerprint,
|
||||||
|
faFolder,
|
||||||
|
faKey,
|
||||||
|
faRotate,
|
||||||
|
faSearch,
|
||||||
|
faWarning
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
FilterableSelect,
|
||||||
|
FormLabel,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Lottie,
|
||||||
|
Pagination,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TBody,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
getUserTablePreference,
|
||||||
|
PreferenceKey,
|
||||||
|
setUserTablePreference
|
||||||
|
} from "@app/helpers/userTablePreferences";
|
||||||
|
import { useDebounce, usePagination, useResetPageHelper } from "@app/hooks";
|
||||||
|
import { useGetImportedSecretsAllEnvs } from "@app/hooks/api";
|
||||||
|
import { useGetProjectSecretsOverview } from "@app/hooks/api/dashboard";
|
||||||
|
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
import { WorkspaceEnv } from "@app/hooks/api/workspace/types";
|
||||||
|
import { useResizableColWidth } from "@app/hooks/useResizableColWidth";
|
||||||
|
import {
|
||||||
|
useDynamicSecretOverview,
|
||||||
|
useFolderOverview,
|
||||||
|
useSecretOverview,
|
||||||
|
useSecretRotationOverview
|
||||||
|
} from "@app/hooks/utils";
|
||||||
|
import { SecretTableResourceCount } from "@app/pages/secret-manager/OverviewPage/components/SecretTableResourceCount";
|
||||||
|
|
||||||
|
import { DynamicSecretRow } from "./components/DynamicSecretRow";
|
||||||
|
import { FolderRow } from "./components/FolderRow";
|
||||||
|
import { SecretRotationRow } from "./components/SecretRotationRow";
|
||||||
|
import { SecretNoAccessRow, SecretRow } from "./components/SecretRow";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum RowType {
|
||||||
|
Folder = "folder",
|
||||||
|
DynamicSecret = "dynamic",
|
||||||
|
Secret = "secret",
|
||||||
|
SecretRotation = "rotation"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Filter = {
|
||||||
|
[key in RowType]: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_FILTER_STATE = {
|
||||||
|
[RowType.Folder]: false,
|
||||||
|
[RowType.DynamicSecret]: false,
|
||||||
|
[RowType.Secret]: false,
|
||||||
|
[RowType.SecretRotation]: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const COL_WIDTH_OFFSET = 220;
|
||||||
|
|
||||||
|
export const CompareEnvironments = ({ secretPath }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const compareEnvironmentsKey = `compare-environments-${currentWorkspace.id}`;
|
||||||
|
|
||||||
|
const [selectedEnvironments, setSelectedEnvironments] = useState<WorkspaceEnv[]>(() => {
|
||||||
|
try {
|
||||||
|
const storedEnvironments = JSON.parse(localStorage.getItem(compareEnvironmentsKey) ?? "[]");
|
||||||
|
|
||||||
|
if (Array.isArray(storedEnvironments) && storedEnvironments.length > 0) {
|
||||||
|
const potentialEnvs: string[] = [];
|
||||||
|
storedEnvironments.forEach((env) => {
|
||||||
|
if (typeof env === "string") {
|
||||||
|
potentialEnvs.push(env);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return currentWorkspace.environments.filter((env) => potentialEnvs.includes(env.id));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// do nothing and proceed
|
||||||
|
}
|
||||||
|
return currentWorkspace.environments.slice(0, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE);
|
||||||
|
|
||||||
|
const {
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
orderDirection,
|
||||||
|
setOrderDirection,
|
||||||
|
setPage,
|
||||||
|
perPage,
|
||||||
|
page,
|
||||||
|
setPerPage,
|
||||||
|
orderBy
|
||||||
|
} = usePagination<DashboardSecretsOrderBy>(DashboardSecretsOrderBy.Name, {
|
||||||
|
initPerPage: getUserTablePreference("secretCompareTable", PreferenceKey.PerPage, 50)
|
||||||
|
});
|
||||||
|
|
||||||
|
const handlePerPageChange = (newPerPage: number) => {
|
||||||
|
setPerPage(newPerPage);
|
||||||
|
setUserTablePreference("secretCompareTable", PreferenceKey.PerPage, newPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const workspaceId = currentWorkspace.id;
|
||||||
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
|
const [debouncedSearchFilter] = useDebounce(searchFilter);
|
||||||
|
const [debouncedSelectedEnvironments] = useDebounce(selectedEnvironments);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
compareEnvironmentsKey,
|
||||||
|
JSON.stringify(selectedEnvironments.map((env) => env.id))
|
||||||
|
);
|
||||||
|
}, [debouncedSelectedEnvironments]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
secretImports,
|
||||||
|
isImportedSecretPresentInEnv,
|
||||||
|
getImportedSecretByKey,
|
||||||
|
getEnvImportedSecretKeyCount
|
||||||
|
} = useGetImportedSecretsAllEnvs({
|
||||||
|
projectId: workspaceId,
|
||||||
|
path: secretPath,
|
||||||
|
environments: (currentWorkspace.environments || []).map(({ slug }) => slug)
|
||||||
|
});
|
||||||
|
|
||||||
|
const compareEnvironments = selectedEnvironments.length
|
||||||
|
? selectedEnvironments
|
||||||
|
: currentWorkspace.environments;
|
||||||
|
|
||||||
|
const isFilteredByResources = Object.values(filter).some(Boolean);
|
||||||
|
const { isPending: isOverviewLoading, data: overview } = useGetProjectSecretsOverview(
|
||||||
|
{
|
||||||
|
projectId: workspaceId,
|
||||||
|
environments: compareEnvironments.map((env) => env.slug),
|
||||||
|
secretPath,
|
||||||
|
orderDirection,
|
||||||
|
orderBy,
|
||||||
|
includeFolders: isFilteredByResources ? filter.folder : true,
|
||||||
|
includeDynamicSecrets: isFilteredByResources ? filter.dynamic : true,
|
||||||
|
includeSecrets: isFilteredByResources ? filter.secret : true,
|
||||||
|
includeImports: true,
|
||||||
|
includeSecretRotations: isFilteredByResources ? filter.rotation : true,
|
||||||
|
search: debouncedSearchFilter,
|
||||||
|
limit,
|
||||||
|
offset
|
||||||
|
},
|
||||||
|
{ enabled: Boolean(compareEnvironments.length) }
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
secrets,
|
||||||
|
folders,
|
||||||
|
dynamicSecrets,
|
||||||
|
secretRotations,
|
||||||
|
totalFolderCount,
|
||||||
|
totalSecretCount,
|
||||||
|
totalDynamicSecretCount,
|
||||||
|
totalSecretRotationCount,
|
||||||
|
totalCount = 0,
|
||||||
|
totalUniqueFoldersInPage,
|
||||||
|
totalUniqueSecretsInPage,
|
||||||
|
totalUniqueSecretImportsInPage,
|
||||||
|
totalUniqueDynamicSecretsInPage,
|
||||||
|
totalUniqueSecretRotationsInPage
|
||||||
|
} = overview ?? {};
|
||||||
|
|
||||||
|
const secretImportsShaped = secretImports
|
||||||
|
?.flatMap(({ data }) => data)
|
||||||
|
.filter(Boolean)
|
||||||
|
.flatMap((item) => item?.secrets || []);
|
||||||
|
|
||||||
|
const handleIsImportedSecretPresentInEnv = (envSlug: string, secretName: string) => {
|
||||||
|
if (secrets?.some((s) => s.key === secretName && s.env === envSlug)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (secretImportsShaped.some((s) => s.key === secretName && s.sourceEnv === envSlug)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isImportedSecretPresentInEnv(envSlug, secretName);
|
||||||
|
};
|
||||||
|
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
|
const { folderNamesAndDescriptions, isFolderPresentInEnv } = useFolderOverview(folders);
|
||||||
|
|
||||||
|
const { dynamicSecretNames, isDynamicSecretPresentInEnv } =
|
||||||
|
useDynamicSecretOverview(dynamicSecrets);
|
||||||
|
|
||||||
|
const { secretRotationNames, isSecretRotationPresentInEnv, getSecretRotationByName } =
|
||||||
|
useSecretRotationOverview(secretRotations);
|
||||||
|
|
||||||
|
const { secKeys, getEnvSecretKeyCount } = useSecretOverview(
|
||||||
|
secrets?.concat(secretImportsShaped) || []
|
||||||
|
);
|
||||||
|
|
||||||
|
const getSecretByKey = useCallback(
|
||||||
|
(env: string, key: string) => {
|
||||||
|
const sec = secrets?.find((s) => s.env === env && s.key === key);
|
||||||
|
return sec;
|
||||||
|
},
|
||||||
|
[secrets]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [tableWidth, setTableWidth] = useState(0);
|
||||||
|
const tableRef = useRef<HTMLTableElement>(null);
|
||||||
|
|
||||||
|
const { handleMouseDown, isResizing, colWidth } = useResizableColWidth({
|
||||||
|
initialWidth: 320,
|
||||||
|
minWidth: 160,
|
||||||
|
maxWidth: tableRef.current
|
||||||
|
? tableRef.current.clientWidth - COL_WIDTH_OFFSET // ensure value column can't collapse completely
|
||||||
|
: 800,
|
||||||
|
ref: tableRef
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleToggleRowType = useCallback(
|
||||||
|
(rowType: RowType) =>
|
||||||
|
setFilter((state) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
[rowType]: !state[rowType]
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isTableEmpty = totalCount === 0;
|
||||||
|
|
||||||
|
const isTableFiltered = isFilteredByResources;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = tableRef.current;
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
setTableWidth(element.clientWidth - 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(handleResize);
|
||||||
|
resizeObserver.observe(element);
|
||||||
|
|
||||||
|
// eslint-disable-next-line consistent-return
|
||||||
|
return () => {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
}, [tableRef]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
// scott: this is reverse to fix z-indexing bug of dropdown with sticky table cols; couldn't resolve with flex-col
|
||||||
|
<div className="flex flex-1 flex-col-reverse overflow-hidden">
|
||||||
|
{!isOverviewLoading && totalCount > 0 && (
|
||||||
|
<Pagination
|
||||||
|
startAdornment={
|
||||||
|
<SecretTableResourceCount
|
||||||
|
dynamicSecretCount={totalDynamicSecretCount}
|
||||||
|
secretCount={totalSecretCount}
|
||||||
|
folderCount={totalFolderCount}
|
||||||
|
secretRotationCount={totalSecretRotationCount}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className="rounded-b-lg border border-solid border-mineshaft-500 bg-mineshaft-700"
|
||||||
|
count={totalCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={handlePerPageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="thin-scrollbar flex flex-1 flex-col overflow-y-auto">
|
||||||
|
<TableContainer
|
||||||
|
ref={tableRef}
|
||||||
|
className={twMerge(
|
||||||
|
"mt-4 flex flex-1 flex-col border-mineshaft-500 bg-mineshaft-700",
|
||||||
|
!isTableEmpty && "rounded-b-none"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
|
{isOverviewLoading ? (
|
||||||
|
<div className="flex h-full flex-col items-center justify-center">
|
||||||
|
<Lottie
|
||||||
|
isAutoPlay
|
||||||
|
icon="infisical_loading"
|
||||||
|
className="h-10 place-self-center self-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : isTableEmpty ? (
|
||||||
|
<EmptyState
|
||||||
|
titleClassName="text-base"
|
||||||
|
className="m-auto bg-mineshaft-700 text-lg"
|
||||||
|
title={
|
||||||
|
isTableFiltered
|
||||||
|
? "No secrets to compare with current filters"
|
||||||
|
: "No secrets to compare"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Table className="border-collapse bg-mineshaft-700">
|
||||||
|
<THead className="sticky top-0 z-20">
|
||||||
|
<Tr className="sticky top-0 z-20">
|
||||||
|
<Th className="sticky left-0 z-10 border-none p-0" style={{ width: colWidth }}>
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
tabIndex={-1}
|
||||||
|
role="button"
|
||||||
|
className={`absolute -right-[0.02rem] z-40 h-full w-0.5 cursor-ew-resize hover:bg-blue-400/20 ${
|
||||||
|
isResizing ? "bg-blue-400/75" : "bg-transparent"
|
||||||
|
}`}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
/>
|
||||||
|
<div className="pointer-events-none absolute -right-[0.02rem] top-[0.67rem] z-30">
|
||||||
|
<div className="h-5 w-0.5 rounded-[1.5px] bg-gray-400 opacity-50" />
|
||||||
|
</div>
|
||||||
|
<div className="flex h-full items-center border-b-2 border-r border-mineshaft-500 bg-mineshaft-700 bg-clip-padding p-0 px-4 py-2.5 text-sm normal-case">
|
||||||
|
Name
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className="ml-1 mt-[0.1rem]"
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={() =>
|
||||||
|
setOrderDirection((prev) =>
|
||||||
|
prev === OrderByDirection.ASC
|
||||||
|
? OrderByDirection.DESC
|
||||||
|
: OrderByDirection.ASC
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
className="h-3"
|
||||||
|
icon={orderDirection === "asc" ? faArrowDown : faArrowUp}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
{compareEnvironments?.map(({ name, slug }, index) => {
|
||||||
|
const envSecKeyCount = getEnvSecretKeyCount(slug);
|
||||||
|
const importedSecKeyCount = getEnvImportedSecretKeyCount(slug);
|
||||||
|
const missingKeyCount = secKeys.length - envSecKeyCount - importedSecKeyCount;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Th
|
||||||
|
className="whitespace-nowrap border-none p-0 text-center"
|
||||||
|
key={`environment-${slug}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-full w-full items-center justify-center gap-x-2 border-b-2 border-mineshaft-500 bg-mineshaft-700 p-0 px-4 py-2.5 text-center text-sm normal-case",
|
||||||
|
index < compareEnvironments.length - 1 && "border-r"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
{missingKeyCount > 0 && (
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-none lowercase"
|
||||||
|
content={
|
||||||
|
<>
|
||||||
|
{missingKeyCount} secret{missingKeyCount > 1 ? "s" : ""} missing
|
||||||
|
compared to other environments on this page
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
variant="primary"
|
||||||
|
className="-mt-[0.05rem] flex h-4 items-center gap-x-1 pt-[0.1rem] font-normal leading-3"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faWarning} className="-mt-[0.1rem] w-2.5" />
|
||||||
|
{missingKeyCount}
|
||||||
|
</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{folderNamesAndDescriptions.map(({ name: folderName }, index) => (
|
||||||
|
<FolderRow
|
||||||
|
folderName={folderName}
|
||||||
|
isFolderPresentInEnv={isFolderPresentInEnv}
|
||||||
|
environments={compareEnvironments}
|
||||||
|
key={`overview-${folderName}-${index + 1}`}
|
||||||
|
colWidth={colWidth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{dynamicSecretNames.map((dynamicSecretName, index) => (
|
||||||
|
<DynamicSecretRow
|
||||||
|
dynamicSecretName={dynamicSecretName}
|
||||||
|
isDynamicSecretInEnv={isDynamicSecretPresentInEnv}
|
||||||
|
environments={compareEnvironments}
|
||||||
|
key={`overview-${dynamicSecretName}-${index + 1}`}
|
||||||
|
colWidth={colWidth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{secretRotationNames.map((secretRotationName, index) => (
|
||||||
|
<SecretRotationRow
|
||||||
|
secretRotationName={secretRotationName}
|
||||||
|
isSecretRotationInEnv={isSecretRotationPresentInEnv}
|
||||||
|
environments={compareEnvironments}
|
||||||
|
getSecretRotationByName={getSecretRotationByName}
|
||||||
|
key={`overview-${secretRotationName}-${index + 1}`}
|
||||||
|
colWidth={colWidth}
|
||||||
|
tableWidth={tableWidth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{secKeys.map((key, index) => (
|
||||||
|
<SecretRow
|
||||||
|
colWidth={colWidth}
|
||||||
|
secretPath={secretPath}
|
||||||
|
getImportedSecretByKey={getImportedSecretByKey}
|
||||||
|
isImportedSecretPresentInEnv={handleIsImportedSecretPresentInEnv}
|
||||||
|
key={`overview-${key}-${index + 1}`}
|
||||||
|
environments={compareEnvironments}
|
||||||
|
secretKey={key}
|
||||||
|
getSecretByKey={getSecretByKey}
|
||||||
|
tableWidth={tableWidth}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<SecretNoAccessRow
|
||||||
|
colWidth={colWidth}
|
||||||
|
environments={compareEnvironments}
|
||||||
|
count={Math.max(
|
||||||
|
(page * perPage > totalCount ? totalCount % perPage : perPage) -
|
||||||
|
(totalUniqueFoldersInPage || 0) -
|
||||||
|
(totalUniqueDynamicSecretsInPage || 0) -
|
||||||
|
(totalUniqueSecretsInPage || 0) -
|
||||||
|
(totalUniqueSecretImportsInPage || 0) -
|
||||||
|
(totalUniqueSecretRotationsInPage || 0),
|
||||||
|
0
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex flex-row items-center justify-center space-x-2">
|
||||||
|
<Input
|
||||||
|
value={searchFilter}
|
||||||
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
|
className="h-full flex-1"
|
||||||
|
placeholder="Search by resource name..."
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faSearch} />}
|
||||||
|
containerClassName="h-10"
|
||||||
|
/>
|
||||||
|
{isTableFiltered && (
|
||||||
|
<Button
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
onClick={() => {
|
||||||
|
setFilter(DEFAULT_FILTER_STATE);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{compareEnvironments.length > 0 && (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline_bg"
|
||||||
|
className={twMerge(
|
||||||
|
"flex h-[2.5rem]",
|
||||||
|
isTableFiltered && "border-primary/40 bg-primary/10"
|
||||||
|
)}
|
||||||
|
leftIcon={
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faFilter}
|
||||||
|
className={isTableFiltered ? "text-primary/80" : undefined}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Filters
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="thin-scrollbar max-h-[70vh] overflow-y-auto"
|
||||||
|
align="end"
|
||||||
|
sideOffset={2}
|
||||||
|
>
|
||||||
|
<DropdownMenuLabel>Filter by Resource</DropdownMenuLabel>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleRowType(RowType.Folder);
|
||||||
|
}}
|
||||||
|
icon={filter[RowType.Folder] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faFolder} className="text-yellow-700" />
|
||||||
|
<span>Folders</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleRowType(RowType.DynamicSecret);
|
||||||
|
}}
|
||||||
|
icon={filter[RowType.DynamicSecret] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faFingerprint} className="text-yellow-700" />
|
||||||
|
<span>Dynamic Secrets</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleRowType(RowType.SecretRotation);
|
||||||
|
}}
|
||||||
|
icon={filter[RowType.SecretRotation] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faRotate} className="text-mineshaft-400" />
|
||||||
|
<span>Secret Rotations</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggleRowType(RowType.Secret);
|
||||||
|
}}
|
||||||
|
icon={filter[RowType.Secret] && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<FontAwesomeIcon icon={faKey} className="text-bunker-300" />
|
||||||
|
<span>Secrets</span>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="!z-[99999999]">
|
||||||
|
<FormLabel label="Select Environments to Compare" />
|
||||||
|
<FilterableSelect
|
||||||
|
value={selectedEnvironments}
|
||||||
|
onChange={(value) => {
|
||||||
|
const selected = value as MultiValue<WorkspaceEnv>;
|
||||||
|
|
||||||
|
setSelectedEnvironments((selected as WorkspaceEnv[]) ?? []);
|
||||||
|
}}
|
||||||
|
placeholder="Leave blank to compare all environments"
|
||||||
|
options={currentWorkspace.environments}
|
||||||
|
getOptionValue={(option) => option.slug}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
isMulti
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import { faFingerprint } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { Tr } from "@app/components/v2";
|
||||||
|
|
||||||
|
import { EnvironmentStatusCell, ResourceNameCell } from "../shared";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dynamicSecretName: string;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
isDynamicSecretInEnv: (name: string, env: string) => boolean;
|
||||||
|
colWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DynamicSecretRow = ({
|
||||||
|
dynamicSecretName,
|
||||||
|
environments = [],
|
||||||
|
isDynamicSecretInEnv,
|
||||||
|
colWidth
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Tr isHoverable className="group border-mineshaft-500">
|
||||||
|
<ResourceNameCell
|
||||||
|
label={dynamicSecretName}
|
||||||
|
icon={faFingerprint}
|
||||||
|
colWidth={colWidth}
|
||||||
|
iconClassName="text-yellow-700"
|
||||||
|
/>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
const isPresent = isDynamicSecretInEnv(dynamicSecretName, slug);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnvironmentStatusCell
|
||||||
|
isLast={i === environments.length - 1}
|
||||||
|
status={isPresent ? "present" : "missing"}
|
||||||
|
key={`dynamic-secret-${dynamicSecretName}-${i + 1}-value`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { DynamicSecretRow } from "./DynamicSecretRow";
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import { faFolder } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { Tr } from "@app/components/v2";
|
||||||
|
|
||||||
|
import { EnvironmentStatusCell, ResourceNameCell } from "../shared";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
folderName: string;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
isFolderPresentInEnv: (name: string, env: string) => boolean;
|
||||||
|
colWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FolderRow = ({
|
||||||
|
folderName,
|
||||||
|
environments = [],
|
||||||
|
isFolderPresentInEnv,
|
||||||
|
colWidth
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Tr isHoverable className="group border-mineshaft-500">
|
||||||
|
<ResourceNameCell
|
||||||
|
label={folderName}
|
||||||
|
icon={faFolder}
|
||||||
|
iconClassName="text-yellow-700"
|
||||||
|
colWidth={colWidth}
|
||||||
|
/>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
const isPresent = isFolderPresentInEnv(folderName, slug);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnvironmentStatusCell
|
||||||
|
isLast={i === environments.length - 1}
|
||||||
|
status={isPresent ? "present" : "missing"}
|
||||||
|
key={`folder-${slug}-${i + 1}-value`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { FolderRow } from "./FolderRow";
|
||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
import { faEye, faEyeSlash, faInfoCircle, faRotate } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { IconButton, TableContainer, Tag, Td, Tooltip, Tr } from "@app/components/v2";
|
||||||
|
import { Blur } from "@app/components/v2/Blur";
|
||||||
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
|
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
|
||||||
|
|
||||||
|
import { EnvironmentStatusCell, ResourceNameCell } from "../shared";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secretRotationName: string;
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
isSecretRotationInEnv: (name: string, env: string) => boolean;
|
||||||
|
getSecretRotationByName: (slug: string, name: string) => TSecretRotationV2 | undefined;
|
||||||
|
colWidth: number;
|
||||||
|
tableWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretRotationRow = ({
|
||||||
|
secretRotationName,
|
||||||
|
environments = [],
|
||||||
|
isSecretRotationInEnv,
|
||||||
|
colWidth,
|
||||||
|
getSecretRotationByName,
|
||||||
|
tableWidth
|
||||||
|
}: Props) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useToggle(false);
|
||||||
|
const [isSecretVisible, setIsSecretVisible] = useToggle();
|
||||||
|
|
||||||
|
const totalCols = environments.length + 1; // secret key row
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tr isHoverable isSelectable onClick={setIsExpanded.toggle} className="group">
|
||||||
|
<ResourceNameCell
|
||||||
|
label={secretRotationName}
|
||||||
|
icon={faRotate}
|
||||||
|
colWidth={colWidth}
|
||||||
|
iconClassName="text-mineshaft-400"
|
||||||
|
isRowExpanded={isExpanded}
|
||||||
|
/>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
const isPresent = isSecretRotationInEnv(secretRotationName, slug);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnvironmentStatusCell
|
||||||
|
isLast={i === environments.length - 1}
|
||||||
|
status={isPresent ? "present" : "missing"}
|
||||||
|
key={`secret-rotation-${slug}-${i + 1}-value`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
{isExpanded &&
|
||||||
|
environments.map(({ name: envName, slug }) => {
|
||||||
|
const secretRotation = getSecretRotationByName(slug, secretRotationName);
|
||||||
|
|
||||||
|
if (!secretRotation) return null;
|
||||||
|
|
||||||
|
const { type, secrets, description } = secretRotation;
|
||||||
|
|
||||||
|
const { name: rotationType, image } = SECRET_ROTATION_MAP[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={`secret-rotation-${slug}-${secretRotationName}`} className="border-b-0">
|
||||||
|
<Td
|
||||||
|
colSpan={totalCols}
|
||||||
|
style={{ minWidth: tableWidth, maxWidth: tableWidth }}
|
||||||
|
className="sticky left-0 bg-clip-padding px-0 py-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{ minWidth: tableWidth, maxWidth: tableWidth }}
|
||||||
|
className="sticky left-0 bg-clip-padding px-0 py-0"
|
||||||
|
>
|
||||||
|
<div className="flex !h-[40px] items-center justify-between gap-x-2 bg-mineshaft-800 px-4">
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex w-full flex-wrap items-center gap-x-2.5">
|
||||||
|
<span>{envName}</span>
|
||||||
|
<Tag className="flex items-center gap-1 px-1.5 py-0 text-xs normal-case">
|
||||||
|
<img
|
||||||
|
src={`/images/integrations/${image}`}
|
||||||
|
className="w-[11px]"
|
||||||
|
alt={`${rotationType} logo`}
|
||||||
|
/>
|
||||||
|
{rotationType}
|
||||||
|
</Tag>
|
||||||
|
{description && (
|
||||||
|
<Tooltip content={description}>
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="text-mineshaft-400" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
side="left"
|
||||||
|
content={isSecretVisible ? "Hide Values" : "Reveal Values"}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
|
ariaLabel={isSecretVisible ? "Hide Values" : "Reveal Values"}
|
||||||
|
onClick={() => setIsSecretVisible.toggle()}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={isSecretVisible ? faEyeSlash : faEye} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<TableContainer className="rounded-none border-0">
|
||||||
|
<table className="secret-table w-full border-b-0 !bg-mineshaft-900">
|
||||||
|
<tbody className="!last:border-b-0 w-full border-t-2 border-mineshaft-600">
|
||||||
|
{secrets.map((secret, index) => {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
className="max-w-sm"
|
||||||
|
content={
|
||||||
|
secret
|
||||||
|
? undefined
|
||||||
|
: "You do not have permission to view this secret."
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={`rotation-secret-${secretRotation.id}-${index}`}
|
||||||
|
>
|
||||||
|
<tr className="hover:bg-mineshaft-800/50">
|
||||||
|
<td
|
||||||
|
style={{
|
||||||
|
width: colWidth
|
||||||
|
}}
|
||||||
|
className="!h-[1px] border-none !p-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex h-full flex-1 items-center border-r border-mineshaft-500 px-4 py-1"
|
||||||
|
style={{
|
||||||
|
width: colWidth
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={twMerge(!secret && "blur", "truncate")}>
|
||||||
|
{secret?.key ?? "********"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="!h-[40px] !px-4">
|
||||||
|
{/* eslint-disable-next-line no-nested-ternary */}
|
||||||
|
{!secret ? (
|
||||||
|
<div className="h-full pl-4 blur">********</div>
|
||||||
|
) : secret.secretValueHidden ? (
|
||||||
|
<Blur
|
||||||
|
className="py-0"
|
||||||
|
tooltipText="You do not have permission to read the value of this secret."
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<InfisicalSecretInput
|
||||||
|
isReadOnly
|
||||||
|
value={secret.value}
|
||||||
|
isVisible={isSecretVisible}
|
||||||
|
secretPath={secretRotation.folder.path}
|
||||||
|
environment={secretRotation.environment.slug}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { SecretRotationRow } from "./SecretRotationRow";
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
import { faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { Tooltip } from "@app/components/v2";
|
||||||
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
defaultValue?: string | null;
|
||||||
|
isOverride?: boolean;
|
||||||
|
isVisible?: boolean;
|
||||||
|
isImportedSecret: boolean;
|
||||||
|
environment: string;
|
||||||
|
secretValueHidden: boolean;
|
||||||
|
secretPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EnvironmentSecretRow = ({
|
||||||
|
defaultValue,
|
||||||
|
isOverride,
|
||||||
|
isImportedSecret,
|
||||||
|
secretValueHidden,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
isVisible
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="group flex w-full cursor-text items-center space-x-2">
|
||||||
|
{secretValueHidden && !isOverride && (
|
||||||
|
<Tooltip content="You do not have access to view the current value">
|
||||||
|
<FontAwesomeIcon className="pl-2" size="sm" icon={faEyeSlash} />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 pl-3 pr-2">
|
||||||
|
<InfisicalSecretInput
|
||||||
|
onChange={() => {}}
|
||||||
|
isReadOnly
|
||||||
|
value={defaultValue as string}
|
||||||
|
key="secret-input"
|
||||||
|
isVisible={isVisible && !secretValueHidden}
|
||||||
|
secretPath={secretPath}
|
||||||
|
environment={environment}
|
||||||
|
isImport={isImportedSecret}
|
||||||
|
defaultValue={secretValueHidden ? "" : undefined}
|
||||||
|
canEditButNotView={secretValueHidden && !isOverride}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
import { faLock } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
import { Tr } from "@app/components/v2";
|
||||||
|
import { Blur } from "@app/components/v2/Blur";
|
||||||
|
|
||||||
|
import { EnvironmentStatusCell, ResourceNameCell } from "../shared";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
environments: { name: string; slug: string }[];
|
||||||
|
count: number;
|
||||||
|
colWidth: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretNoAccessRow = ({ environments = [], count, colWidth }: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Array.from(Array(count)).map((_, j) => (
|
||||||
|
<Tr
|
||||||
|
key={`no-access-secret-${j + 1}`}
|
||||||
|
isHoverable
|
||||||
|
isSelectable
|
||||||
|
className="group border-mineshaft-500"
|
||||||
|
>
|
||||||
|
<ResourceNameCell
|
||||||
|
label={<Blur />}
|
||||||
|
iconClassName="text-bunker-400"
|
||||||
|
icon={faLock}
|
||||||
|
colWidth={colWidth}
|
||||||
|
tooltipContent="You do not have permission to view this secret"
|
||||||
|
/>
|
||||||
|
{environments.map(({ slug }, i) => {
|
||||||
|
return (
|
||||||
|
<EnvironmentStatusCell
|
||||||
|
isLast={i === environments.length - 1}
|
||||||
|
status="no-access"
|
||||||
|
key={`no-access-sec--${slug}-${i + 1}-value`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user