Compare commits

...

34 Commits

Author SHA1 Message Date
x032205
2e256e4282 Tooltip 2025-06-26 18:14:48 -04:00
x032205
dcd21883d1 Clarify relationship between path and key schema for AWS parameter store
docs
2025-06-26 17:02:21 -04:00
Scott Wilson
205442bff5 Merge pull request #3859 from Infisical/overview-ui-improvements
improvement(secret-overview): Add collapsed environment view to secret overview page
2025-06-26 09:24:33 -07:00
Scott Wilson
e8d19eb823 improvement: disable tooltip hover content for env name tooltip 2025-06-26 09:12:11 -07:00
Scott Wilson
5d30215ea7 improvement: increase env tooltip max width and adjust alignment 2025-06-26 07:56:47 -07:00
Scott Wilson
29fedfdde5 Merge pull request #3850 from Infisical/policy-edit-revisions
improvement(project-policies): Revamp edit role page and access tree
2025-06-26 07:46:35 -07:00
Scott Wilson
b5317d1d75 fix: add ability to remove non-conditional rules 2025-06-26 07:37:30 -07:00
Scott Wilson
86c145301e improvement: add collapsed environment view to secret overview page and minor ui adjustments 2025-06-25 16:49:34 -07:00
carlosmonastyrski
6446311b6d Merge pull request #3835 from Infisical/feat/gitlabSecretSync
feat(secret-sync): Add gitlab secret sync
2025-06-25 17:53:12 -03:00
Daniel Hougaard
3e80f1907c Merge pull request #3857 from Infisical/daniel/fix-dotnet-docs
docs: fix redirect for .NET SDK
2025-06-25 23:18:14 +04:00
Daniel Hougaard
79e62eec25 docs: fix redirect for .NET SDK 2025-06-25 23:11:11 +04:00
Daniel Hougaard
c41730c5fb Merge pull request #3856 from Infisical/daniel/fix-docs
fix(docs): sdk and changelog tab not loading
2025-06-25 22:34:09 +04:00
Daniel Hougaard
aac63d3097 fix(docs): sdk and changelog tab not working 2025-06-25 22:32:08 +04:00
x032205
1f7617d132 Merge pull request #3851 from Infisical/ENG-3013
Allow undefined value for tags to prevent unwanted overrides
2025-06-25 12:45:43 -04:00
x032205
18f1f93b5f Review fixes 2025-06-25 12:29:23 -04:00
Scott Wilson
5b4790ee78 improvements: truncate environment selection and only show visualize access when expanded 2025-06-25 09:09:08 -07:00
x032205
5ab2a6bb5d Feedback 2025-06-25 11:56:11 -04:00
Scott Wilson
dcac85fe6c Merge pull request #3847 from Infisical/share-your-own-secret-link-fix
fix(secret-sharing): Support self-hosted for "share your own secret" link
2025-06-25 08:31:13 -07:00
Maidul Islam
2f07471404 Merge pull request #3853 from akhilmhdh/feat/copy-token
feat: added copy token button
2025-06-25 10:55:07 -04:00
Maidul Islam
137fd5ef07 added minor text updates 2025-06-25 10:50:16 -04:00
=
883c7835a1 feat: added copy token button 2025-06-25 15:28:58 +05:30
x032205
9f6dca23db Greptile reviews 2025-06-24 23:19:42 -04:00
x032205
f0a95808e7 Allow undefined value for tags to prevent unwanted overrides 2025-06-24 23:13:53 -04:00
x032205
90a0d0f744 Merge pull request #3848 from Infisical/improve-audit-log-streams
improve audit log streams: add backend logs + DD source
2025-06-24 22:18:04 -04:00
x032205
7f9c9be2c8 review fix 2025-06-24 22:00:45 -04:00
Scott Wilson
8683693103 improvement: address greptile feedback 2025-06-24 15:35:42 -07:00
Scott Wilson
737fffcceb improvement: address greptile feedback 2025-06-24 15:35:08 -07:00
Scott Wilson
ffac24ce75 improvement: revise edit role page and access tree 2025-06-24 15:23:27 -07:00
x032205
6566393e21 Review fixes 2025-06-24 14:39:46 -04:00
x032205
af245b1f16 Add "service: audit-logs" entry for DataDog 2025-06-24 14:22:26 -04:00
x032205
c17df7e951 Improve URL detection 2025-06-24 12:44:16 -04:00
x032205
4d4953e95a improve audit log streams: add backend logs + DD source 2025-06-24 12:35:49 -04:00
Scott Wilson
198e74cd88 fix: include nooppener in window.open 2025-06-23 18:05:48 -07:00
Scott Wilson
8ed0a1de84 fix: correct window open for share your own secret link to handle self-hosted 2025-06-23 18:01:38 -07:00
51 changed files with 1408 additions and 2205 deletions

View File

@@ -0,0 +1,21 @@
export function providerSpecificPayload(url: string) {
const { hostname } = new URL(url);
const payload: Record<string, string> = {};
switch (hostname) {
case "http-intake.logs.datadoghq.com":
case "http-intake.logs.us3.datadoghq.com":
case "http-intake.logs.us5.datadoghq.com":
case "http-intake.logs.datadoghq.eu":
case "http-intake.logs.ap1.datadoghq.com":
case "http-intake.logs.ddog-gov.com":
payload.ddsource = "infisical";
payload.service = "audit-logs";
break;
default:
break;
}
return payload;
}

View File

@@ -13,6 +13,7 @@ import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission"; import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service-types"; import { TPermissionServiceFactory } from "../permission/permission-service-types";
import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal"; import { TAuditLogStreamDALFactory } from "./audit-log-stream-dal";
import { providerSpecificPayload } from "./audit-log-stream-fns";
import { LogStreamHeaders, TAuditLogStreamServiceFactory } from "./audit-log-stream-types"; import { LogStreamHeaders, TAuditLogStreamServiceFactory } from "./audit-log-stream-types";
type TAuditLogStreamServiceFactoryDep = { type TAuditLogStreamServiceFactoryDep = {
@@ -69,10 +70,11 @@ export const auditLogStreamServiceFactory = ({
headers.forEach(({ key, value }) => { headers.forEach(({ key, value }) => {
streamHeaders[key] = value; streamHeaders[key] = value;
}); });
await request await request
.post( .post(
url, url,
{ ping: "ok" }, { ...providerSpecificPayload(url), ping: "ok" },
{ {
headers: streamHeaders, headers: streamHeaders,
// request timeout // request timeout
@@ -137,7 +139,7 @@ export const auditLogStreamServiceFactory = ({
await request await request
.post( .post(
url || logStream.url, url || logStream.url,
{ ping: "ok" }, { ...providerSpecificPayload(url || logStream.url), ping: "ok" },
{ {
headers: streamHeaders, headers: streamHeaders,
// request timeout // request timeout

View File

@@ -1,13 +1,15 @@
import { RawAxiosRequestHeaders } from "axios"; import { AxiosError, RawAxiosRequestHeaders } from "axios";
import { SecretKeyEncoding } from "@app/db/schemas"; import { SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request"; import { request } from "@app/lib/config/request";
import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption"; import { infisicalSymmetricDecrypt } from "@app/lib/crypto/encryption";
import { logger } from "@app/lib/logger";
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal"; import { TAuditLogStreamDALFactory } from "../audit-log-stream/audit-log-stream-dal";
import { providerSpecificPayload } from "../audit-log-stream/audit-log-stream-fns";
import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types"; import { LogStreamHeaders } from "../audit-log-stream/audit-log-stream-types";
import { TLicenseServiceFactory } from "../license/license-service"; import { TLicenseServiceFactory } from "../license/license-service";
import { TAuditLogDALFactory } from "./audit-log-dal"; import { TAuditLogDALFactory } from "./audit-log-dal";
@@ -128,13 +130,29 @@ export const auditLogQueueServiceFactory = async ({
headers[key] = value; headers[key] = value;
}); });
return request.post(url, auditLog, { try {
headers, logger.info(`Streaming audit log [url=${url}] for org [orgId=${orgId}]`);
// request timeout const response = await request.post(
timeout: AUDIT_LOG_STREAM_TIMEOUT, url,
// connection timeout { ...providerSpecificPayload(url), ...auditLog },
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) {
}); headers,
// request timeout
timeout: AUDIT_LOG_STREAM_TIMEOUT,
// connection timeout
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
}
);
logger.info(
`Successfully streamed audit log [url=${url}] for org [orgId=${orgId}] [response=${JSON.stringify(response.data)}]`
);
return response;
} catch (error) {
logger.error(
`Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]`
);
return error;
}
} }
) )
); );
@@ -218,13 +236,29 @@ export const auditLogQueueServiceFactory = async ({
headers[key] = value; headers[key] = value;
}); });
return request.post(url, auditLog, { try {
headers, logger.info(`Streaming audit log [url=${url}] for org [orgId=${orgId}]`);
// request timeout const response = await request.post(
timeout: AUDIT_LOG_STREAM_TIMEOUT, url,
// connection timeout { ...providerSpecificPayload(url), ...auditLog },
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT) {
}); headers,
// request timeout
timeout: AUDIT_LOG_STREAM_TIMEOUT,
// connection timeout
signal: AbortSignal.timeout(AUDIT_LOG_STREAM_TIMEOUT)
}
);
logger.info(
`Successfully streamed audit log [url=${url}] for org [orgId=${orgId}] [response=${JSON.stringify(response.data)}]`
);
return response;
} catch (error) {
logger.error(
`Failed to stream audit log [url=${url}] for org [orgId=${orgId}] [error=${(error as AxiosError).message}]`
);
return error;
}
} }
) )
); );

View File

@@ -307,7 +307,6 @@ export const AwsParameterStoreSyncFns = {
awsParameterStoreSecretsRecord, awsParameterStoreSecretsRecord,
Boolean(syncOptions.tags?.length || syncOptions.syncSecretMetadataAsTags) Boolean(syncOptions.tags?.length || syncOptions.syncSecretMetadataAsTags)
); );
const syncTagsRecord = Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []);
for await (const entry of Object.entries(secretMap)) { for await (const entry of Object.entries(secretMap)) {
const [key, { value, secretMetadata }] = entry; const [key, { value, secretMetadata }] = entry;
@@ -342,13 +341,13 @@ export const AwsParameterStoreSyncFns = {
} }
} }
if (shouldManageTags) { if ((syncOptions.tags !== undefined || syncOptions.syncSecretMetadataAsTags) && shouldManageTags) {
const { tagsToAdd, tagKeysToRemove } = processParameterTags({ const { tagsToAdd, tagKeysToRemove } = processParameterTags({
syncTagsRecord: { syncTagsRecord: {
// configured sync tags take preference over secret metadata // configured sync tags take preference over secret metadata
...(syncOptions.syncSecretMetadataAsTags && ...(syncOptions.syncSecretMetadataAsTags &&
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])), Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
...syncTagsRecord ...(syncOptions.tags && Object.fromEntries(syncOptions.tags?.map((tag) => [tag.key, tag.value]) ?? []))
}, },
awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {} awsTagsRecord: awsParameterStoreTagsRecord[key] ?? {}
}); });

View File

@@ -366,37 +366,39 @@ export const AwsSecretsManagerSyncFns = {
} }
} }
const { tagsToAdd, tagKeysToRemove } = processTags({ if (syncOptions.tags !== undefined || syncOptions.syncSecretMetadataAsTags) {
syncTagsRecord: { const { tagsToAdd, tagKeysToRemove } = processTags({
// configured sync tags take preference over secret metadata syncTagsRecord: {
...(syncOptions.syncSecretMetadataAsTags && // configured sync tags take preference over secret metadata
Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])), ...(syncOptions.syncSecretMetadataAsTags &&
...syncTagsRecord Object.fromEntries(secretMetadata?.map((tag) => [tag.key, tag.value]) ?? [])),
}, ...(syncOptions.tags !== undefined && syncTagsRecord)
awsTagsRecord: Object.fromEntries( },
awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? [] awsTagsRecord: Object.fromEntries(
) awsDescriptionsRecord[key]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
}); )
});
if (tagsToAdd.length) { if (tagsToAdd.length) {
try { try {
await addTags(client, key, tagsToAdd); await addTags(client, key, tagsToAdd);
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
error, error,
secretKey: key secretKey: key
}); });
}
} }
}
if (tagKeysToRemove.length) { if (tagKeysToRemove.length) {
try { try {
await removeTags(client, key, tagKeysToRemove); await removeTags(client, key, tagKeysToRemove);
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
error, error,
secretKey: key secretKey: key
}); });
}
} }
} }
} }
@@ -439,32 +441,34 @@ export const AwsSecretsManagerSyncFns = {
}); });
} }
const { tagsToAdd, tagKeysToRemove } = processTags({ if (syncOptions.tags !== undefined) {
syncTagsRecord, const { tagsToAdd, tagKeysToRemove } = processTags({
awsTagsRecord: Object.fromEntries( syncTagsRecord,
awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? [] awsTagsRecord: Object.fromEntries(
) awsDescriptionsRecord[destinationConfig.secretName]?.Tags?.map((tag) => [tag.Key!, tag.Value!]) ?? []
}); )
});
if (tagsToAdd.length) { if (tagsToAdd.length) {
try { try {
await addTags(client, destinationConfig.secretName, tagsToAdd); await addTags(client, destinationConfig.secretName, tagsToAdd);
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
error, error,
secretKey: destinationConfig.secretName secretKey: destinationConfig.secretName
}); });
}
} }
}
if (tagKeysToRemove.length) { if (tagKeysToRemove.length) {
try { try {
await removeTags(client, destinationConfig.secretName, tagKeysToRemove); await removeTags(client, destinationConfig.secretName, tagKeysToRemove);
} catch (error) { } catch (error) {
throw new SecretSyncError({ throw new SecretSyncError({
error, error,
secretKey: destinationConfig.secretName secretKey: destinationConfig.secretName
}); });
}
} }
} }
} }

