Merge remote-tracking branch 'origin/main' into pki-revamp-v3

This commit is contained in:
Carlos Monastyrski
2025-10-20 14:27:19 -03:00
201 changed files with 7648 additions and 3401 deletions

View File

@@ -40,7 +40,7 @@ describe("Secret Folder Router", async () => {
{ name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
{ name: "folder3", path: "/level1/level2" }
])("Create folder $name in $path", async ({ name, path }) => {
const createdFolder = await createFolder({ path, name });
// check for default environments
@@ -57,7 +57,7 @@ describe("Secret Folder Router", async () => {
{
path: "/",
expected: {
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }],
folders: [{ name: "folder4" }, { name: "level2" }, { name: "folder5" }],
length: 3
}
},
@@ -162,4 +162,25 @@ describe("Secret Folder Router", async () => {
expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(0);
});
test("Creating a duplicate folder should return a 400 error", async () => {
const newFolder = await createFolder({ name: "folder-duplicate", path: "/level1/level2" });
const res = await testServer.inject({
method: "POST",
url: `/api/v1/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
workspaceId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder-duplicate",
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(400);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("error");
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
});
});

View File

@@ -18,7 +18,7 @@ const createFolder = async (dto: { path: string; name: string }) => {
return res.json().folder;
};
const deleteFolder = async (dto: { path: string; id: string }) => {
const deleteFolder = async (dto: { path: string; id: string; forceDelete?: boolean }) => {
const res = await testServer.inject({
method: "DELETE",
url: `/api/v2/folders/${dto.id}`,
@@ -28,7 +28,8 @@ const deleteFolder = async (dto: { path: string; id: string }) => {
body: {
projectId: seedData1.project.id,
environment: seedData1.environment.slug,
path: dto.path
path: dto.path,
forceDelete: dto.forceDelete ?? false
}
});
expect(res.statusCode).toBe(200);
@@ -40,7 +41,7 @@ describe("Secret Folder Router", async () => {
{ name: "folder1", path: "/" }, // one in root
{ name: "folder1", path: "/level1/level2" }, // then create a deep one creating intermediate ones
{ name: "folder2", path: "/" },
{ name: "folder1", path: "/level1/level2" } // this should not create folder return same thing
{ name: "folder3", path: "/level1/level2" }
])("Create folder $name in $path", async ({ name, path }) => {
const createdFolder = await createFolder({ path, name });
// check for default environments
@@ -57,7 +58,7 @@ describe("Secret Folder Router", async () => {
{
path: "/",
expected: {
folders: [{ name: "folder1" }, { name: "level1" }, { name: "folder2" }],
folders: [{ name: "folder4" }, { name: "level2" }, { name: "folder5" }],
length: 3
}
},
@@ -86,7 +87,7 @@ describe("Secret Folder Router", async () => {
folders: expect.arrayContaining(expected.folders.map((el) => expect.objectContaining(el)))
});
await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id })));
await Promise.all(newFolders.map(({ id }) => deleteFolder({ path, id, forceDelete: true })));
});
test("Update a deep folder", async () => {
@@ -162,4 +163,26 @@ describe("Secret Folder Router", async () => {
expect(updatedFolderList).toHaveProperty("folders");
expect(updatedFolderList.folders.length).toEqual(0);
});
test("Creating a duplicate folder should return a 400 error", async () => {
const newFolder = await createFolder({ name: "folder-duplicate", path: "/level1/level2" });
const res = await testServer.inject({
method: "POST",
url: `/api/v2/folders`,
headers: {
authorization: `Bearer ${jwtAuthToken}`
},
body: {
projectId: seedData1.project.id,
environment: seedData1.environment.slug,
name: "folder-duplicate",
path: "/level1/level2"
}
});
expect(res.statusCode).toBe(400);
const payload = JSON.parse(res.payload);
expect(payload).toHaveProperty("error");
await deleteFolder({ path: "/level1/level2", id: newFolder.id });
});
});

View File

@@ -530,6 +530,9 @@ import {
TUsers,
TUsersInsert,
TUsersUpdate,
TVaultExternalMigrationConfigs,
TVaultExternalMigrationConfigsInsert,
TVaultExternalMigrationConfigsUpdate,
TWebhooks,
TWebhooksInsert,
TWebhooksUpdate,
@@ -1377,5 +1380,10 @@ declare module "knex/types/tables" {
TAdditionalPrivilegesInsert,
TAdditionalPrivilegesUpdate
>;
[TableName.VaultExternalMigrationConfig]: KnexOriginal.CompositeTableType<
TVaultExternalMigrationConfigs,
TVaultExternalMigrationConfigsInsert,
TVaultExternalMigrationConfigsUpdate
>;
}
}

View File

@@ -0,0 +1,29 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.VaultExternalMigrationConfig))) {
await knex.schema.createTable(TableName.VaultExternalMigrationConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.string("namespace").notNullable();
t.uuid("connectionId");
t.foreign("connectionId").references("id").inTable(TableName.AppConnection);
t.timestamps(true, true, true);
t.unique(["orgId", "namespace"]);
});
await createOnUpdateTrigger(knex, TableName.VaultExternalMigrationConfig);
}
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.VaultExternalMigrationConfig);
await dropOnUpdateTrigger(knex, TableName.VaultExternalMigrationConfig);
}

View File

@@ -180,5 +180,6 @@ export * from "./user-aliases";
export * from "./user-encryption-keys";
export * from "./user-group-membership";
export * from "./users";
export * from "./vault-external-migration-configs";
export * from "./webhooks";
export * from "./workflow-integrations";

View File

@@ -207,7 +207,9 @@ export enum TableName {
PamFolder = "pam_folders",
PamResource = "pam_resources",
PamAccount = "pam_accounts",
PamSession = "pam_sessions"
PamSession = "pam_sessions",
VaultExternalMigrationConfig = "vault_external_migration_configs"
}
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId";

View File

@@ -0,0 +1,26 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { TImmutableDBKeys } from "./models";
export const VaultExternalMigrationConfigsSchema = z.object({
id: z.string().uuid(),
orgId: z.string().uuid(),
namespace: z.string(),
connectionId: z.string().uuid().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TVaultExternalMigrationConfigs = z.infer<typeof VaultExternalMigrationConfigsSchema>;
export type TVaultExternalMigrationConfigsInsert = Omit<
z.input<typeof VaultExternalMigrationConfigsSchema>,
TImmutableDBKeys
>;
export type TVaultExternalMigrationConfigsUpdate = Partial<
Omit<z.input<typeof VaultExternalMigrationConfigsSchema>, TImmutableDBKeys>
>;

View File

@@ -129,7 +129,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
id: z.string(),
value: z.string(),
algorithm: z.string(),
kmipMetadata: z.record(z.any()).optional()
kmipMetadata: z.record(z.any()).nullish()
})
}
},
@@ -435,7 +435,7 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
key: z.string(),
name: z.string(),
algorithm: z.nativeEnum(SymmetricKeyAlgorithm),
kmipMetadata: z.record(z.any()).optional()
kmipMetadata: z.record(z.any()).nullish()
}),
response: {
200: z.object({

View File

@@ -78,7 +78,7 @@ export type TKmipRegisterDTO = {
name: string;
key: string;
algorithm: SymmetricKeyAlgorithm;
kmipMetadata?: Record<string, unknown>;
kmipMetadata?: Record<string, unknown> | null;
} & KmipOperationBaseDTO;
export type TSetupOrgKmipDTO = {

View File

@@ -214,6 +214,20 @@ export const licenseServiceFactory = ({
const identityUsed = await licenseDAL.countOrgUsersAndIdentities(orgId);
currentPlan.identitiesUsed = identityUsed;
if (currentPlan.identityLimit && currentPlan.identityLimit !== identityUsed) {
try {
await licenseServerCloudApi.request.patch(`/api/license-server/v1/customers/${org.customerId}/cloud-plan`, {
quantity: membersUsed,
quantityIdentities: identityUsed
});
} catch (error) {
logger.error(
error,
`Update seats used: encountered an error when updating plan for customer [customerId=${org.customerId}]`
);
}
}
await keyStore.setItemWithExpiry(
FEATURE_CACHE_KEY(org.id),
LICENSE_SERVER_CLOUD_PLAN_TTL,

View File

@@ -123,16 +123,16 @@ export const IDENTITIES = {
hasDeleteProtection: "Prevents deletion of the identity when enabled."
},
UPDATE: {
identityId: "The ID of the identity to update.",
identityId: "The ID of the machine identity to update.",
name: "The new name of the identity.",
role: "The new role of the identity.",
hasDeleteProtection: "Prevents deletion of the identity when enabled."
},
DELETE: {
identityId: "The ID of the identity to delete."
identityId: "The ID of the machine identity to delete."
},
GET_BY_ID: {
identityId: "The ID of the identity to get details.",
identityId: "The ID of the machine identity to get details.",
orgId: "The ID of the org of the identity"
},
LIST: {
@@ -157,7 +157,7 @@ export const UNIVERSAL_AUTH = {
clientSecret: "Your Machine Identity Client Secret."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
clientSecretTrustedIps:
"A list of IPs or CIDR ranges that the Client Secret can be used from together with the Client ID to get back an access token. You can use 0.0.0.0/0, to allow usage from any network address.",
accessTokenTrustedIps:
@@ -176,13 +176,13 @@ export const UNIVERSAL_AUTH = {
"How long to wait from the most recent failed login until resetting the lockout counter."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
identityId: "The ID of the machine identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
identityId: "The ID of the machine identity to revoke the auth method for."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
identityId: "The ID of the machine identity to update the auth method for.",
clientSecretTrustedIps: "The new list of IPs or CIDR ranges that the Client Secret can be used from.",
accessTokenTrustedIps: "The new list of IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
@@ -196,25 +196,25 @@ export const UNIVERSAL_AUTH = {
"How long to wait from the most recent failed login until resetting the lockout counter."
},
CREATE_CLIENT_SECRET: {
identityId: "The ID of the identity to create a client secret for.",
identityId: "The ID of the machine identity to create a client secret for.",
description: "The description of the client secret.",
numUsesLimit:
"The maximum number of times that the client secret can be used; a value of 0 implies infinite number of uses.",
ttl: "The lifetime for the client secret in seconds."
},
LIST_CLIENT_SECRETS: {
identityId: "The ID of the identity to list client secrets for."
identityId: "The ID of the machine identity to list client secrets for."
},
GET_CLIENT_SECRET: {
identityId: "The ID of the identity to get the client secret from.",
identityId: "The ID of the machine identity to get the client secret from.",
clientSecretId: "The ID of the client secret to get details."
},
REVOKE_CLIENT_SECRET: {
identityId: "The ID of the identity to revoke the client secret from.",
identityId: "The ID of the machine identity to revoke the client secret from.",
clientSecretId: "The ID of the client secret to revoke."
},
CLEAR_CLIENT_LOCKOUTS: {
identityId: "The ID of the identity to clear the client lockouts from."
identityId: "The ID of the machine identity to clear the client lockouts from."
},
RENEW_ACCESS_TOKEN: {
accessToken: "The access token to renew."
@@ -226,13 +226,13 @@ export const UNIVERSAL_AUTH = {
export const LDAP_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login.",
identityId: "The ID of the machine identity to login.",
username: "The username of the LDAP user to login.",
password: "The password of the LDAP user to login."
},
ATTACH: {
templateId: "The ID of the identity auth template to attach the configuration onto.",
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
url: "The URL of the LDAP server.",
allowedFields:
"The comma-separated array of key/value pairs of required fields that the LDAP entry must have in order to authenticate.",
@@ -252,7 +252,7 @@ export const LDAP_AUTH = {
"How long to wait from the most recent failed login until resetting the lockout counter."
},
UPDATE: {
identityId: "The ID of the identity to update the configuration for.",
identityId: "The ID of the machine identity to update the configuration for.",
url: "The new URL of the LDAP server.",
allowedFields: "The comma-separated list of allowed fields to return from the LDAP user.",
searchBase: "The new base DN to search for the LDAP user.",
@@ -272,19 +272,19 @@ export const LDAP_AUTH = {
"How long to wait from the most recent failed login until resetting the lockout counter."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the configuration for."
identityId: "The ID of the machine identity to retrieve the configuration for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the configuration for."
identityId: "The ID of the machine identity to revoke the configuration for."
},
CLEAR_CLIENT_LOCKOUTS: {
identityId: "The ID of the identity to clear the client lockouts from."
identityId: "The ID of the machine identity to clear the client lockouts from."
}
} as const;
export const ALICLOUD_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login.",
identityId: "The ID of the machine identity to login.",
Action: "The Alibaba Cloud API action. For STS GetCallerIdentity, this should be 'GetCallerIdentity'.",
Format: "The response format. For STS GetCallerIdentity, this should be 'JSON'.",
Version: "The API version. This should be in 'YYYY-MM-DD' format (e.g., '2015-04-01').",
@@ -296,7 +296,7 @@ export const ALICLOUD_AUTH = {
Signature: "The signature string calculated based on the request parameters and AccessKey Secret."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
allowedArns: "The comma-separated list of trusted ARNs that are allowed to authenticate with Infisical.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
@@ -304,7 +304,7 @@ export const ALICLOUD_AUTH = {
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
identityId: "The ID of the machine identity to update the auth method for.",
allowedArns: "The comma-separated list of trusted ARNs that are allowed to authenticate with Infisical.",
accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
@@ -312,19 +312,19 @@ export const ALICLOUD_AUTH = {
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
identityId: "The ID of the machine identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
identityId: "The ID of the machine identity to revoke the auth method for."
}
} as const;
export const TLS_CERT_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login."
identityId: "The ID of the machine identity to login."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
allowedCommonNames:
"The comma-separated list of trusted common names that are allowed to authenticate with Infisical.",
caCertificate: "The PEM-encoded CA certificate to validate client certificates.",
@@ -334,7 +334,7 @@ export const TLS_CERT_AUTH = {
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
identityId: "The ID of the machine identity to update the auth method for.",
allowedCommonNames:
"The comma-separated list of trusted common names that are allowed to authenticate with Infisical.",
caCertificate: "The PEM-encoded CA certificate to validate client certificates.",
@@ -344,16 +344,16 @@ export const TLS_CERT_AUTH = {
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
identityId: "The ID of the machine identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
identityId: "The ID of the machine identity to revoke the auth method for."
}
} as const;
export const AWS_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login.",
identityId: "The ID of the machine identity to login.",
iamHttpRequestMethod: "The HTTP request method used in the signed request.",
iamRequestUrl:
"The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/.",
@@ -362,7 +362,7 @@ export const AWS_AUTH = {
iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
allowedPrincipalArns:
"The comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical.",
allowedAccountIds:
@@ -374,7 +374,7 @@ export const AWS_AUTH = {
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
identityId: "The ID of the machine identity to update the auth method for.",
allowedPrincipalArns:
"The new comma-separated list of trusted IAM principal ARNs that are allowed to authenticate with Infisical.",
allowedAccountIds:
@@ -386,21 +386,21 @@ export const AWS_AUTH = {
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
identityId: "The ID of the machine identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
identityId: "The ID of the machine identity to revoke the auth method for."
}
} as const;
export const OCI_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login.",
identityId: "The ID of the machine identity to login.",
userOcid: "The OCID of the user attempting login.",
headers: "The headers of the signed request."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
tenancyOcid: "The OCID of your tenancy.",
allowedUsernames:
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
@@ -410,7 +410,7 @@ export const OCI_AUTH = {
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
identityId: "The ID of the machine identity to update the auth method for.",
tenancyOcid: "The OCID of your tenancy.",
allowedUsernames:
"The comma-separated list of trusted OCI account usernames that are allowed to authenticate with Infisical.",
@@ -420,19 +420,19 @@ export const OCI_AUTH = {
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
identityId: "The ID of the machine identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
identityId: "The ID of the machine identity to revoke the auth method for."
}
} as const;
export const AZURE_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login."
identityId: "The ID of the machine identity to login."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
tenantId: "The tenant ID for the Azure AD organization.",
resource: "The resource URL for the application registered in Azure AD.",
allowedServicePrincipalIds:
@@ -443,7 +443,7 @@ export const AZURE_AUTH = {
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
identityId: "The ID of the machine identity to update the auth method for.",
tenantId: "The new tenant ID for the Azure AD organization.",
resource: "The new resource URL for the application registered in Azure AD.",
allowedServicePrincipalIds:
@@ -454,19 +454,19 @@ export const AZURE_AUTH = {
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
identityId: "The ID of the machine identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
identityId: "The ID of the machine identity to revoke the auth method for."
}
} as const;
export const GCP_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login."
identityId: "The ID of the machine identity to login."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
allowedServiceAccounts:
"The comma-separated list of trusted service account emails corresponding to the GCE resource(s) allowed to authenticate with Infisical.",
allowedProjects:
@@ -479,7 +479,7 @@ export const GCP_AUTH = {
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
identityId: "The ID of the machine identity to update the auth method for.",
allowedServiceAccounts:
"The new comma-separated list of trusted service account emails corresponding to the GCE resource(s) allowed to authenticate with Infisical.",
allowedProjects:
@@ -492,19 +492,19 @@ export const GCP_AUTH = {
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
identityId: "The ID of the machine identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
identityId: "The ID of the machine identity to revoke the auth method for."
}
} as const;
export const KUBERNETES_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login."
identityId: "The ID of the machine identity to login."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
kubernetesHost: "The host string, host:port pair, or URL to the base of the Kubernetes API server.",
caCert: "The PEM-encoded CA cert for the Kubernetes API server.",
tokenReviewerJwt:
@@ -523,7 +523,7 @@ export const KUBERNETES_AUTH = {
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
identityId: "The ID of the machine identity to update the auth method for.",
kubernetesHost: "The new host string, host:port pair, or URL to the base of the Kubernetes API server.",
caCert: "The new PEM-encoded CA cert for the Kubernetes API server.",
tokenReviewerJwt:
@@ -542,41 +542,41 @@ export const KUBERNETES_AUTH = {
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
identityId: "The ID of the machine identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
identityId: "The ID of the machine identity to revoke the auth method for."
}
} as const;
export const TOKEN_AUTH = {
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
accessTokenTrustedIps: "The IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The lifetime for an access token in seconds.",
accessTokenMaxTTL: "The maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
identityId: "The ID of the machine identity to update the auth method for.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
accessTokenTTL: "The new 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."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
identityId: "The ID of the machine identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
identityId: "The ID of the machine identity to revoke the auth method for."
},
GET_TOKENS: {
identityId: "The ID of the identity to list token metadata for.",
identityId: "The ID of the machine identity to list token metadata for.",
offset: "The offset to start from. If you enter 10, it will start from the 10th token.",
limit: "The number of tokens to return."
},
CREATE_TOKEN: {
identityId: "The ID of the identity to create the token for.",
identityId: "The ID of the machine identity to create the token for.",
name: "The name of the token to create."
},
UPDATE_TOKEN: {
@@ -590,10 +590,10 @@ export const TOKEN_AUTH = {
export const OIDC_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login."
identityId: "The ID of the machine identity to login."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
oidcDiscoveryUrl: "The URL used to retrieve the OpenID Connect configuration from the identity provider.",
caCert: "The PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints.",
boundIssuer: "The unique identifier of the identity provider issuing the JWT.",
@@ -607,7 +607,7 @@ export const OIDC_AUTH = {
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
identityId: "The ID of the machine identity to update the auth method for.",
oidcDiscoveryUrl: "The new URL used to retrieve the OpenID Connect configuration from the identity provider.",
caCert: "The new PEM-encoded CA cert for establishing secure communication with the Identity Provider endpoints.",
boundIssuer: "The new unique identifier of the identity provider issuing the JWT.",
@@ -621,19 +621,19 @@ export const OIDC_AUTH = {
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
identityId: "The ID of the machine identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
identityId: "The ID of the machine identity to revoke the auth method for."
}
} as const;
export const JWT_AUTH = {
LOGIN: {
identityId: "The ID of the identity to login."
identityId: "The ID of the machine identity to login."
},
ATTACH: {
identityId: "The ID of the identity to attach the configuration onto.",
identityId: "The ID of the machine identity to attach the configuration onto.",
configurationType: "The configuration for validating JWTs. Must be one of: 'jwks', 'static'",
jwksUrl:
"The URL of the JWKS endpoint. Required if configurationType is 'jwks'. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.",
@@ -650,7 +650,7 @@ export const JWT_AUTH = {
accessTokenNumUsesLimit: "The maximum number of times that an access token can be used."
},
UPDATE: {
identityId: "The ID of the identity to update the auth method for.",
identityId: "The ID of the machine identity to update the auth method for.",
configurationType: "The new configuration for validating JWTs. Must be one of: 'jwks', 'static'",
jwksUrl:
"The new URL of the JWKS endpoint. This endpoint must serve JSON Web Key Sets (JWKS) containing the public keys used to verify JWT signatures.",
@@ -667,10 +667,10 @@ export const JWT_AUTH = {
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used."
},
RETRIEVE: {
identityId: "The ID of the identity to retrieve the auth method for."
identityId: "The ID of the machine identity to retrieve the auth method for."
},
REVOKE: {
identityId: "The ID of the identity to revoke the auth method for."
identityId: "The ID of the machine identity to revoke the auth method for."
}
} as const;
@@ -854,12 +854,12 @@ export const PROJECT_IDENTITIES = {
search: "The text string that identity membership names will be filtered by."
},
GET_IDENTITY_MEMBERSHIP_BY_ID: {
identityId: "The ID of the identity to get the membership for.",
identityId: "The ID of the machine identity to get the membership for.",
projectId: "The ID of the project to get the identity membership for."
},
UPDATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to update the identity membership for.",
identityId: "The ID of the identity to update the membership for.",
identityId: "The ID of the machine identity to update the membership for.",
roles: {
description: "A list of role slugs to assign to the identity project membership.",
role: "The role slug to assign to the newly created identity project membership.",
@@ -872,11 +872,11 @@ export const PROJECT_IDENTITIES = {
},
DELETE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to delete the identity membership from.",
identityId: "The ID of the identity to delete the membership from."
identityId: "The ID of the machine identity to delete the membership from."
},
CREATE_IDENTITY_MEMBERSHIP: {
projectId: "The ID of the project to create the identity membership from.",
identityId: "The ID of the identity to create the membership from.",
identityId: "The ID of the machine identity to create the membership from.",
role: "The role slug to assign to the newly created identity project membership.",
roles: {
description: "A list of role slugs to assign to the newly created identity project membership.",
@@ -950,7 +950,8 @@ export const FOLDERS = {
projectId: "The ID of the project to delete the folder from.",
environment: "The slug of the environment where the folder is located.",
directory: "The directory of the folder to delete. (Deprecated in favor of path)",
path: "The path of the folder to delete."
path: "The path of the folder to delete.",
forceDelete: "Whether to force delete the folder even if it contains resources."
}
} as const;
@@ -1271,7 +1272,7 @@ export const SECRET_TAGS = {
export const IDENTITY_ADDITIONAL_PRIVILEGE = {
CREATE: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to create.",
identityId: "The ID of the machine identity to create.",
slug: "The slug of the privilege to create.",
permissions: `@deprecated - use privilegePermission
The permission object for the privilege.
@@ -1297,7 +1298,7 @@ The permission object for the privilege.
},
UPDATE: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to update.",
identityId: "The ID of the machine identity to update.",
slug: "The slug of the privilege to update.",
newSlug: "The new slug of the privilege to update.",
permissions: `@deprecated - use privilegePermission
@@ -1323,17 +1324,17 @@ The permission object for the privilege.
},
DELETE: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to delete.",
identityId: "The ID of the machine identity to delete.",
slug: "The slug of the privilege to delete."
},
GET_BY_SLUG: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to list.",
identityId: "The ID of the machine identity to list.",
slug: "The slug of the privilege."
},
LIST: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to list.",
identityId: "The ID of the machine identity to list.",
unpacked: "Whether the system should send the permissions as unpacked."
}
};
@@ -1375,7 +1376,7 @@ export const PROJECT_USER_ADDITIONAL_PRIVILEGE = {
export const IDENTITY_ADDITIONAL_PRIVILEGE_V2 = {
CREATE: {
identityId: "The ID of the identity to create the privilege for.",
identityId: "The ID of the machine identity to create the privilege for.",
projectId: "The ID of the project of the identity in.",
slug: "The slug of the privilege to create.",
permission: "The permission for the privilege.",
@@ -1386,7 +1387,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE_V2 = {
},
UPDATE: {
id: "The ID of the identity privilege.",
identityId: "The ID of the identity to update.",
identityId: "The ID of the machine identity to update.",
slug: "The slug of the privilege to update.",
privilegePermission: "The permission for the privilege.",
isTemporary: "Whether the privilege is temporary.",
@@ -1396,12 +1397,12 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE_V2 = {
},
DELETE: {
id: "The ID of the identity privilege.",
identityId: "The ID of the identity to delete.",
identityId: "The ID of the machine identity to delete.",
slug: "The slug of the privilege to delete."
},
GET_BY_SLUG: {
projectSlug: "The slug of the project of the identity in.",
identityId: "The ID of the identity to list.",
identityId: "The ID of the machine identity to list.",
slug: "The slug of the privilege."
},
GET_BY_ID: {
@@ -1409,7 +1410,7 @@ export const IDENTITY_ADDITIONAL_PRIVILEGE_V2 = {
},
LIST: {
projectId: "The ID of the project that the identity is in.",
identityId: "The ID of the identity to list."
identityId: "The ID of the machine identity to list."
}
};

View File

@@ -184,6 +184,7 @@ import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-gr
import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service";
import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue";
import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service";
import { vaultExternalMigrationConfigDALFactory } from "@app/services/external-migration/vault-external-migration-config-dal";
import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal";
import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal";
import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal";
@@ -542,6 +543,8 @@ export const registerRoutes = async (
const membershipRoleDAL = membershipRoleDALFactory(db);
const roleDAL = roleDALFactory(db);
const vaultExternalMigrationConfigDAL = vaultExternalMigrationConfigDALFactory(db);
const eventBusService = eventBusFactory(server.redis);
const sseService = sseServiceFactory(eventBusService, server.redis);
@@ -1371,7 +1374,8 @@ export const registerRoutes = async (
projectDAL,
folderCommitService,
secretApprovalPolicyService,
secretV2BridgeDAL
secretV2BridgeDAL,
dynamicSecretDAL
});
const secretImportService = secretImportServiceFactory({
@@ -1908,13 +1912,6 @@ export const registerRoutes = async (
notificationService
});
const migrationService = externalMigrationServiceFactory({
externalMigrationQueue,
userDAL,
permissionService,
gatewayService
});
const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({
permissionService,
licenseService,
@@ -2252,6 +2249,18 @@ export const registerRoutes = async (
kmsService
});
const migrationService = externalMigrationServiceFactory({
externalMigrationQueue,
userDAL,
permissionService,
gatewayService,
kmsService,
appConnectionService,
vaultExternalMigrationConfigDAL,
secretService,
auditLogService
});
// setup the communication with license key server
await licenseService.init();

View File

@@ -318,7 +318,8 @@ export const registerDeprecatedSecretFolderRouter = async (server: FastifyZodPro
...req.body,
projectId: req.body.workspaceId,
idOrName: req.params.folderIdOrName,
path
path,
forceDelete: true
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,

View File

@@ -13,7 +13,7 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "Renew access token",
description: "Renew machine identity access token",
body: z.object({
accessToken: z.string().trim().describe(UNIVERSAL_AUTH.RENEW_ACCESS_TOKEN.accessToken)
}),
@@ -48,7 +48,7 @@ export const registerIdentityAccessTokenRouter = async (server: FastifyZodProvid
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "Revoke access token",
description: "Revoke machine identity access token",
body: z.object({
accessToken: z.string().trim().describe(UNIVERSAL_AUTH.REVOKE_ACCESS_TOKEN.accessToken)
}),

View File

@@ -21,7 +21,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
schema: {
hide: false,
tags: [ApiDocsTags.AliCloudAuth],
description: "Login with Alibaba Cloud Auth",
description: "Login with Alibaba Cloud Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(ALICLOUD_AUTH.LOGIN.identityId),
Action: z.enum(["GetCallerIdentity"]).describe(ALICLOUD_AUTH.LOGIN.Action),
@@ -108,7 +108,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
schema: {
hide: false,
tags: [ApiDocsTags.AliCloudAuth],
description: "Attach Alibaba Cloud Auth configuration onto identity",
description: "Attach Alibaba Cloud Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -200,7 +200,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
schema: {
hide: false,
tags: [ApiDocsTags.AliCloudAuth],
description: "Update Alibaba Cloud Auth configuration on identity",
description: "Update Alibaba Cloud Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -292,7 +292,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
schema: {
hide: false,
tags: [ApiDocsTags.AliCloudAuth],
description: "Retrieve Alibaba Cloud Auth configuration on identity",
description: "Retrieve Alibaba Cloud Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -340,7 +340,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi
schema: {
hide: false,
tags: [ApiDocsTags.AliCloudAuth],
description: "Delete Alibaba Cloud Auth configuration on identity",
description: "Delete Alibaba Cloud Auth configuration on machine identity",
security: [
{
bearerAuth: []

View File

@@ -23,7 +23,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.AwsAuth],
description: "Login with AWS Auth",
description: "Login with AWS Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(AWS_AUTH.LOGIN.identityId),
iamHttpRequestMethod: z.string().default("POST").describe(AWS_AUTH.LOGIN.iamHttpRequestMethod),
@@ -75,7 +75,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.AwsAuth],
description: "Attach AWS Auth configuration onto identity",
description: "Attach AWS Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -171,7 +171,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.AwsAuth],
description: "Update AWS Auth configuration on identity",
description: "Update AWS Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -255,7 +255,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.AwsAuth],
description: "Retrieve AWS Auth configuration on identity",
description: "Retrieve AWS Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -303,7 +303,7 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.AwsAuth],
description: "Delete AWS Auth configuration on identity",
description: "Delete AWS Auth configuration on machine identity",
security: [
{
bearerAuth: []

View File

@@ -20,7 +20,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.AzureAuth],
description: "Login with Azure Auth",
description: "Login with Azure Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId),
jwt: z.string()
@@ -70,7 +70,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.AzureAuth],
description: "Attach Azure Auth configuration onto identity",
description: "Attach Azure Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -165,7 +165,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.AzureAuth],
description: "Update Azure Auth configuration on identity",
description: "Update Azure Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -255,7 +255,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.AzureAuth],
description: "Retrieve Azure Auth configuration on identity",
description: "Retrieve Azure Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -304,7 +304,7 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.AzureAuth],
description: "Delete Azure Auth configuration on identity",
description: "Delete Azure Auth configuration on machine identity",
security: [
{
bearerAuth: []

View File

@@ -20,7 +20,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.GcpAuth],
description: "Login with GCP Auth",
description: "Login with GCP Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId),
jwt: z.string()
@@ -70,7 +70,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.GcpAuth],
description: "Attach GCP Auth configuration onto identity",
description: "Attach GCP Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -163,7 +163,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.GcpAuth],
description: "Update GCP Auth configuration on identity",
description: "Update GCP Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -249,7 +249,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.GcpAuth],
description: "Retrieve GCP Auth configuration on identity",
description: "Retrieve GCP Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -298,7 +298,7 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.GcpAuth],
description: "Delete GCP Auth configuration on identity",
description: "Delete GCP Auth configuration on machine identity",
security: [
{
bearerAuth: []

View File

@@ -96,7 +96,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.JwtAuth],
description: "Login with JWT Auth",
description: "Login with JWT Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId),
jwt: z.string().trim()
@@ -148,7 +148,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.JwtAuth],
description: "Attach JWT Auth configuration onto identity",
description: "Attach JWT Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -217,7 +217,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.JwtAuth],
description: "Update JWT Auth configuration on identity",
description: "Update JWT Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -283,7 +283,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.JwtAuth],
description: "Retrieve JWT Auth configuration on identity",
description: "Retrieve JWT Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -332,7 +332,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.JwtAuth],
description: "Delete JWT Auth configuration on identity",
description: "Delete JWT Auth configuration on machine identity",
security: [
{
bearerAuth: []

View File

@@ -41,7 +41,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
schema: {
hide: false,
tags: [ApiDocsTags.KubernetesAuth],
description: "Login with Kubernetes Auth",
description: "Login with Kubernetes Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(KUBERNETES_AUTH.LOGIN.identityId),
jwt: z.string().trim()
@@ -93,7 +93,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
schema: {
hide: false,
tags: [ApiDocsTags.KubernetesAuth],
description: "Attach Kubernetes Auth configuration onto identity",
description: "Attach Kubernetes Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -240,7 +240,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
schema: {
hide: false,
tags: [ApiDocsTags.KubernetesAuth],
description: "Update Kubernetes Auth configuration on identity",
description: "Update Kubernetes Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -383,7 +383,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
schema: {
hide: false,
tags: [ApiDocsTags.KubernetesAuth],
description: "Retrieve Kubernetes Auth configuration on identity",
description: "Retrieve Kubernetes Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -432,7 +432,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide
schema: {
hide: false,
tags: [ApiDocsTags.KubernetesAuth],
description: "Delete Kubernetes Auth configuration on identity",
description: "Delete Kubernetes Auth configuration on machine identity",
security: [
{
bearerAuth: []

View File

@@ -120,7 +120,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.LdapAuth],
description: "Login with LDAP Auth",
description: "Login with LDAP Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(LDAP_AUTH.LOGIN.identityId),
username: z.string().describe(LDAP_AUTH.LOGIN.username),
@@ -198,7 +198,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.LdapAuth],
description: "Attach LDAP Auth configuration onto identity",
description: "Attach LDAP Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -389,7 +389,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.LdapAuth],
description: "Update LDAP Auth configuration on identity",
description: "Update LDAP Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -510,7 +510,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.LdapAuth],
description: "Retrieve LDAP Auth configuration on identity",
description: "Retrieve LDAP Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -568,7 +568,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.LdapAuth],
description: "Delete LDAP Auth configuration on identity",
description: "Delete LDAP Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -621,7 +621,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.LdapAuth],
description: "Clear LDAP Auth Lockouts for identity",
description: "Clear LDAP Auth Lockouts for machine identity",
security: [
{
bearerAuth: []

View File

@@ -20,7 +20,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Login with OCI Auth",
description: "Login with OCI Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(OCI_AUTH.LOGIN.identityId),
userOcid: z.string().trim().describe(OCI_AUTH.LOGIN.userOcid),
@@ -87,7 +87,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Attach OCI Auth configuration onto identity",
description: "Attach OCI Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -176,7 +176,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Update OCI Auth configuration on identity",
description: "Update OCI Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -259,7 +259,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Retrieve OCI Auth configuration on identity",
description: "Retrieve OCI Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -307,7 +307,7 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.OciAuth],
description: "Delete OCI Auth configuration on identity",
description: "Delete OCI Auth configuration on machine identity",
security: [
{
bearerAuth: []

View File

@@ -44,7 +44,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.OidcAuth],
description: "Login with OIDC Auth",
description: "Login with OIDC Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(OIDC_AUTH.LOGIN.identityId),
jwt: z.string().trim()
@@ -100,7 +100,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.OidcAuth],
description: "Attach OIDC Auth configuration onto identity",
description: "Attach OIDC Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -201,7 +201,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.OidcAuth],
description: "Update OIDC Auth configuration on identity",
description: "Update OIDC Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -300,7 +300,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.OidcAuth],
description: "Retrieve OIDC Auth configuration on identity",
description: "Retrieve OIDC Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -349,7 +349,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider)
schema: {
hide: false,
tags: [ApiDocsTags.OidcAuth],
description: "Delete OIDC Auth configuration on identity",
description: "Delete OIDC Auth configuration on machine identity",
security: [
{
bearerAuth: []

View File

@@ -34,7 +34,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Create identity",
description: "Create machine identity",
security: [
{
bearerAuth: []
@@ -109,7 +109,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Update identity",
description: "Update machine identity",
security: [
{
bearerAuth: []
@@ -173,7 +173,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Delete identity",
description: "Delete machine identity",
security: [
{
bearerAuth: []
@@ -222,7 +222,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Get an identity by id",
description: "Get a machine identity by id",
security: [
{
bearerAuth: []
@@ -280,7 +280,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "List identities",
description: "List machine identities",
security: [
{
bearerAuth: []
@@ -330,7 +330,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.Identities],
description: "Search identities",
description: "Search machine identities",
security: [
{
bearerAuth: []
@@ -427,7 +427,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "List project memberships that identity with id is part of",
description: "List project memberships that machine identity with id is part of",
security: [
{
bearerAuth: []

View File

@@ -44,7 +44,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
schema: {
hide: false,
tags: [ApiDocsTags.TlsCertAuth],
description: "Login with TLS Certificate Auth",
description: "Login with TLS Certificate Auth for machine identity",
body: z.object({
identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId)
}),
@@ -102,7 +102,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
schema: {
hide: false,
tags: [ApiDocsTags.TlsCertAuth],
description: "Attach TLS Certificate Auth configuration onto identity",
description: "Attach TLS Certificate Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -203,7 +203,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
schema: {
hide: false,
tags: [ApiDocsTags.TlsCertAuth],
description: "Update TLS Certificate Auth configuration on identity",
description: "Update TLS Certificate Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -304,7 +304,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
schema: {
hide: false,
tags: [ApiDocsTags.TlsCertAuth],
description: "Retrieve TLS Certificate Auth configuration on identity",
description: "Retrieve TLS Certificate Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -354,7 +354,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid
schema: {
hide: false,
tags: [ApiDocsTags.TlsCertAuth],
description: "Delete TLS Certificate Auth configuration on identity",
description: "Delete TLS Certificate Auth configuration on machine identity",
security: [
{
bearerAuth: []

View File

@@ -20,7 +20,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.TokenAuth],
description: "Attach Token Auth configuration onto identity",
description: "Attach Token Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -112,7 +112,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.TokenAuth],
description: "Update Token Auth configuration on identity",
description: "Update Token Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -198,7 +198,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.TokenAuth],
description: "Retrieve Token Auth configuration on identity",
description: "Retrieve Token Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -247,7 +247,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.TokenAuth],
description: "Delete Token Auth configuration on identity",
description: "Delete Token Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -297,7 +297,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.TokenAuth],
description: "Create token for identity with Token Auth",
description: "Create token for machine identity with Token Auth",
security: [
{
bearerAuth: []
@@ -361,7 +361,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.TokenAuth],
description: "Get tokens for identity with Token Auth",
description: "Get tokens for machine identity with Token Auth",
security: [
{
bearerAuth: []
@@ -416,7 +416,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.TokenAuth],
description: "Update token for identity with Token Auth",
description: "Update token for machine identity with Token Auth",
security: [
{
bearerAuth: []
@@ -472,7 +472,7 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider
schema: {
hide: false,
tags: [ApiDocsTags.TokenAuth],
description: "Revoke token for identity with Token Auth",
description: "Revoke token for machine identity with Token Auth",
security: [
{
bearerAuth: []

View File

@@ -32,7 +32,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "Login with Universal Auth",
description: "Login with Universal Auth for machine identity",
body: z.object({
clientId: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientId),
clientSecret: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientSecret)
@@ -90,7 +90,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "Attach Universal Auth configuration onto identity",
description: "Attach Universal Auth configuration onto machine identity",
security: [
{
bearerAuth: []
@@ -208,7 +208,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "Update Universal Auth configuration on identity",
description: "Update Universal Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -331,7 +331,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "Retrieve Universal Auth configuration on identity",
description: "Retrieve Universal Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -380,7 +380,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "Delete Universal Auth configuration on identity",
description: "Delete Universal Auth configuration on machine identity",
security: [
{
bearerAuth: []
@@ -429,7 +429,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "Create Universal Auth Client Secret for identity",
description: "Create Universal Auth Client Secret for machine identity",
security: [
{
bearerAuth: []
@@ -487,7 +487,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "List Universal Auth Client Secrets for identity",
description: "List Universal Auth Client Secrets for machine identity",
security: [
{
bearerAuth: []
@@ -537,7 +537,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "Get Universal Auth Client Secret for identity",
description: "Get Universal Auth Client Secret for machine identity",
security: [
{
bearerAuth: []
@@ -567,7 +567,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
...req.auditLogInfo,
orgId: clientSecretData.orgId,
event: {
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET,
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID,
metadata: {
identityId: clientSecretData.identityId,
clientSecretId: clientSecretData.id
@@ -589,7 +589,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "Revoke Universal Auth Client Secrets for identity",
description: "Revoke Universal Auth Client Secrets for machine identity",
security: [
{
bearerAuth: []
@@ -641,7 +641,7 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => {
schema: {
hide: false,
tags: [ApiDocsTags.UniversalAuth],
description: "Clear Universal Auth Lockouts for identity",
description: "Clear Universal Auth Lockouts for machine identity",
security: [
{
bearerAuth: []

View File

@@ -263,7 +263,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
.default("/")
.transform(prefixWithSlash) // Transformations get skipped if path is undefined
.transform(removeTrailingSlash)
.describe(FOLDERS.DELETE.path)
.describe(FOLDERS.DELETE.path),
forceDelete: z.boolean().optional().default(false).describe(FOLDERS.DELETE.forceDelete)
}),
response: {
200: z.object({
@@ -279,7 +280,8 @@ export const registerSecretFolderRouter = async (server: FastifyZodProvider) =>
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
...req.body,
idOrName: req.params.folderIdOrName
idOrName: req.params.folderIdOrName,
forceDelete: req.body.forceDelete
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,

View File

@@ -7,6 +7,7 @@ import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import {
ExternalMigrationProviders,
VaultImportStatus,
VaultMappingType
} from "@app/services/external-migration/external-migration-types";
@@ -113,4 +114,366 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
return { enabled };
}
});
server.route({
method: "GET",
url: "/vault/configs",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
configs: z
.object({
id: z.string(),
orgId: z.string(),
namespace: z.string(),
connectionId: z.string().nullish(),
createdAt: z.date(),
updatedAt: z.date()
})
.array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const configs = await server.services.migration.getVaultExternalMigrationConfigs({
actor: req.permission
});
return { configs };
}
});
server.route({
method: "POST",
url: "/vault/configs",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
connectionId: z.string(),
namespace: z.string()
}),
response: {
200: z.object({
config: z.object({
id: z.string(),
orgId: z.string(),
namespace: z.string(),
connectionId: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const config = await server.services.migration.createVaultExternalMigration({
...req.body,
actor: req.permission
});
return { config };
}
});
server.route({
method: "PUT",
url: "/vault/configs/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
body: z.object({
connectionId: z.string(),
namespace: z.string()
}),
response: {
200: z.object({
config: z.object({
id: z.string(),
orgId: z.string(),
namespace: z.string(),
connectionId: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const config = await server.services.migration.updateVaultExternalMigration({
id: req.params.id,
...req.body,
actor: req.permission
});
return { config };
}
});
server.route({
method: "DELETE",
url: "/vault/configs/:id",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
id: z.string()
}),
response: {
200: z.object({
config: z.object({
id: z.string(),
orgId: z.string(),
namespace: z.string(),
connectionId: z.string().nullable().optional(),
createdAt: z.date(),
updatedAt: z.date()
})
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const config = await server.services.migration.deleteVaultExternalMigration({
id: req.params.id,
actor: req.permission
});
return { config };
}
});
server.route({
method: "GET",
url: "/vault/namespaces",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
namespaces: z.array(z.object({ id: z.string(), name: z.string() }))
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const namespaces = await server.services.migration.getVaultNamespaces({
actor: req.permission
});
return { namespaces };
}
});
server.route({
method: "GET",
url: "/vault/policies",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string()
}),
response: {
200: z.object({
policies: z.array(z.object({ name: z.string(), rules: z.string() }))
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const policies = await server.services.migration.getVaultPolicies({
actor: req.permission,
namespace: req.query.namespace
});
return { policies };
}
});
server.route({
method: "GET",
url: "/vault/mounts",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string()
}),
response: {
200: z.object({
mounts: z.array(z.object({ path: z.string(), type: z.string(), version: z.string().nullish() }))
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const mounts = await server.services.migration.getVaultMounts({
actor: req.permission,
namespace: req.query.namespace
});
return { mounts };
}
});
server.route({
method: "GET",
url: "/vault/auth-mounts",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string(),
authType: z.string().optional()
}),
response: {
200: z.object({
mounts: z.array(z.object({ path: z.string(), type: z.string() }))
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const mounts = await server.services.migration.getVaultAuthMounts({
actor: req.permission,
namespace: req.query.namespace,
authType: req.query.authType
});
return { mounts };
}
});
server.route({
method: "POST",
url: "/vault/import-secrets",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
projectId: z.string(),
environment: z.string(),
secretPath: z.string(),
vaultNamespace: z.string(),
vaultSecretPath: z.string()
}),
response: {
200: z.object({
status: z.nativeEnum(VaultImportStatus)
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const result = await server.services.migration.importVaultSecrets({
actor: req.permission,
auditLogInfo: req.auditLogInfo,
...req.body
});
return result;
}
});
server.route({
method: "GET",
url: "/vault/secret-paths",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string(),
mountPath: z.string()
}),
response: {
200: z.object({
secretPaths: z.string().array()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const secretPaths = await server.services.migration.getVaultSecretPaths({
actor: req.permission,
namespace: req.query.namespace,
mountPath: req.query.mountPath
});
return { secretPaths };
}
});
server.route({
method: "GET",
url: "/vault/auth-roles/kubernetes",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string(),
mountPath: z.string()
}),
response: {
200: z.object({
roles: z.array(
z.object({
name: z.string(),
mountPath: z.string(),
bound_service_account_names: z.array(z.string()),
bound_service_account_namespaces: z.array(z.string()),
token_ttl: z.number().optional(),
token_max_ttl: z.number().optional(),
token_policies: z.array(z.string()).optional(),
token_bound_cidrs: z.array(z.string()).optional(),
token_explicit_max_ttl: z.number().optional(),
token_no_default_policy: z.boolean().optional(),
token_num_uses: z.number().optional(),
token_period: z.number().optional(),
token_type: z.string().optional(),
audience: z.string().optional(),
alias_name_source: z.string().optional(),
config: z.object({
kubernetes_host: z.string(),
kubernetes_ca_cert: z.string().optional(),
issuer: z.string().optional(),
disable_iss_validation: z.boolean().optional(),
disable_local_ca_jwt: z.boolean().optional()
})
})
)
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const roles = await server.services.migration.getVaultKubernetesAuthRoles({
actor: req.permission,
namespace: req.query.namespace,
mountPath: req.query.mountPath
});
return { roles };
}
});
};

View File

@@ -2,3 +2,7 @@ export enum HCVaultConnectionMethod {
AccessToken = "access-token",
AppRole = "app-role"
}
export enum HCVaultAuthType {
Kubernetes = "kubernetes"
}

View File

@@ -12,8 +12,56 @@ import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import { HCVaultConnectionMethod } from "./hc-vault-connection-enums";
import { THCVaultConnection, THCVaultConnectionConfig, THCVaultMountResponse } from "./hc-vault-connection-types";
import { HCVaultAuthType, HCVaultConnectionMethod } from "./hc-vault-connection-enums";
import {
THCVaultAuthMount,
THCVaultAuthMountResponse,
THCVaultConnection,
THCVaultConnectionConfig,
THCVaultKubernetesAuthConfig,
THCVaultKubernetesAuthRole,
THCVaultKubernetesAuthRoleWithConfig,
THCVaultMount,
THCVaultMountResponse
} from "./hc-vault-connection-types";
// Concurrency limit for HC Vault API requests to avoid rate limiting
const HC_VAULT_CONCURRENCY_LIMIT = 20;
/**
* Creates a concurrency limiter that restricts the number of concurrent async operations
* @param limit - Maximum number of concurrent operations
* @returns A function that takes an async function and executes it with concurrency control
*/
const createConcurrencyLimiter = (limit: number) => {
let activeCount = 0;
const queue: Array<() => void> = [];
const next = () => {
activeCount -= 1;
if (queue.length > 0) {
const resolve = queue.shift();
resolve?.();
}
};
return async <T>(fn: () => Promise<T>): Promise<T> => {
// If we're at the limit, wait in queue
if (activeCount >= limit) {
await new Promise<void>((resolve) => {
queue.push(resolve);
});
}
activeCount += 1;
try {
return await fn();
} finally {
next();
}
};
};
export const getHCVaultInstanceUrl = async (config: THCVaultConnectionConfig) => {
const instanceUrl = removeTrailingSlash(config.credentials.instanceUrl);
@@ -181,30 +229,573 @@ export const validateHCVaultConnectionCredentials = async (
}
};
export const listHCVaultMounts = async (
export const listHCVaultPolicies = async (
namespace: string,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
try {
const { data: listData } = await requestWithHCVaultGateway<{
data: {
policies: string[];
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/policy`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
const policyNames = listData.data.policies || [];
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
const policies = await Promise.all(
policyNames.map((policyName) =>
limiter(async () => {
try {
const { data: policyData } = await requestWithHCVaultGateway<{
data: {
name: string;
rules: string;
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/policy/${policyName}`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
return {
name: policyData.data.name,
rules: policyData.data.rules
};
} catch (error: unknown) {
logger.error(error, `Unable to fetch policy details for ${policyName}`);
return {
name: policyName,
rules: ""
};
}
})
)
);
return policies;
} catch (error: unknown) {
logger.error(error, "Unable to list HC Vault policies");
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list policies: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to list policies from HashiCorp Vault"
});
}
};
export const listHCVaultNamespaces = async (
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
const currentNamespace = connection.credentials.namespace || "/";
// Helper function to fetch namespaces at a specific path
const fetchNamespacesAtPath = async (namespacePath: string): Promise<string[] | null> => {
try {
const { data } = await requestWithHCVaultGateway<{
data: {
keys: string[];
key_info?: {
[key: string]: {
id: string;
path: string;
custom_metadata?: Record<string, unknown>;
};
};
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/namespaces?list=true`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespacePath
}
});
return data.data.keys || [];
} catch (error: unknown) {
if (error instanceof AxiosError && error.response?.status === 404) {
// No child namespaces at this path
return null;
}
throw error;
}
};
// Recursive function to get all namespaces at all depths with controlled parallelization
const recursivelyGetAllNamespaces = async (
parentPath: string,
limiter: ReturnType<typeof createConcurrencyLimiter>
): Promise<string[]> => {
const childKeys = await fetchNamespacesAtPath(parentPath);
if (childKeys === null || childKeys.length === 0) {
return [];
}
// Process namespaces in parallel with concurrency control
const namespacesArrays = await Promise.all(
childKeys.map((namespaceKey) =>
limiter(async () => {
// Remove trailing slash from the key
const cleanNamespaceKey = namespaceKey.replace(/\/$/, "");
// Build the full path
let fullNamespacePath: string;
if (parentPath === "/") {
fullNamespacePath = cleanNamespaceKey;
} else {
fullNamespacePath = `${parentPath}/${cleanNamespaceKey}`;
}
// Recursively fetch child namespaces
const childNamespaces = await recursivelyGetAllNamespaces(fullNamespacePath, limiter);
// Return this namespace and all its children
return [fullNamespacePath, ...childNamespaces];
})
)
);
// Flatten the arrays into a single array
return namespacesArrays.flat();
};
try {
// Create concurrency limiter to avoid overwhelming the Vault instance
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
// Get all namespaces starting from currentNamespace
const childNamespaces = await recursivelyGetAllNamespaces(currentNamespace, limiter);
// Build the result array with full paths
const namespaces = childNamespaces.map((path) => ({
id: path,
name: path
}));
// Always include the current/root namespace
namespaces.unshift({
id: currentNamespace,
name: currentNamespace
});
return namespaces;
} catch (error: unknown) {
logger.error(error, "Unable to list HC Vault namespaces");
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list namespaces: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: "Unable to list namespaces from HashiCorp Vault"
});
}
};
export const listHCVaultMounts = async (
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
namespace?: string
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
const targetNamespace = namespace || connection.credentials.namespace;
const { data } = await requestWithHCVaultGateway<THCVaultMountResponse>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/mounts`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {})
...(targetNamespace ? { "X-Vault-Namespace": targetNamespace } : {})
}
});
const mounts: string[] = [];
const mounts: THCVaultMount[] = [];
// Filter for "kv" version 2 type only
Object.entries(data.data).forEach(([path, mount]) => {
if (mount.type === "kv" && mount.options?.version === "2") {
mounts.push(path);
}
mounts.push({
path,
type: mount.type,
version: mount.options?.version
});
});
return mounts;
};
export const listHCVaultSecretPaths = async (
namespace: string,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
filterMountPath?: string
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
const getPaths = async (mountPath: string, secretPath: string, kvVersion: "1" | "2"): Promise<string[] | null> => {
try {
let path: string;
if (kvVersion === "2") {
// For KV v2: /v1/{mount}/metadata/{path}?list=true
path = secretPath ? `${mountPath}/metadata/${secretPath}` : `${mountPath}/metadata`;
} else {
// For KV v1: /v1/{mount}/{path}?list=true
path = secretPath ? `${mountPath}/${secretPath}` : mountPath;
}
const { data } = await requestWithHCVaultGateway<{
data: {
keys: string[];
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/${path}?list=true`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
return data.data.keys;
} catch (error) {
if (error instanceof AxiosError && error.response?.status === 404) {
return null;
}
throw error;
}
};
// Recursive function to get all secret paths in a mount with controlled parallelization
const recursivelyGetAllPaths = async (
mountPath: string,
kvVersion: "1" | "2",
limiter: ReturnType<typeof createConcurrencyLimiter>,
currentPath: string = ""
): Promise<string[]> => {
const paths = await getPaths(mountPath, currentPath, kvVersion);
if (paths === null || paths.length === 0) {
return [];
}
// Process paths in parallel with concurrency control
const secretPathsArrays = await Promise.all(
paths.map((path) =>
limiter(async () => {
const cleanPath = path.endsWith("/") ? path.slice(0, -1) : path;
const fullItemPath = currentPath ? `${currentPath}/${cleanPath}` : cleanPath;
if (path.endsWith("/")) {
// it's a folder so we recurse into it
return recursivelyGetAllPaths(mountPath, kvVersion, limiter, fullItemPath);
}
// it's a secret so we return it
return [`${mountPath}/${fullItemPath}`];
})
)
);
// Flatten the arrays into a single array
return secretPathsArrays.flat();
};
// Get all mounts
const mounts = await listHCVaultMounts(connection, gatewayService, namespace);
// Filter for KV mounts (kv, kv-v1, kv-v2)
let kvMounts = mounts.filter((mount) => mount.type === "kv" || mount.type.startsWith("kv"));
// If filterMountPath is provided, filter to only that mount
if (filterMountPath) {
const normalizedFilterPath = filterMountPath.replace(/\/$/, ""); // Remove trailing slash
kvMounts = kvMounts.filter((mount) => mount.path.replace(/\/$/, "") === normalizedFilterPath);
}
// Create concurrency limiter to avoid overwhelming the Vault instance
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
// Collect all secret paths from all KV mounts in parallel
const allSecretPathsArrays = await Promise.all(
kvMounts.map(async (mount) => {
const kvVersion = mount.version === "2" ? "2" : "1";
const cleanMountPath = mount.path.replace(/\/$/, ""); // Remove trailing slash
return recursivelyGetAllPaths(cleanMountPath, kvVersion, limiter);
})
);
// Flatten the arrays into a single array
const allSecretPaths = allSecretPathsArrays.flat();
return allSecretPaths;
};
export const getHCVaultSecretsForPath = async (
namespace: string,
secretPath: string,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
try {
// Extract mount and path from the secretPath
// secretPath format: {mount}/{path}
const pathParts = secretPath.split("/");
const mountPath = pathParts[0];
const actualPath = pathParts.slice(1).join("/");
if (!mountPath || !actualPath) {
throw new BadRequestError({
message: "Invalid secret path format. Expected format: {mount}/{path}"
});
}
// Get mounts to determine KV version
const mounts = await listHCVaultMounts(connection, gatewayService, namespace);
const mount = mounts.find((m) => m.path.replace(/\/$/, "") === mountPath);
if (!mount) {
throw new BadRequestError({
message: `Mount '${mountPath}' not found in HashiCorp Vault`
});
}
const kvVersion = mount.version === "2" ? "2" : "1";
// Fetch secrets based on KV version
if (kvVersion === "2") {
// For KV v2: /v1/{mount}/data/{path}
const { data } = await requestWithHCVaultGateway<{
data: {
data: Record<string, string>; // KV v2 has nested data structure
metadata: {
created_time: string;
deletion_time: string;
destroyed: boolean;
version: number;
};
};
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/${mountPath}/data/${actualPath}`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
return data.data.data;
}
// For KV v1: /v1/{mount}/{path}
const { data } = await requestWithHCVaultGateway<{
data: Record<string, string>; // KV v1 has flat data structure
lease_duration: number;
lease_id: string;
renewable: boolean;
}>(connection, gatewayService, {
url: `${instanceUrl}/v1/${mountPath}/${actualPath}`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
return data.data;
} catch (error: unknown) {
logger.error(error, "Unable to fetch secrets from HC Vault path");
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to fetch secrets: ${error.message || "Unknown error"}`
});
}
if (error instanceof BadRequestError) {
throw error;
}
throw new BadRequestError({
message: "Unable to fetch secrets from HashiCorp Vault"
});
}
};
export const getHCVaultAuthMounts = async (
namespace: string,
authType: HCVaultAuthType | undefined,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
): Promise<THCVaultAuthMount[]> => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
try {
const { data } = await requestWithHCVaultGateway<THCVaultAuthMountResponse>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/auth`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
});
const authMounts: THCVaultAuthMount[] = [];
Object.entries(data.data).forEach(([path, authMethod]) => {
// If authType is specified, filter by it; otherwise, include all
if (!authType || authMethod.type === authType) {
authMounts.push({
path,
type: authMethod.type,
description: authMethod.description,
accessor: authMethod.accessor
});
}
});
return authMounts;
} catch (error: unknown) {
const authTypeStr = authType || "all";
logger.error(error, `Unable to list HC Vault ${authTypeStr} auth mounts`);
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list ${authTypeStr} auth mounts: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: `Unable to list ${authTypeStr} auth mounts from HashiCorp Vault`
});
}
};
export const getHCVaultKubernetesAuthRoles = async (
namespace: string,
mountPath: string,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
): Promise<THCVaultKubernetesAuthRoleWithConfig[]> => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
// Remove trailing slash from mount path
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
try {
// 1. Get the Kubernetes auth configuration for this mount
const { data: configResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesAuthConfig }>(
connection,
gatewayService,
{
url: `${instanceUrl}/v1/auth/${cleanMountPath}/config`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
}
);
const kubernetesConfig = configResponse.data;
// 2. List all roles in this mount
const { data: roleListResponse } = await requestWithHCVaultGateway<{ data: { keys: string[] } }>(
connection,
gatewayService,
{
url: `${instanceUrl}/v1/auth/${cleanMountPath}/role`,
method: "LIST",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
}
);
const roleNames = roleListResponse.data.keys;
if (!roleNames || roleNames.length === 0) {
return [];
}
// 3. Fetch details for each role with concurrency control
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
const roleDetailsPromises = roleNames.map((roleName) =>
limiter(async () => {
const { data: roleResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesAuthRole }>(
connection,
gatewayService,
{
url: `${instanceUrl}/v1/auth/${cleanMountPath}/role/${roleName}`,
method: "GET",
headers: {
"X-Vault-Token": accessToken,
"X-Vault-Namespace": namespace
}
}
);
// 4. Merge the role with the config
return {
...roleResponse.data,
name: roleName,
config: kubernetesConfig,
mountPath: cleanMountPath
} as THCVaultKubernetesAuthRoleWithConfig;
})
);
const roles = await Promise.all(roleDetailsPromises);
return roles;
} catch (error: unknown) {
logger.error(error, "Unable to list HC Vault Kubernetes auth roles");
if (error instanceof AxiosError) {
const errorMessage =
(error.response?.data as { errors?: string[] })?.errors?.[0] || error.message || "Unknown error";
throw new BadRequestError({
message: `Failed to list Kubernetes auth roles: ${errorMessage}`
});
}
throw new BadRequestError({
message: "Unable to list Kubernetes auth roles from HashiCorp Vault"
});
}
};

View File

@@ -21,7 +21,8 @@ export const hcVaultConnectionService = (
try {
const mounts = await listHCVaultMounts(appConnection, gatewayService);
return mounts;
// Filter for KV version 2 mounts only and extract just the paths
return mounts.filter((mount) => mount.type === "kv" && mount.version === "2").map((mount) => mount.path);
} catch (error) {
logger.error(error, "Failed to establish connection with Hashicorp Vault");
return [];

View File

@@ -33,3 +33,65 @@ export type THCVaultMountResponse = {
};
};
};
export type THCVaultMount = {
path: string;
type: string;
version?: string | null;
};
export type THCVaultAuthMountResponse = {
data: {
[key: string]: {
type: string;
description: string;
accessor: string;
config: {
default_lease_ttl: number;
max_lease_ttl: number;
force_no_cache: boolean;
};
local: boolean;
seal_wrap: boolean;
external_entropy_access: boolean;
options: Record<string, string> | null;
};
};
};
export type THCVaultAuthMount = {
path: string;
type: string;
description: string;
accessor: string;
};
export type THCVaultKubernetesAuthConfig = {
kubernetes_host: string;
kubernetes_ca_cert?: string;
issuer?: string;
disable_iss_validation?: boolean;
disable_local_ca_jwt?: boolean;
};
export type THCVaultKubernetesAuthRole = {
name: string;
bound_service_account_names: string[];
bound_service_account_namespaces: string[];
token_ttl?: number;
token_max_ttl?: number;
token_policies?: string[];
token_bound_cidrs?: string[];
token_explicit_max_ttl?: number;
token_no_default_policy?: boolean;
token_num_uses?: number;
token_period?: number;
token_type?: string;
audience?: string;
alias_name_source?: string;
};
export type THCVaultKubernetesAuthRoleWithConfig = THCVaultKubernetesAuthRole & {
config: THCVaultKubernetesAuthConfig;
mountPath: string;
};

View File

@@ -1,9 +1,33 @@
import { OrgMembershipRole } from "@app/db/schemas";
import {
AuditLogInfo,
EventType,
SecretApprovalEvent,
TAuditLogServiceFactory
} from "@app/ee/services/audit-log/audit-log-types";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
import { DatabaseErrorCode } from "@app/lib/error-codes";
import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { AppConnection } from "../app-connection/app-connection-enums";
import { decryptAppConnectionCredentials } from "../app-connection/app-connection-fns";
import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service";
import {
getHCVaultAuthMounts,
getHCVaultKubernetesAuthRoles,
getHCVaultSecretsForPath,
HCVaultAuthType,
listHCVaultMounts,
listHCVaultPolicies,
listHCVaultSecretPaths,
THCVaultConnection
} from "../app-connection/hc-vault";
import { TKmsServiceFactory } from "../kms/kms-service";
import { TSecretServiceFactory } from "../secret/secret-service";
import { SecretProtectionType } from "../secret/secret-types";
import { TUserDALFactory } from "../user/user-dal";
import {
decryptEnvKeyDataFn,
@@ -15,16 +39,29 @@ import { TExternalMigrationQueueFactory } from "./external-migration-queue";
import {
ExternalMigrationProviders,
ExternalPlatforms,
TCreateVaultExternalMigrationDTO,
TDeleteVaultExternalMigrationDTO,
THasCustomVaultMigrationDTO,
TImportEnvKeyDataDTO,
TImportVaultDataDTO
TImportVaultDataDTO,
TUpdateVaultExternalMigrationDTO,
VaultImportStatus
} from "./external-migration-types";
import { TVaultExternalMigrationConfigDALFactory } from "./vault-external-migration-config-dal";
type TExternalMigrationServiceFactoryDep = {
permissionService: TPermissionServiceFactory;
secretService: TSecretServiceFactory;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
externalMigrationQueue: TExternalMigrationQueueFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
vaultExternalMigrationConfigDAL: Pick<
TVaultExternalMigrationConfigDALFactory,
"create" | "findOne" | "transaction" | "find" | "updateById" | "deleteById" | "findById"
>;
userDAL: Pick<TUserDALFactory, "findById">;
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
};
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
@@ -33,7 +70,12 @@ export const externalMigrationServiceFactory = ({
permissionService,
externalMigrationQueue,
userDAL,
gatewayService
gatewayService,
secretService,
auditLogService,
appConnectionService,
vaultExternalMigrationConfigDAL,
kmsService
}: TExternalMigrationServiceFactoryDep) => {
const importEnvKeyData = async ({
decryptionKey,
@@ -171,9 +213,554 @@ export const externalMigrationServiceFactory = ({
return actorOrgId in vaultMigrationTransformMappings;
};
const validateVaultExternalMigrationConnection = async ({
connection,
namespace
}: {
connection: THCVaultConnection;
namespace: string;
}) => {
// Allow root namespace access when no namespace is configured on the connection
const isRootAccess = namespace === "root" || namespace === "/";
const hasNoNamespace = connection.credentials.namespace === undefined;
if (hasNoNamespace && isRootAccess) {
// Skip validation for root access with no configured namespace
} else if (connection.credentials.namespace !== namespace) {
throw new BadRequestError({ message: "Namespace value does not match the namespace of the connection" });
}
try {
await listHCVaultPolicies(namespace, connection, gatewayService);
await getHCVaultAuthMounts(namespace, HCVaultAuthType.Kubernetes, connection, gatewayService);
const mounts = await listHCVaultMounts(connection, gatewayService);
const sampleKvMount = mounts.find((mount) => mount.type === "kv");
if (sampleKvMount) {
await listHCVaultSecretPaths(namespace, connection, gatewayService, sampleKvMount.path);
}
} catch (error) {
throw new BadRequestError({
message: `Failed to establish namespace confiugration. ${error instanceof Error ? error.message : "Unknown error"}`
});
}
};
const createVaultExternalMigration = async ({ namespace, connectionId, actor }: TCreateVaultExternalMigrationDTO) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can configure vault external migration" });
}
const connection = await appConnectionService.connectAppConnectionById<THCVaultConnection>(
AppConnection.HCVault,
connectionId,
actor
);
await validateVaultExternalMigrationConnection({
connection,
namespace
});
try {
const config = await vaultExternalMigrationConfigDAL.create({
namespace,
connectionId,
orgId: actor.orgId
});
return config;
} catch (error) {
if (
error instanceof DatabaseError &&
(error.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation
) {
throw new BadRequestError({
message: `Vault external migration already exists for this namespace`
});
}
throw error;
}
};
const updateVaultExternalMigration = async ({
id,
namespace,
connectionId,
actor
}: TUpdateVaultExternalMigrationDTO) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can update vault external migration" });
}
if (connectionId) {
const connection = await appConnectionService.connectAppConnectionById<THCVaultConnection>(
AppConnection.HCVault,
connectionId,
actor
);
await validateVaultExternalMigrationConnection({
connection,
namespace
});
}
const config = await vaultExternalMigrationConfigDAL.updateById(id, {
namespace,
connectionId
});
return config;
};
const getVaultExternalMigrationConfigs = async ({ actor }: { actor: OrgServiceActor }) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault external migration configs" });
}
const configs = await vaultExternalMigrationConfigDAL.find({
orgId: actor.orgId
});
return configs;
};
const getVaultNamespaces = async ({ actor }: { actor: OrgServiceActor }) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault namespaces" });
}
// Get all configured namespaces for this org
const vaultConfigs = await vaultExternalMigrationConfigDAL.find({
orgId: actor.orgId
});
// Return the configured namespaces as an array of objects with id and name
// where both id and name are the namespace path
const namespaces = vaultConfigs.map((config) => ({
id: config.namespace,
name: config.namespace
}));
return namespaces;
};
const getVaultPolicies = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault policies" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
const policies = await listHCVaultPolicies(namespace, connection, gatewayService);
return policies;
};
const getVaultMounts = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault mounts" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
const mounts = await listHCVaultMounts(connection, gatewayService, namespace);
return mounts;
};
const getVaultSecretPaths = async ({
actor,
namespace,
mountPath
}: {
actor: OrgServiceActor;
namespace: string;
mountPath: string;
}) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault secret paths" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
const secretPaths = await listHCVaultSecretPaths(namespace, connection, gatewayService, mountPath);
return secretPaths;
};
const importVaultSecrets = async ({
actor,
projectId,
environment,
secretPath,
vaultNamespace,
vaultSecretPath,
auditLogInfo
}: {
actor: OrgServiceActor;
projectId: string;
environment: string;
secretPath: string;
vaultNamespace: string;
vaultSecretPath: string;
auditLogInfo: AuditLogInfo;
}) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can import vault secrets" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace: vaultNamespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
const vaultSecrets = await getHCVaultSecretsForPath(vaultNamespace, vaultSecretPath, connection, gatewayService);
try {
const secretOperation = await secretService.createManySecretsRaw({
actorId: actor.id,
actor: actor.type,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
secretPath,
environment,
projectId,
secrets: Object.entries(vaultSecrets).map(([secretKey, secretValue]) => ({
secretKey,
secretValue
}))
});
if (secretOperation.type === SecretProtectionType.Approval) {
await auditLogService.createAuditLog({
projectId,
...auditLogInfo,
event: {
type: EventType.SECRET_APPROVAL_REQUEST,
metadata: {
committedBy: secretOperation.approval.committerUserId,
secretApprovalRequestId: secretOperation.approval.id,
secretApprovalRequestSlug: secretOperation.approval.slug,
secretPath,
environment,
secrets: Object.entries(vaultSecrets).map(([secretKey]) => ({
secretKey
})),
eventType: SecretApprovalEvent.CreateMany
}
}
});
return { status: VaultImportStatus.ApprovalRequired };
}
return { status: VaultImportStatus.Imported };
} catch (error) {
throw new BadRequestError({
message: `Failed to import Vault secrets. ${error instanceof Error ? error.message : "Unknown error"}`
});
}
};
const deleteVaultExternalMigration = async ({ id, actor }: TDeleteVaultExternalMigrationDTO) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can delete vault external migration configs" });
}
const config = await vaultExternalMigrationConfigDAL.findById(id);
if (!config) {
throw new NotFoundError({ message: "Vault migration config not found" });
}
if (config.orgId !== actor.orgId) {
throw new ForbiddenRequestError({ message: "Config does not belong to this organization" });
}
const deletedConfig = await vaultExternalMigrationConfigDAL.deleteById(id);
return deletedConfig;
};
const getVaultAuthMounts = async ({
actor,
namespace,
authType
}: {
actor: OrgServiceActor;
namespace: string;
authType?: string;
}) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault auth mounts" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
const authMounts = await getHCVaultAuthMounts(namespace, authType as HCVaultAuthType, connection, gatewayService);
return authMounts;
};
const getVaultKubernetesAuthRoles = async ({
actor,
namespace,
mountPath
}: {
actor: OrgServiceActor;
namespace: string;
mountPath: string;
}) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
actor.orgId,
actor.authMethod,
actor.orgId
);
if (!hasRole(OrgMembershipRole.Admin)) {
throw new ForbiddenRequestError({ message: "Only admins can view vault Kubernetes auth roles" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
// Get roles for the specified mount path only
const roles = await getHCVaultKubernetesAuthRoles(namespace, mountPath, connection, gatewayService);
return roles;
};
return {
importEnvKeyData,
importVaultData,
hasCustomVaultMigration
hasCustomVaultMigration,
createVaultExternalMigration,
getVaultExternalMigrationConfigs,
updateVaultExternalMigration,
deleteVaultExternalMigration,
getVaultNamespaces,
getVaultPolicies,
getVaultMounts,
getVaultAuthMounts,
getVaultSecretPaths,
importVaultSecrets,
getVaultKubernetesAuthRoles
};
};

View File

@@ -1,4 +1,4 @@
import { TOrgPermission } from "@app/lib/types";
import { OrgServiceActor, TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "../auth/auth-type";
@@ -121,3 +121,26 @@ export enum ExternalMigrationProviders {
Vault = "vault",
EnvKey = "env-key"
}
export enum VaultImportStatus {
Imported = "imported",
ApprovalRequired = "approval-required"
}
export type TCreateVaultExternalMigrationDTO = {
namespace: string;
connectionId: string;
actor: OrgServiceActor;
};
export type TUpdateVaultExternalMigrationDTO = {
id: string;
namespace: string;
connectionId: string | null;
actor: OrgServiceActor;
};
export type TDeleteVaultExternalMigrationDTO = {
id: string;
actor: OrgServiceActor;
};

View File

@@ -0,0 +1,67 @@
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "@app/lib/errors";
import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex";
export type TVaultExternalMigrationConfigDALFactory = ReturnType<typeof vaultExternalMigrationConfigDALFactory>;
export const vaultExternalMigrationConfigDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.VaultExternalMigrationConfig);
const findOne = async (filter: { orgId: string; namespace: string }, tx?: Knex) => {
try {
const result = await (tx || db?.replicaNode?.() || db)(TableName.VaultExternalMigrationConfig)
.leftJoin(
TableName.AppConnection,
`${TableName.AppConnection}.id`,
`${TableName.VaultExternalMigrationConfig}.connectionId`
)
/* eslint-disable @typescript-eslint/no-misused-promises */
.where(buildFindFilter(prependTableNameToFindFilter(TableName.VaultExternalMigrationConfig, filter)))
.select(selectAllTableCols(TableName.VaultExternalMigrationConfig))
.select(
db.ref("id").withSchema(TableName.AppConnection).as("appConnectionId"),
db.ref("name").withSchema(TableName.AppConnection).as("appConnectionName"),
db.ref("app").withSchema(TableName.AppConnection).as("appConnectionApp"),
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("appConnectionEncryptedCredentials"),
db.ref("orgId").withSchema(TableName.AppConnection).as("appConnectionOrgId"),
db.ref("method").withSchema(TableName.AppConnection).as("appConnectionMethod"),
db.ref("description").withSchema(TableName.AppConnection).as("appConnectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("appConnectionVersion"),
db.ref("gatewayId").withSchema(TableName.AppConnection).as("appConnectionGatewayId"),
db.ref("projectId").withSchema(TableName.AppConnection).as("appConnectionProjectId"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("appConnectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("appConnectionUpdatedAt")
)
.first();
if (!result) return undefined;
return {
...result,
connection: result.appConnectionId
? {
id: result.appConnectionId,
name: result.appConnectionName,
app: result.appConnectionApp,
encryptedCredentials: result.appConnectionEncryptedCredentials,
orgId: result.appConnectionOrgId,
method: result.appConnectionMethod,
description: result.appConnectionDescription,
version: result.appConnectionVersion,
gatewayId: result.appConnectionGatewayId,
projectId: result.appConnectionProjectId,
createdAt: result.appConnectionCreatedAt,
updatedAt: result.appConnectionUpdatedAt
}
: undefined
};
} catch (error) {
throw new DatabaseError({ error, name: "Find one" });
}
};
return { ...orm, findOne };
};

View File

@@ -99,5 +99,5 @@ export type TImportKeyMaterialDTO = {
projectId: string;
orgId: string;
keyUsage: KmsKeyUsage;
kmipMetadata?: Record<string, unknown>;
kmipMetadata?: Record<string, unknown> | null;
};

View File

@@ -5,6 +5,7 @@ import path from "path";
import { v4 as uuidv4, validate as uuidValidate } from "uuid";
import { ActionProjectType, TProjectEnvironments, TSecretFolders, TSecretFoldersInsert } from "@app/db/schemas";
import { TDynamicSecretDALFactory } from "@app/ee/services/dynamic-secret/dynamic-secret-dal";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { TSecretApprovalPolicyServiceFactory } from "@app/ee/services/secret-approval-policy/secret-approval-policy-service";
@@ -12,6 +13,7 @@ import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/
import { PgSqlLock } from "@app/keystore/keystore";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { OrderByDirection, OrgServiceActor } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { buildFolderPath } from "@app/services/secret-folder/secret-folder-fns";
import {
@@ -47,7 +49,11 @@ type TSecretFolderServiceFactoryDep = {
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
secretApprovalPolicyService: Pick<TSecretApprovalPolicyServiceFactory, "getSecretApprovalPolicy">;
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "findByFolderIds" | "invalidateSecretCacheByProjectId">;
secretV2BridgeDAL: Pick<
TSecretV2BridgeDALFactory,
"findByFolderIds" | "invalidateSecretCacheByProjectId" | "findOne"
>;
dynamicSecretDAL: Pick<TDynamicSecretDALFactory, "findOne">;
};
export type TSecretFolderServiceFactory = ReturnType<typeof secretFolderServiceFactory>;
@@ -61,7 +67,8 @@ export const secretFolderServiceFactory = ({
folderCommitService,
projectDAL,
secretApprovalPolicyService,
secretV2BridgeDAL
secretV2BridgeDAL,
dynamicSecretDAL
}: TSecretFolderServiceFactoryDep) => {
const createFolder = async ({
projectId,
@@ -111,24 +118,11 @@ export const secretFolderServiceFactory = ({
});
}
// check if the exact folder already exists
const existingFolder = await folderDAL.findOne(
{
envId: env.id,
parentId: parentFolder.id,
name,
isReserved: false
},
tx
);
if (existingFolder) {
return existingFolder;
}
// exact folder case
if (parentFolder.path === pathWithFolder) {
return parentFolder;
throw new BadRequestError({
message: `Folder with name '${name}' already exists in path '${secretPath}'`
});
}
let currentParentId = parentFolder.id;
@@ -534,13 +528,19 @@ export const secretFolderServiceFactory = ({
projectId,
env,
parentId,
idOrName
idOrName,
actor
}: {
projectId: string;
env: TProjectEnvironments;
parentId: string;
idOrName: string;
actor: ActorType;
}) => {
if (actor === ActorType.IDENTITY) {
return;
}
let targetFolder = await folderDAL
.findOne({
envId: env.id,
@@ -638,7 +638,8 @@ export const secretFolderServiceFactory = ({
actorAuthMethod,
environment,
path: secretPath,
idOrName
idOrName,
forceDelete = false
}: TDeleteFolderDTO) => {
const { permission } = await permissionService.getProjectPermission({
actor,
@@ -664,7 +665,7 @@ export const secretFolderServiceFactory = ({
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`
});
await $checkFolderPolicy({ projectId, env, parentId: parentFolder.id, idOrName });
await $checkFolderPolicy({ projectId, env, parentId: parentFolder.id, idOrName, actor });
let folderToDelete = await folderDAL
.findOne({
@@ -690,6 +691,22 @@ export const secretFolderServiceFactory = ({
throw new NotFoundError({ message: `Folder with ID '${idOrName}' not found` });
}
// Check if folder contains resources (secrets, dynamic secrets, subfolders)
if (!forceDelete) {
const error = new BadRequestError({
message: `Cannot delete folder "${folderToDelete.name}" because it contains resources. Use forceDelete=true to delete it forcefully.`,
name: "deleteFolder"
});
const secretV2 = await secretV2BridgeDAL.findOne({ folderId: folderToDelete.id }).catch(() => null);
if (secretV2) throw error;
const dynamicSecret = await dynamicSecretDAL.findOne({ folderId: folderToDelete.id }).catch(() => null);
if (dynamicSecret) throw error;
const subfolder = await folderDAL.findOne({ parentId: folderToDelete.id }).catch(() => null);
if (subfolder) throw error;
}
const [doc] = await folderDAL.delete(
{
envId: env.id,
@@ -1315,7 +1332,7 @@ export const secretFolderServiceFactory = ({
});
}
await $checkFolderPolicy({ projectId, env, parentId: parentFolder.id, idOrName });
await $checkFolderPolicy({ projectId, env, parentId: parentFolder.id, idOrName, actor });
let folderToDelete = await folderDAL
.findOne({

View File

@@ -37,6 +37,7 @@ export type TDeleteFolderDTO = {
environment: string;
path: string;
idOrName: string;
forceDelete?: boolean;
} & TProjectPermission;
export type TGetFolderDTO = {

View File

@@ -421,6 +421,7 @@
"documentation/guides/node",
"documentation/guides/python",
"documentation/guides/nextjs-vercel",
"documentation/guides/kubernetes-operator",
"documentation/guides/microsoft-power-apps"
]
}

View File

@@ -0,0 +1,239 @@
---
title: "Managing Secrets With Kubernetes Operator"
sidebarTitle: "Kubernetes Operator"
description: "How to use the Infisical Kubernetes Operator to Push Secrets, Pull Secrets, and Generate Dynamic Secrets within your clusters."
---
Infisical's Kubernetes Operator provides a seamless, secure, and automated way to synchronize secrets between your Infisical instance and your Kubernetes clusters. The Operator's three Custom Resource Definitions (CRDs) make this possible. In this guide, we provide the necessary CRDs and configurations for your kubernetes cluster, but you can customize them to fit your use-case.
In this guide, we'll walk through how to:
1. **Install the Infisical Operator on your Kubernetes cluster**.
2. **Configure Authentication using Kubernetes Service Accounts**.
3. **Use Each of the three CRDs**.
- **InfisicalSecret** [Sync secrets from Infisical to Kubernetes]
- **InfisicalPushSecret** [Sync secrets from Kubernetes to Infisical]
- **InfisicalDynamicSecret** [Manage Dynamic Secrets and automatically create time-bound leases]
## Prerequisites
Before we begin, make sure your environment is ready
1. Installed tools
- [helm](https://helm.sh/docs/intro/install/), [git](https://git-scm.com/downloads), [kubectl](https://kubernetes.io/docs/tasks/tools/)
2. Kubernetes Cluster
- Ensure you have access to a running cluster and connect with kubectl
3. PostgreSQL Cluster (for InfisicalDynamicSecret)
- Ensure you have a running database that you have access to
4. Clone [infisical-guides-source-code](https://github.com/Infisical/infisical-guides-source-code) repository
5. Access to an Infisical instance (cloud or self-hosted)
## Step-By-Step Guide
<Steps titleSize="h2">
<Step title="Install the Infisical Operator">
The [Infisical Operator](https://infisical.com/docs/integrations/platforms/kubernetes/overview) runs inside your cluster and is responsible for handling secret synchronization events.
```console
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
helm repo update
helm install infisical-operator infisical-helm-charts/secrets-operator
```
Verify the operator pod is running:
```console
kubectl get pods -n default
```
</Step>
<Step title="Create a Machine Identity and Set Up a Project">
The operator uses a [Machine Identity](https://infisical.com/docs/documentation/platform/identities/machine-identities) to authenticate with Infisical through the Kubernetes Auth Method
1. Login to [Infisical](https://app.infisical.com/)
2. Select **Organization Access** from the left navigational pane
3. Create an Identity and give it a name and a role
4. Once the Machine Identity is created, copy the **Identity ID** (will be used later)
5. Select the created Machine Identity, and add a [Kubernetes Authentication Method](https://infisical.com/docs/documentation/platform/identities/kubernetes-auth). Use these configurations:
- **Allowed Service Account Names**: infisical-service-account, default
- **Allowed Namespaces**: default
- **Kubernetes Host URL** can be found by running ```kubectl cluster-info```
- **Token Reviewer JWT / CA Certificate**: We will be generating these two later and adding them in later, leave it blank for now
6. Once the Machine Identity has been created, navigate back to **Overview**
7. Now select **Add New Project**
- Add a **Project Name**, and select **Secrets Management** as the product type
- Add a description (Optional).
8. Once the Project is created, navigate into the Project to **Add Secrets**. You can add any key-value pair for this example, however if you want to use the InfisicalSecret CRD example provided in this demo, use the following configurations:
- **Key**: SMTP_HOST
- **Value**: smtp@gmail.com
- **Tags**: N/A (Not needed for this demonstration)
- **Environments**: Production
9. Now lets navigate to the **Project Access** tab on the left hand navigation pane.
- Add the machine identity we created, and give it Admin permissions (just for demonstration purposes)
</Step>
<Step title="Set Up RBAC, Service Accounts, and Create Tokens">
Now we will be interacting with the local repository you cloned earlier. Make sure you are in the directory that contains the yaml configurations. Assuming you are in your root user directory:
```console
cd infisical-guides-source-code/kubernetes-operator-demo
```
1. Create the ```infisical-token-reviewer``` service account. This Manifest creates a **service account** that the Infisical Operator uses to authenticate with Kubernetes for token reviews. It allows Infisical to validate Kubernetes tokens securely during the Machine Identity authentication process.
```console
kubectl apply -f infisical-reviewer-service-account.yaml
```
2. Create the token for the reviewer service account. This Yaml defines a **service account token secret** linked to the reviewer account created above. It generates a JWT token that Infisical uses for the Kubernetes Auth Method in your Machine Identity configuration.
```console
kubectl apply -f service-account-reviewer-token.yaml
```
3. This file binds the ```infisical-token-reviewer``` service account to the built-in ```system:auth-delegator``` ClusterRole. That role allows the service account to perform **token review** and **authentication delegation** requests on behalf of other service accounts - a key part of Kubernetes-based identity verification. Without this binding, the Infisical Operator wouldn't have permission to validate tokens.
```console
kubectl apply -f cluster-role-binding.yaml
```
4. Create the service account that will be used by the InfisicalSecret. This file creates a **dedicated service account** ```infisical-service-account``` that the Infisical Operator uses to access and sync secrets within your cluster. It operates as the Operator's working identity in your cluster, separate from the token reviewer.
```console
kubectl apply -f infisical-service-account.yaml
```
5. Create the token for the Infisical service account. This manifest defines a **token secret** for the ```infisical-service-account```. It allows the Infisical operator to authenticate against Infisical's API when syncing secrets. The token will then be manually patched and associated with the service account to make sure Kubernetes mains it persistently.
```console
kubectl apply -f infisical-service-account-token.yaml
```
6. Apply the patch to manually associate the token secret
```console
kubectl patch serviceaccount infisical-service-account -p '{"secrets": [{"name": "infisical-service-account-token"}]}' -n default
```
7. Create the **JWT Token** and **Certificate** and add it to the **Machine Identity** we created under **Kubernetes Auth**. For the generated CA, navigate to the **Advanced** tab to paste the certificate:
- JWT Command
```console
kubectl get secret infisical-token-reviewer-token -n default -o jsonpath='{.data.token}' | base64 -d
```
- CA Command
```console
kubectl get secret infisical-token-reviewer-token -n default -o jsonpath='{.data.ca\.crt}' | base64 -d
```
</Step>
<Step title="Verify Service Accounts and Tokens">
1. Check to see if the service accounts were created
```console
kubectl get serviceaccount -n default | grep infisical
```
2. Verify the tokens were created and linked
```console
kubectl get secrets -n default | grep infisical
```
</Step>
<Step title="Create the InfisicalSecret CRD">
The [InfisicalSecret](https://infisical.com/docs/integrations/platforms/kubernetes/infisical-secret-crd) CRD tells the operator to sync secrets from Infisical to Kubernetes. By referencing your ```identityID```, ```projectSlug```, and ```envSlug```, this CRD tells the Infisical Operator which Infisical secrets to fetch and how to format them into a Kubernetes Secret. Make sure to edit the provided CRD to match your specific Machine Identity ID, Project ID, and which environment your secrets are being pulled from (default is prod).
- **Project Slug**: Can be found when you select your project and navigate to settings
- **Identity ID**: Can be found when you select your machine identity from your organization's access control
1. After editing the ```example-infisical-secret-crd.yaml``` to contain your demo-specific values, apply the yaml in your cluster
```console
kubectl apply -f example-infisical-secret-crd.yaml
```
</Step>
<Step title="Verify the InfisicalSecret Status">
1. Check that the ```InfisicalSecret``` was created successfully
```console
kubectl get infisicalsecret -n default
```
2. Check that the operator created the ```managed-secret```
```console
kubectl get secret managed-secret -n default
```
3. View the secret contents (base64 encoded)
```console
kubectl get secret managed-secret -n default -o jsonpath='{.data}' | jq
```
</Step>
<Step title="Deploy the Demo Application">
1. Deploy the nginx demo deployment that will use the managed secret.
```console
kubectl apply -f demo-deployment.yaml
```
2. Wait 15-20 seconds and then verify the deployments
```console
kubectl get deployments
kubectl get pods -l app=nginx
```
</Step>
<Step title="Verify the Secret is Injected Into the Pod">
1. Check that the environment variable is in the running pod. If everything was successful, at this point you should be able to see the secret populate in the kubernetes pod and have a successful **sync** from Infisical to Kubernetes.
```console
kubectl exec -it $(kubectl get pod -l app=nginx -o jsonpath='{.items[0].metadata.name}') -- env | grep SMTP
```
</Step>
<Step title="Create a Kubernetes Secret to Push Up to Infisical">
Now that we have successfully synced secrets from Infisical to Kubernetes, lets explore how we can push **Kubernetes Secrets** to Infisical.
1. Either create a **Kubernetes Secret** via yaml, or use the one in the repository.
```console
kubectl apply -f source-secret.yaml
```
2. Verify creation of the secret
```console
kubectl get secret push-secret-demo -n default -o yaml
```
</Step>
<Step title="Create the InfisicalPushSecret CRD">
The [InfisicalPushSecret](https://infisical.com/docs/integrations/platforms/kubernetes/infisical-push-secret-crd) CRD tells the operator to sync secrets from Kubernetes to Infisical. Make sure you edit the CRD to include the specific **Project Slug**, and **Identity ID**. The other values present in ```example-push-secret.yaml``` should be configured based on the previously committed yaml configurations.
1. Apply the InfisicalPushSecret CRD provided after making the necessary changes
```console
kubectl apply -f example-push-secret-crd.yaml
```
2. Once your CRD has been configured, go back to your project within Infisical and check to see if your secrets have populated there.
![secrets dashboard](../../images/push-secret.png)
</Step>
<Step title="Create the InfisicalDynamicSecret CRD">
The [InfisicalDynamicSecret](https://infisical.com/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd) CRD allows you to sync dynamic secrets and create leases automatically in Kubernetes as native **Kubernetes Secret** resources Any Pod, Deployment, or other Kubernetes resource can make use of dynamic secrets from Infisical just like any other Kubernetes secret.
1. Navigate to your Infisical **Project** and click on the dropdown next to **Add Secret**. From here you will select **Add Dynamic Secret**
- Select **SQL Database** as the service you would like to connect to.
- Select **PostegreSQL** as the database service. Enter in the connection details for your database, specifically the **Host**, **Port**, **User**, **Password**, and **Database Name**.
- For the **Secret Name**, if you want to use the same name as the one in the cloned **InfisicalDynamicSecret** CRD, use the name **dynamic-secret-lease**. Otherwise you will need to change the **dynamicSecret.secretName** config in the InfisicalDynamicSecret CRD to whatever you name the secret here.
- In the CA (SSL) section, make sure to upload the **CA Certificate** for your database.
- Finally, select **Prod** as the environment (we are keeping this configuration as part of the demonstration).
![secrets dashboard dynamic](../../images/dynamic-secret.png)
2. Edit the ```dynamic-secret-crd``` with the proper machine **Identity ID**, **Project Slug**, **dynamicSecret.secretName** (same as the **Secret Name** you gave to the dynamic secret in Infisical), and managedSecretReference.secretName (name of the kubernetes secret that Infisical Operator will create/populate in the cluster).
- If you want to keep the **managedSecretReference.secretName** then you can leave it as **dynamic-secret-test**
3. Once the changes have been saved, apply the yaml:
```console
kubectl apply -f dynamic-secret-crd.yaml
```
4. After applying the CRD, you should notice that the dynamic secret lease has been created and synced with your cluster. Verify by running:
```console
kubectl get secret dynamic-secret-test -n default -o yaml
```
5. Once the dynamic secret lease has been created, you should see that the secret has data that contains the lease credentials.
![dynamic secrets output](../../images/dynamic-secret-crd.png)
</Step>
</Steps>
**Congratulations! You successfully managed secrets with Kubernetes.**

View File

@@ -1,16 +1,22 @@
---
title: "External Migrations"
sidebarTitle: "Overview"
description: "Learn how to migrate secrets from third-party secrets management platforms to Infisical."
description: "Learn how to migrate resources from third-party secrets management platforms to Infisical."
---
## Overview
Infisical supports migrating secrets from third-party secrets management platforms to Infisical. This is useful if you're looking to easily switch to Infisical and wish to move over your existing secrets from a different platform.
Infisical supports migrating resources from third-party secrets management platforms to Infisical. This is useful if you're looking to easily switch to Infisical and wish to move over your existing resources from a different platform.
Infisical offers two types of migration approaches:
- **In-Platform Migration Tooling**: Configure platform connections to enable granular, on-demand imports of secrets, policies, and configurations directly within the Infisical UI. This allows you to migrate resources incrementally as needed.
- **Bulk Data Import**: Perform one-time organization-level migrations to import all resources from external platforms at once. This is ideal for initial migrations when moving entirely to Infisical.
## Supported Platforms
- [EnvKey](./envkey)
- [Vault](./vault)
We're always looking to add more migration paths for other providers. If we're missing a platform, please open an issue on our [GitHub repository](https://github.com/infisical/infisical/issues).
We're always looking to add more migration paths for other providers. If we're missing a platform, please open an issue on our [GitHub repository](https://github.com/infisical/infisical/issues).

View File

@@ -1,40 +1,239 @@
---
title: "Migrating from Vault to Infisical"
sidebarTitle: "Vault"
description: "Learn how to migrate secrets from Vault to Infisical."
description: "Learn how to migrate resources from Vault to Infisical."
---
## Migrating from Vault
Infisical provides two approaches for migrating from HashiCorp Vault.
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.
### Which approach should I use?
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).
**Choose In-Platform Migration Tooling if you want to:**
- Migrate specific secrets, not everything at once
- Import secrets into existing Infisical projects
- Translate Vault policies to Infisical access controls
- Import Kubernetes authentication configurations
- Have more control over the migration process
### Prerequisites
**Choose Bulk Data Import if you want to:**
- A Vault instance with the KV secret engine enabled.
- An access token to your Vault instance.
### Project Mapping
When migrating from Vault, you'll need to choose how you want to map your Vault resources to Infisical projects.
There are two options for project mapping:
- `Namespace`: This will map your selected Vault namespace to a single Infisical project. When you select this option, each KV secret engine within the namespace will be mapped to a single Infisical project. Each KV secret engine will be mapped to a Infisical environment within the project. This means if you have 3 KV secret engines, you'll have 3 environments inside the same project, where the name of the environments correspond to the name of the KV secret engines.
- `Key Vault`: This will map all the KV secret engines within your Vault instance to a Infisical project. Each KV engine will be created as a Infisical project. This means if you have 3 KV secret engines, you'll have 3 Infisical projects. For each of the created projects, a single default environment will be created called `Production`, which will contain all your secrets from the corresponding KV secret engine.
- Migrate all secrets from Vault in one go
- Automatically create new Infisical projects from your Vault structure
- Perform a one-time migration when moving entirely from Vault to Infisical
## In-Platform Migration Tooling
This migration approach lets you set up a connection to your Vault instance once, then import specific resources as needed throughout Infisical.
### Step 1: Set Up Your Vault Connection
<Steps>
<Step title="Create a Vault policy">
In order to migrate from Vault, you'll need to create a Vault policy that allows Infisical to read the secrets and metadata from the KV v2 secrets engines within your Vault instance.
In your Vault instance, create a policy that allows Infisical to read your secrets, policies, and authentication configurations. This policy grants read-only access and doesn't allow Infisical to modify anything in Vault.
<Accordion title="View the complete policy">
```hcl
# System endpoints - for listing namespaces, policies, mounts, and auth methods
path "sys/namespaces" {
capabilities = ["list"]
}
```python
path "sys/policy" {
capabilities = ["read", "list"]
}
path "sys/policy/*" {
capabilities = ["read"]
}
path "sys/mounts" {
capabilities = ["read"]
}
path "sys/auth" {
capabilities = ["read"]
}
# KV v2 secrets - for listing and reading secrets
# Replace '+' with your actual KV v2 mount paths (e.g., "secret", "kv")
path "+/metadata/*" {
capabilities = ["list", "read"]
}
path "+/data/*" {
capabilities = ["read"]
}
# KV v1 secrets - for listing and reading secrets
# Replace '+' with your actual KV v1 mount paths (e.g., "secret", "kv-v1")
# WARNING: This is broad - ideally specify exact mount names
path "+/*" {
capabilities = ["list", "read"]
}
# Kubernetes auth - for reading auth configuration and roles
path "auth/+/config" {
capabilities = ["read"]
}
path "auth/+/role" {
capabilities = ["list"]
}
path "auth/+/role/*" {
capabilities = ["read"]
}
```
</Accordion>
Save this policy in Vault with the name `infisical-in-platform-migration`.
</Step>
<Step title="Create an App Connection in Infisical">
In Infisical, navigate to **Organization Settings > App Connections** and create a new HashiCorp Vault connection.
Follow the [HashiCorp Vault App Connection documentation](/integrations/app-connections/hashicorp-vault) for detailed setup instructions. When configuring authentication (Token or AppRole), make sure it uses the `infisical-in-platform-migration` policy you created.
</Step>
<Step title="Add Vault Namespaces for Migration">
Navigate to **Organization Settings > External Migrations** in Infisical.
Under the "In-Platform Migration Tooling" section for HashiCorp Vault, click **"+ Add Namespace"**.
![In-Platform Migration Tooling](/images/platform/external-migrations/vault-in-platform/external-migration-overview.png)
Configure your namespace:
![Namespace Configuration](/images/platform/external-migrations/vault-in-platform/namespace-configuration-modal.png)
- **Namespace**: Enter your Vault namespace path (e.g., `admin/namespace1`). If you intend to use the root namespace, set the namespace value to "root".
- **Connection**: Select the App Connection you created in the previous step.
<Note>
You can add multiple namespaces with different connections if you have multiple Vault instances or namespaces to migrate from.
</Note>
</Step>
</Steps>
### Step 2: Import Your Resources
Once your Vault connection is configured, you'll see import options throughout Infisical wherever relevant. Here's what you can import:
#### Import Secrets into a Project
You can import secrets from Vault directly into a specific environment and secret path:
1. Navigate to your project and select a specific environment (e.g., Development, Production)
2. In the secrets view, click the dropdown icon (caret) next to the **"+ Add Secret"** button
3. Select **"Add from HashiCorp Vault"**
![Import Vault Secrets](/images/platform/external-migrations/vault-in-platform/import-vault-secrets-modal.png)
4. Choose your Vault namespace and the secret path you want to import
5. Click **"Import Secrets"**
The secrets will be imported into your current environment and folder path.
#### Import Kubernetes Authentication Configurations
When setting up Kubernetes authentication for a machine identity, you can import the configuration from Vault:
1. Navigate to **Access Control > Machine Identities** and select an identity
2. Click **"Add Authentication Method"** and choose **Kubernetes Auth**
3. In the configuration modal, click **"Load from Vault"**
![Load Kubernetes Auth from Vault](/images/platform/external-migrations/vault-in-platform/import-vault-kubernetes-auth-modal.png)
4. Select your Vault namespace and the Kubernetes role
5. Click **"Load"**
![Kubernetes Auth Form Populated](/images/platform/external-migrations/vault-in-platform/import-vault-kubernetes-auth-modal-form.png)
The authentication settings (service accounts, TTL, policies, etc.) will be automatically populated from your Vault configuration.
<Note>
Sensitive values like service account JWTs cannot be retrieved from Vault and
must be manually provided in the form after importing the configuration.
</Note>
#### Import and Translate Access Control Policies
When configuring project role-based access control, you can import Vault HCL policies and automatically translate them to Infisical permissions.
<Note>
Policy translation is best-effort and provides a starting point based on your
Vault configuration. The translated permissions should be reviewed and
adjusted as needed since Vault and Infisical have different access control
models. Infisical will analyze path patterns and capabilities to suggest
equivalent permissions.
</Note>
**To import and translate a policy:**
1. Navigate to your project, then go to **Access Control > Roles** and create or edit a role
2. In the policy configuration, click **"Add from HashiCorp Vault"**
![Import Vault Policy Button](/images/platform/external-migrations/vault-in-platform/translate-vault-policy-toggle.png)
3. Select your Vault namespace
4. Either choose an existing policy from the dropdown or paste your own HCL policy
![Translate Vault Policy Modal](/images/platform/external-migrations/vault-in-platform/translate-vault-policy-modal.png)
5. Review the automatically translated Infisical permissions
6. Make any adjustments and save
<Tip>
**How policy translation works:**
- Vault path patterns are analyzed to identify KV secret engines and environments
- Vault capabilities (`read`,`list`, `create`, etc.) are mapped to Infisical permissions
- Wildcards in paths are converted to glob patterns
- Secret paths are preserved for granular access control
Always review the translated permissions carefully, as Vault's capability-based model may not map 1:1 with Infisical's permission structure.
</Tip>
---
## Bulk Data Import
This migration approach imports all secrets from your Vault instance in one operation and automatically creates new Infisical projects based on your Vault structure.
### Understanding Project Mapping
Before starting the bulk import, you need to decide how your Vault structure will map to Infisical projects:
<Accordion title="Namespace Mapping (One Project Per Namespace)">
Each Vault namespace becomes a single Infisical project, with each KV secret engine becoming an environment within that project.
**Example:** If you have a namespace with 3 KV secret engines (`dev-secrets`, `staging-secrets`, `prod-secrets`):
- Creates: 1 Infisical project
- Environments: 3 (`dev-secrets`, `staging-secrets`, `prod-secrets`)
</Accordion>
<Accordion title="Key Vault Mapping (One Project Per KV Engine)">
Each KV secret engine becomes its own Infisical project with a single `Production` environment.
**Example:** If you have 3 KV secret engines (`dev-secrets`, `staging-secrets`, `prod-secrets`):
- Creates: 3 Infisical projects (`dev-secrets`, `staging-secrets`, `prod-secrets`)
- Each project has: 1 environment (`Production`)
</Accordion>
### How to Perform a Bulk Import
<Steps>
<Step title="Create a Vault policy for bulk import">
In your Vault instance, create a policy that allows Infisical to read all secrets and metadata. This policy grants read-only access.
<Accordion title="View the bulk import policy">
```hcl
# Allow listing secret engines/mounts
path "sys/mounts" {
capabilities = ["read", "list"]
@@ -63,65 +262,54 @@ There are two options for project mapping:
capabilities = ["read", "list"]
}
```
</Accordion>
Save this policy with the name `infisical-migration`.
Save this policy in Vault with the name `infisical-bulk-migration`.
</Step>
<Step title="Generate an access token">
You can use the Vault CLI to easily generate an access token for the new `infisical-migration` policy that you created in the previous step.
Use the Vault CLI to generate an access token:
```bash
vault token create --policy="infisical-migration"
vault token create --policy="infisical-bulk-migration"
```
After generating the token, you should see the following output:
Copy the `token` value from the output - you'll need it in the next step.
```t
$ vault token create --policy="infisical-migration"
Key Value
--- -----
token <your-access-token>
token_accessor p6kJDiBSzYYdabJUIpGCsCBm
token_duration 768h
token_renewable true
token_policies ["default" "infisical-migration"]
identity_policies []
policies ["default" "infisical-migration"]
```
Copy the `token` field and save it for later, as you'll need this when configuring the migration to Infisical.
</Step>
<Step title="Navigate to Infisical external migrations">
Open the Infisical dashboard and go to Organization Settings > External Migrations.
<Step title="Start the import in Infisical">
In Infisical, navigate to **Organization Settings > External Migrations**.
![Infisical Organization settings](/images/platform/external-migrations/infisical-external-migration-dashboard.png)
Under the "Bulk Data Import" section, click **"+ Import"**.
</Step>
<Step title="Select the Vault platform">
Select the Vault platform and click on Next.
<Step title="Select Vault as the source">
Select **HashiCorp Vault** as the migration source and click **Next**.
![Select Vault platform](/images/platform/external-migrations/infisical-import-vault-modal.png)
</Step>
<Step title="Configure the Vault migration">
Enter the Vault access token that you generated in the previous step and click Import data.
<Step title="Configure and start the migration">
Fill in your Vault connection details:
![Configure Vault migration](/images/platform/external-migrations/infisical-import-vault.png)
- `Vault URL`: The URL of your Vault instance.
- `Vault Namespace`: The namespace of your Vault instance. This is optional, and can be left blank if you're not using namespaces for your Vault instance.
- `Vault Access Token`: The access token that you generated in the previous step.
- **Vault URL**: Your Vault instance URL (e.g., `https://vault.example.com`)
- **Vault Namespace**: Optional - only needed if using Vault Enterprise namespaces
- **Vault Access Token**: The token you generated in step 2
- **Project Mapping**: Choose how to structure your Infisical projects (see [Understanding Project Mapping](#understanding-project-mapping))
- `Project Mapping`: Choose how you want to map your Vault resources to Infisical projects. You can review the mapping options in the [Project Mapping](#project-mapping) section.
Click **"Import Data"** to start the migration.
Click on Import data to start the migration.
<Note>
The import runs in the background and may take several minutes. You'll receive an email when it completes.
</Note>
</Step>
</Steps>
<Note>
It may take several minutes to complete the migration. You will receive an email when the migration is complete, or if there were any errors during the migration process.
</Note>

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

BIN
docs/images/push-secret.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

View File

@@ -63,14 +63,14 @@ export const WishForm = () => {
open={isOpen}
>
<PopoverTrigger asChild>
<div className="text-md mb-3 w-full pl-5 duration-200 hover:text-mineshaft-200">
<div className="mb-3 w-full cursor-pointer pl-5 text-sm whitespace-nowrap text-mineshaft-400 duration-200 hover:text-mineshaft-200">
<FontAwesomeIcon icon={faRocketchat} className="mr-2" />
Request a feature
</div>
</PopoverTrigger>
<PopoverContent
hideCloseBtn
align="start"
align="end"
alignOffset={20}
className="mb-1 w-auto border border-mineshaft-600 bg-mineshaft-900 p-4 drop-shadow-2xl"
sticky="always"

View File

@@ -20,11 +20,11 @@ const badgeVariants = cva(
success: "bg-green/20 text-green",
org: "bg-org-v1/20 text-org-v1 [&_svg]:text-org-v1 flex items-center opacity-100 hover:bg-org-v1/10 [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap",
namespace:
"bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1 w-min whitespace-nowrap",
"bg-namespace-v1/20 text-namespace-v1 [&_svg]:text-namespace-v1 flex opacity-100 hover:bg-namespace-v1/10 items-center [&_svg]:size-3.5 gap-x-1.5 w-min whitespace-nowrap",
project:
"bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap",
"bg-primary/10 text-primary [&_svg]:text-primary opacity-100 hover:bg-primary/10 flex items-center [&_svg]:size-3 w-min gap-x-1.5 whitespace-nowrap",
instance:
"bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1 w-min whitespace-nowrap"
"bg-mineshaft-200/20 text-mineshaft-200 [&_svg]:text-mineshaft-200 opacity-100 hover:bg-mineshaft-200/20 flex items-center [&_svg]:size-3 gap-x-1.5 w-min whitespace-nowrap"
}
}
}

View File

@@ -120,10 +120,16 @@ export type TBreadcrumbFormat =
icon?: ReactNode;
};
const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[] }) => (
<div className="mx-auto max-w-7xl text-white">
<Breadcrumb>
<BreadcrumbList>
const BreadcrumbContainer = ({
breadcrumbs,
className
}: {
breadcrumbs: TBreadcrumbFormat[];
className?: string;
}) => (
<div className={twMerge("mx-auto max-w-8xl overflow-hidden text-white", className)}>
<Breadcrumb className="overflow-hidden">
<BreadcrumbList className="overflow-hidden">
{(breadcrumbs as TBreadcrumbFormat[]).map((el, index) => {
const isNotLastCrumb = index + 1 !== breadcrumbs.length;
const BreadcrumbSegment = isNotLastCrumb ? BreadcrumbLink : BreadcrumbPage;
@@ -165,8 +171,8 @@ const BreadcrumbContainer = ({ breadcrumbs }: { breadcrumbs: TBreadcrumbFormat[]
const Component = el.component;
return (
<React.Fragment key={`breadcrumb-group-${index + 1}`}>
<BreadcrumbItem>
<BreadcrumbSegment>
<BreadcrumbItem className="overflow-hidden">
<BreadcrumbSegment className="overflow-hidden">
<Component />
</BreadcrumbSegment>
</BreadcrumbItem>

View File

@@ -5,29 +5,48 @@ import { ReactNode } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { Badge } from "@app/components/v2";
import { BadgeProps } from "@app/components/v2/Badge/Badge";
import { ProjectType } from "@app/hooks/api/projects/types";
type Props = {
title: ReactNode;
description?: ReactNode;
children?: ReactNode;
className?: string;
scope: "org" | "project" | "namespace" | "instance";
scope: "org" | "namespace" | "instance" | ProjectType | null;
};
const SCOPE_NAME: Record<NonNullable<Props["scope"]>, { label: string; icon: IconDefinition }> = {
org: { label: "Organization", icon: faGlobe },
project: { label: "Project", icon: faCube },
[ProjectType.SecretManager]: { label: "Project", icon: faCube },
[ProjectType.CertificateManager]: { label: "Project", icon: faCube },
[ProjectType.SSH]: { label: "Project", icon: faCube },
[ProjectType.KMS]: { label: "Project", icon: faCube },
[ProjectType.PAM]: { label: "Project", icon: faCube },
[ProjectType.SecretScanning]: { label: "Project", icon: faCube },
namespace: { label: "Namespace", icon: faCubes },
instance: { label: "Server", icon: faServer }
};
const SCOPE_VARIANT: Record<NonNullable<Props["scope"]>, BadgeProps["variant"]> = {
org: "org",
[ProjectType.SecretManager]: "project",
[ProjectType.CertificateManager]: "project",
[ProjectType.SSH]: "project",
[ProjectType.KMS]: "project",
[ProjectType.PAM]: "project",
[ProjectType.SecretScanning]: "project",
namespace: "namespace",
instance: "instance"
};
export const PageHeader = ({ title, description, children, className, scope }: Props) => (
<div className={twMerge("mb-4 w-full", className)}>
<div className={twMerge("mb-10 w-full", className)}>
<div className="flex w-full justify-between">
<div className="mr-4 flex w-full items-center">
<h1 className="text-3xl font-medium text-white capitalize">{title}</h1>
{scope && (
<Badge variant={scope} className="mt-1 ml-2.5">
<Badge variant={SCOPE_VARIANT[scope]} className="mt-1 ml-2.5">
<FontAwesomeIcon icon={SCOPE_NAME[scope].icon} />
{SCOPE_NAME[scope].label}
</Badge>

View File

@@ -1,10 +1,19 @@
import { IconDefinition } from "@fortawesome/free-brands-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import * as TabsPrimitive from "@radix-ui/react-tabs";
import { twMerge } from "tailwind-merge";
export type TabsProps = TabsPrimitive.TabsProps;
export const Tabs = ({ className, children, ...props }: TabsProps) => (
<TabsPrimitive.Root className={twMerge("flex flex-col", className)} {...props}>
<TabsPrimitive.Root
className={twMerge(
"flex",
className,
props.orientation === "vertical" ? "flex-col xl:flex-row xl:gap-x-12" : "flex-col"
)}
{...props}
>
{children}
</TabsPrimitive.Root>
);
@@ -13,7 +22,11 @@ export type TabListProps = TabsPrimitive.TabsListProps;
export const TabList = ({ className, children, ...props }: TabListProps) => (
<TabsPrimitive.List
className={twMerge("flex shrink-0 border-b-2 border-mineshaft-800", className)}
className={twMerge(
"no-scrollbar flex shrink-0 overflow-auto border-b-2 border-mineshaft-800",
"data-[orientation=vertical]:xl:flex-col data-[orientation=vertical]:xl:items-start data-[orientation=vertical]:xl:gap-y-6 data-[orientation=vertical]:xl:border-b-0",
className
)}
{...props}
>
{children}
@@ -26,18 +39,29 @@ export const Tab = ({
className,
children,
variant = "project",
icon,
...props
}: TabProps & { variant?: "project" | "namespace" | "org" }) => (
}: TabProps & {
icon?: IconDefinition;
variant?: "project" | "namespace" | "org" | "instance";
}) => (
<TabsPrimitive.Trigger
className={twMerge(
"flex h-10 items-center justify-center px-3 text-sm font-medium text-mineshaft-400 transition-all select-none first:rounded-tl-md last:rounded-tr-md hover:text-mineshaft-200 data-[state=active]:border-b data-[state=active]:text-white",
"flex h-10 cursor-pointer items-center justify-center border-transparent",
"px-3 text-sm font-medium whitespace-nowrap text-mineshaft-400 transition-all select-none",
"data-[orientation=vertical]:xl:h-5 data-[orientation=vertical]:xl:border-b-0 data-[orientation=vertical]:xl:border-l",
"border-b hover:text-mineshaft-200",
"data-[state=active]:border-mineshaft-400 data-[state=active]:text-white",
"hover:border-mineshaft-400",
variant === "project" && "data-[state=active]:border-primary",
variant === "namespace" && "data-[state=active]:border-namespace-v1",
variant === "org" && "data-[state=active]:border-org-v1",
variant === "instance" && "data-[state=active]:border-mineshaft-300",
className
)}
{...props}
>
{icon && <FontAwesomeIcon icon={icon} className="mr-2" size="xs" />}
{children}
</TabsPrimitive.Trigger>
);
@@ -46,7 +70,10 @@ export type TabPanelProps = TabsPrimitive.TabsContentProps;
export const TabPanel = ({ className, children, ...props }: TabPanelProps) => (
<TabsPrimitive.Content
className={twMerge("grow rounded-br-md rounded-bl-md py-5 outline-hidden", className)}
className={twMerge(
"grow rounded-br-md rounded-bl-md py-5 outline-hidden data-[orientation=vertical]:xl:overflow-x-hidden data-[orientation=vertical]:xl:py-0",
className
)}
{...props}
>
{children}

View File

@@ -43,6 +43,8 @@ export const eventToNameMap: { [K in EventType]: string } = {
[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_SECRET_BY_ID]:
"Get universal auth client secret by id",
[EventType.CREATE_ENVIRONMENT]: "Create environment",
[EventType.UPDATE_ENVIRONMENT]: "Update environment",
[EventType.DELETE_ENVIRONMENT]: "Delete environment",

View File

@@ -48,6 +48,7 @@ export enum EventType {
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_SECRET_BY_ID = "get-identity-universal-auth-client-secret-by-id",
LOGIN_IDENTITY_LDAP_AUTH = "login-identity-ldap-auth",
ADD_IDENTITY_LDAP_AUTH = "add-identity-ldap-auth",

View File

@@ -318,6 +318,14 @@ interface GetIdentityUniversalAuthClientSecretsEvent {
};
}
interface GetIdentityUniversalAuthClientSecretByIdEvent {
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET_BY_ID;
metadata: {
identityId: string;
clientSecretId: string;
};
}
interface RevokeIdentityUniversalAuthClientSecretEvent {
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET;
metadata: {
@@ -906,6 +914,7 @@ export type Event =
| GetIdentityUniversalAuthEvent
| CreateIdentityUniversalAuthClientSecretEvent
| GetIdentityUniversalAuthClientSecretsEvent
| GetIdentityUniversalAuthClientSecretByIdEvent
| RevokeIdentityUniversalAuthClientSecretEvent
| ClearIdentityUniversalAuthLockoutsEvent
| CreateEnvironmentEvent

View File

@@ -1,2 +1,3 @@
export * from "./mutations";
export * from "./queries";
export * from "./types";

View File

@@ -1,8 +1,12 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { projectKeys } from "../projects";
import { externalMigrationQueryKeys } from "./queries";
import { TImportVaultSecretsDTO, TVaultExternalMigrationConfig, VaultImportStatus } from "./types";
export const useImportEnvKey = () => {
const queryClient = useQueryClient();
@@ -65,3 +69,97 @@ export const useImportVault = () => {
}
});
};
export const useImportVaultSecrets = () => {
const queryClient = useQueryClient();
return useMutation<{ status: VaultImportStatus }, object, TImportVaultSecretsDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post<{ status: VaultImportStatus }>(
"/api/v3/external-migration/vault/import-secrets",
dto
);
return data;
},
onSuccess: (_, { projectId, environment, secretPath }) => {
queryClient.invalidateQueries({ queryKey: dashboardKeys.all() });
queryClient.invalidateQueries({
queryKey: secretKeys.getProjectSecret({
projectId,
environment,
secretPath
})
});
}
});
};
export const useCreateVaultExternalMigrationConfig = () => {
const queryClient = useQueryClient();
return useMutation<
TVaultExternalMigrationConfig,
Error,
{ connectionId: string; namespace: string }
>({
mutationFn: async ({ connectionId, namespace }) => {
const { data } = await apiRequest.post<{ config: TVaultExternalMigrationConfig }>(
"/api/v3/external-migration/vault/configs",
{
connectionId,
namespace
}
);
return data.config;
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: externalMigrationQueryKeys.vaultConfigs()
});
}
});
};
export const useUpdateVaultExternalMigrationConfig = () => {
const queryClient = useQueryClient();
return useMutation<
TVaultExternalMigrationConfig,
Error,
{ id: string; connectionId: string; namespace: string }
>({
mutationFn: async ({ id, connectionId, namespace }) => {
const { data } = await apiRequest.put<{ config: TVaultExternalMigrationConfig }>(
`/api/v3/external-migration/vault/configs/${id}`,
{
connectionId,
namespace
}
);
return data.config;
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: externalMigrationQueryKeys.vaultConfigs()
});
}
});
};
export const useDeleteVaultExternalMigrationConfig = () => {
const queryClient = useQueryClient();
return useMutation<TVaultExternalMigrationConfig, Error, { id: string }>({
mutationFn: async ({ id }) => {
const { data } = await apiRequest.delete<{ config: TVaultExternalMigrationConfig }>(
`/api/v3/external-migration/vault/configs/${id}`
);
return data.config;
},
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: externalMigrationQueryKeys.vaultConfigs()
});
}
});
};

View File

@@ -2,12 +2,35 @@ import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { ExternalMigrationProviders } from "./types";
import {
ExternalMigrationProviders,
TVaultExternalMigrationConfig,
VaultKubernetesAuthRole
} from "./types";
const externalMigrationQueryKeys = {
export const externalMigrationQueryKeys = {
customMigrationAvailable: (provider: ExternalMigrationProviders) => [
"custom-migration-available",
provider
],
vaultConfigs: () => ["vault-external-migration-configs"],
vaultNamespaces: () => ["vault-namespaces"],
vaultPolicies: (namespace?: string) => ["vault-policies", namespace],
vaultMounts: (namespace?: string) => ["vault-mounts", namespace],
vaultAuthMounts: (namespace?: string, authType?: string) => [
"vault-auth-mounts",
namespace,
authType
],
vaultSecretPaths: (namespace?: string, mountPath?: string) => [
"vault-secret-paths",
namespace,
mountPath
],
vaultKubernetesAuthRoles: (namespace?: string, mountPath?: string) => [
"vault-kubernetes-auth-roles",
namespace,
mountPath
]
};
@@ -20,3 +43,132 @@ export const useHasCustomMigrationAvailable = (provider: ExternalMigrationProvid
)
});
};
export const useGetVaultExternalMigrationConfigs = () => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultConfigs(),
queryFn: async () => {
const { data } = await apiRequest.get<{ configs: TVaultExternalMigrationConfig[] }>(
"/api/v3/external-migration/vault/configs"
);
return data.configs;
}
});
};
export const useGetVaultNamespaces = () => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultNamespaces(),
queryFn: async () => {
const { data } = await apiRequest.get<{
namespaces: Array<{ id: string; name: string }>;
}>("/api/v3/external-migration/vault/namespaces");
return data.namespaces;
}
});
};
export const useGetVaultPolicies = (enabled = true, namespace?: string) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultPolicies(namespace),
queryFn: async () => {
const { data } = await apiRequest.get<{
policies: Array<{ name: string; rules: string }>;
}>("/api/v3/external-migration/vault/policies", {
params: {
namespace
}
});
return data.policies;
},
enabled
});
};
export const useGetVaultMounts = (enabled = true, namespace?: string) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultMounts(namespace),
queryFn: async () => {
const { data } = await apiRequest.get<{
mounts: Array<{ path: string; type: string; version: string | null }>;
}>("/api/v3/external-migration/vault/mounts", {
params: {
namespace
}
});
return data.mounts;
},
enabled
});
};
export const useGetVaultSecretPaths = (enabled = true, namespace?: string, mountPath?: string) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultSecretPaths(namespace, mountPath),
queryFn: async () => {
if (!namespace || !mountPath) {
throw new Error("Both namespace and mountPath are required");
}
const { data } = await apiRequest.get<{
secretPaths: string[];
}>("/api/v3/external-migration/vault/secret-paths", {
params: {
namespace,
mountPath
}
});
return data.secretPaths;
},
enabled: enabled && !!namespace && !!mountPath
});
};
export const useGetVaultAuthMounts = (enabled = true, namespace?: string, authType?: string) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultAuthMounts(namespace, authType),
queryFn: async () => {
const { data } = await apiRequest.get<{
mounts: Array<{ path: string; type: string }>;
}>("/api/v3/external-migration/vault/auth-mounts", {
params: {
namespace,
...(authType && { authType })
}
});
return data.mounts;
},
enabled
});
};
export const useGetVaultKubernetesAuthRoles = (
enabled = true,
namespace?: string,
mountPath?: string
) => {
return useQuery({
queryKey: externalMigrationQueryKeys.vaultKubernetesAuthRoles(namespace, mountPath),
queryFn: async () => {
if (!namespace || !mountPath) {
throw new Error("Both namespace and mountPath are required");
}
const { data } = await apiRequest.get<{
roles: VaultKubernetesAuthRole[];
}>("/api/v3/external-migration/vault/auth-roles/kubernetes", {
params: {
namespace,
mountPath
}
});
return data.roles;
},
enabled: enabled && !!namespace && !!mountPath
});
};

