mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-11 05:49:05 +00:00
Compare commits
34 Commits
feat/gitla
...
update-aws
Author | SHA1 | Date | |
---|---|---|---|
|
2e256e4282 | ||
|
dcd21883d1 | ||
|
205442bff5 | ||
|
e8d19eb823 | ||
|
5d30215ea7 | ||
|
29fedfdde5 | ||
|
b5317d1d75 | ||
|
86c145301e | ||
|
6446311b6d | ||
|
3e80f1907c | ||
|
79e62eec25 | ||
|
c41730c5fb | ||
|
aac63d3097 | ||
|
1f7617d132 | ||
|
18f1f93b5f | ||
|
5b4790ee78 | ||
|
5ab2a6bb5d | ||
|
dcac85fe6c | ||
|
2f07471404 | ||
|
137fd5ef07 | ||
|
883c7835a1 | ||
|
9f6dca23db | ||
|
f0a95808e7 | ||
|
90a0d0f744 | ||
|
7f9c9be2c8 | ||
|
8683693103 | ||
|
737fffcceb | ||
|
ffac24ce75 | ||
|
6566393e21 | ||
|
af245b1f16 | ||
|
c17df7e951 | ||
|
4d4953e95a | ||
|
198e74cd88 | ||
|
8ed0a1de84 |
@@ -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;
|
||||||
|
}
|
@@ -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
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@@ -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] ?? {}
|
||||||
});
|
});
|
||||||
|
@@ -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
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@@ -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>
|
||||||
|
@@ -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"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -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"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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>
|
||||||
|
|
||||||
|
@@ -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>
|
||||||
|
@@ -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(
|
||||||
() => ({
|
() => ({
|
||||||
|
@@ -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,
|
||||||
|
@@ -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}
|
||||||
|
@@ -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
|
||||||
|
@@ -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,
|
||||||
|
@@ -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: {
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
@@ -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">
|
|
||||||
"kms:ListAliases"
|
|
||||||
</span>
|
|
||||||
,{" "}
|
|
||||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
|
||||||
"kms:DescribeKey"
|
|
||||||
</span>
|
|
||||||
,{" "}
|
|
||||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
|
||||||
"kms:Encrypt"
|
|
||||||
</span>
|
|
||||||
,{" "}
|
|
||||||
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
|
||||||
"kms:Decrypt"
|
|
||||||
</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">
|
||||||
|
"kms:ListAliases"
|
||||||
|
</span>
|
||||||
|
,{" "}
|
||||||
|
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||||
|
"kms:DescribeKey"
|
||||||
|
</span>
|
||||||
|
,{" "}
|
||||||
|
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||||
|
"kms:Encrypt"
|
||||||
|
</span>
|
||||||
|
,{" "}
|
||||||
|
<span className="rounded bg-mineshaft-600 text-mineshaft-300">
|
||||||
|
"kms:Decrypt"
|
||||||
|
</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}
|
||||||
>
|
>
|
||||||
|
@@ -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}
|
||||||
|
@@ -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" />
|
||||||
|
@@ -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}
|
||||||
|
@@ -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",
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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} />
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
@@ -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" />;
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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 />
|
|
||||||
);
|
|
||||||
};
|
|
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
@@ -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}
|
||||||
|
@@ -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}
|
||||||
|
@@ -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}
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user