View File

@@ -2045,7 +2045,7 @@
"tab": "SDKs", "tab": "SDKs",
"groups": [ "groups": [
{ {
"group": "", "group": "Overview",
"pages": ["sdks/overview"] "pages": ["sdks/overview"]
}, },
{ {
@@ -2065,7 +2065,7 @@
"tab": "Changelog", "tab": "Changelog",
"groups": [ "groups": [
{ {
"group": "", "group": "Overview",
"pages": ["changelog/overview"] "pages": ["changelog/overview"]
} }
] ]

View File

@@ -148,3 +148,11 @@ description: "Learn how to configure an AWS Parameter Store Sync for Infisical."
``` ```
</Tab> </Tab>
</Tabs> </Tabs>
## FAQ
<AccordionGroup>
<Accordion title="What's the relationship between 'path' and 'key schema'?">
The path is required and will be prepended to the key schema. For example, if you have a path of `/demo/path/` and a key schema of `INFISICAL_{{secretKey}}`, then the result will be `/demo/path/INFISICAL_{{secretKey}}`.
</Accordion>
</AccordionGroup>

View File

@@ -1,7 +1,7 @@
--- ---
title: "Infisical Java SDK" title: "Infisical Java SDK"
sidebarTitle: "Java" sidebarTitle: "Java"
url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-nodejs-sdk" url: "https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-java-sdk"
icon: "java" icon: "java"
--- ---

View File

@@ -1,7 +1,7 @@
--- ---
title: "Infisical Node.js SDK" title: "Infisical Node.js SDK"
sidebarTitle: "Node.js" sidebarTitle: "Node.js"
url: "https://github.com/Infisical/node-sdk-v2" url: "https://github.com/Infisical/node-sdk-v2?tab=readme-ov-file#infisical-nodejs-sdk"
icon: "node" icon: "node"
--- ---

View File

@@ -43,7 +43,7 @@ def hello_world():
This example demonstrates how to use the Infisical Python SDK with a Flask application. The application retrieves a secret named "NAME" and responds to requests with a greeting that includes the secret value. This example demonstrates how to use the Infisical Python SDK with a Flask application. The application retrieves a secret named "NAME" and responds to requests with a greeting that includes the secret value.
<Warning> <Warning>
We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best. We do not recommend hardcoding your [Machine Identity Tokens](/platform/identities/overview). Setting it as an environment variable would be best.
</Warning> </Warning>
## Installation ## Installation
@@ -314,32 +314,32 @@ By default, `getSecret()` fetches and returns a shared secret. If not found, it
#### Parameters #### Parameters
<ParamField query="Parameters" type="object" optional> <ParamField query="Parameters" type="object" optional>
<Expandable title="properties"> <Expandable title="properties">
<ParamField query="secret_name" type="string" required> <ParamField query="secret_name" type="string" required>
The key of the secret to retrieve The key of the secret to retrieve
</ParamField> </ParamField>
<ParamField query="include_imports" type="boolean"> <ParamField query="include_imports" type="boolean">
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference) Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
</ParamField> </ParamField>
<ParamField query="environment" type="string" required> <ParamField query="environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField> </ParamField>
<ParamField query="project_id" type="string" required> <ParamField query="project_id" type="string" required>
The project ID where the secret lives in. The project ID where the secret lives in.
</ParamField> </ParamField>
<ParamField query="path" type="string" optional> <ParamField query="path" type="string" optional>
The path from where secret should be fetched from. The path from where secret should be fetched from.
</ParamField> </ParamField>
<ParamField query="type" type="string" optional> <ParamField query="type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "personal". The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "personal".
</ParamField> </ParamField>
<ParamField query="include_imports" type="boolean" default="false" optional> <ParamField query="include_imports" type="boolean" default="false" optional>
Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference) Whether or not to include imported secrets from the current path. Read about [secret import](/documentation/platform/secret-reference)
</ParamField> </ParamField>
<ParamField query="expand_secret_references" type="boolean" default="true" optional> <ParamField query="expand_secret_references" type="boolean" default="true" optional>
Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference) Whether or not to expand secret references in the fetched secrets. Read about [secret reference](/documentation/platform/secret-reference)
</ParamField> </ParamField>
</Expandable> </Expandable>
</ParamField> </ParamField>
### client.createSecret(options) ### client.createSecret(options)
@@ -358,26 +358,26 @@ Create a new secret in Infisical.
#### Parameters #### Parameters
<ParamField query="Parameters" type="object" optional> <ParamField query="Parameters" type="object" optional>
<Expandable title="properties"> <Expandable title="properties">
<ParamField query="secret_name" type="string" required> <ParamField query="secret_name" type="string" required>
The key of the secret to create. The key of the secret to create.
</ParamField> </ParamField>
<ParamField query="secret_value" type="string" required> <ParamField query="secret_value" type="string" required>
The value of the secret. The value of the secret.
</ParamField> </ParamField>
<ParamField query="project_id" type="string" required> <ParamField query="project_id" type="string" required>
The project ID where the secret lives in. The project ID where the secret lives in.
</ParamField> </ParamField>
<ParamField query="environment" type="string" required> <ParamField query="environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField> </ParamField>
<ParamField query="path" type="string" optional> <ParamField query="path" type="string" optional>
The path from where secret should be created. The path from where secret should be created.
</ParamField> </ParamField>
<ParamField query="type" type="string" optional> <ParamField query="type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
</ParamField> </ParamField>
</Expandable> </Expandable>
</ParamField> </ParamField>
### client.updateSecret(options) ### client.updateSecret(options)
@@ -396,26 +396,26 @@ Update an existing secret in Infisical.
#### Parameters #### Parameters
<ParamField query="Parameters" type="object" optional> <ParamField query="Parameters" type="object" optional>
<Expandable title="properties"> <Expandable title="properties">
<ParamField query="secret_name" type="string" required> <ParamField query="secret_name" type="string" required>
The key of the secret to update. The key of the secret to update.
</ParamField> </ParamField>
<ParamField query="secret_value" type="string" required> <ParamField query="secret_value" type="string" required>
The new value of the secret. The new value of the secret.
</ParamField> </ParamField>
<ParamField query="project_id" type="string" required> <ParamField query="project_id" type="string" required>
The project ID where the secret lives in. The project ID where the secret lives in.
</ParamField> </ParamField>
<ParamField query="environment" type="string" required> <ParamField query="environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField> </ParamField>
<ParamField query="path" type="string" optional> <ParamField query="path" type="string" optional>
The path from where secret should be updated. The path from where secret should be updated.
</ParamField> </ParamField>
<ParamField query="type" type="string" optional> <ParamField query="type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
</ParamField> </ParamField>
</Expandable> </Expandable>
</ParamField> </ParamField>
### client.deleteSecret(options) ### client.deleteSecret(options)
@@ -433,23 +433,23 @@ Delete a secret in Infisical.
#### Parameters #### Parameters
<ParamField query="Parameters" type="object" optional> <ParamField query="Parameters" type="object" optional>
<Expandable title="properties"> <Expandable title="properties">
<ParamField query="secret_name" type="string"> <ParamField query="secret_name" type="string">
The key of the secret to update. The key of the secret to update.
</ParamField> </ParamField>
<ParamField query="project_id" type="string" required> <ParamField query="project_id" type="string" required>
The project ID where the secret lives in. The project ID where the secret lives in.
</ParamField> </ParamField>
<ParamField query="environment" type="string" required> <ParamField query="environment" type="string" required>
The slug name (dev, prod, etc) of the environment from where secrets should be fetched from. The slug name (dev, prod, etc) of the environment from where secrets should be fetched from.
</ParamField> </ParamField>
<ParamField query="path" type="string" optional> <ParamField query="path" type="string" optional>
The path from where secret should be deleted. The path from where secret should be deleted.
</ParamField> </ParamField>
<ParamField query="type" type="string" optional> <ParamField query="type" type="string" optional>
The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared". The type of the secret. Valid options are "shared" or "personal". If not specified, the default value is "shared".
</ParamField> </ParamField>
</Expandable> </Expandable>
</ParamField> </ParamField>
## Cryptography ## Cryptography
@@ -480,14 +480,14 @@ encryptedData = client.encryptSymmetric(encryptOptions)
#### Parameters #### Parameters
<ParamField query="Parameters" type="object" required> <ParamField query="Parameters" type="object" required>
<Expandable title="properties"> <Expandable title="properties">
<ParamField query="plaintext" type="string"> <ParamField query="plaintext" type="string">
The plaintext you want to encrypt. The plaintext you want to encrypt.
</ParamField> </ParamField>
<ParamField query="key" type="string" required> <ParamField query="key" type="string" required>
The symmetric key to use for encryption. The symmetric key to use for encryption.
</ParamField> </ParamField>
</Expandable> </Expandable>
</ParamField> </ParamField>
#### Returns (object) #### Returns (object)
@@ -512,20 +512,20 @@ decryptedString = client.decryptSymmetric(decryptOptions)
#### Parameters #### Parameters
<ParamField query="Parameters" type="object" required> <ParamField query="Parameters" type="object" required>
<Expandable title="properties"> <Expandable title="properties">
<ParamField query="ciphertext" type="string"> <ParamField query="ciphertext" type="string">
The ciphertext you want to decrypt. The ciphertext you want to decrypt.
</ParamField> </ParamField>
<ParamField query="key" type="string" required> <ParamField query="key" type="string" required>
The symmetric key to use for encryption. The symmetric key to use for encryption.
</ParamField> </ParamField>
<ParamField query="iv" type="string" required> <ParamField query="iv" type="string" required>
The initialization vector to use for decryption. The initialization vector to use for decryption.
</ParamField> </ParamField>
<ParamField query="tag" type="string" required> <ParamField query="tag" type="string" required>
The authentication tag to use for decryption. The authentication tag to use for decryption.
</ParamField> </ParamField>
</Expandable> </Expandable>
</ParamField> </ParamField>
#### Returns (string) #### Returns (string)

View File

@@ -10,24 +10,23 @@ From local development to production, Infisical SDKs provide the easiest way for
- Fetch secrets on demand - Fetch secrets on demand
<CardGroup cols={2}> <CardGroup cols={2}>
<Card title="Node" href="https://github.com/Infisical/node-sdk-v2" icon="node" color="#68a063"> <Card title="Node.js" href="https://github.com/Infisical/node-sdk-v2?tab=readme-ov-file#infisical-nodejs-sdk" icon="node" color="#68a063">
Manage secrets for your Node application on demand Manage secrets for your Node application on demand
</Card> </Card>
<Card href="https://github.com/Infisical/python-sdk-official" title="Python" icon="python" color="#4c8abe"> <Card href="https://github.com/Infisical/python-sdk-official?tab=readme-ov-file#infisical-python-sdk" title="Python" icon="python" color="#4c8abe">
Manage secrets for your Python application on demand Manage secrets for your Python application on demand
</Card> </Card>
<Card href="https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-nodejs-sdk" title="Java" icon="java" color="#e41f23"> <Card href="https://github.com/Infisical/java-sdk?tab=readme-ov-file#infisical-java-sdk" title="Java" icon="java" color="#e41f23">
Manage secrets for your Java application on demand Manage secrets for your Java application on demand
</Card> </Card>
<Card href="/sdks/languages/go" title="Go" icon="golang" color="#367B99"> <Card href="/sdks/languages/go" title="Go" icon="golang" color="#367B99">
Manage secrets for your Go application on demand Manage secrets for your Go application on demand
</Card> </Card>
<Card href="/sdks/languages/csharp" title="C#" icon="bars" color="#368833"> <Card href="https://github.com/Infisical/infisical-dotnet-sdk?tab=readme-ov-file#infisical-net-sdk" title=".NET" icon="bars" color="#368833">
Manage secrets for your C#/.NET application on demand Manage secrets for your .NET application on demand
</Card> </Card>
<Card href="/sdks/languages/ruby" title="Ruby" icon="diamond" color="#367B99">
<Card href="/sdks/languages/ruby" title="Ruby" icon="diamond" color="#367B99"> Manage secrets for your Ruby application on demand
Manage secrets for your Ruby application on demand
</Card> </Card>
</CardGroup> </CardGroup>

View File

@@ -2,10 +2,9 @@ import { useCallback, useEffect, useState } from "react";
import { MongoAbility, MongoQuery } from "@casl/ability"; import { MongoAbility, MongoQuery } from "@casl/ability";
import { import {
faAnglesUp, faAnglesUp,
faArrowUpRightFromSquare,
faDownLeftAndUpRightToCenter,
faUpRightAndDownLeftFromCenter, faUpRightAndDownLeftFromCenter,
faWindowRestore faWindowRestore,
faXmark
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import {
@@ -23,8 +22,8 @@ import {
} from "@xyflow/react"; } from "@xyflow/react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Button, IconButton, Spinner, Tooltip } from "@app/components/v2"; import { Button, IconButton, Select, SelectItem, Spinner, Tooltip } from "@app/components/v2";
import { ProjectPermissionSet } from "@app/context/ProjectPermissionContext"; import { ProjectPermissionSet, ProjectPermissionSub } from "@app/context/ProjectPermissionContext";
import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput"; import { AccessTreeSecretPathInput } from "./nodes/FolderNode/components/AccessTreeSecretPathInput";
import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode"; import { ShowMoreButtonNode } from "./nodes/ShowMoreButtonNode";
@@ -36,15 +35,17 @@ import { ViewMode } from "./types";
export type AccessTreeProps = { export type AccessTreeProps = {
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>; permissions: MongoAbility<ProjectPermissionSet, MongoQuery>;
subject: ProjectPermissionSub;
onClose: () => void;
}; };
const EdgeTypes = { base: BasePermissionEdge }; const EdgeTypes = { base: BasePermissionEdge };
const NodeTypes = { role: RoleNode, folder: FolderNode, showMoreButton: ShowMoreButtonNode }; const NodeTypes = { role: RoleNode, folder: FolderNode, showMoreButton: ShowMoreButtonNode };
const AccessTreeContent = ({ permissions }: AccessTreeProps) => { const AccessTreeContent = ({ permissions, subject, onClose }: AccessTreeProps) => {
const [selectedPath, setSelectedPath] = useState<string>("/"); const [selectedPath, setSelectedPath] = useState<string>("/");
const accessTreeData = useAccessTree(permissions, selectedPath); const accessTreeData = useAccessTree(permissions, selectedPath, subject);
const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData; const { edges, nodes, isLoading, viewMode, setViewMode, environment } = accessTreeData;
const [initialRender, setInitialRender] = useState(true); const [initialRender, setInitialRender] = useState(true);
@@ -78,32 +79,32 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
useEffect(() => { useEffect(() => {
setInitialRender(true); setInitialRender(true);
}, [selectedPath, environment]); }, [selectedPath, environment, subject, viewMode]);
useEffect(() => { useEffect(() => {
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
if (initialRender) { if (initialRender) {
timer = setTimeout(() => { timer = setTimeout(() => {
goToRootNode(); fitView({ duration: 500 });
setInitialRender(false); setInitialRender(false);
}, 500); }, 50);
} }
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [nodes, edges, getViewport(), initialRender, goToRootNode]); }, [nodes, edges, getViewport(), initialRender, fitView]);
const handleToggleModalView = () => const handleToggleModalView = () =>
setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal)); setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Docked : ViewMode.Modal));
const handleToggleUndockedView = () => const handleToggleView = () =>
setViewMode((prev) => (prev === ViewMode.Undocked ? ViewMode.Docked : ViewMode.Undocked)); setViewMode((prev) => (prev === ViewMode.Modal ? ViewMode.Undocked : ViewMode.Modal));
const undockButtonLabel = `${viewMode === ViewMode.Undocked ? "Dock" : "Undock"} View`; const expandButtonLabel = viewMode === ViewMode.Modal ? "Anchor View" : "Expand View";
const windowButtonLabel = `${viewMode === ViewMode.Modal ? "Dock" : "Expand"} View`; const hideButtonLabel = "Hide Access Tree";
return ( return (
<div <div
className={twMerge( className={twMerge(
"w-full", "mt-4 w-full",
viewMode === ViewMode.Modal && "fixed inset-0 z-50 p-10", viewMode === ViewMode.Modal && "fixed inset-0 z-50 p-10",
viewMode === ViewMode.Undocked && viewMode === ViewMode.Undocked &&
"fixed bottom-4 left-20 z-50 h-[40%] w-[38%] min-w-[32rem] lg:w-[34%]" "fixed bottom-4 left-20 z-50 h-[40%] w-[38%] min-w-[32rem] lg:w-[34%]"
@@ -130,7 +131,7 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
type="submit" type="submit"
className="h-10 rounded-r-none bg-mineshaft-700" className="h-10 rounded-r-none bg-mineshaft-700"
leftIcon={<FontAwesomeIcon icon={faWindowRestore} />} leftIcon={<FontAwesomeIcon icon={faWindowRestore} />}
onClick={handleToggleUndockedView} onClick={handleToggleView}
> >
Undock Undock
</Button> </Button>
@@ -176,48 +177,62 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
<Spinner /> <Spinner />
</Panel> </Panel>
)} )}
{viewMode !== ViewMode.Undocked && (
<Panel position="top-left" className="flex gap-2">
<Select
value={environment}
onValueChange={accessTreeData.setEnvironment}
className="w-60"
position="popper"
dropdownContainerClassName="max-w-none"
aria-label="Environment"
>
{Object.values(accessTreeData.environments).map((env) => (
<SelectItem
key={env.slug}
value={env.slug}
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 truncate font-medium">{env.name}</div>
</SelectItem>
))}
</Select>
<AccessTreeSecretPathInput
placeholder="Provide a path, default is /"
environment={environment}
value={selectedPath}
onChange={setSelectedPath}
/>
</Panel>
)}
{viewMode !== ViewMode.Docked && ( {viewMode !== ViewMode.Docked && (
<Panel position="top-right" className="flex gap-1.5"> <Panel position="top-right" className="flex gap-2">
{viewMode !== ViewMode.Undocked && ( <Tooltip position="bottom" align="center" content={expandButtonLabel}>
<AccessTreeSecretPathInput
placeholder="Provide a path, default is /"
environment={environment}
value={selectedPath}
onChange={setSelectedPath}
/>
)}
<Tooltip position="bottom" align="center" content={undockButtonLabel}>
<IconButton <IconButton
className="ml-1 w-10 rounded" className="rounded p-2"
colorSchema="secondary" colorSchema="secondary"
variant="plain" variant="plain"
onClick={handleToggleUndockedView} onClick={handleToggleView}
ariaLabel={undockButtonLabel} ariaLabel={expandButtonLabel}
> >
<FontAwesomeIcon <FontAwesomeIcon
icon={ icon={
viewMode === ViewMode.Undocked viewMode === ViewMode.Undocked
? faArrowUpRightFromSquare ? faUpRightAndDownLeftFromCenter
: faWindowRestore : faWindowRestore
} }
/> />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Tooltip align="end" position="bottom" content={windowButtonLabel}> <Tooltip align="end" position="bottom" content={hideButtonLabel}>
<IconButton <IconButton
className="w-10 rounded" className="rounded p-2"
colorSchema="secondary" colorSchema="secondary"
variant="plain" variant="plain"
onClick={handleToggleModalView} onClick={onClose}
ariaLabel={windowButtonLabel} ariaLabel={hideButtonLabel}
> >
<FontAwesomeIcon <FontAwesomeIcon icon={faXmark} />
icon={
viewMode === ViewMode.Modal
? faDownLeftAndUpRightToCenter
: faUpRightAndDownLeftFromCenter
}
/>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Panel> </Panel>
@@ -253,6 +268,9 @@ const AccessTreeContent = ({ permissions }: AccessTreeProps) => {
}; };
export const AccessTree = (props: AccessTreeProps) => { export const AccessTree = (props: AccessTreeProps) => {
const { subject } = props;
if (!subject) return null;
return ( return (
<AccessTreeErrorBoundary {...props}> <AccessTreeErrorBoundary {...props}>
<AccessTreeProvider> <AccessTreeProvider>

View File

@@ -29,7 +29,7 @@ export type AccessTreeForm = { metadata: { key: string; value: string }[] };
export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children }) => { export const AccessTreeProvider: React.FC<AccessTreeProviderProps> = ({ children }) => {
const [secretName, setSecretName] = useState(""); const [secretName, setSecretName] = useState("");
const formMethods = useForm<AccessTreeForm>({ defaultValues: { metadata: [] } }); const formMethods = useForm<AccessTreeForm>({ defaultValues: { metadata: [] } });
const [viewMode, setViewMode] = useState(ViewMode.Docked); const [viewMode, setViewMode] = useState(ViewMode.Modal);
const value = useMemo( const value = useMemo(
() => ({ () => ({

View File

@@ -33,7 +33,8 @@ type LevelFolderMap = Record<
export const useAccessTree = ( export const useAccessTree = (
permissions: MongoAbility<ProjectPermissionSet, MongoQuery>, permissions: MongoAbility<ProjectPermissionSet, MongoQuery>,
searchPath: string searchPath: string,
subject: ProjectPermissionSub
) => { ) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext(); const { secretName, setSecretName, setViewMode, viewMode } = useAccessTreeContext();
@@ -41,7 +42,6 @@ export const useAccessTree = (
const metadata = useWatch({ control, name: "metadata" }); const metadata = useWatch({ control, name: "metadata" });
const [nodes, setNodes] = useNodesState<Node>([]); const [nodes, setNodes] = useNodesState<Node>([]);
const [edges, setEdges] = useEdgesState<Edge>([]); const [edges, setEdges] = useEdgesState<Edge>([]);
const [subject, setSubject] = useState(ProjectPermissionSub.Secrets);
const [environment, setEnvironment] = useState(currentWorkspace.environments[0]?.slug ?? ""); const [environment, setEnvironment] = useState(currentWorkspace.environments[0]?.slug ?? "");
const { data: environmentsFolders, isPending } = useListProjectEnvironmentsFolders( const { data: environmentsFolders, isPending } = useListProjectEnvironmentsFolders(
currentWorkspace.id currentWorkspace.id
@@ -147,9 +147,7 @@ export const useAccessTree = (
const roleNode = createRoleNode({ const roleNode = createRoleNode({
subject, subject,
environment: slug, environment: slug,
environments: environmentsFolders, environments: environmentsFolders
onSubjectChange: setSubject,
onEnvironmentChange: setEnvironment
}); });
const actionRuleMap = getSubjectActionRuleMap(subject, permissions); const actionRuleMap = getSubjectActionRuleMap(subject, permissions);
@@ -280,7 +278,6 @@ export const useAccessTree = (
subject, subject,
environment, environment,
setEnvironment, setEnvironment,
setSubject,
isLoading: isPending, isLoading: isPending,
environments: currentWorkspace.environments, environments: currentWorkspace.environments,
secretName, secretName,

View File

@@ -81,7 +81,7 @@ export const AccessTreeSecretPathInput = ({
<FontAwesomeIcon icon={faSearch} /> <FontAwesomeIcon icon={faSearch} />
</div> </div>
) : ( ) : (
<Tooltip position="bottom" content="Search paths"> <Tooltip position="bottom" content="Search Paths">
<div <div
className="flex h-10 w-10 cursor-pointer items-center justify-center text-mineshaft-300 hover:text-white" className="flex h-10 w-10 cursor-pointer items-center justify-center text-mineshaft-300 hover:text-white"
onClick={toggleSearch} onClick={toggleSearch}

View File

@@ -3,7 +3,6 @@ import { faFileImport, faFingerprint, faFolder, faKey } from "@fortawesome/free-
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Handle, NodeProps, Position } from "@xyflow/react"; import { Handle, NodeProps, Position } from "@xyflow/react";
import { Select, SelectItem } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types"; import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
@@ -29,7 +28,7 @@ const formatLabel = (text: string) => {
}; };
export const RoleNode = ({ export const RoleNode = ({
data: { subject, environment, onSubjectChange, onEnvironmentChange, environments } data: { subject }
}: NodeProps & { }: NodeProps & {
data: ReturnType<typeof createRoleNode>["data"] & { data: ReturnType<typeof createRoleNode>["data"] & {
onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>; onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>;
@@ -44,61 +43,10 @@ export const RoleNode = ({
className="pointer-events-none !cursor-pointer opacity-0" className="pointer-events-none !cursor-pointer opacity-0"
position={Position.Top} position={Position.Top}
/> />
<div className="flex w-full flex-col items-center justify-center rounded-md border-2 border-mineshaft-500 bg-gradient-to-b from-mineshaft-700 to-mineshaft-800 px-5 py-4 font-inter shadow-2xl"> <div className="flex h-14 w-full flex-col items-center justify-center rounded-md border border-mineshaft bg-mineshaft-800 px-2 py-3 font-inter shadow-lg transition-opacity duration-500">
<div className="flex w-full min-w-[240px] flex-col gap-4"> <div className="flex items-center space-x-2 text-mineshaft-100">
<div className="flex w-full flex-col gap-1.5"> {getSubjectIcon(subject)}
<div className="ml-1 text-xs font-semibold text-mineshaft-200">Subject</div> <span className="text-sm">{formatLabel(subject)} Access</span>
<Select
value={subject}
onValueChange={(value) => onSubjectChange(value as ProjectPermissionSub)}
className="w-full rounded-md border border-mineshaft-600 bg-mineshaft-900/90 text-sm shadow-inner backdrop-blur-sm transition-all hover:border-amber-600/50 focus:border-amber-500"
position="popper"
dropdownContainerClassName="max-w-none"
aria-label="Subject"
>
{[
ProjectPermissionSub.Secrets,
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.DynamicSecrets,
ProjectPermissionSub.SecretImports
].map((sub) => {
return (
<SelectItem
className="relative flex items-center gap-2 py-2 pl-8 pr-8 text-sm capitalize hover:bg-mineshaft-700"
value={sub}
key={sub}
>
<div className="flex items-center gap-3">
{getSubjectIcon(sub)}
<span className="font-medium">{formatLabel(sub)}</span>
</div>
</SelectItem>
);
})}
</Select>
</div>
<div className="flex w-full flex-col gap-1.5">
<div className="ml-1 text-xs font-semibold text-mineshaft-200">Environment</div>
<Select
value={environment}
onValueChange={onEnvironmentChange}
className="w-full rounded-md border border-mineshaft-600 bg-mineshaft-900/90 text-sm shadow-inner backdrop-blur-sm transition-all hover:border-amber-600/50 focus:border-amber-500"
position="popper"
dropdownContainerClassName="max-w-none"
aria-label="Environment"
>
{Object.values(environments).map((env) => (
<SelectItem
key={env.slug}
value={env.slug}
className="relative py-2 pl-6 pr-8 text-sm hover:bg-mineshaft-700"
>
<div className="ml-3 font-medium">{env.name}</div>
</SelectItem>
))}
</Select>
</div>
</div> </div>
</div> </div>
<Handle <Handle

View File

@@ -1,5 +1,3 @@
import { Dispatch, SetStateAction } from "react";
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types"; import { TProjectEnvironmentsFolders } from "@app/hooks/api/secretFolders/types";
@@ -8,24 +6,18 @@ import { PermissionNode } from "../types";
export const createRoleNode = ({ export const createRoleNode = ({
subject, subject,
environment, environment,
environments, environments
onSubjectChange,
onEnvironmentChange
}: { }: {
subject: string; subject: ProjectPermissionSub;
environment: string; environment: string;
environments: TProjectEnvironmentsFolders; environments: TProjectEnvironmentsFolders;
onSubjectChange: Dispatch<SetStateAction<ProjectPermissionSub>>;
onEnvironmentChange: (value: string) => void;
}) => ({ }) => ({
id: `role-${subject}-${environment}`, id: `role-${subject}-${environment}`,
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
data: { data: {
subject, subject,
environment, environment,
environments, environments
onSubjectChange,
onEnvironmentChange
}, },
type: PermissionNode.Role, type: PermissionNode.Role,
height: 48, height: 48,

View File

@@ -39,16 +39,6 @@ export const positionElements = (nodes: Node[], edges: Edge[]) => {
const positionedNodes = nodes.map((node) => { const positionedNodes = nodes.map((node) => {
const { x, y } = dagre.node(node.id); const { x, y } = dagre.node(node.id);
if (node.type === "role") {
return {
...node,
position: {
x: x - (node.width ? node.width / 2 : 0),
y: y - 150
}
};
}
return { return {
...node, ...node,
position: { position: {

View File

@@ -173,17 +173,19 @@ export const ProjectTemplateEditRoleForm = ({
<div className="p-4"> <div className="p-4">
<div className="mb-2 text-lg">Policies</div> <div className="mb-2 text-lg">Policies</div>
<PermissionEmptyState /> <PermissionEmptyState />
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( <div>
<GeneralPermissionPolicies {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
subject={subject} <GeneralPermissionPolicies
actions={PROJECT_PERMISSION_OBJECT[subject].actions} subject={subject}
title={PROJECT_PERMISSION_OBJECT[subject].title} actions={PROJECT_PERMISSION_OBJECT[subject].actions}
key={`project-permission-${subject}`} title={PROJECT_PERMISSION_OBJECT[subject].title}
isDisabled={isDisabled} key={`project-permission-${subject}`}
> isDisabled={isDisabled}
{renderConditionalComponents(subject, isDisabled)} >
</GeneralPermissionPolicies> {renderConditionalComponents(subject, isDisabled)}
))} </GeneralPermissionPolicies>
))}
</div>
</div> </div>
</FormProvider> </FormProvider>
</form> </form>

View File

@@ -30,7 +30,29 @@ export const AwsParameterStoreSyncFields = () => {
/> />
<Controller <Controller
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message} label="Path"> <FormControl
isError={Boolean(error)}
errorText={error?.message}
label="Path"
tooltipText={
<>
The path is required and will be prepended to the key schema. For example, if you
have a path of{" "}
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
/demo/path/
</code>{" "}
and a key schema of{" "}
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
INFISICAL_{"{{secretKey}}"}
</code>
, then the result will be{" "}
<code className="rounded bg-mineshaft-600 px-0.5 py-px text-sm text-mineshaft-300">
/demo/path/INFISICAL_{"{{secretKey}}"}
</code>
</>
}
tooltipClassName="max-w-lg"
>
<Input value={value} onChange={onChange} placeholder="Path..." /> <Input value={value} onChange={onChange} placeholder="Path..." />
</FormControl> </FormControl>
)} )}

View File

@@ -22,87 +22,19 @@ import { SecretSync } from "@app/hooks/api/secretSyncs";
import { TSecretSyncForm } from "../schemas"; import { TSecretSyncForm } from "../schemas";
export const AwsParameterStoreSyncOptionsFields = () => { const AwsTagsSection = () => {
const { control, watch } = useFormContext< const { control } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSParameterStore } TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
>(); >();
const region = watch("destinationConfig.region");
const connectionId = useWatch({ name: "connection.id", control });
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
{
connectionId,
region,
destination: SecretSync.AWSParameterStore
},
{ enabled: Boolean(connectionId && region) }
);
const tagFields = useFieldArray({ const tagFields = useFieldArray({
control, control,
name: "syncOptions.tags" name: "syncOptions.tags"
}); });
return ( return (
<> <div className="mb-4 mt-2 flex flex-col pl-2">
<Controller <div className="grid max-h-[20vh] grid-cols-12 items-end gap-2 overflow-y-auto">
name="syncOptions.keyId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="The AWS KMS key to encrypt parameters with"
isError={Boolean(error)}
errorText={error?.message}
label="KMS Key"
>
<FilterableSelect
isLoading={isKmsKeysPending && Boolean(connectionId && region)}
isDisabled={!connectionId}
value={kmsKeys.find((org) => org.alias === value) ?? null}
onChange={(option) =>
onChange((option as SingleValue<TAwsConnectionKmsKey>)?.alias ?? null)
}
// eslint-disable-next-line react/no-unstable-nested-components
noOptionsMessage={({ inputValue }) =>
inputValue ? undefined : (
<p>
To configure a KMS key, ensure the following permissions are present on the
selected IAM role:{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:ListAliases&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:DescribeKey&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Encrypt&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Decrypt&#34;
</span>
.
</p>
)
}
options={kmsKeys}
placeholder="Leave blank to use default KMS key"
getOptionLabel={(option) =>
option.alias === "alias/aws/ssm" ? `${option.alias} (Default)` : option.alias
}
getOptionValue={(option) => option.alias}
/>
</FormControl>
)}
/>
<FormLabel
label="Resource Tags"
tooltipText="Add resource tags to parameters synced by Infisical"
/>
<div className="mb-3 grid max-h-[20vh] grid-cols-12 flex-col items-end gap-2 overflow-y-auto">
{tagFields.fields.map(({ id: tagFieldId }, i) => ( {tagFields.fields.map(({ id: tagFieldId }, i) => (
<Fragment key={tagFieldId}> <Fragment key={tagFieldId}>
<div className="col-span-5"> <div className="col-span-5">
@@ -164,12 +96,118 @@ export const AwsParameterStoreSyncOptionsFields = () => {
Add Tag Add Tag
</Button> </Button>
</div> </div>
</div>
);
};
export const AwsParameterStoreSyncOptionsFields = () => {
const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSParameterStore }
>();
const region = watch("destinationConfig.region");
const connectionId = useWatch({ name: "connection.id", control });
const watchedTags = watch("syncOptions.tags");
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
{
connectionId,
region,
destination: SecretSync.AWSParameterStore
},
{ enabled: Boolean(connectionId && region) }
);
return (
<>
<Controller
name="syncOptions.keyId"
control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
tooltipText="The AWS KMS key to encrypt parameters with"
isError={Boolean(error)}
errorText={error?.message}
label="KMS Key"
>
<FilterableSelect
isLoading={isKmsKeysPending && Boolean(connectionId && region)}
isDisabled={!connectionId}
value={kmsKeys.find((org) => org.alias === value) ?? null}
onChange={(option) =>
onChange((option as SingleValue<TAwsConnectionKmsKey>)?.alias ?? null)
}
// eslint-disable-next-line react/no-unstable-nested-components
noOptionsMessage={({ inputValue }) =>
inputValue ? undefined : (
<p>
To configure a KMS key, ensure the following permissions are present on the
selected IAM role:{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:ListAliases&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:DescribeKey&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Encrypt&#34;
</span>
,{" "}
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
&#34;kms:Decrypt&#34;
</span>
.
</p>
)
}
options={kmsKeys}
placeholder="Leave blank to use default KMS key"
getOptionLabel={(option) =>
option.alias === "alias/aws/ssm" ? `${option.alias} (Default)` : option.alias
}
getOptionValue={(option) => option.alias}
/>
</FormControl>
)}
/>
<Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="overwrite-tags"
thumbClassName="bg-mineshaft-800"
isChecked={Array.isArray(watchedTags)}
onCheckedChange={(isChecked) => {
if (isChecked) {
setValue("syncOptions.tags", []);
} else {
setValue("syncOptions.tags", undefined);
}
}}
>
<p className="w-fit">
Configure Resource Tags{" "}
<Tooltip
className="max-w-md"
content={
<p>
If enabled, AWS resource tags will be overwritten using static values defined below.
</p>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
</Tooltip>
</p>
</Switch>
{Array.isArray(watchedTags) && <AwsTagsSection />}
<Controller <Controller
name="syncOptions.syncSecretMetadataAsTags" name="syncOptions.syncSecretMetadataAsTags"
control={control} control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl <FormControl
className="mt-6" className="mt-4"
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
> >

View File

@@ -23,14 +23,93 @@ import { AwsSecretsManagerSyncMappingBehavior } from "@app/hooks/api/secretSyncs
import { TSecretSyncForm } from "../schemas"; import { TSecretSyncForm } from "../schemas";
const AwsTagsSection = () => {
const { control } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>();
const tagFields = useFieldArray({
control,
name: "syncOptions.tags"
});
return (
<div className="mb-4 mt-2 flex flex-col pl-2">
<div className="grid max-h-[20vh] grid-cols-12 items-end gap-2 overflow-y-auto">
{tagFields.fields.map(({ id: tagFieldId }, i) => (
<Fragment key={tagFieldId}>
<div className="col-span-5">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`syncOptions.tags.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input className="text-xs" {...field} />
</FormControl>
)}
/>
</div>
<div className="col-span-6">
{i === 0 && (
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional />
)}
<Controller
control={control}
name={`syncOptions.tags.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input className="text-xs" {...field} />
</FormControl>
)}
/>
</div>
<Tooltip content="Remove tag" position="right">
<IconButton
variant="plain"
ariaLabel="Remove tag"
className="col-span-1 mb-1.5"
colorSchema="danger"
size="xs"
onClick={() => tagFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</Fragment>
))}
</div>
<div className="mt-2 flex">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => tagFields.append({ key: "", value: "" })}
>
Add Tag
</Button>
</div>
</div>
);
};
export const AwsSecretsManagerSyncOptionsFields = () => { export const AwsSecretsManagerSyncOptionsFields = () => {
const { control, watch } = useFormContext< const { control, watch, setValue } = useFormContext<
TSecretSyncForm & { destination: SecretSync.AWSSecretsManager } TSecretSyncForm & { destination: SecretSync.AWSSecretsManager }
>(); >();
const region = watch("destinationConfig.region"); const region = watch("destinationConfig.region");
const connectionId = useWatch({ name: "connection.id", control }); const connectionId = useWatch({ name: "connection.id", control });
const mappingBehavior = watch("destinationConfig.mappingBehavior"); const mappingBehavior = watch("destinationConfig.mappingBehavior");
const watchedTags = watch("syncOptions.tags");
const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys( const { data: kmsKeys = [], isPending: isKmsKeysPending } = useListAwsConnectionKmsKeys(
{ {
@@ -41,11 +120,6 @@ export const AwsSecretsManagerSyncOptionsFields = () => {
{ enabled: Boolean(connectionId && region) } { enabled: Boolean(connectionId && region) }
); );
const tagFields = useFieldArray({
control,
name: "syncOptions.tags"
});
return ( return (
<> <>
<Controller <Controller
@@ -102,78 +176,50 @@ export const AwsSecretsManagerSyncOptionsFields = () => {
</FormControl> </FormControl>
)} )}
/> />
<FormLabel label="Tags" tooltipText="Add tags to secrets synced by Infisical" />
<div className="mb-3 grid max-h-[20vh] grid-cols-12 flex-col items-end gap-2 overflow-y-auto"> <Switch
{tagFields.fields.map(({ id: tagFieldId }, i) => ( className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
<Fragment key={tagFieldId}> id="overwrite-tags"
<div className="col-span-5"> thumbClassName="bg-mineshaft-800"
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>} isChecked={Array.isArray(watchedTags)}
<Controller onCheckedChange={(isChecked) => {
control={control} if (isChecked) {
name={`syncOptions.tags.${i}.key`} setValue("syncOptions.tags", []);
render={({ field, fieldState: { error } }) => ( } else {
<FormControl setValue("syncOptions.tags", undefined);
isError={Boolean(error?.message)} }
errorText={error?.message} }}
className="mb-0" >
> <p className="w-fit">
<Input className="text-xs" {...field} /> Configure Secret Tags{" "}
</FormControl> <Tooltip
)} className="max-w-md"
/> content={
</div> <p>
<div className="col-span-6"> If enabled, AWS secret tags will be overwritten using static values defined below.
{i === 0 && ( </p>
<FormLabel label="Value" className="text-xs text-mineshaft-400" isOptional /> }
)} >
<Controller <FontAwesomeIcon icon={faQuestionCircle} size="sm" className="ml-1" />
control={control} </Tooltip>
name={`syncOptions.tags.${i}.value`} </p>
render={({ field, fieldState: { error } }) => ( </Switch>
<FormControl
isError={Boolean(error?.message)} {Array.isArray(watchedTags) && <AwsTagsSection />}
errorText={error?.message}
className="mb-0"
>
<Input className="text-xs" {...field} />
</FormControl>
)}
/>
</div>
<Tooltip content="Remove tag" position="right">
<IconButton
variant="plain"
ariaLabel="Remove tag"
className="col-span-1 mb-1.5"
colorSchema="danger"
size="xs"
onClick={() => tagFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
</Fragment>
))}
</div>
<div className="mb-6 mt-2 flex">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => tagFields.append({ key: "", value: "" })}
>
Add Tag
</Button>
</div>
{mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne && ( {mappingBehavior === AwsSecretsManagerSyncMappingBehavior.OneToOne && (
<Controller <Controller
name="syncOptions.syncSecretMetadataAsTags" name="syncOptions.syncSecretMetadataAsTags"
control={control} control={control}
render={({ field: { value, onChange }, fieldState: { error } }) => ( render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl isError={Boolean(error?.message)} errorText={error?.message}> <FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mt-4"
>
<Switch <Switch
className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80" className="bg-mineshaft-400/50 shadow-inner data-[state=checked]:bg-green/80"
id="overwrite-existing-secrets" id="sync-metadata-as-tags"
thumbClassName="bg-mineshaft-800" thumbClassName="bg-mineshaft-800"
isChecked={value} isChecked={value}
onCheckedChange={onChange} onCheckedChange={onChange}

View File

@@ -40,9 +40,9 @@ export const Checkbox = ({
<div className={twMerge("flex items-center font-inter text-bunker-300", containerClassName)}> <div className={twMerge("flex items-center font-inter text-bunker-300", containerClassName)}>
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
className={twMerge( className={twMerge(
"flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500", "flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border border-mineshaft-400/50 bg-mineshaft-600 shadow transition-all hover:bg-mineshaft-500",
isDisabled && "bg-bunker-400 hover:bg-bunker-400", isDisabled && "bg-bunker-400 hover:bg-bunker-400",
isChecked && "bg-primary hover:bg-primary", isChecked && "border-primary/30 bg-primary/10",
Boolean(children) && "mr-3", Boolean(children) && "mr-3",
className className
)} )}
@@ -53,7 +53,10 @@ export const Checkbox = ({
id={id} id={id}
> >
<CheckboxPrimitive.Indicator <CheckboxPrimitive.Indicator
className={twMerge(`${checkIndicatorBg || "text-bunker-800"}`, indicatorClassName)} className={twMerge(
`${checkIndicatorBg || "mt-[0.1rem] text-mineshaft-200"}`,
indicatorClassName
)}
> >
{isIndeterminate ? ( {isIndeterminate ? (
<FontAwesomeIcon icon={faMinus} size="sm" /> <FontAwesomeIcon icon={faMinus} size="sm" />

View File

@@ -1,5 +1,6 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip"; import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { TooltipProps as RootProps } from "@radix-ui/react-tooltip";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "content"> & { export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "content"> & {
@@ -14,6 +15,7 @@ export type TooltipProps = Omit<TooltipPrimitive.TooltipContentProps, "open" | "
isDisabled?: boolean; isDisabled?: boolean;
center?: boolean; center?: boolean;
size?: "sm" | "md"; size?: "sm" | "md";
rootProps?: RootProps;
}; };
export const Tooltip = ({ export const Tooltip = ({
@@ -28,12 +30,14 @@ export const Tooltip = ({
isDisabled, isDisabled,
position = "top", position = "top",
size = "md", size = "md",
rootProps,
...props ...props
}: TooltipProps) => }: TooltipProps) =>
// just render children if tooltip content is empty // just render children if tooltip content is empty
content ? ( content ? (
<TooltipPrimitive.Root <TooltipPrimitive.Root
delayDuration={50} delayDuration={50}
{...rootProps}
open={isOpen} open={isOpen}
defaultOpen={defaultOpen} defaultOpen={defaultOpen}
onOpenChange={onOpenChange} onOpenChange={onOpenChange}

View File

@@ -161,6 +161,18 @@ export type IdentityManagementSubjectFields = {
identityId: string; identityId: string;
}; };
export type ConditionalProjectPermissionSubject =
| ProjectPermissionSub.SecretSyncs
| ProjectPermissionSub.Secrets
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.Identity
| ProjectPermissionSub.SshHosts
| ProjectPermissionSub.PkiSubscribers
| ProjectPermissionSub.CertificateTemplates
| ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation;
export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = {
[PermissionConditionOperators.$EQ]: "equal to", [PermissionConditionOperators.$EQ]: "equal to",
[PermissionConditionOperators.$IN]: "in", [PermissionConditionOperators.$IN]: "in",

View File

@@ -23,6 +23,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { Link, linkOptions, useLocation, useNavigate, useRouter } from "@tanstack/react-router"; import { Link, linkOptions, useLocation, useNavigate, useRouter } from "@tanstack/react-router";
import { Mfa } from "@app/components/auth/Mfa"; import { Mfa } from "@app/components/auth/Mfa";
import { createNotification } from "@app/components/notifications";
import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { import {
@@ -49,6 +50,7 @@ import {
} from "@app/hooks/api"; } from "@app/hooks/api";
import { authKeys } from "@app/hooks/api/auth/queries"; import { authKeys } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types"; import { MfaMethod } from "@app/hooks/api/auth/types";
import { getAuthToken } from "@app/hooks/api/reactQuery";
import { SubscriptionPlan } from "@app/hooks/api/types"; import { SubscriptionPlan } from "@app/hooks/api/types";
import { AuthMethod } from "@app/hooks/api/users/types"; import { AuthMethod } from "@app/hooks/api/users/types";
import { ProjectType } from "@app/hooks/api/workspace/types"; import { ProjectType } from "@app/hooks/api/workspace/types";
@@ -159,6 +161,16 @@ export const MinimizedOrgSidebar = () => {
} }
}; };
const handleCopyToken = async () => {
try {
await window.navigator.clipboard.writeText(getAuthToken());
createNotification({ type: "success", text: "Copied current login session token to clipboard" });
} catch (error) {
console.log(error);
createNotification({ type: "error", text: "Failed to copy user token to clipboard" });
}
};
if (shouldShowMfa) { if (shouldShowMfa) {
return ( return (
<div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700"> <div className="flex max-h-screen min-h-screen flex-col items-center justify-center gap-2 overflow-y-auto bg-gradient-to-tr from-mineshaft-600 via-mineshaft-800 to-bunker-700">
@@ -595,6 +607,16 @@ export const MinimizedOrgSidebar = () => {
</DropdownMenuItem> </DropdownMenuItem>
</a> </a>
<div className="mt-1 h-1 border-t border-mineshaft-600" /> <div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={handleCopyToken}>
Copy Token
<Tooltip
content="This token is linked to your current login session and can only access resources within the organization you're currently logged into."
className="max-w-3xl"
>
<FontAwesomeIcon icon={faInfoCircle} className="mb-[0.06rem] pl-1.5 text-xs" />
</Tooltip>
</DropdownMenuItem>
<div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}> <DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
Log Out Log Out
</DropdownMenuItem> </DropdownMenuItem>

View File

@@ -352,19 +352,21 @@ export const IdentityProjectAdditionalPrivilegeModifySection = ({
<div className="p-4"> <div className="p-4">
<div className="mb-2 text-lg">Policies</div> <div className="mb-2 text-lg">Policies</div>
{(isCreate || !isPending) && <PermissionEmptyState />} {(isCreate || !isPending) && <PermissionEmptyState />}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map( <div>
(permissionSubject) => ( {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map(
<GeneralPermissionPolicies (permissionSubject) => (
subject={permissionSubject} <GeneralPermissionPolicies
actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions} subject={permissionSubject}
title={PROJECT_PERMISSION_OBJECT[permissionSubject].title} actions={PROJECT_PERMISSION_OBJECT[permissionSubject].actions}
key={`project-permission-${permissionSubject}`} title={PROJECT_PERMISSION_OBJECT[permissionSubject].title}
isDisabled={isDisabled} key={`project-permission-${permissionSubject}`}
> isDisabled={isDisabled}
{renderConditionalComponents(permissionSubject, isDisabled)} >
</GeneralPermissionPolicies> {renderConditionalComponents(permissionSubject, isDisabled)}
) </GeneralPermissionPolicies>
)} )
)}
</div>
</div> </div>
</FormProvider> </FormProvider>
</form> </form>

View File

@@ -348,17 +348,19 @@ export const MembershipProjectAdditionalPrivilegeModifySection = ({
<div className="p-4"> <div className="p-4">
<div className="mb-2 text-lg">Policies</div> <div className="mb-2 text-lg">Policies</div>
{(isCreate || !isPending) && <PermissionEmptyState />} {(isCreate || !isPending) && <PermissionEmptyState />}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => ( <div>
<GeneralPermissionPolicies {(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]).map((subject) => (
subject={subject} <GeneralPermissionPolicies
actions={PROJECT_PERMISSION_OBJECT[subject].actions} subject={subject}
title={PROJECT_PERMISSION_OBJECT[subject].title} actions={PROJECT_PERMISSION_OBJECT[subject].actions}
key={`project-permission-${subject}`} title={PROJECT_PERMISSION_OBJECT[subject].title}
isDisabled={isDisabled} key={`project-permission-${subject}`}
> isDisabled={isDisabled}
{renderConditionalComponents(subject, isDisabled)} >
</GeneralPermissionPolicies> {renderConditionalComponents(subject, isDisabled)}
))} </GeneralPermissionPolicies>
))}
</div>
</div> </div>
</FormProvider> </FormProvider>
</form> </form>

View File

@@ -1,5 +1,7 @@
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { faCopy, faEdit, faEllipsisV, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useParams } from "@tanstack/react-router"; import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@@ -12,19 +14,17 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
PageHeader, PageHeader
Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api"; import { useDeleteProjectRole, useGetProjectRoleBySlug } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { usePopUp } from "@app/hooks/usePopUp"; import { usePopUp } from "@app/hooks/usePopUp";
import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal"; import { DuplicateProjectRoleModal } from "@app/pages/project/RoleDetailsBySlugPage/components/DuplicateProjectRoleModal";
import { RolePermissionsSection } from "@app/pages/project/RoleDetailsBySlugPage/components/RolePermissionsSection";
import { ProjectAccessControlTabs } from "@app/types/project"; import { ProjectAccessControlTabs } from "@app/types/project";
import { RoleDetailsSection } from "./components/RoleDetailsSection";
import { RoleModal } from "./components/RoleModal"; import { RoleModal } from "./components/RoleModal";
import { RolePermissionsSection } from "./components/RolePermissionsSection";
const Page = () => { const Page = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -88,17 +88,29 @@ const Page = () => {
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white"> <div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && ( {data && (
<div className="mx-auto mb-6 w-full max-w-7xl"> <div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.name}> <PageHeader
title={
<div className="flex flex-col">
<div>
<span>{data.name}</span>
<p className="text-sm font-[400] normal-case leading-3 text-mineshaft-400">
{data.slug} {data.description && `- ${data.description}`}
</p>
</div>
</div>
}
>
{isCustomRole && ( {isCustomRole && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg"> <DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400"> <Button
<Tooltip content="More options"> colorSchema="secondary"
<Button variant="outline_bg">More</Button> rightIcon={<FontAwesomeIcon icon={faEllipsisV} className="ml-2" />}
</Tooltip> >
</div> Options
</Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="p-1"> <DropdownMenuContent align="end" sideOffset={2} className="p-1">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={ProjectPermissionSub.Role} a={ProjectPermissionSub.Role}
@@ -113,6 +125,7 @@ const Page = () => {
roleSlug roleSlug
}) })
} }
icon={<FontAwesomeIcon icon={faEdit} />}
disabled={!isAllowed} disabled={!isAllowed}
> >
Edit Role Edit Role
@@ -128,6 +141,7 @@ const Page = () => {
className={twMerge( className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50" !isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)} )}
icon={<FontAwesomeIcon icon={faCopy} />}
onClick={() => { onClick={() => {
handlePopUpOpen("duplicateRole"); handlePopUpOpen("duplicateRole");
}} }}
@@ -143,13 +157,9 @@ const Page = () => {
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuItem <DropdownMenuItem
className={twMerge( icon={<FontAwesomeIcon icon={faTrash} />}
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={() => handlePopUpOpen("deleteRole")} onClick={() => handlePopUpOpen("deleteRole")}
disabled={!isAllowed} isDisabled={!isAllowed}
> >
Delete Role Delete Role
</DropdownMenuItem> </DropdownMenuItem>
@@ -159,12 +169,7 @@ const Page = () => {
</DropdownMenu> </DropdownMenu>
)} )}
</PageHeader> </PageHeader>
<div className="flex"> <RolePermissionsSection roleSlug={roleSlug} isDisabled={!isCustomRole} />
<div className="mr-4 w-96">
<RoleDetailsSection roleSlug={roleSlug} handlePopUpOpen={handlePopUpOpen} />
</div>
<RolePermissionsSection roleSlug={roleSlug} isDisabled={!isCustomRole} />
</div>
</div> </div>
)} )}
<RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} /> <RoleModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />

View File

@@ -24,7 +24,7 @@ export const AddPoliciesButton = ({ isDisabled }: Props) => {
] as const); ] as const);
return ( return (
<> <div>
<Button <Button
className="h-10 rounded-r-none" className="h-10 rounded-r-none"
variant="outline_bg" variant="outline_bg"
@@ -73,6 +73,6 @@ export const AddPoliciesButton = ({ isDisabled }: Props) => {
isOpen={popUp.applyTemplate.isOpen} isOpen={popUp.applyTemplate.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("applyTemplate", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("applyTemplate", isOpen)}
/> />
</> </div>
); );
}; };

View File

@@ -0,0 +1,206 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
ConditionalProjectPermissionSubject,
PermissionConditionOperators
} from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
export const ConditionsFields = ({
isDisabled,
subject,
position,
selectOptions
}: {
isDisabled: boolean | undefined;
subject: ConditionalProjectPermissionSubject;
position: number;
selectOptions: [{ value: string; label: string }, ...{ value: string; label: string }[]];
}) => {
const {
control,
watch,
setValue,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${subject}.${position}.conditions`
});
const conditionErrorMessage =
errors?.permissions?.[subject]?.[position]?.conditions?.message ||
errors?.permissions?.[subject]?.[position]?.conditions?.root?.message;
return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2">
<div className="flex w-full items-center justify-between">
<div className="mt-2.5 flex items-center text-gray-300">
<span>Conditions</span>
<Tooltip
className="max-w-sm"
content={
<>
<p>
Conditions determine when a policy will be applied (always if no conditions are
present).
</p>
<p className="mt-3">
All conditions must evaluate to true for the policy to take effect.
</p>
</>
}
>
<FontAwesomeIcon size="xs" className="ml-1 text-mineshaft-400" icon={faInfoCircle} />
</Tooltip>
</div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="outline_bg"
size="xs"
className="mt-2"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: selectOptions[0].value,
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
<div className="mt-2 flex flex-col space-y-2">
{Boolean(items.fields.length) &&
items.fields.map((el, index) => {
const condition =
(watch(`permissions.${subject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex items-start gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${subject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue(
`permissions.${subject}.${position}.conditions.${index}.operator`,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
}}
position="popper"
className="w-full"
>
{selectOptions.map(({ value, label }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-44 items-center space-x-2">
<Controller
control={control}
name={`permissions.${subject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
position="popper"
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-bunker-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${subject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="remove"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
);
})}
</div>
{conditionErrorMessage && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{conditionErrorMessage}</span>
</div>
)}
</div>
);
};

View File

@@ -1,26 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -28,162 +8,17 @@ type Props = {
}; };
export const DynamicSecretPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const DynamicSecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
setValue,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions`
});
const conditionErrorMessage =
errors?.permissions?.[ProjectPermissionSub.DynamicSecrets]?.[position]?.conditions?.message ||
errors?.permissions?.[ProjectPermissionSub.DynamicSecrets]?.[position]?.conditions?.root
?.message;
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.DynamicSecrets}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> { value: "environment", label: "Environment Slug" },
All conditions must evaluate to true for the policy to take effect. { value: "secretPath", label: "Secret Path" },
</p> { value: "metadataKey", label: "Metadata Key" },
<div className="mt-2 flex flex-col space-y-2"> { value: "metadataValue", label: "Metadata Value" }
{items.fields.map((el, index) => { ]}
const condition = watch( />
`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}`
) as {
lhs: string;
rhs: string;
operator: string;
};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue(
`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.operator`,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
}}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
<SelectItem value="metadataKey">Metadata Key</SelectItem>
<SelectItem value="metadataValue">Metadata Value</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.DynamicSecrets}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{conditionErrorMessage && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{conditionErrorMessage}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -1,180 +1,26 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
isDisabled?: boolean; isDisabled?: boolean;
type: type:
| ProjectPermissionSub.DynamicSecrets
| ProjectPermissionSub.SecretFolders | ProjectPermissionSub.SecretFolders
| ProjectPermissionSub.SecretImports | ProjectPermissionSub.SecretImports
| ProjectPermissionSub.SecretRotation; | ProjectPermissionSub.SecretRotation;
}; };
export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => { export const GeneralPermissionConditions = ({ position = 0, isDisabled, type }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${type}.${position}.conditions`
});
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={type}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> { value: "environment", label: "Environment Slug" },
All conditions must evaluate to true for the policy to take effect. { value: "secretPath", label: "Secret Path" }
</p> ]}
<div className="mt-2 flex flex-col space-y-2"> />
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${type}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${type}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[type]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[type]?.[position]?.conditions?.message}</span>
</div>
)}
<div>{}</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -3,6 +3,7 @@ import { Control, Controller, useFieldArray, useFormContext, useWatch } from "re
import { import {
faChevronDown, faChevronDown,
faChevronRight, faChevronRight,
faDiagramProject,
faGripVertical, faGripVertical,
faInfoCircle, faInfoCircle,
faPlus, faPlus,
@@ -27,6 +28,7 @@ type Props<T extends ProjectPermissionSub> = {
actions: TProjectPermissionObject[T]["actions"]; actions: TProjectPermissionObject[T]["actions"];
children?: JSX.Element; children?: JSX.Element;
isDisabled?: boolean; isDisabled?: boolean;
onShowAccessTree?: (subject: ProjectPermissionSub) => void;
}; };
type ActionProps = { type ActionProps = {
@@ -71,7 +73,8 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
actions, actions,
children, children,
title, title,
isDisabled isDisabled,
onShowAccessTree
}: Props<T>) => { }: Props<T>) => {
const { control, watch } = useFormContext<TFormSchema>(); const { control, watch } = useFormContext<TFormSchema>();
const { fields, remove, insert, move } = useFieldArray({ const { fields, remove, insert, move } = useFieldArray({
@@ -89,7 +92,7 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
const [draggedItem, setDraggedItem] = useState<number | null>(null); const [draggedItem, setDraggedItem] = useState<number | null>(null);
const [dragOverItem, setDragOverItem] = useState<number | null>(null); const [dragOverItem, setDragOverItem] = useState<number | null>(null);
if (!watchFields || !Array.isArray(watchFields) || watchFields.length === 0) return <div />; if (!watchFields || !Array.isArray(watchFields) || watchFields.length === 0) return null;
const handleDragStart = (_: React.DragEvent, index: number) => { const handleDragStart = (_: React.DragEvent, index: number) => {
setDraggedItem(index); setDraggedItem(index);
@@ -121,9 +124,9 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
}; };
return ( return (
<div className="border border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"> <div className="overflow-clip border border-mineshaft-600 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md">
<div <div
className="flex cursor-pointer items-center space-x-8 px-5 py-4 text-sm text-gray-300" className="flex h-14 cursor-pointer items-center px-5 py-4 text-sm text-gray-300"
role="button" role="button"
tabIndex={0} tabIndex={0}
onClick={() => setIsOpen.toggle()} onClick={() => setIsOpen.toggle()}
@@ -133,20 +136,50 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
} }
}} }}
> >
<div> <FontAwesomeIcon className="mr-8" icon={isOpen ? faChevronDown : faChevronRight} />
<FontAwesomeIcon icon={isOpen ? faChevronDown : faChevronRight} />
</div> <div className="flex-grow text-base">{title}</div>
<div className="flex-grow">{title}</div>
{fields.length > 1 && ( {fields.length > 1 && (
<div> <div>
<Tag size="xs" className="px-2"> <Tag size="xs" className="mr-2 px-2">
{fields.length} rules {fields.length} Rules
</Tag> </Tag>
</div> </div>
)} )}
{isOpen && onShowAccessTree && (
<Button
leftIcon={<FontAwesomeIcon icon={faDiagramProject} />}
variant="outline_bg"
size="xs"
className="ml-2"
onClick={(e) => {
e.stopPropagation();
onShowAccessTree(subject);
}}
>
Visualize Access
</Button>
)}
{!isDisabled && isOpen && isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="outline_bg"
className="ml-2"
size="xs"
onClick={(e) => {
e.stopPropagation();
insert(fields.length, [
{ read: false, edit: false, create: false, delete: false } as any
]);
}}
isDisabled={isDisabled}
>
Add Rule
</Button>
)}
</div> </div>
{isOpen && ( {isOpen && (
<div key={`select-${subject}-type`} className="flex flex-col space-y-4 bg-bunker-800 p-6"> <div key={`select-${subject}-type`} className="flex flex-col space-y-3 bg-bunker-700 p-3">
{fields.map((el, rootIndex) => { {fields.map((el, rootIndex) => {
let isFullReadAccessEnabled = false; let isFullReadAccessEnabled = false;
@@ -154,78 +187,103 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any); isFullReadAccessEnabled = watch(`permissions.${subject}.${rootIndex}.read` as any);
} }
const isInverted = watch(`permissions.${subject}.${rootIndex}.inverted` as any);
return ( return (
<div <div
key={el.id} key={el.id}
className={twMerge( className={twMerge(
"relative bg-mineshaft-800 p-5 pr-10 first:rounded-t-md last:rounded-b-md", "relative rounded-md border-l-[6px] bg-mineshaft-800 px-5 py-4 transition-colors duration-300",
dragOverItem === rootIndex ? "border-2 border-blue-400" : "", isInverted ? "border-l-red-600/50" : "border-l-green-600/50",
dragOverItem === rootIndex ? "border-2 border-primary/50" : "",
draggedItem === rootIndex ? "opacity-50" : "" draggedItem === rootIndex ? "opacity-50" : ""
)} )}
onDragOver={(e) => handleDragOver(e, rootIndex)} onDragOver={(e) => handleDragOver(e, rootIndex)}
onDrop={handleDrop} onDrop={handleDrop}
> >
{!isDisabled && ( {isConditionalSubjects(subject) && (
<Tooltip position="left" content="Drag to reorder permission"> <div className="mb-4 flex items-center gap-3">
<div
draggable
onDragStart={(e) => handleDragStart(e, rootIndex)}
onDragEnd={handleDragEnd}
className="absolute right-3 top-2 cursor-move rounded-md bg-mineshaft-700 p-2 text-gray-400 hover:text-gray-200"
>
<FontAwesomeIcon icon={faGripVertical} />
</div>
</Tooltip>
)}
<div className="mb-4 flex items-center">
{isConditionalSubjects(subject) && (
<div className="flex w-full items-center text-gray-300"> <div className="flex w-full items-center text-gray-300">
<div className="w-1/4">Permission</div> <div className="mr-3">Permission</div>
<div className="mr-4 w-1/4"> <Controller
<Controller defaultValue={false as any}
defaultValue={false as any} name={`permissions.${subject}.${rootIndex}.inverted`}
name={`permissions.${subject}.${rootIndex}.inverted`} render={({ field }) => (
render={({ field }) => ( <Select
<Select value={String(field.value)}
value={String(field.value)} onValueChange={(val) => field.onChange(val === "true")}
onValueChange={(val) => field.onChange(val === "true")} containerClassName="w-40"
containerClassName="w-full" className="w-full"
className="w-full" isDisabled={isDisabled}
isDisabled={isDisabled} position="popper"
> >
<SelectItem value="false">Allow</SelectItem> <SelectItem value="false">Allow</SelectItem>
<SelectItem value="true">Forbid</SelectItem> <SelectItem value="true">Forbid</SelectItem>
</Select> </Select>
)} )}
/>
<Tooltip
asChild
content={
<>
<p>
Whether to allow or forbid the selected actions when the following
conditions (if any) are met.
</p>
<p className="mt-2">Forbid rules must come after allow rules.</p>
</>
}
>
<FontAwesomeIcon
icon={faInfoCircle}
size="sm"
className="ml-2 text-bunker-400"
/> />
</div> </Tooltip>
<div> {!isDisabled && (
<Tooltip <Button
asChild leftIcon={<FontAwesomeIcon icon={faTrash} />}
content={ variant="outline_bg"
<> size="xs"
<p> className="ml-auto mr-3"
Whether to allow or forbid the selected actions when the following onClick={() => remove(rootIndex)}
conditions (if any) are met. isDisabled={isDisabled}
</p>
<p className="mt-2">Forbid rules must come after allow rules.</p>
</>
}
> >
<FontAwesomeIcon Remove Rule
icon={faInfoCircle} </Button>
size="sm" )}
className="text-gray-400" {!isDisabled && (
/> <Tooltip position="left" content="Drag to reorder permission">
<div
draggable
onDragStart={(e) => handleDragStart(e, rootIndex)}
onDragEnd={handleDragEnd}
className="cursor-move text-bunker-300 hover:text-bunker-200"
>
<FontAwesomeIcon icon={faGripVertical} />
</div>
</Tooltip> </Tooltip>
</div> )}
</div> </div>
)} </div>
</div> )}
<div className="flex gap-4 text-gray-300"> <div className="flex flex-col text-gray-300">
<div className="w-1/4">Actions</div> <div className="flex w-full justify-between">
<div className="flex flex-grow flex-wrap justify-start gap-8"> <div className="mb-2">Actions</div>
{!isDisabled && !isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faTrash} />}
variant="outline_bg"
size="xs"
className="ml-auto"
onClick={() => remove(rootIndex)}
isDisabled={isDisabled}
>
Remove Rule
</Button>
)}
</div>
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{actions.map(({ label, value }, index) => { {actions.map(({ label, value }, index) => {
if (typeof value !== "string") return undefined; if (typeof value !== "string") return undefined;
@@ -255,41 +313,6 @@ export const GeneralPermissionPolicies = <T extends keyof NonNullable<TFormSchem
cloneElement(children, { cloneElement(children, {
position: rootIndex position: rootIndex
})} })}
<div
className={twMerge(
"mt-4 flex justify-start space-x-4",
isConditionalSubjects(subject) && "justify-end"
)}
>
{!isDisabled && isConditionalSubjects(subject) && (
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-2"
onClick={() => {
insert(rootIndex + 1, [
{ read: false, edit: false, create: false, delete: false } as any
]);
}}
isDisabled={isDisabled}
>
Add policy
</Button>
)}
{!isDisabled && (
<Button
leftIcon={<FontAwesomeIcon icon={faTrash} />}
variant="outline_bg"
size="xs"
className="mt-2 hover:border-red"
onClick={() => remove(rootIndex)}
isDisabled={isDisabled}
>
Remove policy
</Button>
)}{" "}
</div>
</div> </div>
); );
})} })}

View File

@@ -1,23 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -25,150 +8,12 @@ type Props = {
}; };
export const IdentityManagementPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const IdentityManagementPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.Identity;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.Identity}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[{ value: "identityId", label: "Identity ID" }]}
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> />
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="identityId">Identity ID</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>{}</div>
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "identityId",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -17,17 +17,36 @@ export const getConditionOperatorHelperInfo = (type: PermissionConditionOperator
} }
}; };
// scott: we may need to pass the subject in the future to further refine returned items
export const renderOperatorSelectItems = (type: string) => { export const renderOperatorSelectItems = (type: string) => {
if (type === "secretTags") { switch (type) {
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>; case "secretTags":
return <SelectItem value={PermissionConditionOperators.$IN}>Contains</SelectItem>;
case "identityId":
return (
<>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</>
);
case "hostname":
case "name":
return (
<>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</>
);
default:
return (
<>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</>
);
} }
return (
<>
<SelectItem value={PermissionConditionOperators.$EQ}>Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$NEQ}>Not Equal</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob Match</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</>
);
}; };

View File

@@ -12,7 +12,7 @@ export const PermissionEmptyState = () => {
([key, value]) => key && value?.length > 0 ([key, value]) => key && value?.length > 0
); );
if (isNotEmptyPermissions) return <div />; if (isNotEmptyPermissions) return null;
return <EmptyState title="No policies applied" className="py-8" />; return <EmptyState title="No policies applied" className="py-8" />;
}; };

View File

@@ -1,23 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -25,149 +8,12 @@ type Props = {
}; };
export const PkiSubscriberPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const PkiSubscriberPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.PkiSubscribers;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.PkiSubscribers}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[{ value: "name", label: "Name" }]}
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> />
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="name">Name</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "name",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -1,23 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -25,149 +8,12 @@ type Props = {
}; };
export const PkiTemplatePermissionConditions = ({ position = 0, isDisabled }: Props) => { export const PkiTemplatePermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.CertificateTemplates;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.CertificateTemplates}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[{ value: "name", label: "Name" }]}
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> />
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="name">Name</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="delete"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "name",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -1,98 +0,0 @@
import { faCheck, faCopy, faPencil } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetProjectRoleBySlug } from "@app/hooks/api";
import { ProjectMembershipRole } from "@app/hooks/api/roles/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
roleSlug: string;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["role"]>, data?: object) => void;
};
export const RoleDetailsSection = ({ roleSlug, handlePopUpOpen }: Props) => {
const [copyTextId, isCopyingId, setCopyTextId] = useTimedReset<string>({
initialState: "Copy ID to clipboard"
});
const { currentWorkspace } = useWorkspace();
const { data } = useGetProjectRoleBySlug(currentWorkspace?.id ?? "", roleSlug as string);
const isCustomRole = !Object.values(ProjectMembershipRole).includes(
(data?.slug ?? "") as ProjectMembershipRole
);
return data ? (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Project Role Details</h3>
{isCustomRole && (
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Role}>
{(isAllowed) => {
return (
<Tooltip content="Edit Role">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
handlePopUpOpen("role", {
roleSlug
});
}}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
</ProjectPermissionCan>
)}
</div>
<div className="pt-4">
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Role ID</p>
<div className="group flex align-top">
<p className="text-sm text-mineshaft-300">{data.id}</p>
<div className="opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<Tooltip content={copyTextId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
className="group relative ml-2"
onClick={() => {
navigator.clipboard.writeText(data.id);
setCopyTextId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Name</p>
<p className="text-sm text-mineshaft-300">{data.name}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Slug</p>
<p className="text-sm text-mineshaft-300">{data.slug}</p>
</div>
<div className="mb-4">
<p className="text-sm font-semibold text-mineshaft-300">Description</p>
<p className="text-sm text-mineshaft-300">
{data.description?.length ? data.description : "-"}
</p>
</div>
</div>
</div>
) : (
<div />
);
};

View File

@@ -1,4 +1,4 @@
import { useMemo } from "react"; import { useMemo, useState } from "react";
import { FormProvider, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability"; import { MongoAbility, MongoQuery, RawRuleOf } from "@casl/ability";
import { faSave } from "@fortawesome/free-solid-svg-icons"; import { faSave } from "@fortawesome/free-solid-svg-icons";
@@ -88,6 +88,8 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
roleSlug as string roleSlug as string
); );
const [showAccessTree, setShowAccessTree] = useState<ProjectPermissionSub | null>(null);
const form = useForm<TFormSchema>({ const form = useForm<TFormSchema>({
values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined, values: role ? { ...role, permissions: rolePermission2Form(role.permissions) } : undefined,
resolver: zodResolver(projectRoleFormSchema) resolver: zodResolver(projectRoleFormSchema)
@@ -133,71 +135,90 @@ export const RolePermissionsSection = ({ roleSlug, isDisabled }: Props) => {
[JSON.stringify(permissions)] [JSON.stringify(permissions)]
); );
const isSecretManagerProject = currentWorkspace.type === ProjectType.SecretManager;
return ( return (
<div className="w-full"> <div className="w-full">
{currentWorkspace.type === ProjectType.SecretManager && (
<AccessTree permissions={formattedPermissions} />
)}
<form <form
onSubmit={handleSubmit(onSubmit)} onSubmit={handleSubmit(onSubmit)}
className="w-full rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4" className="flex h-full w-full flex-1 flex-col rounded-lg border border-mineshaft-600 bg-mineshaft-900 py-4"
> >
<FormProvider {...form}> <FormProvider {...form}>
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-4"> <div className="mx-4 flex items-center justify-between border-b border-mineshaft-400 pb-4">
<h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3> <div>
<div className="flex items-center space-x-4"> <h3 className="text-lg font-semibold text-mineshaft-100">Policies</h3>
{isCustomRole && ( <p className="text-sm leading-3 text-mineshaft-400">
<> Configure granular access policies
{isDirty && ( </p>
<Button
className="mr-4 text-mineshaft-300"
variant="link"
isDisabled={isSubmitting}
isLoading={isSubmitting}
onClick={() => reset()}
>
Discard
</Button>
)}
<div className="flex items-center">
<Button
variant="outline_bg"
type="submit"
className={twMerge(
"mr-4 h-10 border",
isDirty && "bg-primary text-black hover:bg-primary hover:opacity-80"
)}
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />}
>
Save
</Button>
<AddPoliciesButton isDisabled={isDisabled} />
</div>
</>
)}
</div> </div>
</div> {isCustomRole && (
<div className="py-4"> <div className="flex items-center gap-2">
{!isPending && <PermissionEmptyState />} {isDirty && (
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[]) <Button
.filter((subject) => !EXCLUDED_PERMISSION_SUBS.includes(subject)) className="mr-4 text-mineshaft-300"
.filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject]) variant="link"
.map((subject) => ( isDisabled={isSubmitting}
<GeneralPermissionPolicies isLoading={isSubmitting}
subject={subject} onClick={() => reset()}
actions={PROJECT_PERMISSION_OBJECT[subject].actions} >
title={PROJECT_PERMISSION_OBJECT[subject].title} Discard
key={`project-permission-${subject}`} </Button>
isDisabled={isDisabled} )}
<Button
colorSchema="secondary"
type="submit"
className={twMerge("h-10 border")}
isDisabled={isSubmitting || !isDirty}
isLoading={isSubmitting}
leftIcon={<FontAwesomeIcon icon={faSave} />}
> >
{renderConditionalComponents(subject, isDisabled)} Save
</GeneralPermissionPolicies> </Button>
))} <div className="ml-2 border-l border-mineshaft-500 pl-4">
<AddPoliciesButton isDisabled={isDisabled} />
</div>
</div>
)}
</div>
<div className="flex flex-1 flex-col overflow-hidden pl-4 pr-1">
<div className="thin-scrollbar flex-1 overflow-y-scroll py-4">
{!isPending && <PermissionEmptyState />}
{(Object.keys(PROJECT_PERMISSION_OBJECT) as ProjectPermissionSub[])
.filter((subject) => !EXCLUDED_PERMISSION_SUBS.includes(subject))
.filter((subject) => ProjectTypePermissionSubjects[currentWorkspace.type][subject])
.map((subject) => (
<GeneralPermissionPolicies
subject={subject}
actions={PROJECT_PERMISSION_OBJECT[subject].actions}
title={PROJECT_PERMISSION_OBJECT[subject].title}
key={`project-permission-${subject}`}
isDisabled={isDisabled}
onShowAccessTree={
isSecretManagerProject &&
[
ProjectPermissionSub.Secrets,
ProjectPermissionSub.SecretFolders,
ProjectPermissionSub.DynamicSecrets,
ProjectPermissionSub.SecretImports
].includes(subject)
? setShowAccessTree
: undefined
}
>
{renderConditionalComponents(subject, isDisabled)}
</GeneralPermissionPolicies>
))}
</div>
</div> </div>
</FormProvider> </FormProvider>
</form> </form>
{isSecretManagerProject && showAccessTree && (
<AccessTree
permissions={formattedPermissions}
subject={showAccessTree}
onClose={() => setShowAccessTree(null)}
/>
)}
</div> </div>
); );
}; };

View File

@@ -1,23 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { PermissionConditionOperators } from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -25,159 +8,17 @@ type Props = {
}; };
export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const SecretPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
setValue,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.secrets.${position}.conditions`
});
const conditionErrorMessage =
errors?.permissions?.secrets?.[position]?.conditions?.message ||
errors?.permissions?.secrets?.[position]?.conditions?.root?.message;
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.Secrets}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> { value: "environment", label: "Environment Slug" },
All conditions must evaluate to true for the policy to take effect. { value: "secretPath", label: "Secret Path" },
</p> { value: "secretName", label: "Secret Name" },
<div className="mt-2 flex flex-col space-y-2"> { value: "secretTags", label: "Secret Tags" }
{items.fields.map((el, index) => { ]}
const condition = watch(`permissions.secrets.${position}.conditions.${index}`) as { />
lhs: string;
rhs: string;
operator: string;
};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.secrets.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue(
`permissions.secrets.${position}.conditions.${index}.operator`,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
}}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
<SelectItem value="secretName">Secret Name</SelectItem>
<SelectItem value="secretTags">Secret Tags</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.secrets.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.secrets.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{conditionErrorMessage && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{conditionErrorMessage}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -1,26 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import {
getConditionOperatorHelperInfo,
renderOperatorSelectItems
} from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -28,159 +8,15 @@ type Props = {
}; };
export const SecretSyncPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const SecretSyncPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
setValue,
formState: { errors }
} = useFormContext<TFormSchema>();
const items = useFieldArray({
control,
name: `permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions`
});
const conditionErrorMessage =
errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.message ||
errors?.permissions?.[ProjectPermissionSub.SecretSyncs]?.[position]?.conditions?.root?.message;
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.SecretSyncs}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> { value: "environment", label: "Environment Slug" },
All conditions must evaluate to true for the policy to take effect. { value: "secretPath", label: "Secret Path" }
</p> ]}
<div className="mt-2 flex flex-col space-y-2"> />
{items.fields.map((el, index) => {
const condition = watch(
`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}`
) as {
lhs: string;
rhs: string;
operator: string;
};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => {
setValue(
`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.operator`,
PermissionConditionOperators.$IN as never
);
field.onChange(e);
}}
className="w-full"
>
<SelectItem value="environment">Environment Slug</SelectItem>
<SelectItem value="secretPath">Secret Path</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
{renderOperatorSelectItems(condition.lhs)}
</Select>
</FormControl>
)}
/>
<div>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${ProjectPermissionSub.SecretSyncs}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="remove"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{conditionErrorMessage && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{conditionErrorMessage}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "environment",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -1,23 +1,6 @@
import { Controller, useFieldArray, useFormContext } from "react-hook-form"; import { ProjectPermissionSub } from "@app/context/ProjectPermissionContext/types";
import { faInfoCircle, faPlus, faTrash, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { import { ConditionsFields } from "./ConditionsFields";
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import {
PermissionConditionOperators,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { getConditionOperatorHelperInfo } from "./PermissionConditionHelpers";
import { TFormSchema } from "./ProjectRoleModifySection.utils";
type Props = { type Props = {
position?: number; position?: number;
@@ -25,149 +8,12 @@ type Props = {
}; };
export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props) => { export const SshHostPermissionConditions = ({ position = 0, isDisabled }: Props) => {
const {
control,
watch,
formState: { errors }
} = useFormContext<TFormSchema>();
const permissionSubject = ProjectPermissionSub.SshHosts;
const items = useFieldArray({
control,
name: `permissions.${permissionSubject}.${position}.conditions`
});
return ( return (
<div className="mt-6 border-t border-t-mineshaft-600 bg-mineshaft-800 pt-2"> <ConditionsFields
<p className="mt-2 text-gray-300">Conditions</p> isDisabled={isDisabled}
<p className="text-sm text-mineshaft-400"> subject={ProjectPermissionSub.SshHosts}
Conditions determine when a policy will be applied (always if no conditions are present). position={position}
</p> selectOptions={[{ value: "hostname", label: "Hostname" }]}
<p className="mb-3 text-sm leading-4 text-mineshaft-400"> />
All conditions must evaluate to true for the policy to take effect.
</p>
<div className="mt-2 flex flex-col space-y-2">
{items.fields.map((el, index) => {
const condition =
(watch(`permissions.${permissionSubject}.${position}.conditions.${index}`) as {
lhs: string;
rhs: string;
operator: string;
}) || {};
return (
<div
key={el.id}
className="flex gap-2 bg-mineshaft-800 first:rounded-t-md last:rounded-b-md"
>
<div className="w-1/4">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.lhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value="hostname">Hostname</SelectItem>
</Select>
</FormControl>
)}
/>
</div>
<div className="flex w-36 items-center space-x-2">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.operator`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => field.onChange(e)}
className="w-full"
>
<SelectItem value={PermissionConditionOperators.$EQ}>Equals</SelectItem>
<SelectItem value={PermissionConditionOperators.$GLOB}>Glob</SelectItem>
<SelectItem value={PermissionConditionOperators.$IN}>In</SelectItem>
</Select>
</FormControl>
)}
/>
<Tooltip
asChild
content={getConditionOperatorHelperInfo(
condition?.operator as PermissionConditionOperators
)}
className="max-w-xs"
>
<FontAwesomeIcon icon={faInfoCircle} size="xs" className="text-gray-400" />
</Tooltip>
</div>
<div className="flex-grow">
<Controller
control={control}
name={`permissions.${permissionSubject}.${position}.conditions.${index}.rhs`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0 flex-grow"
>
<Input {...field} onChange={(e) => field.onChange(e.target.value.trim())} />
</FormControl>
)}
/>
</div>
<div>
<IconButton
ariaLabel="plus"
variant="outline_bg"
className="p-2.5"
onClick={() => items.remove(index)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
</div>
);
})}
</div>
{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message && (
<div className="flex items-center space-x-2 py-2 text-sm text-gray-400">
<FontAwesomeIcon icon={faWarning} className="text-red" />
<span>{errors?.permissions?.[permissionSubject]?.[position]?.conditions?.message}</span>
</div>
)}
<div>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
variant="star"
size="xs"
className="mt-3"
isDisabled={isDisabled}
onClick={() =>
items.append({
lhs: "hostname",
operator: PermissionConditionOperators.$EQ,
rhs: ""
})
}
>
Add Condition
</Button>
</div>
</div>
); );
}; };

View File

@@ -70,7 +70,7 @@ export const PasswordContainer = ({
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
size="sm" size="sm"
onClick={() => window.open("https://app.infisical.com/share-secret", "_blank")} onClick={() => window.open("/share-secret", "_blank", "noopener")}
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />} rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />}
> >
Share Your Own Secret Share Your Own Secret

View File

@@ -76,7 +76,7 @@ export const SecretContainer = ({ secret, secretKey: key }: Props) => {
colorSchema="primary" colorSchema="primary"
variant="outline_bg" variant="outline_bg"
size="sm" size="sm"
onClick={() => window.open("https://app.infisical.com/share-secret", "_blank")} onClick={() => window.open("/share-secret", "_blank", "noopener")}
rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />} rightIcon={<FontAwesomeIcon icon={faArrowRight} className="pl-2" />}
> >
Share Your Own Secret Share Your Own Secret

View File

@@ -6,6 +6,9 @@ import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { import {
faAngleDown, faAngleDown,
faArrowDown, faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRightToBracket,
faArrowUp, faArrowUp,
faFileImport, faFileImport,
faFingerprint, faFingerprint,
@@ -69,7 +72,7 @@ import {
PreferenceKey, PreferenceKey,
setUserTablePreference setUserTablePreference
} from "@app/helpers/userTablePreferences"; } from "@app/helpers/userTablePreferences";
import { useDebounce, usePagination, usePopUp, useResetPageHelper } from "@app/hooks"; import { useDebounce, usePagination, usePopUp, useResetPageHelper, useToggle } from "@app/hooks";
import { import {
useCreateFolder, useCreateFolder,
useCreateSecretV3, useCreateSecretV3,
@@ -164,6 +167,18 @@ export const OverviewPage = () => {
const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter); const [debouncedSearchFilter, setDebouncedSearchFilter] = useDebounce(searchFilter);
const secretPath = (routerSearch?.secretPath as string) || "/"; const secretPath = (routerSearch?.secretPath as string) || "/";
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const [collapseEnvironments, setCollapseEnvironments] = useToggle(
Boolean(localStorage.getItem("overview-collapse-environments"))
);
const handleToggleNarrowHeader = () => {
setCollapseEnvironments.toggle();
if (collapseEnvironments) {
localStorage.removeItem("overview-collapse-environments");
} else {
localStorage.setItem("overview-collapse-environments", "true");
}
};
const [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE); const [filter, setFilter] = useState<Filter>(DEFAULT_FILTER_STATE);
const [filterHistory, setFilterHistory] = useState< const [filterHistory, setFilterHistory] = useState<
@@ -1173,47 +1188,81 @@ export const OverviewPage = () => {
className="thin-scrollbar rounded-b-none" className="thin-scrollbar rounded-b-none"
> >
<Table> <Table>
<THead> <THead className={collapseEnvironments ? "h-24" : ""}>
<Tr className="sticky top-0 z-20 border-0"> <Tr
<Th className="sticky left-0 z-20 min-w-[20rem] border-b-0 p-0"> className={twMerge("sticky top-0 z-20 border-0", collapseEnvironments && "h-24")}
<div className="flex items-center border-b border-r border-mineshaft-600 pb-3 pl-3 pr-5 pt-3.5"> >
<Tooltip <Th
className="max-w-[20rem] whitespace-nowrap capitalize" className={twMerge(
content={ "sticky left-0 z-20 min-w-[20rem] border-b-0 p-0",
totalCount > 0 collapseEnvironments && "h-24"
? `${ )}
!allRowsSelectedOnPage.isChecked ? "Select" : "Unselect" >
} all folders and secrets on page` <div
: "" className={twMerge(
} "flex h-full border-b border-mineshaft-600 pb-3 pl-3 pr-5",
!collapseEnvironments && "border-r pt-3.5"
)}
>
<div
className={twMerge("flex items-center", collapseEnvironments && "mt-auto")}
> >
<div className="ml-2 mr-4"> <Tooltip
<Checkbox className="max-w-[20rem] whitespace-nowrap capitalize"
isDisabled={totalCount === 0} content={
id="checkbox-select-all-rows" totalCount > 0
isChecked={allRowsSelectedOnPage.isChecked} ? `${
isIndeterminate={allRowsSelectedOnPage.isIndeterminate} !allRowsSelectedOnPage.isChecked ? "Select" : "Unselect"
onCheckedChange={toggleSelectAllRows} } all folders and secrets on page`
: ""
}
>
<div className="ml-2 mr-4">
<Checkbox
isDisabled={totalCount === 0}
id="checkbox-select-all-rows"
isChecked={allRowsSelectedOnPage.isChecked}
isIndeterminate={allRowsSelectedOnPage.isIndeterminate}
onCheckedChange={toggleSelectAllRows}
/>
</div>
</Tooltip>
Name
<IconButton
variant="plain"
className="ml-2"
ariaLabel="sort"
onClick={() =>
setOrderDirection((prev) =>
prev === OrderByDirection.ASC
? OrderByDirection.DESC
: OrderByDirection.ASC
)
}
>
<FontAwesomeIcon
icon={orderDirection === "asc" ? faArrowDown : faArrowUp}
/> />
</div> </IconButton>
</Tooltip> </div>
Name <Tooltip
<IconButton content={
variant="plain" collapseEnvironments ? "Expand Environments" : "Collapse Environments"
className="ml-2"
ariaLabel="sort"
onClick={() =>
setOrderDirection((prev) =>
prev === OrderByDirection.ASC
? OrderByDirection.DESC
: OrderByDirection.ASC
)
} }
className="capitalize"
> >
<FontAwesomeIcon <IconButton
icon={orderDirection === "asc" ? faArrowDown : faArrowUp} ariaLabel="Toggle Environment View"
/> variant="plain"
</IconButton> colorSchema="secondary"
className="ml-auto mt-auto h-min p-1"
onClick={handleToggleNarrowHeader}
>
<FontAwesomeIcon
icon={collapseEnvironments ? faArrowLeft : faArrowRight}
/>
</IconButton>
</Tooltip>
</div> </div>
</Th> </Th>
{visibleEnvs?.map(({ name, slug }, index) => { {visibleEnvs?.map(({ name, slug }, index) => {
@@ -1223,28 +1272,76 @@ export const OverviewPage = () => {
return ( return (
<Th <Th
className="min-table-row min-w-[11rem] border-b-0 p-0 text-center" className={twMerge(
"min-table-row border-b-0 p-0 text-xs",
collapseEnvironments && index === visibleEnvs.length - 1 && "mr-8",
collapseEnvironments ? "h-24 w-[1rem]" : "min-w-[11rem] text-center"
)}
key={`secret-overview-${name}-${index + 1}`} key={`secret-overview-${name}-${index + 1}`}
> >
<div className="flex items-center justify-center border-b border-mineshaft-600 px-5 pb-[0.83rem] pt-3.5"> <Tooltip
<button content={
type="button" collapseEnvironments ? (
className="text-sm font-medium duration-100 hover:text-mineshaft-100" <p className="whitespace-break-spaces">{name}</p>
onClick={() => handleExploreEnvClick(slug)} ) : (
""
)
}
side="bottom"
sideOffset={-1}
align="end"
className="max-w-xl text-xs normal-case"
rootProps={{
disableHoverableContent: true
}}
>
<div
className={twMerge(
"border-b border-mineshaft-600",
collapseEnvironments
? "relative h-24 w-[2.9rem]"
: "flex items-center justify-center px-5 pb-[0.82rem] pt-3.5",
collapseEnvironments &&
index === visibleEnvs.length - 1 &&
"overflow-clip"
)}
> >
{name} <div
</button> className={twMerge(
{missingKeyCount > 0 && ( "border-mineshaft-600",
<Tooltip collapseEnvironments
className="max-w-none lowercase" ? "ml-[0.85rem] h-24 -skew-x-[16rad] transform border-l text-xs"
content={`${missingKeyCount} secrets missing\n compared to other environments`} : "flex items-center justify-center"
)}
/>
<button
type="button"
className={twMerge(
"duration-100 hover:text-mineshaft-100",
collapseEnvironments &&
(index === visibleEnvs.length - 1
? "bottom-[1.75rem] w-14"
: "bottom-10 w-20"),
collapseEnvironments
? "absolute -rotate-[72.25deg] text-left !text-[12px] font-normal"
: "flex items-center text-center text-sm font-medium"
)}
onClick={() => handleExploreEnvClick(slug)}
> >
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100"> <p className="truncate font-medium">{name}</p>
<span className="text-bunker-100">{missingKeyCount}</span> </button>
</div> {!collapseEnvironments && missingKeyCount > 0 && (
</Tooltip> <Tooltip
)} className="max-w-none lowercase"
</div> content={`${missingKeyCount} secrets missing\n compared to other environments`}
>
<div className="ml-2 flex h-[1.1rem] cursor-default items-center justify-center rounded-sm border border-red-400 bg-red-600 p-1 text-xs font-medium text-bunker-100">
<span className="text-bunker-100">{missingKeyCount}</span>
</div>
</Tooltip>
)}
</div>
</Tooltip>
</Th> </Th>
); );
})} })}
@@ -1409,16 +1506,40 @@ export const OverviewPage = () => {
/> />
</Td> </Td>
{visibleEnvs?.map(({ name, slug }) => ( {visibleEnvs?.map(({ name, slug }) => (
<Td key={`explore-${name}-btn`} className="border-0 border-mineshaft-600 p-0"> <Td
<div className="flex w-full items-center justify-center border-r border-t border-mineshaft-600 px-5 py-2"> key={`explore-${name}-btn`}
<Button className="border-0 border-r border-mineshaft-600 p-0"
size="xs" >
variant="outline_bg" <div
isFullWidth className={twMerge(
onClick={() => handleExploreEnvClick(slug)} "flex w-full items-center justify-center border-t border-mineshaft-600 py-2"
> )}
Explore >
</Button> {collapseEnvironments ? (
<Tooltip className="normal-case" content="Explore Environment">
<IconButton
ariaLabel="Explore Environment"
size="xs"
variant="outline_bg"
className="mx-auto h-[1.76rem] rounded"
onClick={() => handleExploreEnvClick(slug)}
>
<FontAwesomeIcon icon={faArrowRightToBracket} />
</IconButton>
</Tooltip>
) : (
<Button
leftIcon={
<FontAwesomeIcon className="mr-1" icon={faArrowRightToBracket} />
}
variant="outline_bg"
size="xs"
className="mx-2 w-full"
onClick={() => handleExploreEnvClick(slug)}
>
Explore
</Button>
)}
</div> </div>
</Td> </Td>
))} ))}

View File

@@ -36,7 +36,7 @@ export const SecretOverviewDynamicSecretRow = ({
isPresent ? "text-green-600" : "text-red-600" isPresent ? "text-green-600" : "text-red-600"
)} )}
> >
<div className="flex justify-center"> <div className="mx-auto flex w-[0.03rem] justify-center">
<FontAwesomeIcon <FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
icon={isPresent ? faCheck : faXmark} icon={isPresent ? faCheck : faXmark}

View File

@@ -70,7 +70,7 @@ export const SecretOverviewFolderRow = ({
isPresent ? "text-green-600" : "text-red-600" isPresent ? "text-green-600" : "text-red-600"
)} )}
> >
<div className="flex justify-center"> <div className="mx-auto flex w-[0.03rem] justify-center">
<FontAwesomeIcon <FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
icon={isPresent ? faCheck : faXmark} icon={isPresent ? faCheck : faXmark}

View File

@@ -94,7 +94,7 @@ export const SecretOverviewSecretRotationRow = ({
isPresent ? "text-green-600" : "text-red-600" isPresent ? "text-green-600" : "text-red-600"
)} )}
> >
<div className="flex justify-center"> <div className="mx-auto flex w-[0.03rem] justify-center">
<FontAwesomeIcon <FontAwesomeIcon
// eslint-disable-next-line no-nested-ternary // eslint-disable-next-line no-nested-ternary
icon={isPresent ? faCheck : faXmark} icon={isPresent ? faCheck : faXmark}

View File

@@ -1,8 +1,8 @@
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faCircle } from "@fortawesome/free-regular-svg-icons";
import { import {
faAngleDown, faAngleDown,
faCheck, faCheck,
faCircle,
faCodeBranch, faCodeBranch,
faEye, faEye,
faEyeSlash, faEyeSlash,
@@ -145,14 +145,14 @@ export const SecretOverviewTableRow = ({
<Td <Td
key={`sec-overview-${slug}-${i + 1}-value`} key={`sec-overview-${slug}-${i + 1}-value`}
className={twMerge( className={twMerge(
"px-0 py-0 group-hover:bg-mineshaft-700", "border-r border-mineshaft-600 px-0 py-3 group-hover:bg-mineshaft-700",
isFormExpanded && "border-t-2 border-mineshaft-500", isFormExpanded && "border-t-2 border-mineshaft-500",
(isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "", (isSecretPresent && !isSecretEmpty) || isSecretImported ? "text-green-600" : "",
isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "", isSecretPresent && isSecretEmpty && !isSecretImported ? "text-yellow" : "",
!isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : "" !isSecretPresent && !isSecretEmpty && !isSecretImported ? "text-red-600" : ""
)} )}
> >
<div className="h-full w-full border-r border-mineshaft-600 px-5 py-[0.85rem]"> <div className="mx-auto flex w-[0.03rem] justify-center">
<div className="flex justify-center"> <div className="flex justify-center">
{!isSecretEmpty && ( {!isSecretEmpty && (
<Tooltip <Tooltip