View File

@@ -2,3 +2,50 @@ export enum ExternalMigrationProviders {
Vault = "vault",
EnvKey = "env-key"
}
export enum VaultImportStatus {
Imported = "imported",
ApprovalRequired = "approval-required"
}
export type TVaultExternalMigrationConfig = {
id: string;
orgId: string;
namespace: string;
connectionId: string | null;
createdAt: string;
updatedAt: string;
};
export type TImportVaultSecretsDTO = {
projectId: string;
environment: string;
secretPath: string;
vaultNamespace: string;
vaultSecretPath: string;
};
export type VaultKubernetesAuthRole = {
name: string;
bound_service_account_names: string[];
bound_service_account_namespaces: string[];
token_ttl?: number;
token_max_ttl?: number;
token_policies?: string[];
token_bound_cidrs?: string[];
token_explicit_max_ttl?: number;
token_no_default_policy?: boolean;
token_num_uses?: number;
token_period?: number;
token_type?: string;
audience?: string;
alias_name_source?: string;
mountPath: string;
config: {
kubernetes_host: string;
kubernetes_ca_cert?: string;
issuer?: string;
disable_iss_validation?: boolean;
disable_local_ca_jwt?: boolean;
};
};

View File

@@ -2,6 +2,7 @@ export {
useCreateFolder,
useDeleteFolder,
useGetFoldersByEnv,
useGetOrCreateFolder,
useGetProjectFolders,
useUpdateFolder
} from "./queries";

View File

@@ -140,6 +140,59 @@ export const useGetFoldersByEnv = ({
return { folders, folderNames, isFolderPresentInEnv, getFolderByNameAndEnv };
};
export const useGetOrCreateFolder = () => {
const queryClient = useQueryClient();
return useMutation<TSecretFolder, object, TCreateFolderDTO>({
mutationFn: async (dto) => {
const { data: existingFolder } = await apiRequest.get<{ folders: TSecretFolder[] }>(
"/api/v2/folders",
{
params: {
projectId: dto.projectId,
environment: dto.environment,
path: dto.path || "/"
}
}
);
const folder = existingFolder.folders.find((f) => f.name === dto.name);
if (folder) return folder;
const { data } = await apiRequest.post("/api/v2/folders", {
...dto,
projectId: dto.projectId
});
return data;
},
onSuccess: (_, { projectId, environment, path }) => {
queryClient.invalidateQueries({
queryKey: dashboardKeys.getDashboardSecrets({
projectId,
secretPath: path ?? "/"
})
});
queryClient.invalidateQueries({
queryKey: folderQueryKeys.getSecretFolders({ projectId, environment, path })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.list({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.count({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ projectId, environment, directory: path })
});
}
});
};
export const useCreateFolder = () => {
const queryClient = useQueryClient();
@@ -170,6 +223,9 @@ export const useCreateFolder = () => {
queryClient.invalidateQueries({
queryKey: commitKeys.count({ projectId, environment, directory: path })
});
queryClient.invalidateQueries({
queryKey: commitKeys.history({ projectId, environment, directory: path })
});
}
});
};
@@ -218,12 +274,13 @@ export const useDeleteFolder = () => {
const queryClient = useQueryClient();
return useMutation<object, object, TDeleteFolderDTO>({
mutationFn: async ({ path = "/", folderId, environment, projectId }) => {
mutationFn: async ({ path = "/", folderId, environment, projectId, forceDelete = true }) => {
const { data } = await apiRequest.delete(`/api/v2/folders/${folderId}`, {
data: {
environment,
projectId,
path
path,
forceDelete
}
});
return data;

View File

@@ -59,6 +59,7 @@ export type TDeleteFolderDTO = {
environment: string;
folderId: string;
path?: string;
forceDelete?: boolean;
};
export type TUpdateFolderBatchDTO = {

View File

@@ -42,7 +42,7 @@
--font-inter: "Inter", sans-serif;
--color-org-v1: #30B3FF;
--color-namespace-v1: #96ff59;
--max-width-8xl: 88rem; /* 1408px */
/* Primary */
--color-primary-50: #fffff5;
--color-primary-100: #fcfce8;

View File

@@ -12,7 +12,7 @@ import { RedisBanner } from "@app/layouts/OrganizationLayout/components/RedisBan
import { SmtpBanner } from "@app/layouts/OrganizationLayout/components/SmtpBanner";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
import { AdminSidebar } from "./Sidebar";
import { AdminNavBar } from "./AdminNavBar";
export const AdminLayout = () => {
const { t } = useTranslation();
@@ -33,9 +33,9 @@ export const AdminLayout = () => {
{!isLoading && !serverDetails?.emailConfigured && <SmtpBanner />}
{!isLoading && subscription.auditLogs && <AuditLogBanner />}
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<AdminSidebar />
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-4 pt-8 pb-4 dark:scheme-dark">
<div className="flex grow flex-col overflow-y-hidden">
<AdminNavBar />
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark">
<Outlet />
</div>
</div>

View File

@@ -0,0 +1,99 @@
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faArrowLeft,
faBuilding,
faCog,
faDatabase,
faKey,
faLock,
faPlug,
faUserTie
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useMatchRoute } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Tab, TabList, Tabs, Tooltip } from "@app/components/v2";
const generalTabs = [
{
label: "General",
icon: faCog,
link: "/admin/"
},
{
label: "Resource Overview",
icon: faBuilding,
link: "/admin/resources/overview"
},
{
label: "Access Control",
icon: faUserTie,
link: "/admin/access-management"
},
{
label: "Encryption",
icon: faLock,
link: "/admin/encryption"
},
{
label: "Authentication",
icon: faCheckCircle,
link: "/admin/authentication"
},
{
label: "Integrations",
icon: faPlug,
link: "/admin/integrations"
},
{
label: "Caching",
icon: faDatabase,
link: "/admin/caching"
},
{
label: "Environment Variables",
icon: faKey,
link: "/admin/environment"
}
];
export const AdminNavBar = () => {
const matchRoute = useMatchRoute();
return (
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="px-4"
>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Tooltip position="bottom" content="Back to organization">
<Link to="/organization/projects">
<Tab value="back">
<FontAwesomeIcon icon={faArrowLeft} />
</Tab>
</Link>
</Tooltip>
{generalTabs.map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<Tab variant="instance" value={isActive ? "selected" : ""}>
{tab.label}
</Tab>
</Link>
);
})}
</TabList>
</Tabs>
</nav>
</motion.div>
</div>
);
};

View File

@@ -1,126 +0,0 @@
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faBuilding,
faChevronLeft,
faCog,
faDatabase,
faKey,
faLock,
faPlug,
faUserTie
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useMatchRoute } from "@tanstack/react-router";
import { Menu, MenuGroup, MenuItem } from "@app/components/v2";
const generalTabs = [
{
label: "General",
icon: faCog,
link: "/admin/"
},
{
label: "Encryption",
icon: faLock,
link: "/admin/encryption"
},
{
label: "Authentication",
icon: faCheckCircle,
link: "/admin/authentication"
},
{
label: "Integrations",
icon: faPlug,
link: "/admin/integrations"
},
{
label: "Caching",
icon: faDatabase,
link: "/admin/caching"
},
{
label: "Environment Variables",
icon: faKey,
link: "/admin/environment"
}
];
const othersTabs = [
{
label: "Access Controls",
icon: faUserTie,
link: "/admin/access-management"
},
{
label: "Resource Overview",
icon: faBuilding,
link: "/admin/resources/overview"
}
];
export const AdminSidebar = () => {
const matchRoute = useMatchRoute();
return (
<aside className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:scheme-dark">
<div className="flex-1">
<Menu>
<MenuGroup title="Configuration">
{generalTabs.map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<MenuItem isSelected={Boolean(isActive)}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={tab.icon} />
</div>
{tab.label}
</div>
</MenuItem>
</Link>
);
})}
</MenuGroup>
<MenuGroup title="Others">
{othersTabs.map((tab) => {
const isActive = matchRoute({ to: tab.link, fuzzy: false });
return (
<Link key={tab.link} to={tab.link}>
<MenuItem isSelected={Boolean(isActive)}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={tab.icon} />
</div>
{tab.label}
</div>
</MenuItem>
</Link>
);
})}
</MenuGroup>
</Menu>
</div>
<Menu>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faChevronLeft}
flip="vertical"
/>
}
>
Back to Organization
</MenuItem>
</Link>
</Menu>
</nav>
</aside>
);
};

View File

@@ -1,9 +1,7 @@
import { faBook, faCog, faCube, faHome, faLock, faUsers } from "@fortawesome/free-solid-svg-icons";
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 { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission } from "@app/context";
import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePrivilegeModeBanner";
@@ -12,138 +10,81 @@ export const KmsLayout = () => {
const { currentProject } = useProject();
const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="unlock" />
KMS
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/kms/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCube} />
</div>
Overview
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/kmip"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faLock} />
</div>
KMIP
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/kms/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/kms/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
variant="project"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/kms/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Overview</Tab>}
</Link>
</Menu>
</div>
<Link
to="/projects/kms/$projectId/kmip"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>KMIP</Tab>}
</Link>
<Link
to="/projects/kms/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/kms/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/kms/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
);

View File

@@ -13,7 +13,7 @@ import { useFetchServerStatus } from "@app/hooks/api";
import { AuditLogBanner } from "./components/AuditLogBanner";
import { InsecureConnectionBanner } from "./components/InsecureConnectionBanner";
import { Navbar } from "./components/NavBar";
import { OrgSidebar } from "./components/OrgSidebar";
import { OrgNavBar } from "./components/OrgNavBar";
import { RedisBanner } from "./components/RedisBanner";
import { SmtpBanner } from "./components/SmtpBanner";
@@ -41,16 +41,16 @@ export const OrganizationLayout = () => {
className={`dark hidden ${containerHeight} w-full flex-col overflow-x-hidden bg-bunker-800 transition-all md:flex`}
>
<Navbar />
{!isLoading && !serverDetails?.redisConfigured && <RedisBanner />}
{!isLoading && !serverDetails?.emailConfigured && <SmtpBanner />}
{!isLoading && subscription.auditLogs && <AuditLogBanner />}
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<OrgSidebar isHidden={isInsideProject} />
<div className="flex grow flex-col overflow-y-hidden">
<OrgNavBar isHidden={isInsideProject} />
{!isLoading && !isInsideProject && !serverDetails?.redisConfigured && <RedisBanner />}
{!isLoading && !isInsideProject && !serverDetails?.emailConfigured && <SmtpBanner />}
{!isLoading && !isInsideProject && subscription.auditLogs && <AuditLogBanner />}
{!window.isSecureContext && !isInsideProject && <InsecureConnectionBanner />}
<main
className={twMerge(
"flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-4 pt-8 pb-4 dark:scheme-dark",
isInsideProject && "p-0"
"flex-1 overflow-x-hidden bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark",
isInsideProject ? "overflow-y-hidden p-0" : "overflow-y-auto"
)}
>
<Outlet />

View File

@@ -9,6 +9,7 @@ import {
faEnvelope,
faExclamationTriangle,
faGlobe,
faInfinity,
faInfo,
faInfoCircle,
faServer,
@@ -43,7 +44,7 @@ import { envConfig } from "@app/config/env";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { isInfisicalCloud } from "@app/helpers/platform";
import { useToggle } from "@app/hooks";
import { projectKeys, useGetOrganizations, useLogoutUser } from "@app/hooks/api";
import { projectKeys, useGetOrganizations, useGetOrgTrialUrl, useLogoutUser } from "@app/hooks/api";
import { authKeys, selectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery";
@@ -162,6 +163,8 @@ export const Navbar = () => {
await navigateUserToOrg(navigate, orgId);
};
const { mutateAsync } = useGetOrgTrialUrl();
const logout = useLogoutUser();
const logOutUser = async () => {
try {
@@ -201,144 +204,192 @@ export const Navbar = () => {
const isServerAdminPanel = location.pathname.startsWith("/admin");
const isOrgScope = breadcrumbs?.length === 1; // TODO: scott/akhil is this adequate?
const isOrgScope = location.pathname.startsWith("/organization"); // TODO: scott/akhil is this adequate?
return (
<div className="z-10 flex min-h-12 items-center border-b border-mineshaft-600 bg-mineshaft-800 px-4">
<div>
<Link to="/organization/projects">
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
</Link>
</div>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
{isServerAdminPanel ? (
<>
<Link
to="/admin"
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
>
<div>
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
</div>
<div className="whitespace-nowrap">Server Console</div>
<div className="z-10 flex min-h-12 items-center bg-mineshaft-900 px-4 pt-1">
<div className="mr-auto flex items-center overflow-hidden">
<div className="shrink-0">
<Link to="/organization/projects">
<img alt="infisical logo" src="/images/logotransparent.png" className="h-4" />
</Link>
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
{breadcrumbs ? (
// scott: remove /admin as we show server console above
<BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} />
) : null}
</>
) : (
<>
<div className="flex items-center">
<DropdownMenu modal={false}>
<Link to="/organization/projects">
<div className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary">
<Badge
variant="org"
className={twMerge("text-sm", !isOrgScope && "bg-transparent opacity-75")}
>
<FontAwesomeIcon icon={faGlobe} />
{currentOrg?.name}
</Badge>
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
{getPlan(subscription)}
</div>
{subscription.cardDeclined && (
<Tooltip
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
className="max-w-xs"
</div>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
{isServerAdminPanel ? (
<>
<Link
to="/admin"
className="group flex cursor-pointer items-center gap-2 text-sm text-white transition-all duration-100 hover:text-primary"
>
<div>
<FontAwesomeIcon icon={faServer} className="text-xs text-bunker-300" />
</div>
<div className="whitespace-nowrap">Server Console</div>
</Link>
<p className="pr-3 pl-3 text-lg text-mineshaft-400/70">/</p>
{breadcrumbs ? (
// scott: remove /admin as we show server console above
<BreadcrumbContainer breadcrumbs={breadcrumbs.slice(1) as TBreadcrumbFormat[]} />
) : null}
</>
) : (
<>
<div className="flex items-center overflow-hidden">
<DropdownMenu modal={false}>
<Link className="overflow-hidden" to="/organization/projects">
<div className="group flex cursor-pointer items-center gap-2 overflow-hidden text-sm text-white transition-all duration-100 hover:text-primary">
<Badge
variant="org"
className={twMerge(
"max-w-full min-w-0 cursor-pointer text-sm",
!isOrgScope &&
"bg-transparent text-mineshaft-200 hover:bg-transparent hover:underline"
)}
>
<div className="flex items-center">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="animate-pulse cursor-help text-xs text-primary-400"
/>
</div>
</Tooltip>
)}
</div>
</Link>
<DropdownMenuTrigger asChild>
<div>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="switch-org"
className="px-2 py-1"
>
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
</IconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">organizations</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
if (org.googleSsoAuthEnforced) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
<FontAwesomeIcon icon={faGlobe} />
<p className="truncate">{currentOrg?.name}</p>
</Badge>
<div className="mr-1 rounded-sm border border-mineshaft-500 px-1 text-xs text-bunker-300 no-underline!">
{getPlan(subscription)}
</div>
{subscription.cardDeclined && (
<Tooltip
content={`Your payment could not be processed${subscription.cardDeclinedReason ? `: ${subscription.cardDeclinedReason}` : ""}. Please update your payment method to continue enjoying premium features.`}
className="max-w-xs"
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
<div className="flex items-center">
<FontAwesomeIcon
icon={faExclamationTriangle}
className="animate-pulse cursor-help text-xs text-primary-400"
/>
</div>
</Button>
</DropdownMenuItem>
);
})}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem icon={<FontAwesomeIcon icon={faSignOut} />} onClick={logOutUser}>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
{breadcrumbs ? (
<BreadcrumbContainer breadcrumbs={breadcrumbs as TBreadcrumbFormat[]} />
) : null}
</>
</Tooltip>
)}
</div>
</Link>
<DropdownMenuTrigger asChild>
<div>
<IconButton
variant="plain"
colorSchema="secondary"
ariaLabel="switch-org"
className="px-2 py-1"
>
<FontAwesomeIcon icon={faCaretDown} className="text-xs text-bunker-300" />
</IconButton>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
side="bottom"
className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "220px" }}
>
<div className="px-2 py-1 text-xs text-mineshaft-400 capitalize">
organizations
</div>
{orgs?.map((org) => {
return (
<DropdownMenuItem key={org.id}>
<Button
onClick={async () => {
if (currentOrg?.id === org.id) return;
if (org.authEnforced) {
// org has an org-level auth method enabled (e.g. SAML)
// -> logout + redirect to SAML SSO
await logout.mutateAsync();
if (org.orgAuthMethod === AuthMethod.OIDC) {
window.open(`/api/v1/sso/oidc/login?orgSlug=${org.slug}`);
} else {
window.open(`/api/v1/sso/redirect/saml2/organizations/${org.slug}`);
}
window.close();
return;
}
if (org.googleSsoAuthEnforced) {
await logout.mutateAsync();
window.open(`/api/v1/sso/redirect/google?org_slug=${org.slug}`);
window.close();
return;
}
handleOrgChange(org?.id);
}}
variant="plain"
colorSchema="secondary"
size="xs"
className="flex w-full items-center justify-start p-0 font-normal"
leftIcon={
currentOrg?.id === org.id && (
<FontAwesomeIcon icon={faCheck} className="mr-3 text-primary" />
)
}
>
<div className="flex w-full max-w-[150px] items-center justify-between truncate">
{org.name}
</div>
</Button>
</DropdownMenuItem>
);
})}
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faSignOut} />}
onClick={logOutUser}
>
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{!isOrgScope && (
<>
<p className="pr-3 pl-1 text-lg text-mineshaft-400/70">/</p>
{breadcrumbs ? (
<BreadcrumbContainer
className="min-w-[15rem] flex-1"
breadcrumbs={[breadcrumbs[0]] as TBreadcrumbFormat[]}
/>
) : null}
</>
)}
</>
)}
</div>
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
<Tooltip content="Start Free Pro Trial">
<Button
variant="plain"
className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
leftIcon={<FontAwesomeIcon icon={faInfinity} />}
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
>
Free Pro Trial
</Button>
</Tooltip>
)}
{user.superAdmin && !location.pathname.startsWith("/admin") && (
<Link
className="mr-2 rounded-md border border-mineshaft-500 px-2.5 py-1.5 text-sm whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600"
to="/admin"
>
<FontAwesomeIcon icon={faServer} className="mr-2" />
Server Console
</Link>
)}
<div className="grow" />
<DropdownMenu modal={false}>
<DropdownMenuTrigger>
<div className="rounded-l-md border border-r-0 border-mineshaft-500 px-2.5 py-1 hover:bg-mineshaft-600">

View File

@@ -16,15 +16,15 @@ export const Notification = ({ notification, onDelete }: Props) => {
return (
<div
className={twMerge(
"group relative flex cursor-pointer items-start border-b border-mineshaft-600 p-3 transition-colors",
"group relative flex cursor-pointer items-start border-b border-mineshaft-600 p-2 transition-colors",
notification.link ? "hover:bg-mineshaft-700" : "cursor-default",
!notification.isRead && "bg-mineshaft-800"
)}
>
<div className="flex w-full min-w-0 flex-col">
<div className="flex w-full min-w-0 flex-col p-1">
<div className="flex gap-2">
{!notification.isRead && (
<FontAwesomeIcon icon={faCircle} className="mt-1.5 size-2 text-yellow-400" />
<FontAwesomeIcon icon={faCircle} className="mt-0.5 size-2 text-yellow-400" />
)}
<Tooltip
content={<Markdown>{notification.title}</Markdown>}
@@ -45,7 +45,7 @@ export const Notification = ({ notification, onDelete }: Props) => {
</span>
)}
</div>
<div className="flex w-0 shrink-0 justify-end opacity-0 transition-all group-hover:w-[24px] group-hover:opacity-100">
<div className="mt-0.5 flex w-0 shrink-0 justify-end opacity-0 transition-all group-hover:w-[24px] group-hover:opacity-100">
<IconButton
ariaLabel="delete"
variant="plain"

View File

@@ -0,0 +1,109 @@
import { Link, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { usePopUp } from "@app/hooks";
type Props = {
isHidden?: boolean;
};
export const OrgNavBar = ({ isHidden }: Props) => {
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { pathname } = useLocation();
return (
<>
{!isHidden && (
<div className="dark hidden w-full flex-col overflow-x-hidden border-b border-mineshaft-600 bg-mineshaft-900 px-4 md:flex">
<motion.div
key="menu-org-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link to="/organization/projects">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Overview
</Tab>
)}
</Link>
<Link to="/organization/app-connections">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
App Connections
</Tab>
)}
</Link>
<Link to="/organization/networking">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Networking
</Tab>
)}
</Link>
<Link to="/organization/secret-sharing">
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""} variant="org">
Secret Sharing
</Tab>
)}
</Link>
<Link to="/organization/access-management">
{({ isActive }) => (
<Tab
variant="org"
value={
isActive ||
pathname.match(
/organization\/members|organization\/identities|organization\/groups|organization\/roles/
)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link to="/organization/audit-logs">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Audit Logs
</Tab>
)}
</Link>
<Link to="/organization/billing">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Usage & Billing
</Tab>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<Tab variant="org" value={isActive ? "selected" : ""}>
Settings
</Tab>
)}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
</div>
)}
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
</>
);
};

View File

@@ -0,0 +1 @@
export { OrgNavBar } from "./OrgNavBar";

View File

@@ -1,214 +0,0 @@
import {
faBook,
faCog,
faInfinity,
faMoneyBill,
faNetworkWired,
faPlug,
faShare,
faTable,
faUsers,
faUserTie
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link } from "@tanstack/react-router";
import { AnimatePresence, motion } from "framer-motion";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import { Menu, MenuGroup, MenuItem, Tooltip } from "@app/components/v2";
import { useOrganization, useSubscription, useUser } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useGetOrgTrialUrl } from "@app/hooks/api";
type Props = {
isHidden?: boolean;
};
export const OrgSidebar = ({ isHidden }: Props) => {
const { subscription } = useSubscription();
const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl();
const { currentOrg } = useOrganization();
const { popUp, handlePopUpToggle } = usePopUp(["createOrg"] as const);
return (
<>
<AnimatePresence mode="popLayout">
{!isHidden && (
<motion.aside
key="org-sidebar"
transition={{ duration: 0.3 }}
initial={{ opacity: 0, translateX: -240 }}
animate={{ opacity: 1, translateX: 0 }}
exit={{ opacity: 0, translateX: -240 }}
layout
className="dark z-10 w-60 border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-800 to-mineshaft-900"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<Menu>
<MenuGroup title="Overview">
<Link to="/organization/projects">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faTable} />
</div>
Overview
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/access-management">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Organization Access
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/billing">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faMoneyBill} className="mr-4" />
</div>
Usage & Billing
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/audit-logs">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} className="mr-4" />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/settings">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} className="mr-4" />
</div>
Organization Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Resources">
<Link to="/organization/app-connections">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} className="mr-4" />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
<Link to="/organization/networking">
{({ isActive }) => (
<MenuItem variant="org" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faNetworkWired} className="mr-4" />
</div>
Networking
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
<div className="grow" />
<Menu>
{subscription &&
subscription.slug === "starter" &&
!subscription.has_used_trial && (
<Tooltip content="Start Free Pro Trial">
<MenuItem
variant="org"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faInfinity}
/>
}
onClick={async () => {
if (!subscription || !currentOrg) return;
// direct user to start pro trial
const url = await mutateAsync({
orgId: currentOrg.id,
success_url: window.location.href
});
window.location.href = url;
}}
>
Pro Trial
</MenuItem>
</Tooltip>
)}
<Link to="/organization/secret-sharing">
<MenuItem
variant="org"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faShare} />
</div>
}
>
Share Secret
</MenuItem>
</Link>
{user.superAdmin && (
<Link to="/admin">
<MenuItem
variant="org"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon
className="mx-1 inline-block shrink-0"
icon={faUserTie}
/>
</div>
}
>
Server Console
</MenuItem>
</Link>
)}
</Menu>
</nav>
</motion.aside>
)}
</AnimatePresence>
<CreateOrgModal
isOpen={popUp?.createOrg?.isOpen}
onClose={() => handlePopUpToggle("createOrg", false)}
/>
</>
);
};

View File

@@ -1 +0,0 @@
export { OrgSidebar } from "./OrgSidebar";

View File

@@ -1,19 +1,9 @@
import { useEffect } from "react";
import {
faBook,
faBoxOpen,
faCog,
faDisplay,
faHome,
faUser,
faUsers
} from "@fortawesome/free-solid-svg-icons";
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 { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission, useSubscription } from "@app/context";
import { usePopUp } from "@app/hooks";
@@ -23,7 +13,7 @@ export const PamLayout = () => {
const { currentProject } = useProject();
const { subscription } = useSubscription();
const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"]);
useEffect(() => {
@@ -35,152 +25,85 @@ export const PamLayout = () => {
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="groups" />
PAM
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/pam/$projectId/accounts"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUser} />
</div>
Accounts
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/resources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBoxOpen} />
</div>
Resources
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/sessions"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faDisplay} />
</div>
Sessions
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/pam/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/pam/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/pam/$projectId/accounts"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Accounts</Tab>}
</Link>
</Menu>
</div>
<Link
to="/projects/pam/$projectId/resources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Resources</Tab>}
</Link>
<Link
to="/projects/pam/$projectId/sessions"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Sessions</Tab>}
</Link>
<Link
to="/projects/pam/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/pam/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/pam/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
<UpgradePlanModal

View File

@@ -1,9 +1,7 @@
import { useTranslation } from "react-i18next";
import { faArrowLeft, faMobile } from "@fortawesome/free-solid-svg-icons";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet } from "@tanstack/react-router";
import { WishForm } from "@app/components/features/WishForm";
import { Outlet } from "@tanstack/react-router";
import { InsecureConnectionBanner } from "../OrganizationLayout/components/InsecureConnectionBanner";
@@ -12,27 +10,10 @@ export const PersonalSettingsLayout = () => {
return (
<>
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden md:flex">
<div className="dark hidden h-screen w-full flex-col overflow-x-hidden bg-bunker-800 md:flex">
{!window.isSecureContext && <InsecureConnectionBanner />}
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<aside className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60">
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:scheme-dark">
<div className="grow">
<Link to="/organization/projects">
<div className="my-6 flex cursor-default items-center justify-center pr-2 text-sm text-mineshaft-300 hover:text-mineshaft-100">
<FontAwesomeIcon icon={faArrowLeft} className="pr-3" />
Back to organization
</div>
</Link>
</div>
<div className="relative mt-10 flex w-full cursor-default flex-col items-center px-3 text-sm text-mineshaft-400">
{(window.location.origin.includes("https://app.infisical.com") ||
window.location.origin.includes("https://gamma.infisical.com")) && <WishForm />}
</div>
)
</nav>
</aside>
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 dark:scheme-dark">
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4 dark:scheme-dark">
<Outlet />
</main>
</div>

View File

@@ -1,23 +1,10 @@
import { useTranslation } from "react-i18next";
import {
faBell,
faBook,
faCertificate,
faCog,
faFileLines,
faHome,
faMobile,
faPlug,
faPuzzlePiece,
faSitemap,
faStamp,
faUsers
} from "@fortawesome/free-solid-svg-icons";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
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 { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission, useSubscription } from "@app/context";
import {
useListWorkspaceCertificateTemplates,
@@ -43,131 +30,88 @@ export const PkiManagerLayout = () => {
const showLegacySection =
subscription.pkiLegacyTemplates || hasExistingSubscribers || hasExistingTemplates;
const location = useLocation();
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="note" />
PKI Manager
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/cert-management/$projectId/policies"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faFileLines} />
</div>
Certificate Policies
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificates"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCertificate} />
</div>
Certificates
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificate-authorities"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faStamp} />
</div>
Certificate Authorities
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/alerting"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBell} />
</div>
Alerting
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPuzzlePiece} />
</div>
Integrations
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/cert-management/$projectId/policies"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Policies</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/certificates"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive || location.pathname.match(/\/pki-collections\//)
? "selected"
: ""
}
>
Certificates
</Tab>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/certificate-authorities"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive || location.pathname.match(/\/ca\//) ? "selected" : ""}>
Certificate Authorities
</Tab>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/alerting"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Alerting</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Integrations</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>App Connections</Tab>
)}
</Link>
{showLegacySection && (
<MenuGroup title="Legacy">
<>
{(subscription.pkiLegacyTemplates || hasExistingSubscribers) && (
<Link
to="/projects/cert-management/$projectId/subscribers"
@@ -176,14 +120,7 @@ export const PkiManagerLayout = () => {
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} variant="project">
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faSitemap} />
</div>
Subscribers
</div>
</MenuItem>
<Tab value={isActive ? "selected" : ""}>Subscribers (Legacy)</Tab>
)}
</Link>
)}
@@ -195,96 +132,55 @@ export const PkiManagerLayout = () => {
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive} variant="project">
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faFileLines} />
</div>
Certificate Templates
</div>
</MenuItem>
<Tab value={isActive ? "selected" : ""}>Certificate Templates (Legacy)</Tab>
)}
</Link>
)}
</MenuGroup>
</>
)}
<MenuGroup title="Others">
<Link
to="/projects/cert-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/cert-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
<Link
to="/projects/cert-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
</Menu>
</div>
<Link
to="/projects/cert-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/cert-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">

View File

@@ -15,7 +15,7 @@ export const AssumePrivilegeModeBanner = () => {
if (!assumedPrivilegeDetails) return null;
return (
<div className="z-10 -mx-4 flex items-center justify-center gap-2 rounded-sm border border-mineshaft-600 bg-primary-400 p-2 text-mineshaft-800 shadow-sm">
<div className="flex w-full items-center border-b border-yellow/50 bg-yellow/30 px-4 py-2 text-sm text-yellow-200">
<div>
<FontAwesomeIcon icon={faInfoCircle} className="mr-2" />
You are currently viewing the project with privileges of{" "}
@@ -24,7 +24,7 @@ export const AssumePrivilegeModeBanner = () => {
{assumedPrivilegeDetails?.actorName}
</b>
</div>
<div>
<div className="ml-auto">
<Button
size="xs"
variant="outline_bg"

View File

@@ -35,10 +35,19 @@ import {
import { getProjectHomePage } from "@app/helpers/project";
import { usePopUp } from "@app/hooks";
import { useGetUserProjects } from "@app/hooks/api";
import { Project } from "@app/hooks/api/projects/types";
import { Project, ProjectType } from "@app/hooks/api/projects/types";
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
const PROJECT_TYPE_NAME: Record<ProjectType, string> = {
[ProjectType.SecretManager]: "Secrets Management",
[ProjectType.CertificateManager]: "PKI",
[ProjectType.SSH]: "SSH",
[ProjectType.KMS]: "KMS",
[ProjectType.PAM]: "PAM",
[ProjectType.SecretScanning]: "Secret Scanning"
};
export const ProjectSelect = () => {
const [searchProject, setSearchProject] = useState("");
const { currentProject: currentWorkspace } = useProject();
@@ -99,26 +108,24 @@ export const ProjectSelect = () => {
}, [projects, projectFavorites, currentWorkspace]);
return (
<div className="-mr-2 flex w-full items-center gap-1">
<div className="mr-2 flex items-center gap-1 overflow-hidden">
<DropdownMenu modal={false}>
<Link
to={getProjectHomePage(currentWorkspace.type, currentWorkspace.environments)}
params={{
projectId: currentWorkspace.id
}}
className="group flex cursor-pointer items-center gap-x-1.5 overflow-hidden hover:text-white"
>
<div className="relative flex cursor-pointer items-center gap-2 text-sm text-white duration-100 hover:text-primary">
<Tooltip content={currentWorkspace.name} className="max-w-96 break-words">
<Badge
variant="project"
className="max-w-44 overflow-hidden text-sm text-ellipsis whitespace-nowrap"
>
<FontAwesomeIcon icon={faCube} />
{currentWorkspace?.name}
</Badge>
</Tooltip>
</div>
<p className="inline-block truncate text-mineshaft-200 group-hover:underline">
{currentWorkspace?.name}
</p>
<Badge variant="project" className="cursor-pointer">
<FontAwesomeIcon icon={faCube} />
<span>
{currentWorkspace.type ? PROJECT_TYPE_NAME[currentWorkspace.type] : "Project"}
</span>
</Badge>
</Link>
<DropdownMenuTrigger asChild>
<div>

View File

@@ -1,21 +1,10 @@
import { useTranslation } from "react-i18next";
import {
faArrowsSpin,
faBook,
faCheckToSlot,
faCog,
faHome,
faMobile,
faPlug,
faPuzzlePiece,
faUsers,
faVault
} from "@fortawesome/free-solid-svg-icons";
import { faMobile } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, Outlet, useLocation } from "@tanstack/react-router";
import { motion } from "framer-motion";
import { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
import { useProject, useProjectPermission } from "@app/context";
import {
useGetAccessRequestsCount,
@@ -28,10 +17,10 @@ import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePri
export const SecretManagerLayout = () => {
const { currentProject, projectId } = useProject();
const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
const { t } = useTranslation();
const projectSlug = currentProject?.slug || "";
const location = useLocation();
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({
projectId
@@ -54,208 +43,131 @@ export const SecretManagerLayout = () => {
return (
<>
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="vault" />
Secrets Manager
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/secret-management/$projectId/overview"
params={{
projectId: currentProject.id,
...(currentProject.environments.length
? { envSlug: currentProject.environments[0]?.slug }
: {})
}}
>
{({ isActive }) => (
<MenuItem
variant="project"
isSelected={
isActive ||
location.pathname.startsWith(
`/projects/secret-management/${currentProject.id}/overview`
)
}
>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faVault} />
</div>
Secrets
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPuzzlePiece} />
</div>
Integrations
</div>
</MenuItem>
)}
</Link>
{Boolean(secretRotations?.length) && (
<Link
to="/projects/secret-management/$projectId/secret-rotation"
params={{
projectId: currentProject.id
}}
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/secret-management/$projectId/overview"
params={{
projectId: currentProject.id,
...(currentProject.environments.length
? { envSlug: currentProject.environments[0]?.slug }
: {})
}}
>
{({ isActive }) => (
<Tab
value={
isActive || location.pathname.match(/\/secrets\/|\/commits\//)
? "selected"
: ""
}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faArrowsSpin} />
</div>
Secret Rotations
</div>
</MenuItem>
)}
</Link>
Overview
</Tab>
)}
<Link
to="/projects/secret-management/$projectId/approval"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCheckToSlot} />
</div>
Approvals
{Boolean(
secretApprovalReqCount?.open ||
accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/secret-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
variant="project"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
</Link>
</Menu>
</div>
<Link
to="/projects/secret-management/$projectId/approval"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>
Approvals
{Boolean(
secretApprovalReqCount?.open || accessApprovalRequestCount?.pendingCount
) && (
<Badge variant="primary" className="ml-1.5">
{pendingRequestsCount}
</Badge>
)}
</Tab>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/integrations"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Integrations</Tab>}
</Link>
{Boolean(secretRotations?.length) && (
<Link
to="/projects/secret-management/$projectId/secret-rotation"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>Secret Rotations</Tab>
)}
</Link>
)}
<Link
to="/projects/secret-management/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>App Connections</Tab>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/secret-management/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/secret-management/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
<div className="z-200 flex h-screen w-screen flex-col items-center justify-center bg-bunker-800 md:hidden">

View File

@@ -1,17 +1,7 @@
import {
faBook,
faCog,
faDatabase,
faHome,
faMagnifyingGlass,
faPlug,
faUsers
} from "@fortawesome/free-solid-svg-icons";
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 { Badge, Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Badge, Tab, TabList, Tabs } from "@app/components/v2";
import {
ProjectPermissionSub,
useProject,
@@ -29,6 +19,7 @@ export const SecretScanningLayout = () => {
const { permission } = useProjectPermission();
const { subscription } = useSubscription();
const location = useLocation();
const { data: unresolvedFindings } = useGetSecretScanningUnresolvedFindingCount(
currentProject.id,
@@ -45,158 +36,94 @@ export const SecretScanningLayout = () => {
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="secret-scan" />
Secret Scanning
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/secret-scanning/$projectId/data-sources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faDatabase} />
</div>
Data Sources
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/findings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex w-full gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faMagnifyingGlass} />
</div>
<span>Findings</span>
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="mr-2 ml-auto h-min">
{unresolvedFindings}
</Badge>
)}
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faPlug} />
</div>
App Connections
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/secret-scanning/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
variant="project"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/secret-scanning/$projectId/data-sources"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Data Sources</Tab>}
</Link>
</Menu>
</div>
<Link
to="/projects/secret-scanning/$projectId/findings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab value={isActive ? "selected" : ""}>
Findings
{Boolean(unresolvedFindings) && (
<Badge variant="primary" className="ml-2 h-min">
{unresolvedFindings}
</Badge>
)}
</Tab>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/app-connections"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>App Connections</Tab>}
</Link>
<Link
to="/projects/secret-scanning/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/secret-scanning/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/secret-scanning/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
);

View File

@@ -1,17 +1,8 @@
import {
faBook,
faCog,
faHome,
faServer,
faStamp,
faUsers
} from "@fortawesome/free-solid-svg-icons";
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 { ProjectPermissionCan } from "@app/components/permissions";
import { Lottie, Menu, MenuGroup, MenuItem } from "@app/components/v2";
import { Tab, TabList, Tabs } from "@app/components/v2";
import {
ProjectPermissionActions,
ProjectPermissionSub,
@@ -24,149 +15,104 @@ import { AssumePrivilegeModeBanner } from "../ProjectLayout/components/AssumePri
export const SshLayout = () => {
const { currentProject } = useProject();
const { assumedPrivilegeDetails } = useProjectPermission();
const location = useLocation();
return (
<div className="dark hidden h-full w-full flex-col overflow-x-hidden md:flex">
<div className="flex grow flex-col overflow-y-hidden md:flex-row">
<div className="border-b border-mineshaft-600 bg-mineshaft-900">
<motion.div
key="menu-project-items"
initial={{ x: -150 }}
animate={{ x: 0 }}
exit={{ x: -150 }}
transition={{ duration: 0.2 }}
className="dark w-full border-r border-mineshaft-600 bg-linear-to-tr from-mineshaft-700 via-mineshaft-800 to-mineshaft-900 md:w-60"
className="px-4"
>
<nav className="items-between flex h-full flex-col overflow-y-auto dark:scheme-dark">
<div className="flex items-center gap-3 border-b border-mineshaft-600 px-4 py-3.5 text-lg text-white">
<Lottie className="inline-block h-5 w-5 shrink-0" icon="terminal" />
SSH
</div>
<div className="flex-1">
<Menu>
<MenuGroup title="Resources">
<Link
to="/projects/ssh/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faServer} />
</div>
Hosts
</div>
</MenuItem>
)}
</Link>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) =>
isAllowed && (
<Link
to="/projects/ssh/$projectId/cas"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faStamp} />
</div>
Certificates Authority
</div>
</MenuItem>
)}
</Link>
)
}
</ProjectPermissionCan>
</MenuGroup>
<MenuGroup title="Others">
<Link
to="/projects/ssh/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faUsers} />
</div>
Project Access
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/ssh/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faBook} />
</div>
Audit Logs
</div>
</MenuItem>
)}
</Link>
<Link
to="/projects/ssh/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<MenuItem variant="project" isSelected={isActive}>
<div className="mx-1 flex gap-2">
<div className="w-6">
<FontAwesomeIcon icon={faCog} />
</div>
Project Settings
</div>
</MenuItem>
)}
</Link>
</MenuGroup>
</Menu>
</div>
<div>
<Menu>
<Link to="/organization/projects">
<MenuItem
variant="project"
className="relative flex items-center gap-2 overflow-hidden text-sm text-mineshaft-400 hover:text-mineshaft-300"
leftIcon={
<div className="w-6">
<FontAwesomeIcon className="mx-1 inline-block shrink-0" icon={faHome} />
</div>
}
>
Organization Home
</MenuItem>
<nav className="w-full">
<Tabs value="selected">
<TabList className="border-b-0">
<Link
to="/projects/ssh/$projectId/overview"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive || location.pathname.match(/\/ssh-host-groups\//) ? "selected" : ""
}
>
Hosts
</Tab>
)}
</Link>
</Menu>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Read}
a={ProjectPermissionSub.SshCertificateAuthorities}
>
{(isAllowed) =>
isAllowed && (
<Link
to="/projects/ssh/$projectId/cas"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={isActive || location.pathname.match(/\/ca\//) ? "selected" : ""}
>
Certificate Authorities
</Tab>
)}
</Link>
)
}
</ProjectPermissionCan>
<Link
to="/projects/ssh/$projectId/access-management"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => (
<Tab
value={
isActive ||
location.pathname.match(/\/groups\/|\/identities\/|\/members\/|\/roles\//)
? "selected"
: ""
}
>
Access Control
</Tab>
)}
</Link>
<Link
to="/projects/ssh/$projectId/audit-logs"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Audit Logs</Tab>}
</Link>
<Link
to="/projects/ssh/$projectId/settings"
params={{
projectId: currentProject.id
}}
>
{({ isActive }) => <Tab value={isActive ? "selected" : ""}>Settings</Tab>}
</Link>
</TabList>
</Tabs>
</nav>
</motion.div>
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 p-4 pt-8">
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<Outlet />
</div>
</div>
{assumedPrivilegeDetails && <AssumePrivilegeModeBanner />}
<div className="flex-1 overflow-x-hidden overflow-y-auto bg-bunker-800 px-12 pt-10 pb-4">
<Outlet />
</div>
</div>
);

View File

@@ -13,8 +13,8 @@ export const AccessManagementPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Access Control" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Access Control"

View File

@@ -13,8 +13,8 @@ export const AuthenticationPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Authentication"

View File

@@ -13,8 +13,8 @@ export const CachingPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Caching"

View File

@@ -13,8 +13,8 @@ export const EncryptionPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Encryption"

View File

@@ -13,8 +13,8 @@ export const EnvironmentPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Environment Variables"

View File

@@ -15,8 +15,8 @@ export const GeneralPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="General"

View File

@@ -13,8 +13,8 @@ export const IntegrationsPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Admin" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Integrations"

View File

@@ -13,18 +13,24 @@ export const ResourceOverviewPage = () => {
<Helmet>
<title>{t("common.head-title", { title: "Resource Overview" })}</title>
</Helmet>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="instance"
title="Resource Overview"
description="Manage resources within your Infisical instance."
/>
<Tabs defaultValue="tab-organizations">
<Tabs orientation="vertical" defaultValue="tab-organizations">
<TabList>
<Tab value="tab-organizations">Organizations</Tab>
<Tab value="tab-users">Users</Tab>
<Tab value="tab-identities">Identities</Tab>
<Tab variant="instance" value="tab-organizations">
Organizations
</Tab>
<Tab variant="instance" value="tab-users">
Users
</Tab>
<Tab variant="instance" value="tab-identities">
Identities
</Tab>
</TabList>
<TabPanel value="tab-organizations">
<OrganizationsTable />

View File

@@ -4,19 +4,20 @@ import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types";
import { PkiAlertsSection } from "./components";
export const AlertingPage = () => {
const { t } = useTranslation();
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Alerting" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.CertificateManager}
title="Alerting"
description="Configure alerts for expiring certificates and CAs to maintain security and compliance."
/>

View File

@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet";
import { useNavigate, useParams } from "@tanstack/react-router";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { createNotification } from "@app/components/notifications";
@@ -18,6 +20,7 @@ import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useProject } from "@app/context";
import { CaType, useDeleteCa, useGetCa } from "@app/hooks/api";
import { TInternalCertificateAuthority } from "@app/hooks/api/ca/types";
import { ProjectType } from "@app/hooks/api/projects/types";
import { usePopUp } from "@app/hooks/usePopUp";
import { CaInstallCertModal } from "../CertificateAuthoritiesPage/components/CaInstallCertModal";
@@ -84,10 +87,24 @@ const Page = () => {
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader scope="project" title={data.name}>
<div className="mx-auto mb-6 w-full max-w-8xl">
<Link
to="/projects/cert-management/$projectId/certificate-authorities"
params={{
projectId
}}
className="mb-4 flex items-center gap-x-2 text-sm text-mineshaft-400"
>
<FontAwesomeIcon icon={faChevronLeft} />
Certificate Authorities
</Link>
<PageHeader
scope={ProjectType.CertificateManager}
description="Manage certificate authority"
title={data.name}
>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">

View File

@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { ProjectPermissionCan } from "@app/components/permissions";
import { PageHeader } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types";
import { ExternalCaSection } from "./components/ExternalCaSection";
import { CaSection } from "./components";
@@ -11,13 +12,13 @@ import { CaSection } from "./components";
export const CertificateAuthoritiesPage = () => {
const { t } = useTranslation();
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Certificate Authorities" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.CertificateManager}
title="Certificate Authorities"
description="Manage certificate authorities for issuing and signing certificates"
/>

View File

@@ -9,6 +9,7 @@ import {
ProjectPermissionSub,
useProjectPermission
} from "@app/context";
import { ProjectType } from "@app/hooks/api/projects/types";
import { PkiCollectionSection } from "../AlertingPage/components";
import { CertificatesSection } from "./components";
@@ -27,13 +28,13 @@ export const CertificatesPage = () => {
);
return (
<div className="container mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<div className="mx-auto flex h-full flex-col justify-between bg-bunker-800 text-white">
<Helmet>
<title>{t("common.head-title", { title: "Certificates" })}</title>
</Helmet>
<div className="mx-auto mb-6 w-full max-w-7xl">
<div className="mx-auto mb-6 w-full max-w-8xl">
<PageHeader
scope="project"
scope={ProjectType.CertificateManager}
title="Certificates"
description="View and track issued certificates, monitor expiration dates, and manage certificate lifecycles."
/>

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