Compare commits

...

44 Commits

Author SHA1 Message Date
d74b819f57 improvements: make logged in status disclaimer in email more prominent and only add email auth method if not already present 2025-01-28 09:53:40 -08:00
6af7c5c371 improvements: remove removed property reference and remove excess padding/margin on secret sync pages 2025-01-27 19:12:05 -08:00
72468d5428 feature: setup password 2025-01-27 18:51:35 -08:00
c7ec9ff816 Merge pull request #3050 from Infisical/daniel/k8-logs
feat(k8-operator): better error status
2025-01-27 23:53:23 +01:00
554e268f88 chore: update helm 2025-01-27 23:51:08 +01:00
a8a27c3045 feat(k8-operator): better error status 2025-01-27 23:48:20 +01:00
1181c684db Merge pull request #3036 from Infisical/identity-auth-ui-improvements
Improvement: Overhaul Identity Auth UI Section
2025-01-27 10:51:39 -05:00
dda436bcd9 Merge pull request #3046 from akhilmhdh/fix/breadcrumb-bug-github
fix: resolved github breadcrumb issue
2025-01-27 20:36:06 +05:30
=
89124b18d2 fix: resolved github breadcrumb issue 2025-01-27 20:29:06 +05:30
a534a4975c chore: revert license 2025-01-24 20:50:54 -08:00
79a616dc1c improvements: address feedback 2025-01-24 20:21:21 -08:00
a93bfa69c9 Merge pull request #3042 from Infisical/daniel/fix-approvals-for-personal-secrets
fix: approvals triggering for personal secrets
2025-01-25 04:50:19 +01:00
598d14fc54 improvement: move edit/delete identity buttons to dropdown 2025-01-24 19:34:03 -08:00
08a0550cd7 fix: correct dependency arra 2025-01-24 19:21:33 -08:00
d7503573b1 Merge pull request #3041 from Infisical/daniel/remove-caching-from-docs
docs: update node guid eand remove cache references
2025-01-25 04:15:53 +01:00
b5a89edeed Update node.mdx 2025-01-25 03:59:06 +01:00
860eaae4c8 fix: approvals triggering for personal secrets 2025-01-25 03:44:43 +01:00
c7a4b6c4e9 docs: update node guid eand remove cache references 2025-01-25 03:12:36 +01:00
c12c6dcc6e Merge pull request #2987 from Infisical/daniel/k8s-multi-managed-secrets
feat(k8-operator/infisicalsecret-crd): multiple secret references
2025-01-25 02:59:07 +01:00
99c9b644df improvements: address feedback 2025-01-24 12:55:56 -08:00
8741414cfa Update routeTree.gen.ts 2025-01-24 18:28:48 +01:00
b8d29793ec fix: rename managedSecretReferneces to managedKubeSecretReferences 2025-01-24 18:26:56 +01:00
92013dbfbc fix: routes 2025-01-24 18:26:34 +01:00
c5319588fe chore: fix routes geneartion 2025-01-24 18:26:23 +01:00
9efb8eaf78 Update infisical-secret-crd.mdx 2025-01-24 18:24:26 +01:00
dfc973c7f7 chore(k8-operator): update helm 2025-01-24 18:24:26 +01:00
3013d1977c docs(k8-operator): updated infisicalsecret crd docs 2025-01-24 18:24:26 +01:00
f358e8942d feat(k8-operator): multiple managed secrets 2025-01-24 18:24:26 +01:00
c3970d1ea2 Merge pull request #3038 from isaiahmartin847/typo-fix/Role-based-Access-Controls
Fixed the typo in the Role-based Access Controls docs.
2025-01-24 01:30:34 -05:00
2dc00a638a fixed the typo in the /access-controls/role-based-access-controls page in the docs. 2025-01-23 23:15:40 -07:00
94aed485a5 chore: optimize imports 2025-01-23 12:22:40 -08:00
e382941424 improvement: overhaul identity auth ui section 2025-01-23 12:18:09 -08:00
bab9c1f454 Merge pull request #3024 from Infisical/team-city-integration-fix
Fix: UI Fix for Team City Integrations Create Page
2025-01-23 18:14:32 +01:00
2bd4770fb4 Merge pull request #3035 from akhilmhdh/fix/env-ui
feat: updated ui validation for env to 64 like api
2025-01-23 16:32:04 +05:30
=
31905fab6e feat: updated ui validation for env to 64 like api 2025-01-23 16:26:13 +05:30
784acf16d0 Merge pull request #3032 from Infisical/correct-app-connections-docs
Improvements: Minor Secret Sync improvements and Correct App Connections Env Vars and Move Sync/Connections to Groups in Docs
2025-01-23 03:29:33 -05:00
114b89c952 Merge pull request #3033 from Infisical/daniel/update-python-docs
docs(guides): updated python guide
2025-01-23 03:28:11 -05:00
81420198cb fix: display aws connection credentials error and sync status on details page 2025-01-22 21:00:01 -08:00
0ff18e277f docs: redact info in image 2025-01-22 20:02:03 -08:00
e093f70301 docs: add new aws connection images 2025-01-22 19:58:24 -08:00
8e2ff18f35 docs: improve aws connection docs 2025-01-22 19:58:06 -08:00
3fbfecf7a9 docs: correct aws env vars in aws connection self-hosted docs 2025-01-22 18:46:36 -08:00
9087def21c docs: correct github connection env vars and move connections and syncs to group 2025-01-22 18:40:24 -08:00
586dbd79b0 fix: fix team city integrations create page 2025-01-21 18:37:01 -08:00
83 changed files with 4831 additions and 3382 deletions

View File

@ -203,7 +203,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
verifier: z.string().trim(),
password: z.string().trim()
}),
response: {
200: z.object({
@ -218,7 +219,69 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
userId: token.userId
});
return { message: "Successfully updated backup private key" };
return { message: "Successfully reset password" };
}
});
server.route({
method: "POST",
url: "/email/password-setup",
config: {
rateLimit: authRateLimit
},
schema: {
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req) => {
await server.services.password.sendPasswordSetupEmail(req.permission);
return {
message: "A password setup link has been sent"
};
}
});
server.route({
method: "POST",
url: "/password-setup",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
password: z.string().trim(),
token: z.string().trim()
}),
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req, res) => {
await server.services.password.setupPassword(req.body, req.permission);
const appCfg = getConfig();
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return { message: "Successfully setup password" };
}
});
};

View File

@ -81,11 +81,14 @@ export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig
};
export const validateAwsConnectionCredentials = async (appConnection: TAwsConnectionConfig) => {
const awsConfig = await getAwsConnectionConfig(appConnection);
const sts = new AWS.STS(awsConfig);
let resp: Awaited<ReturnType<ReturnType<typeof sts.getCallerIdentity>["promise"]>>;
let resp: AWS.STS.GetCallerIdentityResponse & {
$response: AWS.Response<AWS.STS.GetCallerIdentityResponse, AWS.AWSError>;
};
try {
const awsConfig = await getAwsConnectionConfig(appConnection);
const sts = new AWS.STS(awsConfig);
resp = await sts.getCallerIdentity().promise();
} catch (e: unknown) {
throw new BadRequestError({
@ -93,7 +96,7 @@ export const validateAwsConnectionCredentials = async (appConnection: TAwsConnec
});
}
if (resp.$response.httpResponse.statusCode !== 200)
if (resp?.$response.httpResponse.statusCode !== 200)
throw new InternalServerError({
message: `Unable to validate credentials: ${
resp.$response.error?.message ??

View File

@ -57,6 +57,12 @@ export const getTokenConfig = (tokenType: TokenType) => {
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt };
}
case TokenType.TOKEN_EMAIL_PASSWORD_SETUP: {
// generate random hex
const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date(new Date().getTime() + 86400000);
return { token, expiresAt };
}
case TokenType.TOKEN_USER_UNLOCK: {
const token = crypto.randomBytes(16).toString("hex");
const expiresAt = new Date(new Date().getTime() + 259200000);

View File

@ -6,6 +6,7 @@ export enum TokenType {
TOKEN_EMAIL_MFA = "emailMfa",
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup",
TOKEN_USER_UNLOCK = "userUnlock"
}

View File

@ -4,6 +4,8 @@ import jwt from "jsonwebtoken";
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { BadRequestError } from "@app/lib/errors";
import { OrgServiceActor } from "@app/lib/types";
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types";
@ -11,8 +13,13 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TTotpConfigDALFactory } from "../totp/totp-config-dal";
import { TUserDALFactory } from "../user/user-dal";
import { TAuthDALFactory } from "./auth-dal";
import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type";
import { AuthTokenType } from "./auth-type";
import {
TChangePasswordDTO,
TCreateBackupPrivateKeyDTO,
TResetPasswordViaBackupKeyDTO,
TSetupPasswordViaBackupKeyDTO
} from "./auth-password-type";
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
type TAuthPasswordServiceFactoryDep = {
authDAL: TAuthDALFactory;
@ -169,8 +176,13 @@ export const authPaswordServiceFactory = ({
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
userId
userId,
password
}: TResetPasswordViaBackupKeyDTO) => {
const cfg = getConfig();
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
await userDAL.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2,
protectedKey,
@ -180,7 +192,8 @@ export const authPaswordServiceFactory = ({
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
verifier,
hashedPassword
});
await userDAL.updateById(userId, {
@ -267,6 +280,108 @@ export const authPaswordServiceFactory = ({
return backupKey;
};
const sendPasswordSetupEmail = async (actor: OrgServiceActor) => {
if (actor.type !== ActorType.USER)
throw new BadRequestError({ message: `Actor of type ${actor.type} cannot set password` });
const user = await userDAL.findById(actor.id);
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
if (!user.isAccepted || !user.authMethods)
throw new BadRequestError({ message: `You must complete signup to set a password` });
const cfg = getConfig();
const token = await tokenService.createTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
userId: user.id
});
const email = user.email ?? user.username;
await smtpService.sendMail({
template: SmtpTemplates.SetupPassword,
recipients: [email],
subjectLine: "Infisical Password Setup",
substitutions: {
email,
token,
callback_url: cfg.SITE_URL ? `${cfg.SITE_URL}/password-setup` : ""
}
});
};
const setupPassword = async (
{
encryptedPrivateKey,
protectedKeyTag,
protectedKey,
protectedKeyIV,
salt,
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
password,
token
}: TSetupPasswordViaBackupKeyDTO,
actor: OrgServiceActor
) => {
try {
await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
userId: actor.id,
code: token
});
} catch (e) {
throw new BadRequestError({ message: "Expired or invalid token. Please try again." });
}
await userDAL.transaction(async (tx) => {
const user = await userDAL.findById(actor.id, tx);
if (!user) throw new BadRequestError({ message: `Could not find user with ID ${actor.id}` });
if (!user.isAccepted || !user.authMethods)
throw new BadRequestError({ message: `You must complete signup to set a password` });
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await userDAL.updateById(
actor.id,
{
authMethods: [...user.authMethods, AuthMethod.EMAIL]
},
tx
);
}
const cfg = getConfig();
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
await userDAL.updateUserEncryptionByUserId(
actor.id,
{
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
hashedPassword,
serverPrivateKey: null,
clientPublicKey: null
},
tx
);
});
await tokenService.revokeAllMySessions(actor.id);
};
return {
generateServerPubKey,
changePassword,
@ -274,6 +389,8 @@ export const authPaswordServiceFactory = ({
sendPasswordResetEmail,
verifyPasswordResetEmail,
createBackupPrivateKey,
getBackupPrivateKeyOfUser
getBackupPrivateKeyOfUser,
sendPasswordSetupEmail,
setupPassword
};
};

View File

@ -23,6 +23,20 @@ export type TResetPasswordViaBackupKeyDTO = {
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string;
};
export type TSetupPasswordViaBackupKeyDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string;
token: string;
};
export type TCreateBackupPrivateKeyDTO = {

View File

@ -30,6 +30,7 @@ export enum SmtpTemplates {
NewDeviceJoin = "newDevice.handlebars",
OrgInvite = "organizationInvitation.handlebars",
ResetPassword = "passwordReset.handlebars",
SetupPassword = "passwordSetup.handlebars",
SecretLeakIncident = "secretLeakIncident.handlebars",
WorkspaceInvite = "workspaceInvitation.handlebars",
ScimUserProvisioned = "scimUserProvisioned.handlebars",

View File

@ -0,0 +1,17 @@
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Password Setup</title>
</head>
<body>
<h2>Setup your password</h2>
<p>Someone requested to set up a password for your account.</p>
<p><strong>Make sure you are already logged in to Infisical in the current browser before clicking the link below.</strong></p>
<a href="{{callback_url}}?token={{token}}&to={{email}}">Setup password</a>
<p>If you didn't initiate this request, please contact
{{#if isCloud}}us immediately at team@infisical.com.{{else}}your administrator immediately.{{/if}}</p>
{{emailFooter}}
</body>
</html>

View File

@ -5,7 +5,7 @@ title: "Node"
This guide demonstrates how to use Infisical to manage secrets for your Node stack from local development to production. It uses:
- Infisical (you can use [Infisical Cloud](https://app.infisical.com) or a [self-hosted instance of Infisical](https://infisical.com/docs/self-hosting/overview)) to store your secrets.
- The [@infisical/sdk](https://github.com/Infisical/sdk/tree/main/languages/node) Node.js client SDK to fetch secrets back to your Node application on demand.
- The [@infisical/sdk](https://github.com/Infisical/node-sdk-v2) Node.js client SDK to fetch secrets back to your Node application on demand.
## Project Setup
@ -46,29 +46,43 @@ Finally, create an index.js file containing the application code.
```js
const express = require('express');
const { InfisicalClient } = require("@infisical/sdk");
const { InfisicalSDK } = require("@infisical/sdk");
const app = express();
const PORT = 3000;
const client = new InfisicalClient({
auth: {
universalAuth: {
clientId: "YOUR_CLIENT_ID",
clientSecret: "YOUR_CLIENT_SECRET",
let client;
const setupClient = () => {
if (client) {
return;
}
}
});
const infisicalSdk = new InfisicalSDK({
siteUrl: "your-infisical-instance.com" // Optional, defaults to https://app.infisical.com
});
await infisicalSdk.auth().universalAuth.login({
clientId: "<machine-identity-client-id>",
clientSecret: "<machine-identity-client-secret>"
});
// If authentication was successful, assign the client
client = infisicalSdk;
}
app.get("/", async (req, res) => {
// access value
const name = await client.getSecret({
environment: "dev",
projectId: "PROJECT_ID",
path: "/",
type: "shared",
const name = await client.secrets().getSecret({
environment: "dev", // dev, staging, prod, etc.
projectId: "<project-id>",
secretPath: "/",
secretName: "NAME"
});
@ -76,13 +90,13 @@ app.get("/", async (req, res) => {
});
app.listen(PORT, async () => {
// initialize client
console.log(`App listening on port ${PORT}`);
// initialize http server and Infisical
await setupClient();
console.log(`Server listening on port ${PORT}`);
});
```
Here, we initialized a `client` instance of the Infisical Node SDK with the Infisical Token
Here, we initialized a `client` instance of the Infisical Node SDK with the [Machine Identity](/documentation/platform/identities/overview)
that we created earlier, giving access to the secrets in the development environment of the
project in Infisical that we created earlier.
@ -94,16 +108,12 @@ node index.js
The client fetched the secret with the key `NAME` from Infisical that we returned in the response of the endpoint.
At this stage, you know how to fetch secrets from Infisical back to your Node application. By using Infisical Tokens scoped to different environments, you can easily manage secrets across various stages of your project in Infisical, from local development to production.
At this stage, you know how to fetch secrets from Infisical back to your Node application.
By using Machine Identities scoped to different projects and environments, you can easily manage secrets across various stages of your project in Infisical, from local development to production.
## FAQ
<AccordionGroup>
<Accordion title="Isn't it inefficient if my app makes a request every time it needs a secret?">
The client SDK caches every secret and implements a 5-minute waiting period before
re-requesting it. The waiting period can be controlled by setting the `cacheTTL` parameter at
the time of initializing the client.
</Accordion>
<Accordion title="What if a request for a secret fails?">
The SDK caches every secret and falls back to the cached value if a request fails. If no cached
value ever-existed, the SDK falls back to whatever value is on `process.env`.
@ -124,4 +134,4 @@ At this stage, you know how to fetch secrets from Infisical back to your Node ap
See also:
- Explore the [Node SDK](https://github.com/Infisical/sdk/tree/main/languages/node)
- Explore the [Node SDK](https://github.com/Infisical/node-sdk-v2)

View File

@ -3,7 +3,7 @@ title: "Role-based Access Controls"
description: "Learn how to use RBAC to manage user permissions."
---
Infisical's Role-based Access Controls (RBAC) enable the usage of predefined and custom roles that imply a set of permissions for user and machine identities. Such roles male it possible to restrict access to resources and the range of actions that can be performed.
Infisical's Role-based Access Controls (RBAC) enable the usage of predefined and custom roles that imply a set of permissions for user and machine identities. Such roles make it possible to restrict access to resources and the range of actions that can be performed.
In general, access controls can be split up across [projects](/documentation/platform/project) and [organizations](/documentation/platform/organization).

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 KiB

View File

@ -9,10 +9,6 @@ Infisical supports two methods for connecting to AWS.
<Tab title="Assume Role (Recommended)">
Infisical will assume the provided role in your AWS account securely, without the need to share any credentials.
**Prerequisites:**
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
<Accordion title="Self-Hosted Instance">
To connect your self-hosted Infisical instance with AWS, you need to set up an AWS IAM User account that can assume the configured AWS IAM Role.
@ -47,8 +43,8 @@ Infisical supports two methods for connecting to AWS.
![Access Key Step 3](/images/integrations/aws/integrations-aws-access-key-3.png)
</Step>
<Step title="Set Up Connection Keys">
1. Set the access key as **INF_APP_CONNECTION_AWS_CLIENT_ID**.
2. Set the secret key as **INF_APP_CONNECTION_AWS_CLIENT_SECRET**.
1. Set the access key as **INF_APP_CONNECTION_AWS_ACCESS_KEY_ID**.
2. Set the secret key as **INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY**.
</Step>
</Steps>
</Accordion>
@ -63,7 +59,11 @@ Infisical supports two methods for connecting to AWS.
4. Optionally, enable **Require external ID** and enter your **Organization ID** to further enhance security.
</Step>
<Step title="Add Required Permissions for the IAM Role">
<Step title="Add Required Permissions to the IAM Role">
Navigate to your IAM role permissions and click **Create Inline Policy**.
![IAM Role Create Policy](/images/app-connections/aws/assume-role-create-policy.png)
Depending on your use case, add one or more of the following policies to your IAM Role:
<Tabs>
@ -199,22 +199,13 @@ Infisical supports two methods for connecting to AWS.
<Tab title="Access Key">
Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance.
**Prerequisites:**
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
<Steps>
<Step title="Create the Managing User IAM Role for Infisical">
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
![IAM Role Creation](/images/integrations/aws/integration-aws-iam-assume-role.png)
<Step title="Add Required Permissions to the IAM User">
Navigate to your IAM user permissions and click **Create Inline Policy**.
2. Select **AWS Account** as the **Trusted Entity Type**.
3. Choose **Another AWS Account** and enter **381492033652** (Infisical AWS Account ID). This restricts the role to be assumed only by Infisical. If self-hosting, provide your AWS account number instead.
4. Optionally, enable **Require external ID** and enter your **Organization ID** to further enhance security.
</Step>
![User IAM Create Policy](/images/app-connections/aws/access-key-create-policy.png)
<Step title="Add Required Permissions for the IAM Role">
Depending on your use case, add one or more of the following policies to your IAM Role:
Depending on your use case, add one or more of the following policies to your user:
<Tabs>
<Tab title="Secret Sync">

View File

@ -9,10 +9,6 @@ Infisical supports two methods for connecting to GitHub.
<Tab title="GitHub App (Recommended)">
Infisical will use a GitHub App with finely grained permissions to connect to GitHub.
**Prerequisites:**
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
<Accordion title="Self-Hosted Instance">
Using the GitHub integration with app authentication on a self-hosted instance of Infisical requires configuring an application on GitHub
and registering your instance with it.
@ -61,9 +57,9 @@ Infisical supports two methods for connecting to GitHub.
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_ID`: The **Client ID** of your GitHub application.
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_SECRET`: The **Client Secret** of your GitHub application.
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_SLUG`: The **Slug** of your GitHub application. This is the one found in the URL.
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_APP_ID`: The **App ID** of your GitHub application.
- `INF_APP_CONNECTION_GITHUB_APP_CLIENT_PRIVATE_KEY`: The **Private Key** of your GitHub application.
- `INF_APP_CONNECTION_GITHUB_APP_SLUG`: The **Slug** of your GitHub application. This is the one found in the URL.
- `INF_APP_CONNECTION_GITHUB_APP_ID`: The **App ID** of your GitHub application.
- `INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY`: The **Private Key** of your GitHub application.
Once added, restart your Infisical instance and use the GitHub integration via app authentication.
</Step>
@ -100,10 +96,6 @@ Infisical supports two methods for connecting to GitHub.
<Tab title="OAuth">
Infisical will use an OAuth App to connect to GitHub.
**Prerequisites:**
- Set up and add envars to [Infisical Cloud](https://app.infisical.com)
<Accordion title="Self-Hosted Instance">
Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub
and registering your instance with it.

View File

@ -26,8 +26,8 @@ spec:
name: <service-account-name>
namespace: <service-account-namespace>
managedSecretReference:
secretName: managed-secret
managedKubeSecretReferences:
- secretName: managed-secret
secretNamespace: default
creationPolicy: "Orphan"
template:
@ -541,18 +541,32 @@ The managed secret properties specify where to store the secrets retrieved from
This includes defining the name and namespace of the Kubernetes secret that will hold these secrets.
The Infisical operator will automatically create the Kubernetes secret in the specified name/namespace and ensure it stays up-to-date.
<Accordion title="managedSecretReference">
<Note>
The `managedSecretReference` field is deprecated and will be removed in a future release.
Replace it with `managedKubeSecretReferences`, which now accepts an array of references to support multiple managed secrets in a single InfisicalSecret CRD.
Example:
```yaml
managedKubeSecretReferences:
- secretName: managed-secret
secretNamespace: default
creationPolicy: "Orphan"
```
</Note>
<Accordion title="managedKubeSecretReferences">
</Accordion>
<Accordion title="managedSecretReference.secretName">
<Accordion title="managedKubeSecretReferences[].secretName">
The name of the managed Kubernetes secret to be created
</Accordion>
<Accordion title="managedSecretReference.secretNamespace">
<Accordion title="managedKubeSecretReferences[].secretNamespace">
The namespace of the managed Kubernetes secret to be created.
</Accordion>
<Accordion title="managedSecretReference.secretType">
<Accordion title="managedKubeSecretReferences[].secretType">
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
</Accordion>
<Accordion title="managedSecretReference.creationPolicy">
<Accordion title="managedKubeSecretReferences[].creationPolicy">
Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator.
This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically.
@ -573,18 +587,18 @@ This is useful for tools such as ArgoCD, where every resource requires an owner
Fetching secrets from Infisical as is via the operator may not be enough. This is where templating functionality may be helpful.
Using Go templates, you can format, combine, and create new key-value pairs from secrets fetched from Infisical before storing them as Kubernetes Secrets.
<Accordion title="managedSecretReference.template">
<Accordion title="managedKubeSecretReferences[].template">
</Accordion>
<Accordion title="managedSecretReference.template.includeAllSecrets">
<Accordion title="managedKubeSecretReferences[].template.includeAllSecrets">
This property controls what secrets are included in your managed secret when using templates.
When set to `true`, all secrets fetched from your Infisical project will be added into your managed Kubernetes secret resource.
**Use this option when you would like to sync all secrets from Infisical to Kubernetes but want to template a subset of them.**
When set to `false`, only secrets defined in the `managedSecretReference.template.data` field of the template will be included in the managed secret.
When set to `false`, only secrets defined in the `managedKubeSecretReferences[].template.data` field of the template will be included in the managed secret.
Use this option when you would like to sync **only** a subset of secrets from Infisical to Kubernetes.
</Accordion>
<Accordion title="managedSecretReference.template.data">
<Accordion title="managedKubeSecretReferences[].template.data">
Define secret keys and their corresponding templates.
Each data value uses a Golang template with access to all secrets retrieved from the specified scope.
@ -600,8 +614,8 @@ type TemplateSecret struct {
#### Example template configuration:
```yaml
managedSecretReference:
secretName: managed-secret
managedKubeSecretReferences:
- secretName: managed-secret
secretNamespace: default
template:
includeAllSecrets: true
@ -652,10 +666,10 @@ The example below assumes that the `BINARY_KEY_BASE64` secret is stored as a bas
The resulting managed secret will contain the decoded value of `BINARY_KEY_BASE64`.
```yaml
managedSecretReference:
secretName: managed-secret
secretNamespace: default
template:
managedKubeSecretReferences:
secretName: managed-secret
secretNamespace: default
template:
includeAllSecrets: true
data:
BINARY_KEY: "{{ decodeBase64ToBytes .BINARY_KEY_BASE64.Value }}"
@ -913,7 +927,7 @@ spec:
..
authentication:
...
managedSecretReference:
managedKubeSecretReferences:
...
```

View File

@ -347,17 +347,27 @@
"group": "App Connections",
"pages": [
"integrations/app-connections/overview",
{
"group": "Connections",
"pages": [
"integrations/app-connections/aws",
"integrations/app-connections/github"
]
}
]
},
{
"group": "Secret Syncs",
"pages": [
"integrations/secret-syncs/overview",
{
"group": "Syncs",
"pages": [
"integrations/secret-syncs/aws-parameter-store",
"integrations/secret-syncs/github"
]
}
]
},
{
"group": "Infrastructure Integrations",

View File

@ -34,12 +34,6 @@ From local development to production, Infisical SDKs provide the easiest way for
## FAQ
<AccordionGroup>
<Accordion title="Isn't it inefficient if my app makes a request every time it needs a secret?">
The client SDK caches every secret and implements a 5-minute waiting period before re-requesting it. The waiting period can be controlled by
setting the `cacheTTL` parameter at the time of initializing the client.
Note: The exact parameter name may differ depending on the language.
</Accordion>
<Accordion title="What if a request for a secret fails?">
The SDK caches every secret and falls back to the cached value if a request fails. If no cached
value ever-existed, the SDK falls back to whatever value is on the process environment.

View File

@ -13,7 +13,8 @@ export const ROUTE_PATHS = Object.freeze({
"/_restrict-login-signup/login/provider/success"
),
SignUpSsoPage: setRoute("/signup/sso", "/_restrict-login-signup/signup/sso"),
PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset")
PasswordResetPage: setRoute("/password-reset", "/_restrict-login-signup/password-reset"),
PasswordSetupPage: setRoute("/password-setup", "/_authenticate/password-setup")
},
Organization: {
SecretScanning: setRoute(

View File

@ -23,6 +23,7 @@ import {
MfaMethod,
ResetPasswordDTO,
SendMfaTokenDTO,
SetupPasswordDTO,
SRP1DTO,
SRPR1Res,
TOauthTokenExchangeDTO,
@ -286,7 +287,8 @@ export const useResetPassword = () => {
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
salt: details.salt,
verifier: details.verifier
verifier: details.verifier,
password: details.password
},
{
headers: {
@ -336,3 +338,23 @@ export const checkUserTotpMfa = async () => {
return data.isVerified;
};
export const useSendPasswordSetupEmail = () => {
return useMutation({
mutationFn: async () => {
const { data } = await apiRequest.post("/api/v1/password/email/password-setup");
return data;
}
});
};
export const useSetupPassword = () => {
return useMutation({
mutationFn: async (payload: SetupPasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/password-setup", payload);
return data;
}
});
};

View File

@ -133,6 +133,20 @@ export type ResetPasswordDTO = {
salt: string;
verifier: string;
verificationToken: string;
password: string;
};
export type SetupPasswordDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
token: string;
password: string;
};
export type IssueBackupPrivateKeyDTO = {

View File

@ -923,7 +923,7 @@ export const useAddIdentityTokenAuth = () => {
});
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
queryClient.invalidateQueries({
queryKey: identitiesKeys.getIdentityUniversalAuth(identityId)
queryKey: identitiesKeys.getIdentityTokenAuth(identityId)
});
}
});
@ -959,7 +959,7 @@ export const useUpdateIdentityTokenAuth = () => {
});
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
queryClient.invalidateQueries({
queryKey: identitiesKeys.getIdentityUniversalAuth(identityId)
queryKey: identitiesKeys.getIdentityTokenAuth(identityId)
});
}
});

View File

@ -182,7 +182,7 @@ export const queryClient = new QueryClient({
createNotification({
title: "Bad Request",
type: "error",
text: `${serverResponse.message}${serverResponse.message.endsWith(".") ? "" : "."}`,
text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`,
copyActions: [
{
value: serverResponse.reqId,

View File

@ -136,7 +136,8 @@ export const PasswordResetPage = () => {
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
verificationToken
verificationToken,
password: newPassword
});
navigate({ to: "/login" });

View File

@ -0,0 +1,349 @@
import crypto from "crypto";
import { FormEvent, useState } from "react";
import { faCheck, faEye, faEyeSlash, faKey, faX } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import jsrp from "jsrp";
import { createNotification } from "@app/components/notifications";
import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto";
import { Button, Card, CardTitle, FormControl, Input } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { useSetupPassword } from "@app/hooks/api/auth/queries";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
export const PasswordSetupPage = () => {
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [passwordsMatch, setPasswordsMatch] = useState(true);
const [passwordErrorTooShort, setPasswordErrorTooShort] = useState(true);
const [passwordErrorTooLong, setPasswordErrorTooLong] = useState(false);
const [passwordErrorNoLetterChar, setPasswordErrorNoLetterChar] = useState(true);
const [passwordErrorNoNumOrSpecialChar, setPasswordErrorNoNumOrSpecialChar] = useState(true);
const [passwordErrorRepeatedChar, setPasswordErrorRepeatedChar] = useState(false);
const [passwordErrorEscapeChar, setPasswordErrorEscapeChar] = useState(false);
const [passwordErrorLowEntropy, setPasswordErrorLowEntropy] = useState(false);
const [passwordErrorBreached, setPasswordErrorBreached] = useState(false);
const [isRedirecting, setIsRedirecting] = useState(false);
const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordSetupPage.id });
const navigate = useNavigate();
const setupPassword = useSetupPassword();
const parsedUrl = search;
const token = parsedUrl.token as string;
const email = (parsedUrl.to as string)?.replace(" ", "+").trim();
const handleSetPassword = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const errorCheck = await passwordCheck({
password,
setPasswordErrorTooShort,
setPasswordErrorTooLong,
setPasswordErrorNoLetterChar,
setPasswordErrorNoNumOrSpecialChar,
setPasswordErrorRepeatedChar,
setPasswordErrorEscapeChar,
setPasswordErrorLowEntropy,
setPasswordErrorBreached
});
if (password !== confirmPassword) {
setPasswordsMatch(false);
return;
}
setPasswordsMatch(true);
if (!errorCheck) {
client.init(
{
username: email,
password
},
async () => {
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
if (!derivedKey) throw new Error("Failed to derive key from password");
const key = crypto.randomBytes(32);
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: localStorage.getItem("PRIVATE_KEY") as string,
secret: key
});
// create the protected key by encrypting the symmetric key
// [key] with the derived key
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
try {
await setupPassword.mutateAsync({
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
token,
password
});
setIsRedirecting(true);
createNotification({
type: "success",
title: "Password successfully set",
text: "Redirecting to login..."
});
setTimeout(() => {
window.location.href = "/login";
}, 3000);
} catch (error) {
createNotification({
type: "error",
text: (error as Error).message ?? "Error setting password"
});
navigate({ to: "/personal-settings" });
}
});
}
);
}
};
const isInvalidPassword =
passwordErrorTooShort ||
passwordErrorTooLong ||
passwordErrorNoLetterChar ||
passwordErrorNoNumOrSpecialChar ||
passwordErrorRepeatedChar ||
passwordErrorEscapeChar ||
passwordErrorLowEntropy ||
passwordErrorBreached;
return (
<div className="flex h-screen w-full flex-col items-center justify-center bg-bunker-800">
<form onSubmit={handleSetPassword}>
<Card className="flex w-full max-w-lg flex-col rounded-md border border-mineshaft-600 px-8 py-4">
<CardTitle
className="p-0 pb-4 pt-2 text-left text-xl"
subTitle="Make sure to store your password somewhere safe."
>
<div className="flex flex-row items-center">
<div className="flex items-center pb-0.5">
<FontAwesomeIcon icon={faKey} />
</div>
<span className="ml-2.5">Set Password</span>
</div>
</CardTitle>
<FormControl label="Password">
<Input
value={password}
type={showPassword ? "text" : "password"}
autoComplete="new-password"
onChange={(e) => {
setPassword(e.target.value);
passwordCheck({
password: e.target.value,
setPasswordErrorTooShort,
setPasswordErrorTooLong,
setPasswordErrorNoLetterChar,
setPasswordErrorNoNumOrSpecialChar,
setPasswordErrorRepeatedChar,
setPasswordErrorEscapeChar,
setPasswordErrorLowEntropy,
setPasswordErrorBreached
});
}}
rightIcon={
<button
type="button"
onClick={() => {
setShowPassword((prev) => !prev);
}}
className="cursor-pointer self-end text-gray-400"
>
{showPassword ? (
<FontAwesomeIcon size="sm" icon={faEyeSlash} />
) : (
<FontAwesomeIcon size="sm" icon={faEye} />
)}
</button>
}
/>
</FormControl>
<FormControl
label="Confirm Password"
errorText="Passwords must match"
isError={!passwordsMatch}
>
<Input
value={confirmPassword}
type={showConfirmPassword ? "text" : "password"}
autoComplete="new-password"
onChange={(e) => setConfirmPassword(e.target.value)}
rightIcon={
<button
type="button"
onClick={() => {
setShowConfirmPassword((prev) => !prev);
}}
className="cursor-pointer self-end text-gray-400"
>
{showConfirmPassword ? (
<FontAwesomeIcon size="sm" icon={faEyeSlash} />
) : (
<FontAwesomeIcon size="sm" icon={faEye} />
)}
</button>
}
/>
</FormControl>
<div className="mb-4 flex w-full max-w-md flex-col items-start rounded-md bg-mineshaft-700 px-2 py-2 transition-opacity duration-100">
<div className="mb-1 text-sm text-gray-400">Password must contain:</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorTooShort ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorTooShort ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at least 14 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorTooLong ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorTooLong ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at most 100 characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNoLetterChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorNoLetterChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at least 1 letter character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorNoNumOrSpecialChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${
passwordErrorNoNumOrSpecialChar ? "text-gray-400" : "text-gray-600"
} text-sm`}
>
at least 1 number or special character
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorRepeatedChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorRepeatedChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
at most 3 repeated, consecutive characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorEscapeChar ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorEscapeChar ? "text-gray-400" : "text-gray-600"} text-sm`}
>
no escape characters
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorLowEntropy ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorLowEntropy ? "text-gray-400" : "text-gray-600"} text-sm`}
>
no personal information
</div>
</div>
<div className="ml-1 flex flex-row items-center justify-start">
{passwordErrorBreached ? (
<FontAwesomeIcon icon={faX} className="text-md mr-2.5 text-red" />
) : (
<FontAwesomeIcon icon={faCheck} className="text-md mr-2 text-primary" />
)}
<div
className={`${passwordErrorBreached ? "text-gray-400" : "text-gray-600"} text-sm`}
>
password not found in a data breach.
</div>
</div>
</div>
<Button
isDisabled={isInvalidPassword || setupPassword.isPending || isRedirecting}
colorSchema="secondary"
type="submit"
isLoading={setupPassword.isPending}
>
Submit
</Button>
</Card>
</form>
</div>
);
};

View File

@ -0,0 +1,15 @@
import { createFileRoute } from "@tanstack/react-router";
import { zodValidator } from "@tanstack/zod-adapter";
import { z } from "zod";
import { PasswordSetupPage } from "./PasswordSetupPage";
const PasswordSetupPageQueryParamsSchema = z.object({
token: z.string(),
to: z.string()
});
export const Route = createFileRoute("/_authenticate/password-setup")({
component: PasswordSetupPage,
validateSearch: zodValidator(PasswordSetupPageQueryParamsSchema)
});

View File

@ -1,12 +1,13 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
import { createNotification } from "@app/components/notifications";
import { ROUTE_PATHS } from "@app/const/routes";
import { userKeys } from "@app/hooks/api";
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
import { fetchUserDetails } from "@app/hooks/api/users/queries";
export const Route = createFileRoute("/_authenticate")({
beforeLoad: async ({ context }) => {
beforeLoad: async ({ context, location }) => {
if (!context.serverConfig.initialized) {
throw redirect({ to: "/admin/signup" });
}
@ -26,7 +27,7 @@ export const Route = createFileRoute("/_authenticate")({
});
});
if (!data.organizationId) {
if (!data.organizationId && location.pathname !== ROUTE_PATHS.Auth.PasswordSetupPage.path) {
throw redirect({ to: "/login/select-organization" });
}

View File

@ -7,10 +7,10 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAuthMethodModalContent } from "./IdentityAuthMethodModalContent";
type Props = {
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>,
state?: boolean
) => void;
};
@ -34,7 +34,7 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
title={
isSelectedAuthAlreadyConfigured
? `Edit ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
: `Create new ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
: `Add ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
}
>
<IdentityAuthMethodModalContent

View File

@ -4,30 +4,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import {
Badge,
DeleteActionModal,
FormControl,
Select,
SelectItem,
Tooltip
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
import {
IdentityAuthMethod,
identityAuthToNameMap,
useDeleteIdentityJwtAuth
} from "@app/hooks/api/identities";
import { Badge, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
@ -40,10 +18,10 @@ import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
type Props = {
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>,
state?: boolean
) => void;
@ -56,13 +34,7 @@ type Props = {
setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void;
};
type TRevokeOptions = {
identityId: string;
organizationId: string;
};
type TRevokeMethods = {
revokeMethod: (revokeOptions: TRevokeOptions) => Promise<any>;
render: () => JSX.Element;
};
@ -96,18 +68,6 @@ export const IdentityAuthMethodModalContent = ({
initialAuthMethod,
setSelectedAuthMethod
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth();
const { control, watch } = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: async () => {
@ -149,10 +109,9 @@ export const IdentityAuthMethodModalContent = ({
const methodMap: Record<IdentityAuthMethod, TRevokeMethods | undefined> = {
[IdentityAuthMethod.UNIVERSAL_AUTH]: {
revokeMethod: revokeUniversalAuth,
render: () => (
<IdentityUniversalAuthForm
identityAuthMethodData={identityAuthMethodData}
identityId={identityAuthMethodData.identityId}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
@ -160,10 +119,9 @@ export const IdentityAuthMethodModalContent = ({
},
[IdentityAuthMethod.OIDC_AUTH]: {
revokeMethod: revokeOidcAuth,
render: () => (
<IdentityOidcAuthForm
identityAuthMethodData={identityAuthMethodData}
identityId={identityAuthMethodData.identityId}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
@ -171,10 +129,9 @@ export const IdentityAuthMethodModalContent = ({
},
[IdentityAuthMethod.TOKEN_AUTH]: {
revokeMethod: revokeTokenAuth,
render: () => (
<IdentityTokenAuthForm
identityAuthMethodData={identityAuthMethodData}
identityId={identityAuthMethodData.identityId}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
@ -182,10 +139,9 @@ export const IdentityAuthMethodModalContent = ({
},
[IdentityAuthMethod.AZURE_AUTH]: {
revokeMethod: revokeAzureAuth,
render: () => (
<IdentityAzureAuthForm
identityAuthMethodData={identityAuthMethodData}
identityId={identityAuthMethodData.identityId}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
@ -193,10 +149,9 @@ export const IdentityAuthMethodModalContent = ({
},
[IdentityAuthMethod.GCP_AUTH]: {
revokeMethod: revokeGcpAuth,
render: () => (
<IdentityGcpAuthForm
identityAuthMethodData={identityAuthMethodData}
identityId={identityAuthMethodData.identityId}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
@ -204,10 +159,9 @@ export const IdentityAuthMethodModalContent = ({
},
[IdentityAuthMethod.KUBERNETES_AUTH]: {
revokeMethod: revokeKubernetesAuth,
render: () => (
<IdentityKubernetesAuthForm
identityAuthMethodData={identityAuthMethodData}
identityId={identityAuthMethodData.identityId}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
@ -215,10 +169,9 @@ export const IdentityAuthMethodModalContent = ({
},
[IdentityAuthMethod.AWS_AUTH]: {
revokeMethod: revokeAwsAuth,
render: () => (
<IdentityAwsAuthForm
identityAuthMethodData={identityAuthMethodData}
identityId={identityAuthMethodData.identityId}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
@ -226,10 +179,9 @@ export const IdentityAuthMethodModalContent = ({
},
[IdentityAuthMethod.JWT_AUTH]: {
revokeMethod: revokeJwtAuth,
render: () => (
<IdentityJwtAuthForm
identityAuthMethodData={identityAuthMethodData}
identityId={identityAuthMethodData.identityId}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
@ -294,42 +246,6 @@ export const IdentityAuthMethodModalContent = ({
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
/>
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${
identityAuthMethodData?.authMethod
? identityAuthToNameMap[identityAuthMethodData.authMethod]
: "the auth method"
} on ${identityAuthMethodData?.name ?? ""}?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Remove"
onDeleteApproved={async () => {
if (!identityAuthMethodData.authMethod || !orgId || !selectedMethodItem) {
return;
}
try {
await selectedMethodItem.revokeMethod({
identityId: identityAuthMethodData.identityId,
organizationId: orgId
});
createNotification({
text: "Successfully removed auth method",
type: "success"
});
handlePopUpToggle("revokeAuthMethod", false);
handlePopUpToggle("identityAuthMethod", false);
} catch {
createNotification({
text: "Failed to remove auth method",
type: "error"
});
}
}}
/>
</>
);
};

View File

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,17 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import {
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useAddIdentityAwsAuth,
useGetIdentityAwsAuth,
useUpdateIdentityAwsAuth
} from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z
.object({
stsEndpoint: z.string(),
@ -49,21 +59,18 @@ export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
identityId?: string;
isUpdate?: boolean;
};
export const IdentityAwsAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
identityId,
isUpdate
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
@ -71,11 +78,9 @@ export const IdentityAwsAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", {
const { data } = useGetIdentityAwsAuth(identityId ?? "", {
enabled: isUpdate
});
@ -143,7 +148,7 @@ export const IdentityAwsAuthForm = ({
accessTokenTrustedIps
}: FormData) => {
try {
if (!identityAuthMethodData) return;
if (!identityId) return;
if (data) {
await updateMutateAsync({
@ -151,7 +156,7 @@ export const IdentityAwsAuthForm = ({
stsEndpoint,
allowedPrincipalArns,
allowedAccountIds,
identityId: identityAuthMethodData.identityId,
identityId,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@ -160,7 +165,7 @@ export const IdentityAwsAuthForm = ({
} else {
await addMutateAsync({
organizationId: orgId,
identityId: identityAuthMethodData.identityId,
identityId,
stsEndpoint: stsEndpoint || "",
allowedPrincipalArns: allowedPrincipalArns || "",
allowedAccountIds: allowedAccountIds || "",
@ -188,7 +193,21 @@ export const IdentityAwsAuthForm = ({
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
? IdentityFormTab.Advanced
: IdentityFormTab.Configuration
);
})}
>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
<TabList>
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
<Controller
control={control}
defaultValue="2592000"
@ -272,6 +291,8 @@ export const IdentityAwsAuthForm = ({
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@ -340,7 +361,8 @@ export const IdentityAwsAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex justify-between">
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
@ -349,7 +371,7 @@ export const IdentityAwsAuthForm = ({
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
{isUpdate ? "Update" : "Add"}
</Button>
<Button
@ -360,18 +382,6 @@ export const IdentityAwsAuthForm = ({
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);
};

View File

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,17 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import {
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useAddIdentityAzureAuth,
useGetIdentityAzureAuth,
useUpdateIdentityAzureAuth
} from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z
.object({
tenantId: z.string().min(1),
@ -44,21 +54,18 @@ export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
identityId?: string;
isUpdate?: boolean;
};
export const IdentityAzureAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
identityId,
isUpdate
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
@ -66,11 +73,9 @@ export const IdentityAzureAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", {
const { data } = useGetIdentityAzureAuth(identityId ?? "", {
enabled: isUpdate
});
@ -139,12 +144,12 @@ export const IdentityAzureAuthForm = ({
accessTokenTrustedIps
}: FormData) => {
try {
if (!identityAuthMethodData) return;
if (!identityId) return;
if (data) {
await updateMutateAsync({
organizationId: orgId,
identityId: identityAuthMethodData.identityId,
identityId,
tenantId,
resource,
allowedServicePrincipalIds,
@ -156,7 +161,7 @@ export const IdentityAzureAuthForm = ({
} else {
await addMutateAsync({
organizationId: orgId,
identityId: identityAuthMethodData.identityId,
identityId,
tenantId: tenantId || "",
resource: resource || "",
allowedServicePrincipalIds: allowedServicePrincipalIds || "",
@ -184,7 +189,21 @@ export const IdentityAzureAuthForm = ({
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
? IdentityFormTab.Advanced
: IdentityFormTab.Configuration
);
})}
>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
<TabList>
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
<Controller
control={control}
defaultValue="2592000"
@ -268,6 +287,8 @@ export const IdentityAzureAuthForm = ({
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@ -336,7 +357,8 @@ export const IdentityAzureAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex justify-between">
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
@ -345,7 +367,7 @@ export const IdentityAzureAuthForm = ({
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
{isUpdate ? "Update" : "Add"}
</Button>
<Button
@ -356,18 +378,6 @@ export const IdentityAzureAuthForm = ({
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);
};

View File

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,17 +6,29 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, Select, SelectItem } from "@app/components/v2";
import {
Button,
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useAddIdentityGcpAuth,
useGetIdentityGcpAuth,
useUpdateIdentityGcpAuth
} from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z
.object({
type: z.enum(["iam", "gce"]),
@ -45,21 +57,18 @@ export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
identityId?: string;
isUpdate?: boolean;
};
export const IdentityGcpAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
identityId,
isUpdate
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
@ -67,11 +76,9 @@ export const IdentityGcpAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", {
const { data } = useGetIdentityGcpAuth(identityId ?? "", {
enabled: isUpdate
});
@ -146,11 +153,11 @@ export const IdentityGcpAuthForm = ({
accessTokenTrustedIps
}: FormData) => {
try {
if (!identityAuthMethodData) return;
if (!identityId) return;
if (data) {
await updateMutateAsync({
identityId: identityAuthMethodData.identityId,
identityId,
organizationId: orgId,
type,
allowedServiceAccounts,
@ -163,7 +170,7 @@ export const IdentityGcpAuthForm = ({
});
} else {
await addMutateAsync({
identityId: identityAuthMethodData.identityId,
identityId,
organizationId: orgId,
type,
allowedServiceAccounts: allowedServiceAccounts || "",
@ -193,7 +200,21 @@ export const IdentityGcpAuthForm = ({
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
? IdentityFormTab.Advanced
: IdentityFormTab.Configuration
);
})}
>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
<TabList>
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
<Controller
control={control}
name="type"
@ -253,7 +274,11 @@ export const IdentityGcpAuthForm = ({
control={control}
name="allowedZones"
render={({ field, fieldState: { error } }) => (
<FormControl label="Allowed Zones" isError={Boolean(error)} errorText={error?.message}>
<FormControl
label="Allowed Zones"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
</FormControl>
)}
@ -301,6 +326,8 @@ export const IdentityGcpAuthForm = ({
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@ -369,7 +396,8 @@ export const IdentityGcpAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex justify-between">
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
@ -378,7 +406,7 @@ export const IdentityGcpAuthForm = ({
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{!isUpdate ? "Create" : "Edit"}
{isUpdate ? "Update" : "Add"}
</Button>
<Button
@ -389,18 +417,6 @@ export const IdentityGcpAuthForm = ({
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);
};

View File

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
@ -14,17 +14,22 @@ import {
Input,
Select,
SelectItem,
Tab,
TabList,
TabPanel,
Tabs,
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import { useAddIdentityJwtAuth, useUpdateIdentityJwtAuth } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
import { useGetIdentityJwtAuth } from "@app/hooks/api/identities/queries";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const commonSchema = z.object({
accessTokenTrustedIps: z
.array(
@ -85,21 +90,18 @@ export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
identityId?: string;
isUpdate?: boolean;
};
export const IdentityJwtAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
identityId,
isUpdate
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
@ -107,11 +109,9 @@ export const IdentityJwtAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityJwtAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityJwtAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityJwtAuth(identityAuthMethodData?.identityId ?? "", {
const { data } = useGetIdentityJwtAuth(identityId ?? "", {
enabled: isUpdate
});
@ -218,13 +218,13 @@ export const IdentityJwtAuthForm = ({
boundSubject
}: FormData) => {
try {
if (!identityAuthMethodData) {
if (!identityId) {
return;
}
if (data) {
await updateMutateAsync({
identityId: identityAuthMethodData.identityId,
identityId,
organizationId: orgId,
configurationType,
jwksUrl,
@ -241,7 +241,7 @@ export const IdentityJwtAuthForm = ({
});
} else {
await addMutateAsync({
identityId: identityAuthMethodData.identityId,
identityId,
configurationType,
jwksUrl,
jwksCaCert,
@ -275,7 +275,21 @@ export const IdentityJwtAuthForm = ({
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
? IdentityFormTab.Advanced
: IdentityFormTab.Configuration
);
})}
>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
<TabList>
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
<Controller
control={control}
name="configurationType"
@ -345,7 +359,6 @@ export const IdentityJwtAuthForm = ({
/>
</>
)}
{selectedConfigurationType === IdentityJwtConfigurationType.STATIC && (
<>
{publicKeyFields.map(({ id }, index) => (
@ -410,7 +423,6 @@ export const IdentityJwtAuthForm = ({
</div>
</>
)}
<Controller
control={control}
name="boundIssuer"
@ -583,6 +595,8 @@ export const IdentityJwtAuthForm = ({
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@ -651,7 +665,8 @@ export const IdentityJwtAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex justify-between">
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
@ -671,18 +686,6 @@ export const IdentityJwtAuthForm = ({
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);
};

View File

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,17 +6,28 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, TextArea } from "@app/components/v2";
import {
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs,
TextArea
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useAddIdentityKubernetesAuth,
useGetIdentityKubernetesAuth,
useUpdateIdentityKubernetesAuth
} from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z
.object({
kubernetesHost: z.string().min(1),
@ -47,21 +58,18 @@ export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
identityId?: string;
isUpdate?: boolean;
};
export const IdentityKubernetesAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
identityId,
isUpdate
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
@ -69,11 +77,9 @@ export const IdentityKubernetesAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "", {
const { data } = useGetIdentityKubernetesAuth(identityId ?? "", {
enabled: isUpdate
});
@ -154,7 +160,7 @@ export const IdentityKubernetesAuthForm = ({
accessTokenTrustedIps
}: FormData) => {
try {
if (!identityAuthMethodData) return;
if (!identityId) return;
if (data) {
await updateMutateAsync({
@ -165,7 +171,7 @@ export const IdentityKubernetesAuthForm = ({
allowedNamespaces,
allowedAudience,
caCert,
identityId: identityAuthMethodData.identityId,
identityId,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@ -174,7 +180,7 @@ export const IdentityKubernetesAuthForm = ({
} else {
await addMutateAsync({
organizationId: orgId,
identityId: identityAuthMethodData.identityId,
identityId,
kubernetesHost: kubernetesHost || "",
tokenReviewerJwt,
allowedNames: allowedNames || "",
@ -205,7 +211,29 @@ export const IdentityKubernetesAuthForm = ({
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
[
"kubernetesHost",
"tokenReviewerJwt",
"accessTokenTTL",
"accessTokenMaxTTL",
"accessTokenNumUsesLimit",
"allowedNames",
"allowedNamespaces"
].includes(Object.keys(fields)[0])
? IdentityFormTab.Configuration
: IdentityFormTab.Advanced
);
})}
>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
<TabList>
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
<Controller
control={control}
defaultValue="2592000"
@ -237,20 +265,6 @@ export const IdentityKubernetesAuthForm = ({
</FormControl>
)}
/>
<Controller
control={control}
name="allowedNames"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Service Account Names"
isError={Boolean(error)}
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
errorText={error?.message}
>
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
@ -268,30 +282,15 @@ export const IdentityKubernetesAuthForm = ({
/>
<Controller
control={control}
defaultValue=""
name="allowedAudience"
name="allowedNames"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Audience"
label="Allowed Service Account Names"
isError={Boolean(error)}
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
errorText={error?.message}
tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim."
>
<Input {...field} placeholder="" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server."
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
</FormControl>
)}
/>
@ -340,6 +339,37 @@ export const IdentityKubernetesAuthForm = ({
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
<Controller
control={control}
defaultValue=""
name="allowedAudience"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Allowed Audience"
isError={Boolean(error)}
errorText={error?.message}
tooltipText="An optional audience claim that the service account JWT token must have to authenticate with Infisical. Leave empty to allow any audience claim."
>
<Input {...field} placeholder="" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
tooltipText="An optional PEM-encoded CA cert for the Kubernetes API server. This is used by the TLS client for secure communication with the Kubernetes API server."
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@ -409,7 +439,8 @@ export const IdentityKubernetesAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex justify-between">
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
@ -418,7 +449,7 @@ export const IdentityKubernetesAuthForm = ({
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Create"}
{isUpdate ? "Update" : "Add"}
</Button>
<Button
@ -429,18 +460,6 @@ export const IdentityKubernetesAuthForm = ({
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);
};

View File

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
@ -7,14 +7,26 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input, TextArea, Tooltip } from "@app/components/v2";
import {
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs,
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z.object({
accessTokenTrustedIps: z
.array(
@ -48,21 +60,18 @@ export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData: {
identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
identityId?: string;
isUpdate?: boolean;
};
export const IdentityOidcAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
identityId,
isUpdate
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
@ -70,11 +79,9 @@ export const IdentityOidcAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "", {
const { data } = useGetIdentityOidcAuth(identityId ?? "", {
enabled: isUpdate
});
@ -160,13 +167,13 @@ export const IdentityOidcAuthForm = ({
boundSubject
}: FormData) => {
try {
if (!identityAuthMethodData) {
if (!identityId) {
return;
}
if (data) {
await updateMutateAsync({
identityId: identityAuthMethodData.identityId,
identityId,
organizationId: orgId,
oidcDiscoveryUrl,
caCert,
@ -181,7 +188,7 @@ export const IdentityOidcAuthForm = ({
});
} else {
await addMutateAsync({
identityId: identityAuthMethodData.identityId,
identityId,
oidcDiscoveryUrl,
caCert,
boundIssuer,
@ -213,7 +220,21 @@ export const IdentityOidcAuthForm = ({
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
["accessTokenTrustedIps", "caCert", "boundClaims"].includes(Object.keys(fields)[0])
? IdentityFormTab.Advanced
: IdentityFormTab.Configuration
);
})}
>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
<TabList>
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
<Controller
control={control}
name="oidcDiscoveryUrl"
@ -250,15 +271,6 @@ export const IdentityOidcAuthForm = ({
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl label="CA Certificate" errorText={error?.message} isError={Boolean(error)}>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
<Controller
control={control}
name="boundSubject"
@ -301,6 +313,63 @@ export const IdentityOidcAuthForm = ({
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
{boundClaimsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@ -380,48 +449,6 @@ export const IdentityOidcAuthForm = ({
Add Claims
</Button>
</div>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max TTL (seconds)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="2592000" type="number" min="1" step="1" />
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Access Token Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</FormControl>
)}
/>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@ -490,7 +517,8 @@ export const IdentityOidcAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex justify-between">
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
@ -499,7 +527,7 @@ export const IdentityOidcAuthForm = ({
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Create"}
{isUpdate ? "Update" : "Add"}
</Button>
<Button
@ -510,18 +538,6 @@ export const IdentityOidcAuthForm = ({
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);
};

View File

@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -5,16 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import {
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useAddIdentityTokenAuth,
useGetIdentityTokenAuth,
useUpdateIdentityTokenAuth
} from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z
.object({
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
@ -39,21 +51,18 @@ export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData?: {
identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
identityId?: string;
isUpdate?: boolean;
};
export const IdentityTokenAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
identityId,
isUpdate
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
@ -61,12 +70,9 @@ export const IdentityTokenAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "", {
const { data } = useGetIdentityTokenAuth(identityId ?? "", {
enabled: isUpdate
});
@ -91,6 +97,30 @@ export const IdentityTokenAuthForm = ({
remove: removeAccessTokenTrustedIp
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
useEffect(() => {
if (data) {
reset({
accessTokenTTL: String(data.accessTokenTTL),
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
accessTokenTrustedIps: data.accessTokenTrustedIps.map(
({ ipAddress, prefix }: IdentityTrustedIp) => {
return {
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
};
}
)
});
} else {
reset({
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
}
}, [data]);
const onFormSubmit = async ({
accessTokenTTL,
accessTokenMaxTTL,
@ -98,12 +128,12 @@ export const IdentityTokenAuthForm = ({
accessTokenTrustedIps
}: FormData) => {
try {
if (!identityAuthMethodData) return;
if (!identityId) return;
if (data) {
await updateMutateAsync({
organizationId: orgId,
identityId: identityAuthMethodData.identityId,
identityId,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@ -112,7 +142,7 @@ export const IdentityTokenAuthForm = ({
} else {
await addMutateAsync({
organizationId: orgId,
identityId: identityAuthMethodData.identityId,
identityId,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
@ -137,7 +167,21 @@ export const IdentityTokenAuthForm = ({
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
["accessTokenTrustedIps"].includes(Object.keys(fields)[0])
? IdentityFormTab.Advanced
: IdentityFormTab.Configuration
);
})}
>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
<TabList>
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
<Controller
control={control}
defaultValue="2592000"
@ -180,6 +224,8 @@ export const IdentityTokenAuthForm = ({
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{accessTokenTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@ -248,7 +294,8 @@ export const IdentityTokenAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex justify-between">
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
@ -257,7 +304,7 @@ export const IdentityTokenAuthForm = ({
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Update" : "Create"}
{isUpdate ? "Update" : "Add"}
</Button>
<Button
@ -268,18 +315,6 @@ export const IdentityTokenAuthForm = ({
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Remove Auth Method
</Button>
)}
</div>
</form>
);
};

View File

@ -1,344 +0,0 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
// DeleteActionModal,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import {
useCreateIdentityUniversalAuthClientSecret,
useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
description: z.string(),
ttl: z
.string()
.refine((value) => Number(value) <= 315360000, "TTL cannot be greater than 315360000"),
numUsesLimit: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["universalAuthClientSecret", "revokeClientSecret"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["revokeClientSecret"]>,
data?: {
clientSecretPrefix: string;
clientSecretId: string;
}
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["universalAuthClientSecret", "revokeClientSecret"]>,
state?: boolean
) => void;
};
export const IdentityUniversalAuthClientSecretModal = ({
popUp,
handlePopUpOpen,
handlePopUpToggle
}: Props) => {
const { t } = useTranslation();
const [token, setToken] = useState("");
const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false);
const [isClientIdCopied, setIsClientIdCopied] = useToggle(false);
const popUpData = popUp?.universalAuthClientSecret?.data as {
identityId?: string;
name?: string;
};
const { data, isPending } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? "");
const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(popUpData?.identityId ?? "");
const { mutateAsync: createClientSecretMutateAsync } =
useCreateIdentityUniversalAuthClientSecret();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
description: "",
ttl: "",
numUsesLimit: ""
}
});
useEffect(() => {
let timer: NodeJS.Timeout;
if (isClientSecretCopied) {
timer = setTimeout(() => setIsClientSecretCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isClientSecretCopied]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isClientIdCopied) {
timer = setTimeout(() => setIsClientIdCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isClientIdCopied]);
const onFormSubmit = async ({ description, ttl, numUsesLimit }: FormData) => {
try {
if (!popUpData?.identityId) return;
const { clientSecret } = await createClientSecretMutateAsync({
identityId: popUpData.identityId,
description,
ttl: Number(ttl),
numUsesLimit: Number(numUsesLimit)
});
setToken(clientSecret);
createNotification({
text: "Successfully created client secret",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create client secret",
type: "error"
});
}
};
const hasToken = Boolean(token);
return (
<Modal
isOpen={popUp?.universalAuthClientSecret?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("universalAuthClientSecret", isOpen);
reset();
setToken("");
}}
>
<ModalContent title={`Manage Client ID/Secrets for ${popUpData?.name ?? ""}`}>
<h2 className="mb-4">Client ID</h2>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{identityUniversalAuth?.clientId ?? ""}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(identityUniversalAuth?.clientId ?? "");
setIsClientIdCopied.on();
}}
>
<FontAwesomeIcon icon={isClientIdCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
<h2 className="mb-4">New Client Secret</h2>
{hasToken ? (
<div>
<div className="mb-4 flex items-center justify-between">
<p>We will only show this secret once</p>
<Button
colorSchema="secondary"
type="submit"
onClick={() => {
reset();
setToken("");
}}
>
Got it
</Button>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{token}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(token);
setIsClientSecretCopied.on();
}}
>
<FontAwesomeIcon icon={isClientSecretCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
</div>
) : (
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
<Controller
control={control}
defaultValue=""
name="description"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Description (optional)"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="Description" />
</FormControl>
)}
/>
<div className="flex">
<Controller
control={control}
defaultValue=""
name="ttl"
render={({ field, fieldState: { error } }) => (
<FormControl
label="TTL (seconds - optional)"
isError={Boolean(error)}
errorText={error?.message}
>
<div className="flex">
<Input {...field} placeholder="0" type="number" min="0" step="1" />
</div>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue="0"
name="numUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Max Number of Uses"
isError={Boolean(error)}
errorText={error?.message}
className="ml-4"
>
<div className="flex">
<Input {...field} placeholder="0" type="number" min="0" step="1" />
<Button
className="ml-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
</div>
</FormControl>
)}
/>
</div>
</form>
)}
<h2 className="mb-4">Client Secrets</h2>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>Description</Th>
<Th>Num Uses</Th>
<Th>Expires At</Th>
<Th>Client Secret</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={5} innerKey="org-identities-client-secrets" />}
{!isPending &&
data &&
data.length > 0 &&
data.map(
({
id,
description,
clientSecretTTL,
clientSecretPrefix,
clientSecretNumUses,
clientSecretNumUsesLimit,
createdAt
}) => {
let expiresAt;
if (clientSecretTTL > 0) {
expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000);
}
return (
<Tr className="h-10 items-center" key={`mi-client-secret-${id}`}>
<Td>{description === "" ? "-" : description}</Td>
<Td>{`${clientSecretNumUses}${
clientSecretNumUsesLimit ? `/${clientSecretNumUsesLimit}` : ""
}`}</Td>
<Td>{expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"}</Td>
<Td>{`${clientSecretPrefix}****`}</Td>
<Td>
<IconButton
onClick={() => {
handlePopUpOpen("revokeClientSecret", {
clientSecretPrefix,
clientSecretId: id
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Td>
</Tr>
);
}
)}
{!isPending && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No client secrets have been created for this identity yet"
icon={faKey}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</ModalContent>
</Modal>
);
};

View File

@ -1,4 +1,4 @@
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -6,17 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, FormControl, IconButton, Input } from "@app/components/v2";
import {
Button,
FormControl,
IconButton,
Input,
Tab,
TabList,
TabPanel,
Tabs
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import {
useAddIdentityUniversalAuth,
useGetIdentityUniversalAuth,
useUpdateIdentityUniversalAuth
} from "@app/hooks/api";
import { IdentityAuthMethod } from "@app/hooks/api/identities";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z
.object({
accessTokenTTL: z
@ -52,33 +62,27 @@ export type FormData = z.infer<typeof schema>;
type Props = {
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
identityAuthMethodData?: {
identityId: string;
name: string;
configuredAuthMethods?: IdentityAuthMethod[];
authMethod?: IdentityAuthMethod;
};
identityId?: string;
isUpdate?: boolean;
};
export const IdentityUniversalAuthForm = ({
handlePopUpOpen,
handlePopUpToggle,
identityAuthMethodData
identityId,
isUpdate
}: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { subscription } = useSubscription();
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
identityAuthMethodData.authMethod! || ""
);
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "", {
const { data } = useGetIdentityUniversalAuth(identityId ?? "", {
enabled: isUpdate
});
@ -149,13 +153,13 @@ export const IdentityUniversalAuthForm = ({
accessTokenTrustedIps
}: FormData) => {
try {
if (!identityAuthMethodData) return;
if (!identityId) return;
if (data) {
// update universal auth configuration
await updateMutateAsync({
organizationId: orgId,
identityId: identityAuthMethodData.identityId,
identityId,
clientSecretTrustedIps,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
@ -167,7 +171,7 @@ export const IdentityUniversalAuthForm = ({
await addMutateAsync({
organizationId: orgId,
identityId: identityAuthMethodData.identityId,
identityId,
clientSecretTrustedIps,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
@ -195,7 +199,21 @@ export const IdentityUniversalAuthForm = ({
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<form
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
["accessTokenTrustedIps", "clientSecretTrustedIps"].includes(Object.keys(fields)[0])
? IdentityFormTab.Advanced
: IdentityFormTab.Configuration
);
})}
>
<Tabs value={tabValue} onValueChange={(value) => setTabValue(value as IdentityFormTab)}>
<TabList>
<Tab value={IdentityFormTab.Configuration}>Configuration</Tab>
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
<Controller
control={control}
defaultValue="2592000"
@ -238,6 +256,8 @@ export const IdentityUniversalAuthForm = ({
</FormControl>
)}
/>
</TabPanel>
<TabPanel value={IdentityFormTab.Advanced}>
{clientSecretTrustedIpsFields.map(({ id }, index) => (
<div className="mb-3 flex items-end space-x-2" key={id}>
<Controller
@ -374,7 +394,8 @@ export const IdentityUniversalAuthForm = ({
Add IP Address
</Button>
</div>
<div className="flex justify-between">
</TabPanel>
</Tabs>
<div className="flex items-center">
<Button
className="mr-4"
@ -383,7 +404,7 @@ export const IdentityUniversalAuthForm = ({
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isUpdate ? "Edit" : "Create"}
{isUpdate ? "Update" : "Add"}
</Button>
<Button
colorSchema="secondary"
@ -393,18 +414,6 @@ export const IdentityUniversalAuthForm = ({
Cancel
</Button>
</div>
{isUpdate && (
<Button
size="sm"
colorSchema="danger"
isLoading={isSubmitting}
isDisabled={isSubmitting}
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
>
Delete Auth Method
</Button>
)}
</div>
</form>
);
};

View File

@ -0,0 +1,4 @@
export enum IdentityFormTab {
Advanced = "advanced",
Configuration = "configuration"
}

View File

@ -1,45 +1,24 @@
import { useState } from "react";
import { Helmet } from "react-helmet";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "@tanstack/react-router";
import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
PageHeader,
Tooltip
} from "@app/components/v2";
import { DeleteActionModal, PageHeader } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import {
IdentityAuthMethod,
useDeleteIdentity,
useGetIdentityById,
useRevokeIdentityTokenAuthToken,
useRevokeIdentityUniversalAuthClientSecret
} from "@app/hooks/api";
import { Identity } from "@app/hooks/api/identities/types";
import { useDeleteIdentity, useGetIdentityById } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal";
import { OrgAccessControlTabSections } from "@app/types/org";
import { IdentityAuthMethodModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
import { IdentityModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal";
import { IdentityUniversalAuthClientSecretModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal";
import {
IdentityAuthenticationSection,
IdentityClientSecretModal,
IdentityDetailsSection,
IdentityProjectsSection,
IdentityTokenListModal,
IdentityTokenModal
IdentityProjectsSection
} from "./components";
const Page = () => {
@ -52,25 +31,13 @@ const Page = () => {
const orgId = currentOrg?.id || "";
const { data } = useGetIdentityById(identityId);
const { mutateAsync: deleteIdentity } = useDeleteIdentity();
const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken();
const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret();
const [selectedAuthMethod, setSelectedAuthMethod] = useState<
Identity["authMethods"][number] | null
>(null);
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"identity",
"deleteIdentity",
"identityAuthMethod",
"revokeAuthMethod",
"token",
"tokenList",
"revokeToken",
"clientSecret",
"revokeClientSecret",
"universalAuthClientSecret", // list of client secrets
"upgradePlan"
"upgradePlan",
"viewAuthMethod"
] as const);
const onDeleteIdentitySubmit = async (id: string) => {
@ -104,148 +71,15 @@ const Page = () => {
}
};
const onRevokeTokenSubmit = async ({
identityId: parentIdentityId,
tokenId,
name
}: {
identityId: string;
tokenId: string;
name: string;
}) => {
try {
await revokeToken({
identityId: parentIdentityId,
tokenId
});
handlePopUpClose("revokeToken");
createNotification({
text: `Successfully revoked token ${name ?? ""}`,
type: "success"
});
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to delete identity";
createNotification({
text,
type: "error"
});
}
};
const onDeleteClientSecretSubmit = async ({ clientSecretId }: { clientSecretId: string }) => {
try {
if (!data?.identity.id || selectedAuthMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) return;
await revokeClientSecret({
identityId: data?.identity.id,
clientSecretId
});
handlePopUpToggle("revokeClientSecret", false);
createNotification({
text: "Successfully deleted client secret",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete client secret",
type: "error"
});
}
};
return (
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
{data && (
<div className="mx-auto mb-6 w-full max-w-7xl">
<PageHeader title={data.identity.name}>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<Button variant="outline_bg">More</Button>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("identity", {
identityId,
name: data.identity.name,
role: data.role,
customRole: data.customRole
});
}}
disabled={!isAllowed}
>
Edit Identity
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
allAuthMethods: data.identity.authMethods
});
}}
disabled={!isAllowed}
>
Add new auth method
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionActions.Delete}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("deleteIdentity", {
identityId,
name: data.identity.name
});
}}
disabled={!isAllowed}
>
Delete Identity
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</PageHeader>
<PageHeader title={data.identity.name} />
<div className="flex">
<div className="mr-4 w-96">
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
<IdentityAuthenticationSection
selectedAuthMethod={selectedAuthMethod}
setSelectedAuthMethod={setSelectedAuthMethod}
identityId={identityId}
handlePopUpOpen={handlePopUpOpen}
/>
@ -260,18 +94,6 @@ const Page = () => {
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<IdentityTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityTokenListModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<IdentityClientSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityUniversalAuthClientSecretModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
@ -290,41 +112,11 @@ const Page = () => {
)
}
/>
<DeleteActionModal
isOpen={popUp.revokeToken.isOpen}
title={`Are you sure want to revoke ${
(popUp?.revokeToken?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("revokeToken", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const revokeTokenData = popUp?.revokeToken?.data as {
identityId: string;
tokenId: string;
name: string;
};
return onRevokeTokenSubmit(revokeTokenData);
}}
/>
<DeleteActionModal
isOpen={popUp.revokeClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${
(popUp?.revokeClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix ||
""
}************?`}
onChange={(isOpen) => handlePopUpToggle("revokeClientSecret", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const deleteClientSecretData = popUp?.revokeClientSecret?.data as {
clientSecretId: string;
clientSecretPrefix: string;
};
return onDeleteClientSecretSubmit({
clientSecretId: deleteClientSecretData.clientSecretId
});
}}
<ViewIdentityAuthModal
isOpen={popUp.viewAuthMethod.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("viewAuthMethod", isOpen)}
authMethod={popUp.viewAuthMethod.data}
identityId={identityId}
/>
</div>
);

View File

@ -1,140 +1,56 @@
import { useEffect } from "react";
import { faPencil, faPlus } from "@fortawesome/free-solid-svg-icons";
import { faCog, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, IconButton, Select, SelectItem, Tooltip } from "@app/components/v2";
import { Button } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useGetIdentityById } from "@app/hooks/api";
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
import { Identity } from "@app/hooks/api/identities/types";
import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityClientSecrets } from "./IdentityClientSecrets";
import { IdentityTokens } from "./IdentityTokens";
type Props = {
identityId: string;
setSelectedAuthMethod: (authMethod: Identity["authMethods"][number] | null) => void;
selectedAuthMethod: Identity["authMethods"][number] | null;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
[
"clientSecret",
"identityAuthMethod",
"revokeClientSecret",
"token",
"revokeToken",
"universalAuthClientSecret",
"tokenList"
]
>,
data?: object
popUpName: keyof UsePopUpState<["identityAuthMethod", "viewAuthMethod"]>,
data?: object | IdentityAuthMethod
) => void;
};
export const IdentityAuthenticationSection = ({
identityId,
setSelectedAuthMethod,
selectedAuthMethod,
handlePopUpOpen
}: Props) => {
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => {
const { data } = useGetIdentityById(identityId);
useEffect(() => {
if (!data?.identity) return;
if (data.identity.authMethods?.length) {
setSelectedAuthMethod(data.identity.authMethods[0]);
}
// eslint-disable-next-line consistent-return
return () => setSelectedAuthMethod(null);
}, [data?.identity]);
return data ? (
<div className="mt-4 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">Authentication</h3>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {
return (
<Tooltip content="Add new auth method">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() =>
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
allAuthMethods: data.identity.authMethods
})
}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</Tooltip>
);
}}
</OrgPermissionCan>
</div>
{data.identity.authMethods.length > 0 ? (
<>
<div className="py-4">
<div className="flex justify-between">
<p className="mb-0.5 ml-px text-sm font-semibold text-mineshaft-300">Auth Method</p>
</div>
<div className="flex items-center gap-2">
<div className="w-full">
<Select
className="w-full"
value={selectedAuthMethod as string}
onValueChange={(value) => setSelectedAuthMethod(value as IdentityAuthMethod)}
<div className="flex flex-col divide-y divide-mineshaft-400/50">
{data.identity.authMethods.map((authMethod) => (
<button
key={authMethod}
onClick={() => handlePopUpOpen("viewAuthMethod", authMethod)}
type="button"
className="flex w-full items-center justify-between bg-mineshaft-900 px-4 py-2 text-sm hover:bg-mineshaft-700 data-[state=open]:bg-mineshaft-600"
>
{(data.identity?.authMethods || []).map((authMethod) => (
<SelectItem key={authMethod || authMethod} value={authMethod}>
{identityAuthToNameMap[authMethod]}
</SelectItem>
<span>{identityAuthToNameMap[authMethod]}</span>
<FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" />
</button>
))}
</Select>
</div>
<div>
<Tooltip content="Edit auth method">
<IconButton
onClick={() => {
handlePopUpOpen("identityAuthMethod", {
identityId,
name: data.identity.name,
authMethod: selectedAuthMethod,
allAuthMethods: data.identity.authMethods
});
}}
ariaLabel="copy icon"
variant="plain"
className="group relative"
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>{" "}
</div>
</div>
</div>
{selectedAuthMethod === IdentityAuthMethod.UNIVERSAL_AUTH && (
<IdentityClientSecrets identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
{selectedAuthMethod === IdentityAuthMethod.TOKEN_AUTH && (
<IdentityTokens identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
)}
</>
) : (
<div className="w-full space-y-2 pt-2">
<p className="text-sm text-mineshaft-300">
No authentication methods configured. Get started by creating a new auth method.
</p>
</div>
)}
{!Object.values(IdentityAuthMethod).every((method) =>
data.identity.authMethods.includes(method)
) && (
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("identityAuthMethod", {
identityId,
@ -143,12 +59,14 @@ export const IdentityAuthenticationSection = ({
});
}}
variant="outline_bg"
className="w-full"
className="mt-3 w-full"
size="xs"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
Create Auth Method
{data.identity.authMethods.length ? "Add" : "Create"} Auth Method
</Button>
</div>
)}
</OrgPermissionCan>
)}
</div>
) : (

View File

@ -1,4 +1,4 @@
import { faEllipsis, faKey } from "@fortawesome/free-solid-svg-icons";
import { faEllipsisVertical, faKey } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
@ -8,6 +8,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Tooltip
} from "@app/components/v2";
import { useGetIdentityById, useGetIdentityTokensTokenAuth } from "@app/hooks/api";
@ -27,10 +28,13 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
return (
<div>
{tokens?.length ? (
<div className="flex justify-between">
<p className="text-sm font-semibold text-mineshaft-300">{`Access Tokens (${tokens.length})`}</p>
<div className="flex items-center justify-between border-b border-bunker-400 pb-1">
<p className="text-sm font-medium text-bunker-300">{`Access Tokens (${tokens.length})`}</p>
<Button
variant="link"
size="xs"
className="underline"
variant="plain"
colorSchema="secondary"
onClick={() => {
handlePopUpOpen("tokenList", {
identityId,
@ -50,16 +54,16 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
);
return (
<div
className="group flex items-center justify-between py-2 last:pb-0"
className="group flex items-center justify-between border-b border-mineshaft-500 px-2 py-2 last:pb-0"
key={`identity-token-${token.id}`}
>
<div className="flex items-center">
<FontAwesomeIcon size="1x" icon={faKey} />
<div className="ml-4">
<p className="text-sm font-semibold text-mineshaft-300">
<FontAwesomeIcon size="xs" className="text-mineshaft-400" icon={faKey} />
<div className="ml-3">
<p className="text-sm font-medium text-mineshaft-300">
{token.name ? token.name : "-"}
</p>
<p className="text-sm text-mineshaft-300">
<p className="text-xs text-mineshaft-400">
{token.isAccessTokenRevoked
? "Revoked"
: `Expires on ${format(expiresAt, "yyyy-MM-dd")}`}
@ -67,14 +71,19 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild className="rounded-lg">
<div className="opacity-0 transition-opacity duration-300 hover:text-primary-400 group-hover:opacity-100 data-[state=open]:text-primary-400">
<Tooltip content="More options">
<FontAwesomeIcon size="sm" icon={faEllipsis} />
<DropdownMenuTrigger asChild>
<Tooltip side="right" content="More options">
<IconButton
colorSchema="secondary"
variant="plain"
size="xs"
ariaLabel="More options"
>
<FontAwesomeIcon icon={faEllipsisVertical} />
</IconButton>
</Tooltip>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1">
<DropdownMenuContent align="start" className="z-[101] p-1">
<DropdownMenuItem
onClick={async () => {
handlePopUpOpen("token", {
@ -106,8 +115,9 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
})}
<Button
className="mr-4 mt-4 w-full"
colorSchema="primary"
colorSchema="secondary"
type="submit"
size="xs"
onClick={() => {
handlePopUpOpen("token", {
identityId

View File

@ -1,8 +1,25 @@
import { faCheck, faCopy, faKey, faPencil } from "@fortawesome/free-solid-svg-icons";
import {
faCheck,
faChevronDown,
faCopy,
faEdit,
faKey,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { OrgPermissionCan } from "@app/components/permissions";
import { IconButton, Tag, Tooltip } from "@app/components/v2";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
IconButton,
Tag,
Tooltip
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { useTimedReset } from "@app/hooks";
import { useGetIdentityById } from "@app/hooks/api";
@ -11,7 +28,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
type Props = {
identityId: string;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "token", "clientSecret"]>,
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "deleteIdentity"]>,
data?: object
) => void;
};
@ -26,31 +43,61 @@ export const IdentityDetailsSection = ({ identityId, handlePopUpOpen }: Props) =
<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">Identity Details</h3>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="xs"
rightIcon={<FontAwesomeIcon className="ml-1" icon={faChevronDown} />}
colorSchema="secondary"
>
Options
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[120px]" align="end">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => {
return (
<Tooltip content="Edit Identity">
<IconButton
isDisabled={!isAllowed}
ariaLabel="copy icon"
variant="plain"
className="group relative"
onClick={() => {
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
)}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={async () => {
handlePopUpOpen("identity", {
identityId,
name: data.identity.name,
role: data.role,
customRole: data.customRole,
metadata: data.metadata
customRole: data.customRole
});
}}
disabled={!isAllowed}
>
<FontAwesomeIcon icon={faPencil} />
</IconButton>
</Tooltip>
);
}}
Edit Identity
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Delete} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<DropdownMenuItem
className={twMerge(
isAllowed
? "hover:!bg-red-500 hover:!text-white"
: "pointer-events-none cursor-not-allowed opacity-50"
)}
onClick={async () => {
handlePopUpOpen("deleteIdentity", {
identityId,
name: data.identity.name
});
}}
icon={<FontAwesomeIcon icon={faTrash} />}
disabled={!isAllowed}
>
Delete Identity
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="pt-4">
<div className="mb-4">

View File

@ -1,270 +0,0 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { faCheck, faCopy, faKey, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useToggle } from "@app/hooks";
import {
useCreateTokenIdentityTokenAuth,
useGetIdentityTokensTokenAuth,
useGetIdentityUniversalAuthClientSecrets
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
name: z.string()
});
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["tokenList", "revokeToken"]>;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["revokeToken"]>, data?: object) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["tokenList", "revokeToken"]>,
state?: boolean
) => void;
};
export const IdentityTokenListModal = ({ popUp, handlePopUpOpen, handlePopUpToggle }: Props) => {
const { t } = useTranslation();
const [token, setToken] = useState("");
const [isClientSecretCopied, setIsClientSecretCopied] = useToggle(false);
const [isClientIdCopied, setIsClientIdCopied] = useToggle(false);
const popUpData = popUp?.tokenList?.data as {
identityId: string;
name: string;
};
const { data: tokens } = useGetIdentityTokensTokenAuth(popUpData?.identityId ?? "");
const { data, isPending } = useGetIdentityUniversalAuthClientSecrets(popUpData?.identityId ?? "");
const { mutateAsync: createToken } = useCreateTokenIdentityTokenAuth();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name: ""
}
});
useEffect(() => {
let timer: NodeJS.Timeout;
if (isClientSecretCopied) {
timer = setTimeout(() => setIsClientSecretCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isClientSecretCopied]);
useEffect(() => {
let timer: NodeJS.Timeout;
if (isClientIdCopied) {
timer = setTimeout(() => setIsClientIdCopied.off(), 2000);
}
return () => clearTimeout(timer);
}, [isClientIdCopied]);
const onFormSubmit = async ({ name }: FormData) => {
try {
if (!popUpData?.identityId) return;
const newTokenData = await createToken({
identityId: popUpData.identityId,
name
});
setToken(newTokenData.accessToken);
createNotification({
text: "Successfully created token",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to create token",
type: "error"
});
}
};
const hasToken = Boolean(token);
return (
<Modal
isOpen={popUp?.tokenList?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("tokenList", isOpen);
reset();
setToken("");
}}
>
<ModalContent title={`Manage Access Tokens for ${popUpData?.name ?? ""}`}>
<h2 className="mb-4">New Token</h2>
{hasToken ? (
<div>
<div className="mb-4 flex items-center justify-between">
<p>We will only show this token once</p>
<Button
colorSchema="secondary"
type="submit"
onClick={() => {
reset();
setToken("");
}}
>
Got it
</Button>
</div>
<div className="mb-8 flex items-center justify-between rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
<p className="mr-4 break-all">{token}</p>
<IconButton
ariaLabel="copy icon"
colorSchema="secondary"
className="group relative"
onClick={() => {
navigator.clipboard.writeText(token);
setIsClientSecretCopied.on();
}}
>
<FontAwesomeIcon icon={isClientSecretCopied ? faCheck : faCopy} />
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
{t("common.click-to-copy")}
</span>
</IconButton>
</div>
</div>
) : (
<form onSubmit={handleSubmit(onFormSubmit)} className="mb-8">
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message}>
<div className="flex">
<Input {...field} placeholder="My Token" />
<Button
className="ml-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
Create
</Button>
</div>
</FormControl>
)}
/>
</form>
)}
<h2 className="mb-4">Tokens</h2>
<TableContainer>
<Table>
<THead>
<Tr>
<Th>name</Th>
<Th>Num Uses</Th>
<Th>Created At</Th>
<Th>Max Expires At</Th>
<Th className="w-5" />
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={5} innerKey="identities-tokens" />}
{!isPending &&
tokens?.map(
({
id,
createdAt,
name,
accessTokenNumUses,
accessTokenNumUsesLimit,
accessTokenMaxTTL,
isAccessTokenRevoked
}) => {
const expiresAt = new Date(
new Date(createdAt).getTime() + accessTokenMaxTTL * 1000
);
return (
<Tr className="h-10 items-center" key={`mi-client-secret-${id}`}>
<Td>{name === "" ? "-" : name}</Td>
<Td>{`${accessTokenNumUses}${
accessTokenNumUsesLimit ? `/${accessTokenNumUsesLimit}` : ""
}`}</Td>
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
<Td>
{isAccessTokenRevoked ? "Revoked" : `${format(expiresAt, "yyyy-MM-dd")}`}
</Td>
<Td>
{!isAccessTokenRevoked && (
<IconButton
onClick={() => {
handlePopUpOpen("revokeToken", {
identityId: popUpData?.identityId,
tokenId: id,
name
});
}}
size="lg"
colorSchema="primary"
variant="plain"
ariaLabel="update"
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
)}
</Td>
</Tr>
);
}
)}
{!isPending && data && data?.length === 0 && (
<Tr>
<Td colSpan={5}>
<EmptyState
title="No tokens have been created for this identity yet"
icon={faKey}
/>
</Td>
</Tr>
)}
</TBody>
</Table>
</TableContainer>
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,20 @@
import { ReactNode } from "react";
type Props = {
label: string;
children: ReactNode;
className?: string;
};
export const IdentityAuthFieldDisplay = ({ label, children, className }: Props) => {
return (
<div className={className}>
<span className="text-sm text-mineshaft-400">{label}</span>
{children ? (
<p className="break-words text-base leading-4">{children}</p>
) : (
<p className="text-base italic leading-4 text-bunker-400">Not set</p>
)}
</div>
);
};

View File

@ -0,0 +1,239 @@
import { useState } from "react";
import { faBan, faEdit, faKey, faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
IconButton,
Pagination,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useRevokeIdentityTokenAuthToken } from "@app/hooks/api";
import { IdentityAccessToken } from "@app/hooks/api/identities/types";
import { IdentityTokenModal } from "@app/pages/organization/IdentityDetailsByIDPage/components";
type Props = {
tokens: IdentityAccessToken[];
identityId: string;
};
export const IdentityTokenAuthTokensTable = ({ tokens, identityId }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle, handlePopUpClose } = usePopUp([
"token",
"revokeToken"
] as const);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(5);
const { mutateAsync: revokeToken } = useRevokeIdentityTokenAuthToken();
const onRevokeTokenSubmit = async ({
identityId: parentIdentityId,
tokenId,
name
}: {
identityId: string;
tokenId: string;
name: string;
}) => {
try {
await revokeToken({
identityId: parentIdentityId,
tokenId
});
handlePopUpClose("revokeToken");
createNotification({
text: `Successfully revoked token ${name ?? ""}`,
type: "success"
});
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to revoke token";
createNotification({
text,
type: "error"
});
}
};
return (
<div className="col-span-2 mt-3">
<div className="flex items-end justify-between border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Access Tokens</span>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<Button
size="xs"
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("token", {
identityId
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="secondary"
>
Add Token
</Button>
)}
</OrgPermissionCan>
</div>
<TableContainer className="mt-4 rounded-none border-none">
<Table>
{Boolean(tokens?.length) && (
<THead>
<Tr className="text-xs font-medium">
<Th className="py-1 font-normal">Name</Th>
<Th className="whitespace-nowrap py-1 font-normal">Number of Uses</Th>
<Th className="py-1 font-normal">Expires</Th>
<Th className="w-5 py-1 font-normal" />
</Tr>
</THead>
)}
<TBody>
{tokens
.slice((page - 1) * perPage, perPage * page)
.map(
({
createdAt,
isAccessTokenRevoked,
name,
accessTokenTTL,
accessTokenNumUsesLimit,
accessTokenNumUses,
id
}) => {
let expiresAt;
if (accessTokenTTL > 0) {
expiresAt = new Date(new Date(createdAt).getTime() + accessTokenTTL * 1000);
}
return (
<Tr className="text-xs hover:bg-mineshaft-700" key={id}>
<Td>{name || "-"}</Td>
<Td>
{`${accessTokenNumUses}${accessTokenNumUsesLimit ? `/${accessTokenNumUsesLimit}` : ""}`}
</Td>
<Td className="whitespace-nowrap">
{/* eslint-disable-next-line no-nested-ternary */}
{isAccessTokenRevoked
? "Revoked"
: expiresAt
? format(expiresAt, "yyyy-MM-dd")
: "-"}
</Td>
<Td>
<div className="flex items-center gap-2">
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Tooltip content={isAllowed ? "Edit Token" : "Access Restricted"}>
<IconButton
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("token", {
identityId,
tokenId: id,
name
});
}}
size="xs"
variant="plain"
ariaLabel="Edit token"
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
{!isAccessTokenRevoked && (
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Tooltip content={isAllowed ? "Revoke Token" : "Access Restricted"}>
<IconButton
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("revokeToken", {
identityId,
tokenId: id,
name
});
}}
size="xs"
colorSchema="danger"
variant="plain"
ariaLabel="Revoke token"
>
<FontAwesomeIcon icon={faBan} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
)}
</div>
</Td>
</Tr>
);
}
)}
</TBody>
</Table>
{!tokens?.length && (
<EmptyState iconSize="1x" title="No access tokens have been generated" icon={faKey} />
)}
{tokens.length > 0 && (
<Pagination
count={tokens.length}
page={page}
perPage={perPage}
perPageList={[5]}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
</TableContainer>
<DeleteActionModal
isOpen={popUp.revokeToken.isOpen}
title={`Are you sure want to revoke ${
(popUp?.revokeToken?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("revokeToken", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const revokeTokenData = popUp?.revokeToken?.data as {
identityId: string;
tokenId: string;
name: string;
};
return onRevokeTokenSubmit(revokeTokenData);
}}
/>
<IdentityTokenModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</div>
);
};

View File

@ -0,0 +1,196 @@
import { useState } from "react";
import { faKey, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DeleteActionModal,
EmptyState,
IconButton,
Pagination,
Table,
TableContainer,
TBody,
Td,
Th,
THead,
Tooltip,
Tr
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { usePopUp } from "@app/hooks";
import { useRevokeIdentityUniversalAuthClientSecret } from "@app/hooks/api";
import { ClientSecretData } from "@app/hooks/api/identities/types";
import { IdentityClientSecretModal } from "@app/pages/organization/IdentityDetailsByIDPage/components";
type Props = {
clientSecrets: ClientSecretData[];
identityId: string;
};
export const IdentityUniversalAuthClientSecretsTable = ({ clientSecrets, identityId }: Props) => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"revokeClientSecret",
"clientSecret"
] as const);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(5);
const { mutateAsync: revokeClientSecret } = useRevokeIdentityUniversalAuthClientSecret();
const onDeleteClientSecretSubmit = async (clientSecretId: string) => {
try {
await revokeClientSecret({
identityId,
clientSecretId
});
handlePopUpToggle("revokeClientSecret", false);
createNotification({
text: "Successfully deleted client secret",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete client secret",
type: "error"
});
}
};
return (
<div className="col-span-2">
<div className="flex items-end justify-between border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Client Secrets</span>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
size="xs"
onClick={() => {
handlePopUpOpen("clientSecret", {
identityId
});
}}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
colorSchema="secondary"
>
Add Client Secret
</Button>
)}
</OrgPermissionCan>
</div>
<TableContainer className="mt-4 rounded-none border-none">
<Table>
{Boolean(clientSecrets?.length) && (
<THead>
<Tr className="text-xs font-medium">
<Th className="py-1 font-normal">Secret</Th>
<Th className="py-1 font-normal">Description</Th>
<Th className="whitespace-nowrap py-1 font-normal">Number of Uses</Th>
<Th className="py-1 font-normal">Expires</Th>
<Th className="w-5 py-1 font-normal" />
</Tr>
</THead>
)}
<TBody>
{clientSecrets
.slice((page - 1) * perPage, perPage * page)
.map(
({
createdAt,
clientSecretTTL,
description,
clientSecretNumUses,
clientSecretPrefix,
clientSecretNumUsesLimit,
id
}) => {
let expiresAt;
if (clientSecretTTL > 0) {
expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000);
}
return (
<Tr className="text-xs hover:bg-mineshaft-700" key={id}>
<Td>{clientSecretPrefix}***</Td>
<Td>{description || "-"}</Td>
<Td>
{`${clientSecretNumUses}${clientSecretNumUsesLimit ? `/${clientSecretNumUsesLimit}` : ""}`}
</Td>
<Td className="whitespace-nowrap">
{expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"}
</Td>
<Td>
<OrgPermissionCan
I={OrgPermissionActions.Edit}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Tooltip content={isAllowed ? "Delete Secret" : "Access Restricted"}>
<IconButton
isDisabled={!isAllowed}
onClick={() => {
handlePopUpOpen("revokeClientSecret", {
clientSecretPrefix,
clientSecretId: id
});
}}
size="xs"
colorSchema="danger"
variant="plain"
ariaLabel="Delete secret"
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</Tooltip>
)}
</OrgPermissionCan>
</Td>
</Tr>
);
}
)}
</TBody>
</Table>
{!clientSecrets?.length && (
<EmptyState iconSize="1x" title="No client secrets have been generated" icon={faKey} />
)}
{clientSecrets.length > 0 && (
<Pagination
count={clientSecrets.length}
page={page}
perPage={perPage}
perPageList={[5]}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
/>
)}
</TableContainer>
<DeleteActionModal
isOpen={popUp.revokeClientSecret.isOpen}
title={`Are you sure want to delete the client secret ${
(popUp?.revokeClientSecret?.data as { clientSecretPrefix: string })?.clientSecretPrefix ||
""
}************?`}
onChange={(isOpen) => handlePopUpToggle("revokeClientSecret", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => {
const deleteClientSecretData = popUp?.revokeClientSecret?.data as {
clientSecretId: string;
clientSecretPrefix: string;
};
return onDeleteClientSecretSubmit(deleteClientSecretData.clientSecretId);
}}
/>
<IdentityClientSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</div>
);
};

View File

@ -0,0 +1,174 @@
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { DeleteActionModal, Modal, ModalContent } from "@app/components/v2";
import { useOrganization } from "@app/context";
import { usePopUp } from "@app/hooks";
import {
IdentityAuthMethod,
identityAuthToNameMap,
useDeleteIdentityAwsAuth,
useDeleteIdentityAzureAuth,
useDeleteIdentityGcpAuth,
useDeleteIdentityJwtAuth,
useDeleteIdentityKubernetesAuth,
useDeleteIdentityOidcAuth,
useDeleteIdentityTokenAuth,
useDeleteIdentityUniversalAuth
} from "@app/hooks/api";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityAwsAuthContent } from "./ViewIdentityAwsAuthContent";
import { ViewIdentityAzureAuthContent } from "./ViewIdentityAzureAuthContent";
import { ViewIdentityGcpAuthContent } from "./ViewIdentityGcpAuthContent";
import { ViewIdentityJwtAuthContent } from "./ViewIdentityJwtAuthContent";
import { ViewIdentityKubernetesAuthContent } from "./ViewIdentityKubernetesAuthContent";
import { ViewIdentityOidcAuthContent } from "./ViewIdentityOidcAuthContent";
import { ViewIdentityTokenAuthContent } from "./ViewIdentityTokenAuthContent";
import { ViewIdentityUniversalAuthContent } from "./ViewIdentityUniversalAuthContent";
type Props = {
identityId: string;
authMethod?: IdentityAuthMethod;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onDeleteAuthMethod: () => void;
};
type TRevokeOptions = {
identityId: string;
organizationId: string;
};
export const Content = ({
identityId,
authMethod,
onDeleteAuthMethod
}: Pick<Props, "authMethod" | "identityId" | "onDeleteAuthMethod">) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"revokeAuthMethod",
"upgradePlan",
"identityAuthMethod"
] as const);
const { mutateAsync: revokeUniversalAuth } = useDeleteIdentityUniversalAuth();
const { mutateAsync: revokeTokenAuth } = useDeleteIdentityTokenAuth();
const { mutateAsync: revokeKubernetesAuth } = useDeleteIdentityKubernetesAuth();
const { mutateAsync: revokeGcpAuth } = useDeleteIdentityGcpAuth();
const { mutateAsync: revokeAwsAuth } = useDeleteIdentityAwsAuth();
const { mutateAsync: revokeAzureAuth } = useDeleteIdentityAzureAuth();
const { mutateAsync: revokeOidcAuth } = useDeleteIdentityOidcAuth();
const { mutateAsync: revokeJwtAuth } = useDeleteIdentityJwtAuth();
let Component: (props: ViewAuthMethodProps) => JSX.Element;
let revokeMethod: (revokeOptions: TRevokeOptions) => Promise<any>;
const handleDelete = () => handlePopUpOpen("revokeAuthMethod");
switch (authMethod) {
case IdentityAuthMethod.UNIVERSAL_AUTH:
revokeMethod = revokeUniversalAuth;
Component = ViewIdentityUniversalAuthContent;
break;
case IdentityAuthMethod.TOKEN_AUTH:
revokeMethod = revokeTokenAuth;
Component = ViewIdentityTokenAuthContent;
break;
case IdentityAuthMethod.KUBERNETES_AUTH:
revokeMethod = revokeKubernetesAuth;
Component = ViewIdentityKubernetesAuthContent;
break;
case IdentityAuthMethod.GCP_AUTH:
revokeMethod = revokeGcpAuth;
Component = ViewIdentityGcpAuthContent;
break;
case IdentityAuthMethod.AWS_AUTH:
revokeMethod = revokeAwsAuth;
Component = ViewIdentityAwsAuthContent;
break;
case IdentityAuthMethod.AZURE_AUTH:
revokeMethod = revokeAzureAuth;
Component = ViewIdentityAzureAuthContent;
break;
case IdentityAuthMethod.OIDC_AUTH:
revokeMethod = revokeOidcAuth;
Component = ViewIdentityOidcAuthContent;
break;
case IdentityAuthMethod.JWT_AUTH:
revokeMethod = revokeJwtAuth;
Component = ViewIdentityJwtAuthContent;
break;
default:
throw new Error(`Unhandled Auth Method: ${authMethod}`);
}
const handleDeleteAuthMethod = async () => {
try {
await revokeMethod({
identityId,
organizationId: orgId
});
createNotification({
text: "Successfully removed auth method",
type: "success"
});
handlePopUpToggle("revokeAuthMethod", false);
onDeleteAuthMethod();
} catch {
createNotification({
text: "Failed to remove auth method",
type: "error"
});
}
};
return (
<>
<Component
identityId={identityId}
onDelete={handleDelete}
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
<DeleteActionModal
isOpen={popUp?.revokeAuthMethod?.isOpen}
title={`Are you sure want to remove ${identityAuthToNameMap[authMethod]} on this identity?`}
onChange={(isOpen) => handlePopUpToggle("revokeAuthMethod", isOpen)}
deleteKey="confirm"
buttonText="Remove"
onDeleteApproved={handleDeleteAuthMethod}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
/>
</>
);
};
export const ViewIdentityAuthModal = ({
isOpen,
onOpenChange,
authMethod,
identityId
}: Omit<Props, "onDeleteAuthMethod">) => {
if (!identityId || !authMethod) return null;
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent className="max-w-2xl" title={identityAuthToNameMap[authMethod]}>
<Content
identityId={identityId}
authMethod={authMethod}
onDeleteAuthMethod={() => onOpenChange(false)}
/>
</ModalContent>
</Modal>
);
};

View File

@ -0,0 +1,79 @@
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { EmptyState, Spinner } from "@app/components/v2";
import { useGetIdentityAwsAuth } from "@app/hooks/api";
import { IdentityAwsAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAwsAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityAwsAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityAwsAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find AWS Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityAwsAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Principal ARNs">
{data.allowedPrincipalArns
?.split(",")
.map((arn) => arn.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Account IDs">
{data.allowedAccountIds
?.split(",")
.map((id) => id.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="STS Endpoint">
{data.stsEndpoint}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};

View File

@ -0,0 +1,76 @@
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { EmptyState, Spinner } from "@app/components/v2";
import { useGetIdentityAzureAuth } from "@app/hooks/api";
import { IdentityAzureAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAzureAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityAzureAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityAzureAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find Azure Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityAzureAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Tenant ID">
{data.tenantId}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Resource / Audience">
{data.resource}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Principal IDs">
{data.allowedServicePrincipalIds
?.split(",")
.map((id) => id.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};

View File

@ -0,0 +1,69 @@
import { ReactNode } from "react";
import { faChevronDown, faEdit, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
type Props = {
children: ReactNode;
onEdit: VoidFunction;
onDelete: VoidFunction;
};
export const ViewIdentityContentWrapper = ({ children, onDelete, onEdit }: Props) => {
return (
<div className="flex flex-col gap-4">
<div>
<div className="flex items-end justify-between border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Details</span>
<div className="flex items-center gap-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
size="xs"
rightIcon={<FontAwesomeIcon className="ml-1" icon={faChevronDown} />}
colorSchema="secondary"
>
Options
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="min-w-[120px]" align="end">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<DropdownMenuItem
isDisabled={!isAllowed}
onClick={onEdit}
icon={<FontAwesomeIcon icon={faEdit} />}
>
Edit
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
{(isAllowed) => (
<DropdownMenuItem
isDisabled={!isAllowed}
onClick={onDelete}
icon={<FontAwesomeIcon icon={faTrash} />}
>
Delete
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
<div className="mt-3 grid grid-cols-2 gap-3">{children}</div>
</div>
</div>
);
};

View File

@ -0,0 +1,89 @@
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { EmptyState, Spinner } from "@app/components/v2";
import { useGetIdentityGcpAuth } from "@app/hooks/api";
import { IdentityGcpAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityGcpAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityGcpAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityGcpAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find GCP Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityGcpAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Type">
{data.type === "gce" ? "GCP ID Token Auth" : "GCP IAM Auth"}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Account Emails">
{data.allowedServiceAccounts
?.split(",")
.map((account) => account.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
{data.type === "gce" && (
<>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Projects">
{data.allowedProjects
?.split(",")
.map((project) => project.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Zones">
{data.allowedZones
?.split(",")
.map((zone) => zone.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
</>
)}
</ViewIdentityContentWrapper>
);
};

View File

@ -0,0 +1,152 @@
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { useGetIdentityJwtAuth } from "@app/hooks/api";
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
import { IdentityJwtAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm";
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
export const ViewIdentityJwtAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityJwtAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find JWT Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityJwtAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Configuration Type">
{data.configurationType === IdentityJwtConfigurationType.JWKS ? "JWKS" : "Static"}
</IdentityAuthFieldDisplay>
{data.configurationType === IdentityJwtConfigurationType.JWKS ? (
<>
<IdentityAuthFieldDisplay className="col-span-2" label="JWKS URL">
{data.jwksUrl}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="JWKS CA Certificate">
{data.jwksCaCert && (
<Tooltip
side="right"
className="max-w-xl p-2"
content={
<p className="break-words rounded bg-mineshaft-600 p-2">{data.jwksCaCert}</p>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
</>
) : (
<IdentityAuthFieldDisplay className="col-span-2" label="Public Keys">
{data.publicKeys.length && (
<div className="flex flex-wrap gap-1">
{data.publicKeys.map((key, index) => (
<Tooltip
side="right"
className="max-w-xl p-2"
key={key}
content={
<p className="whitespace-normal break-words rounded bg-mineshaft-600 p-2">
{key}
</p>
}
>
<div className="inline-block w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Key {index + 1}</span>
</Badge>
</div>
</Tooltip>
))}
</div>
)}
</IdentityAuthFieldDisplay>
)}
<IdentityAuthFieldDisplay className="col-span-2" label="Issuer">
{data.boundIssuer}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Subject">
{data.boundSubject}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Audiences">
{data.boundAudiences
?.split(",")
.map((name) => name.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Claims">
{Object.keys(data.boundClaims).length && (
<Tooltip
side="right"
className="max-w-xl p-2"
content={
<pre className="whitespace-pre-wrap rounded bg-mineshaft-600 p-2">
{JSON.stringify(data.boundClaims, null, 2)}
</pre>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};

View File

@ -0,0 +1,121 @@
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { useGetIdentityKubernetesAuth } from "@app/hooks/api";
import { IdentityKubernetesAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityKubernetesAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityKubernetesAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState
icon={faBan}
title="Could not find Kubernetes Auth associated with this Identity."
/>
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityKubernetesAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay
className="col-span-2"
label="Kubernetes Host / Base Kubernetes API URL"
>
{data.kubernetesHost}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Token Reviewer JWT">
<Tooltip
side="right"
className="max-w-xl p-2"
content={
<p className="break-words rounded bg-mineshaft-600 p-2">{data.tokenReviewerJwt}</p>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Service Account Names">
{data.allowedNames
?.split(",")
.map((name) => name.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Namespaces">
{data.allowedNamespaces
?.split(",")
.map((namespace) => namespace.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Allowed Audience">
{data.allowedAudience}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="CA Certificate">
{data.caCert && (
<Tooltip
side="right"
className="max-w-xl p-2"
content={<p className="break-words rounded bg-mineshaft-600 p-2">{data.caCert}</p>}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};

View File

@ -0,0 +1,116 @@
import { faBan, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Badge, EmptyState, Spinner, Tooltip } from "@app/components/v2";
import { useGetIdentityOidcAuth } from "@app/hooks/api";
import { IdentityOidcAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityOidcAuthForm";
import { ViewIdentityContentWrapper } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityContentWrapper";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { ViewAuthMethodProps } from "./types";
export const ViewIdentityOidcAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityOidcAuth(identityId);
if (isPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find OIDC Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityOidcAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="OIDC Discovery URL">
{data.oidcDiscoveryUrl}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Issuer">
{data.boundIssuer}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="CA Certificate">
{data.caCert && (
<Tooltip
side="right"
className="max-w-xl p-2"
content={<p className="break-words rounded bg-mineshaft-600 p-2">{data.caCert}</p>}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Subject">
{data.boundSubject}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Audiences">
{data.boundAudiences
?.split(",")
.map((name) => name.trim())
.join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay className="col-span-2" label="Claims">
{Object.keys(data.boundClaims).length && (
<Tooltip
side="right"
className="max-w-xl p-2"
content={
<pre className="whitespace-pre-wrap rounded bg-mineshaft-600 p-2">
{JSON.stringify(data.boundClaims, null, 2)}
</pre>
}
>
<div className="w-min">
<Badge className="flex h-5 w-min items-center gap-1.5 whitespace-nowrap bg-mineshaft-400/50 text-bunker-300">
<FontAwesomeIcon icon={faEye} />
<span>Reveal</span>
</Badge>
</div>
</Tooltip>
)}
</IdentityAuthFieldDisplay>
</ViewIdentityContentWrapper>
);
};

View File

@ -0,0 +1,68 @@
import { faBan } from "@fortawesome/free-solid-svg-icons";
import { EmptyState, Spinner } from "@app/components/v2";
import { useGetIdentityTokenAuth, useGetIdentityTokensTokenAuth } from "@app/hooks/api";
import { IdentityTokenAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityTokenAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { IdentityTokenAuthTokensTable } from "./IdentityTokenAuthTokensTable";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityTokenAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityTokenAuth(identityId);
const { data: tokens = [], isPending: clientSecretsPending } =
useGetIdentityTokensTokenAuth(identityId);
if (isPending || clientSecretsPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState icon={faBan} title="Could not find Token Auth associated with this Identity." />
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityTokenAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityTokenAuthTokensTable tokens={tokens} identityId={identityId} />
</ViewIdentityContentWrapper>
);
};

View File

@ -0,0 +1,106 @@
import { faBan, faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { EmptyState, IconButton, Spinner, Tooltip } from "@app/components/v2";
import { useTimedReset } from "@app/hooks";
import {
useGetIdentityUniversalAuth,
useGetIdentityUniversalAuthClientSecrets
} from "@app/hooks/api";
import { IdentityUniversalAuthForm } from "@app/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthForm";
import { IdentityAuthFieldDisplay } from "./IdentityAuthFieldDisplay";
import { IdentityUniversalAuthClientSecretsTable } from "./IdentityUniversalAuthClientSecretsTable";
import { ViewAuthMethodProps } from "./types";
import { ViewIdentityContentWrapper } from "./ViewIdentityContentWrapper";
export const ViewIdentityUniversalAuthContent = ({
identityId,
handlePopUpToggle,
handlePopUpOpen,
onDelete,
popUp
}: ViewAuthMethodProps) => {
const { data, isPending } = useGetIdentityUniversalAuth(identityId);
const { data: clientSecrets = [], isPending: clientSecretsPending } =
useGetIdentityUniversalAuthClientSecrets(identityId);
const [copyTextClientId, isCopyingClientId, setCopyTextClientId] = useTimedReset<string>({
initialState: "Copy Client ID to clipboard"
});
if (isPending || clientSecretsPending) {
return (
<div className="flex w-full items-center justify-center">
<Spinner className="text-mineshaft-400" />
</div>
);
}
if (!data) {
return (
<EmptyState
icon={faBan}
title="Could not find Universal Auth associated with this Identity."
/>
);
}
if (popUp.identityAuthMethod.isOpen) {
return (
<IdentityUniversalAuthForm
identityId={identityId}
isUpdate
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
);
}
return (
<ViewIdentityContentWrapper
onEdit={() => handlePopUpOpen("identityAuthMethod")}
onDelete={onDelete}
>
<IdentityAuthFieldDisplay label="Access Token TLL (seconds)">
{data.accessTokenTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max TLL (seconds)">
{data.accessTokenMaxTTL}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Max Number of Uses">
{data.accessTokenNumUsesLimit}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Access Token Trusted IPs">
{data.accessTokenTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<IdentityAuthFieldDisplay label="Client Secret Trusted IPs">
{data.clientSecretTrustedIps.map((ip) => ip.ipAddress).join(", ")}
</IdentityAuthFieldDisplay>
<div className="col-span-2 my-3">
<div className="mb-3 border-b border-mineshaft-500 pb-2">
<span className="text-bunker-300">Client ID</span>
</div>
<div className="flex items-center gap-2">
<span className="text-sm">{data.clientId}</span>
<Tooltip content={copyTextClientId}>
<IconButton
ariaLabel="copy icon"
variant="plain"
onClick={() => {
navigator.clipboard.writeText(data.clientId);
setCopyTextClientId("Copied");
}}
>
<FontAwesomeIcon icon={isCopyingClientId ? faCheck : faCopy} />
</IconButton>
</Tooltip>
</div>
</div>
<IdentityUniversalAuthClientSecretsTable
clientSecrets={clientSecrets}
identityId={identityId}
/>
</ViewIdentityContentWrapper>
);
};

View File

@ -0,0 +1 @@
export * from "./ViewIdentityAuthModal";

View File

@ -0,0 +1,12 @@
import { UsePopUpState } from "@app/hooks/usePopUp";
export type ViewAuthMethodProps = {
identityId: string;
onDelete: () => void;
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan", "identityAuthMethod"]>) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
state?: boolean
) => void;
popUp: UsePopUpState<["revokeAuthMethod", "upgradePlan", "identityAuthMethod"]>;
};

View File

@ -2,5 +2,4 @@ export { IdentityAuthenticationSection } from "./IdentityAuthenticationSection/I
export { IdentityClientSecretModal } from "./IdentityClientSecretModal";
export { IdentityDetailsSection } from "./IdentityDetailsSection";
export { IdentityProjectsSection } from "./IdentityProjectsSection/IdentityProjectsSection";
export { IdentityTokenListModal } from "./IdentityTokenListModal";
export { IdentityTokenModal } from "./IdentityTokenModal";

View File

@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { ProjectPermissionCan } from "@app/components/permissions";
import { Badge, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { Badge, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
import { IntegrationsListPageTabs } from "@app/types/integrations";
@ -45,14 +45,12 @@ export const IntegrationsListPage = () => {
<meta name="og:description" content={t("integrations.description") as string} />
</Helmet>
<div className="container relative mx-auto max-w-7xl pb-12 text-white">
<div className="mx-6 mb-8">
<div className="mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
<h1 className="text-3xl font-semibold">Integrations</h1>
<p className="text-base text-bunker-300">
Manage integrations with third-party services.
</p>
</div>
<div className="mx-2 mb-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
<div className="mb-8">
<PageHeader
title="Integrations"
description="Manage integrations with third-party services."
/>
<div className="mb-4 mt-4 flex flex-col rounded-r border-l-2 border-l-primary bg-mineshaft-300/5 px-4 py-2.5">
<div className="mb-1 flex items-center text-sm">
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1 text-primary" />
Integrations Update

View File

@ -289,7 +289,8 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
</DropdownMenuTrigger>
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
<DropdownMenuLabel>Status</DropdownMenuLabel>
{Object.values(SecretSyncStatus).map((status) => (
{[SecretSyncStatus.Running, SecretSyncStatus.Succeeded, SecretSyncStatus.Failed].map(
(status) => (
<DropdownMenuItem
onClick={(e) => {
e.preventDefault();
@ -316,7 +317,8 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
<span className="capitalize">{STATUS_ICON_MAP[status].name}</span>
</div>
</DropdownMenuItem>
))}
)
)}
<DropdownMenuLabel>Service</DropdownMenuLabel>
{secretSyncs.length ? (
[...new Set(secretSyncs.map(({ destination }) => destination))].map((destination) => {

View File

@ -180,10 +180,12 @@ export const SecretListView = ({
try {
// personal secret change
let personalAction = false;
if (overrideAction === "deleted") {
await handleSecretOperation("delete", SecretType.Personal, oldKey, {
secretId: orgSecret.idOverride
});
personalAction = true;
} else if (overrideAction && idOverride) {
await handleSecretOperation("update", SecretType.Personal, oldKey, {
value: valueOverride,
@ -191,14 +193,16 @@ export const SecretListView = ({
secretId: orgSecret.idOverride,
skipMultilineEncoding: modSecret.skipMultilineEncoding
});
personalAction = true;
} else if (overrideAction) {
await handleSecretOperation("create", SecretType.Personal, oldKey, {
value: valueOverride
});
personalAction = true;
}
// shared secret change
if (!isSharedSecUnchanged) {
if (!isSharedSecUnchanged && !personalAction) {
await handleSecretOperation("update", SecretType.Shared, oldKey, {
value,
tags: tagIds,
@ -232,8 +236,9 @@ export const SecretListView = ({
});
handlePopUpClose("secretDetail");
createNotification({
type: isProtectedBranch ? "info" : "success",
text: isProtectedBranch
type: isProtectedBranch && !personalAction ? "info" : "success",
text:
isProtectedBranch && !personalAction
? "Requested changes have been sent for review"
: "Successfully saved secrets"
});
@ -283,7 +288,12 @@ export const SecretListView = ({
text: "Failed to delete secret"
});
}
}, [(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key, environment, secretPath]);
}, [
(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key,
environment,
secretPath,
isProtectedBranch
]);
// for optimization on minimise re-rendering of secret items
const onCreateTag = useCallback(() => handlePopUpOpen("createTag"), []);

View File

@ -73,7 +73,7 @@ const PageContent = () => {
return (
<>
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
<div className="mx-auto mb-6 w-full max-w-7xl px-6 py-6">
<div className="mx-auto mb-6 w-full max-w-7xl">
<Button
variant="link"
type="submit"
@ -89,7 +89,6 @@ const PageContent = () => {
}
});
}}
className="mb-4"
>
Secret Syncs
</Button>

View File

@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { ProjectPermissionCan } from "@app/components/permissions";
import { SecretSyncLabel } from "@app/components/secret-syncs";
import { SecretSyncLabel, SecretSyncStatusBadge } from "@app/components/secret-syncs";
import { IconButton } from "@app/components/v2";
import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
@ -57,6 +57,11 @@ export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: Props) =
<div className="space-y-3">
<SecretSyncLabel label="Name">{name}</SecretSyncLabel>
<SecretSyncLabel label="Description">{description}</SecretSyncLabel>
{syncStatus && (
<SecretSyncLabel label="Status">
<SecretSyncStatusBadge status={syncStatus} />
</SecretSyncLabel>
)}
{lastSyncedAt && (
<SecretSyncLabel label="Last Synced">
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}

View File

@ -19,7 +19,7 @@ const schema = z.object({
environmentName: z
.string()
.min(1, { message: "Environment Name field must be at least 1 character" }),
environmentSlug: slugSchema()
environmentSlug: slugSchema({ max: 64 })
});
export type FormData = z.infer<typeof schema>;

View File

@ -17,7 +17,7 @@ type Props = {
const schema = z.object({
name: z.string(),
slug: slugSchema({ min: 1 })
slug: slugSchema({ min: 1, max: 64 })
});
export type FormData = z.infer<typeof schema>;

View File

@ -18,7 +18,7 @@ export const Route = createFileRoute(
beforeLoad: ({ context, params }) => {
return {
breadcrumbs: [
...context.breadcrumbs,
...(context?.breadcrumbs || []),
{
label: "Integrations",
link: linkOptions({

View File

@ -99,15 +99,10 @@ export const TeamcityConfigurePage = () => {
}
};
const filteredBuildConfigs = targetBuildConfigs?.concat({
name: "",
buildConfigId: ""
});
return integrationAuth &&
selectedSourceEnvironment &&
integrationAuthApps &&
filteredBuildConfigs &&
targetBuildConfigs &&
targetAppId ? (
<div className="flex h-full w-full items-center justify-center">
<Helmet>
@ -185,7 +180,7 @@ export const TeamcityConfigurePage = () => {
))
) : (
<SelectItem value="none" key="target-app-none">
No project found
No projects found
</SelectItem>
)}
</Select>
@ -195,15 +190,22 @@ export const TeamcityConfigurePage = () => {
value={targetBuildConfigId}
onValueChange={(val) => setTargetBuildConfigId(val)}
className="w-full border border-mineshaft-500"
isDisabled={targetBuildConfigs.length === 0}
>
{filteredBuildConfigs.map((buildConfig: any) => (
{targetBuildConfigs.length ? (
targetBuildConfigs.map((buildConfig: any) => (
<SelectItem
value={buildConfig.buildConfigId}
key={`target-build-config-${buildConfig.buildConfigId}`}
>
{buildConfig.name}
</SelectItem>
))}
))
) : (
<SelectItem value="none" key="target-app-none">
No build configs found
</SelectItem>
)}
</Select>
</FormControl>
<Button

View File

@ -11,6 +11,7 @@ import attemptChangePassword from "@app/components/utilities/attemptChangePasswo
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import { Button, FormControl, Input } from "@app/components/v2";
import { useUser } from "@app/context";
import { useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
type Errors = {
tooShort?: string;
@ -45,6 +46,7 @@ export const ChangePasswordSection = () => {
});
const [errors, setErrors] = useState<Errors>({});
const [isLoading, setIsLoading] = useState(false);
const sendSetupPasswordEmail = useSendPasswordSetupEmail();
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
try {
@ -80,6 +82,24 @@ export const ChangePasswordSection = () => {
}
};
const onSetupPassword = async () => {
try {
await sendSetupPasswordEmail.mutateAsync();
createNotification({
title: "Password setup verification email sent",
text: "Check your email to confirm password setup",
type: "info"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to send password setup email",
type: "error"
});
}
};
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
@ -142,6 +162,16 @@ export const ChangePasswordSection = () => {
<Button type="submit" colorSchema="secondary" isLoading={isLoading} isDisabled={isLoading}>
Save
</Button>
<p className="mt-2 font-inter text-sm text-mineshaft-400">
Need to setup a password?{" "}
<button
onClick={onSetupPassword}
type="button"
className="underline underline-offset-2 hover:text-mineshaft-200"
>
Click here
</button>
</p>
</form>
);
};

View File

@ -24,6 +24,7 @@ import { Route as authSignUpInvitePageRouteImport } from './pages/auth/SignUpInv
import { Route as authRequestNewInvitePageRouteImport } from './pages/auth/RequestNewInvitePage/route'
import { Route as authPasswordResetPageRouteImport } from './pages/auth/PasswordResetPage/route'
import { Route as authEmailNotVerifiedPageRouteImport } from './pages/auth/EmailNotVerifiedPage/route'
import { Route as authPasswordSetupPageRouteImport } from './pages/auth/PasswordSetupPage/route'
import { Route as userLayoutImport } from './pages/user/layout'
import { Route as organizationLayoutImport } from './pages/organization/layout'
import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route'
@ -310,6 +311,14 @@ const authEmailNotVerifiedPageRouteRoute =
getParentRoute: () => middlewaresRestrictLoginSignupRoute,
} as any)
const authPasswordSetupPageRouteRoute = authPasswordSetupPageRouteImport.update(
{
id: '/password-setup',
path: '/password-setup',
getParentRoute: () => middlewaresAuthenticateRoute,
} as any,
)
const userLayoutRoute = userLayoutImport.update({
id: '/_layout',
getParentRoute: () => AuthenticatePersonalSettingsRoute,
@ -1577,6 +1586,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof middlewaresRestrictLoginSignupImport
parentRoute: typeof rootRoute
}
'/_authenticate/password-setup': {
id: '/_authenticate/password-setup'
path: '/password-setup'
fullPath: '/password-setup'
preLoaderRoute: typeof authPasswordSetupPageRouteImport
parentRoute: typeof middlewaresAuthenticateImport
}
'/_restrict-login-signup/email-not-verified': {
id: '/_restrict-login-signup/email-not-verified'
path: '/email-not-verified'
@ -3397,12 +3413,14 @@ const AuthenticatePersonalSettingsRouteWithChildren =
)
interface middlewaresAuthenticateRouteChildren {
authPasswordSetupPageRouteRoute: typeof authPasswordSetupPageRouteRoute
middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren
AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren
}
const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren =
{
authPasswordSetupPageRouteRoute: authPasswordSetupPageRouteRoute,
middlewaresInjectOrgDetailsRoute:
middlewaresInjectOrgDetailsRouteWithChildren,
AuthenticatePersonalSettingsRoute:
@ -3487,6 +3505,7 @@ export interface FileRoutesByFullPath {
'/cli-redirect': typeof authCliRedirectPageRouteRoute
'/share-secret': typeof publicShareSecretPageRouteRoute
'': typeof organizationLayoutRouteWithChildren
'/password-setup': typeof authPasswordSetupPageRouteRoute
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
'/password-reset': typeof authPasswordResetPageRouteRoute
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
@ -3657,6 +3676,7 @@ export interface FileRoutesByTo {
'/cli-redirect': typeof authCliRedirectPageRouteRoute
'/share-secret': typeof publicShareSecretPageRouteRoute
'': typeof organizationLayoutRouteWithChildren
'/password-setup': typeof authPasswordSetupPageRouteRoute
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
'/password-reset': typeof authPasswordResetPageRouteRoute
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
@ -3824,6 +3844,7 @@ export interface FileRoutesById {
'/share-secret': typeof publicShareSecretPageRouteRoute
'/_authenticate': typeof middlewaresAuthenticateRouteWithChildren
'/_restrict-login-signup': typeof middlewaresRestrictLoginSignupRouteWithChildren
'/_authenticate/password-setup': typeof authPasswordSetupPageRouteRoute
'/_restrict-login-signup/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
'/_restrict-login-signup/password-reset': typeof authPasswordResetPageRouteRoute
'/_restrict-login-signup/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
@ -4004,6 +4025,7 @@ export interface FileRouteTypes {
| '/cli-redirect'
| '/share-secret'
| ''
| '/password-setup'
| '/email-not-verified'
| '/password-reset'
| '/requestnewinvite'
@ -4173,6 +4195,7 @@ export interface FileRouteTypes {
| '/cli-redirect'
| '/share-secret'
| ''
| '/password-setup'
| '/email-not-verified'
| '/password-reset'
| '/requestnewinvite'
@ -4338,6 +4361,7 @@ export interface FileRouteTypes {
| '/share-secret'
| '/_authenticate'
| '/_restrict-login-signup'
| '/_authenticate/password-setup'
| '/_restrict-login-signup/email-not-verified'
| '/_restrict-login-signup/password-reset'
| '/_restrict-login-signup/requestnewinvite'
@ -4562,6 +4586,7 @@ export const routeTree = rootRoute
"/_authenticate": {
"filePath": "middlewares/authenticate.tsx",
"children": [
"/_authenticate/password-setup",
"/_authenticate/_inject-org-details",
"/_authenticate/personal-settings"
]
@ -4579,6 +4604,10 @@ export const routeTree = rootRoute
"/_restrict-login-signup/admin/signup"
]
},
"/_authenticate/password-setup": {
"filePath": "auth/PasswordSetupPage/route.tsx",
"parent": "/_authenticate"
},
"/_restrict-login-signup/email-not-verified": {
"filePath": "auth/EmailNotVerifiedPage/route.tsx",
"parent": "/_restrict-login-signup"

View File

@ -335,6 +335,7 @@ export const routes = rootRoute("root.tsx", [
route("/verify-email", "auth/VerifyEmailPage/route.tsx")
]),
middleware("authenticate.tsx", [
route("/password-setup", "auth/PasswordSetupPage/route.tsx"),
route("/personal-settings", [
layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")])
]),

View File

@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: v0.8.5
version: v0.8.7
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v0.8.5"
appVersion: "v0.8.7"

View File

@ -1,4 +1,3 @@
{{- if .Values.installCRDs }}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
@ -262,6 +261,48 @@ spec:
hostAPI:
description: Infisical host to pull secrets from
type: string
managedKubeSecretReferences:
items:
properties:
creationPolicy:
default: Orphan
description: 'The Kubernetes Secret creation policy. Enum with
values: ''Owner'', ''Orphan''. Owner creates the secret and
sets .metadata.ownerReferences of the InfisicalSecret CRD that
created it. Orphan will not set the secret owner. This will
result in the secret being orphaned and not deleted when the
resource is deleted.'
type: string
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret is located
type: string
secretType:
default: Opaque
description: 'The Kubernetes Secret type (experimental feature).
More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
type: string
template:
description: The template to transform the secret data
properties:
data:
additionalProperties:
type: string
description: The template key values
type: object
includeAllSecrets:
description: This injects all retrieved secrets into the top
level of your template. Secrets defined in the template
will take precedence over the injected ones.
type: boolean
type: object
required:
- secretName
- secretNamespace
type: object
type: array
managedSecretReference:
properties:
creationPolicy:
@ -338,7 +379,6 @@ spec:
- secretNamespace
type: object
required:
- managedSecretReference
- resyncInterval
type: object
status:
@ -426,4 +466,3 @@ status:
plural: ""
conditions: []
storedVersions: []
{{- end }}

View File

@ -32,7 +32,7 @@ controllerManager:
- ALL
image:
repository: infisical/kubernetes-operator
tag: v0.8.5
tag: v0.8.7
resources:
limits:
cpu: 500m

View File

@ -134,9 +134,12 @@ type InfisicalSecretSpec struct {
// +kubebuilder:validation:Optional
Authentication Authentication `json:"authentication"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:Optional
ManagedSecretReference ManagedKubeSecretConfig `json:"managedSecretReference"`
// +kubebuilder:validation:Optional
ManagedKubeSecretReferences []ManagedKubeSecretConfig `json:"managedKubeSecretReferences"`
// +kubebuilder:default:=60
ResyncInterval int `json:"resyncInterval"`

View File

@ -565,6 +565,13 @@ func (in *InfisicalSecretSpec) DeepCopyInto(out *InfisicalSecretSpec) {
out.TokenSecretReference = in.TokenSecretReference
out.Authentication = in.Authentication
in.ManagedSecretReference.DeepCopyInto(&out.ManagedSecretReference)
if in.ManagedKubeSecretReferences != nil {
in, out := &in.ManagedKubeSecretReferences, &out.ManagedKubeSecretReferences
*out = make([]ManagedKubeSecretConfig, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
out.TLS = in.TLS
}

View File

@ -261,6 +261,48 @@ spec:
hostAPI:
description: Infisical host to pull secrets from
type: string
managedKubeSecretReferences:
items:
properties:
creationPolicy:
default: Orphan
description: 'The Kubernetes Secret creation policy. Enum with
values: ''Owner'', ''Orphan''. Owner creates the secret and
sets .metadata.ownerReferences of the InfisicalSecret CRD
that created it. Orphan will not set the secret owner. This
will result in the secret being orphaned and not deleted when
the resource is deleted.'
type: string
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret is located
type: string
secretType:
default: Opaque
description: 'The Kubernetes Secret type (experimental feature).
More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types'
type: string
template:
description: The template to transform the secret data
properties:
data:
additionalProperties:
type: string
description: The template key values
type: object
includeAllSecrets:
description: This injects all retrieved secrets into the
top level of your template. Secrets defined in the template
will take precedence over the injected ones.
type: boolean
type: object
required:
- secretName
- secretNamespace
type: object
type: array
managedSecretReference:
properties:
creationPolicy:
@ -339,7 +381,6 @@ spec:
- secretNamespace
type: object
required:
- managedSecretReference
- resyncInterval
type: object
status:

View File

@ -97,8 +97,8 @@ spec:
secretsPath: "/path"
recursive: true
managedSecretReference:
secretName: managed-secret
managedSecretReferences:
- secretName: managed-secret
secretNamespace: default
creationPolicy: "Orphan" ## Owner | Orphan
# secretType: kubernetes.io/dockerconfigjson

View File

@ -1,108 +0,0 @@
package controllers
import (
"context"
"fmt"
"sync"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/constants"
"github.com/go-logr/logr"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX = "secrets.infisical.com/managed-secret"
const AUTO_RELOAD_DEPLOYMENT_ANNOTATION = "secrets.infisical.com/auto-reload" // needs to be set to true for a deployment to start auto redeploying
func (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret) (int, error) {
listOfDeployments := &v1.DeploymentList{}
err := r.Client.List(ctx, listOfDeployments, &client.ListOptions{Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace})
if err != nil {
return 0, fmt.Errorf("unable to get deployments in the [namespace=%v] [err=%v]", infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, err)
}
managedKubeSecretNameAndNamespace := types.NamespacedName{
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
}
managedKubeSecret := &corev1.Secret{}
err = r.Client.Get(ctx, managedKubeSecretNameAndNamespace, managedKubeSecret)
if err != nil {
return 0, fmt.Errorf("unable to fetch Kubernetes secret to update deployment: %v", err)
}
var wg sync.WaitGroup
// Iterate over the deployments and check if they use the managed secret
for _, deployment := range listOfDeployments.Items {
deployment := deployment
if deployment.Annotations[AUTO_RELOAD_DEPLOYMENT_ANNOTATION] == "true" && r.IsDeploymentUsingManagedSecret(deployment, infisicalSecret) {
// Start a goroutine to reconcile the deployment
wg.Add(1)
go func(d v1.Deployment, s corev1.Secret) {
defer wg.Done()
if err := r.ReconcileDeployment(ctx, logger, d, s); err != nil {
logger.Error(err, fmt.Sprintf("unable to reconcile deployment with [name=%v]. Will try next requeue", deployment.ObjectMeta.Name))
}
}(deployment, *managedKubeSecret)
}
}
wg.Wait()
return 0, nil
}
// Check if the deployment uses managed secrets
func (r *InfisicalSecretReconciler) IsDeploymentUsingManagedSecret(deployment v1.Deployment, infisicalSecret v1alpha1.InfisicalSecret) bool {
managedSecretName := infisicalSecret.Spec.ManagedSecretReference.SecretName
for _, container := range deployment.Spec.Template.Spec.Containers {
for _, envFrom := range container.EnvFrom {
if envFrom.SecretRef != nil && envFrom.SecretRef.LocalObjectReference.Name == managedSecretName {
return true
}
}
for _, env := range container.Env {
if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil && env.ValueFrom.SecretKeyRef.LocalObjectReference.Name == managedSecretName {
return true
}
}
}
for _, volume := range deployment.Spec.Template.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == managedSecretName {
return true
}
}
return false
}
// This function ensures that a deployment is in sync with a Kubernetes secret by comparing their versions.
// If the version of the secret is different from the version annotation on the deployment, the annotation is updated to trigger a restart of the deployment.
func (r *InfisicalSecretReconciler) ReconcileDeployment(ctx context.Context, logger logr.Logger, deployment v1.Deployment, secret corev1.Secret) error {
annotationKey := fmt.Sprintf("%s.%s", DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX, secret.Name)
annotationValue := secret.Annotations[constants.SECRET_VERSION_ANNOTATION]
if deployment.Annotations[annotationKey] == annotationValue &&
deployment.Spec.Template.Annotations[annotationKey] == annotationValue {
logger.Info(fmt.Sprintf("The [deploymentName=%v] is already using the most up to date managed secrets. No action required.", deployment.ObjectMeta.Name))
return nil
}
logger.Info(fmt.Sprintf("Deployment is using outdated managed secret. Starting re-deployment [deploymentName=%v]", deployment.ObjectMeta.Name))
if deployment.Spec.Template.Annotations == nil {
deployment.Spec.Template.Annotations = make(map[string]string)
}
deployment.Annotations[annotationKey] = annotationValue
deployment.Spec.Template.Annotations[annotationKey] = annotationValue
if err := r.Client.Update(ctx, &deployment); err != nil {
return fmt.Errorf("failed to update deployment annotation: %v", err)
}
return nil
}

View File

@ -21,14 +21,14 @@ func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: "Failed to sync secrets. This can be caused by invalid service token or an invalid API host that is set. Check operator logs for more info",
Message: fmt.Sprintf("Failed to sync secrets. This can be caused by invalid access token or an invalid API host that is set. Error: %v", errorToConditionOn),
})
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/AutoRedeployReady",
Status: metav1.ConditionFalse,
Reason: "Stopped",
Message: "Auto redeployment has been stopped because the operator failed to sync secrets",
Message: fmt.Sprintf("Auto redeployment has been stopped because the operator failed to sync secrets. Error: %v", errorToConditionOn),
})
} else {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{

View File

@ -13,6 +13,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
defaultErrors "errors"
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/api"
controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerhelpers"
@ -35,8 +37,6 @@ func (r *InfisicalSecretReconciler) GetLogger(req ctrl.Request) logr.Logger {
return r.BaseLogger.WithValues("infisicalsecret", req.NamespacedName)
}
var resourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables)
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/finalizers,verbs=update
@ -71,6 +71,30 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
}
}
// It's important we don't directly modify the CRD object, so we create a copy of it and move existing data into it.
managedKubeSecretReferences := infisicalSecretCRD.Spec.ManagedKubeSecretReferences
if infisicalSecretCRD.Spec.ManagedSecretReference.SecretName != "" && managedKubeSecretReferences != nil && len(managedKubeSecretReferences) > 0 {
errMessage := "InfisicalSecret CRD cannot have both managedSecretReference and managedKubeSecretReferences"
logger.Error(defaultErrors.New(errMessage), errMessage)
return ctrl.Result{}, defaultErrors.New(errMessage)
}
if infisicalSecretCRD.Spec.ManagedSecretReference.SecretName != "" {
logger.Info("\n\n\nThe field `managedSecretReference` will be deprecated in the near future, please use `managedKubeSecretReferences` instead.\n\nRefer to the documentation for more information: https://infisical.com/docs/integrations/platforms/kubernetes/infisical-secret-crd\n\n\n")
if managedKubeSecretReferences == nil {
managedKubeSecretReferences = []secretsv1alpha1.ManagedKubeSecretConfig{}
}
managedKubeSecretReferences = append(managedKubeSecretReferences, infisicalSecretCRD.Spec.ManagedSecretReference)
}
if len(managedKubeSecretReferences) == 0 {
errMessage := "InfisicalSecret CRD must have at least one managed secret reference set in the `managedKubeSecretReferences` field"
logger.Error(defaultErrors.New(errMessage), errMessage)
return ctrl.Result{}, defaultErrors.New(errMessage)
}
// Remove finalizers if they exist. This is to support previous InfisicalSecret CRD's that have finalizers on them.
// In order to delete secrets with finalizers, we first remove the finalizers so we can use the simplified and improved deletion process
if !infisicalSecretCRD.ObjectMeta.DeletionTimestamp.IsZero() && len(infisicalSecretCRD.ObjectMeta.Finalizers) > 0 {
@ -127,7 +151,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
api.API_CA_CERTIFICATE = ""
}
err = r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCRD)
err = r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCRD, managedKubeSecretReferences)
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCRD, err)
if err != nil {
@ -138,7 +162,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
}, nil
}
numDeployments, err := controllerhelpers.ReconcileDeploymentsWithManagedSecrets(ctx, r.Client, logger, infisicalSecretCRD.Spec.ManagedSecretReference)
numDeployments, err := controllerhelpers.ReconcileDeploymentsWithMultipleManagedSecrets(ctx, r.Client, logger, managedKubeSecretReferences)
r.SetInfisicalAutoRedeploymentReady(ctx, logger, &infisicalSecretCRD, numDeployments, err)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to reconcile auto redeployment. Will requeue after [requeueTime=%v]", requeueTime))

View File

@ -165,10 +165,10 @@ var infisicalSecretTemplateFunctions = template.FuncMap{
},
}
func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedSecretReference v1alpha1.ManagedKubeSecretConfig, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
plainProcessedSecrets := make(map[string][]byte)
secretType := infisicalSecret.Spec.ManagedSecretReference.SecretType
managedTemplateData := infisicalSecret.Spec.ManagedSecretReference.Template
secretType := managedSecretReference.SecretType
managedTemplateData := managedSecretReference.Template
if managedTemplateData == nil || managedTemplateData.IncludeAllSecrets {
for _, secret := range secretsFromAPI {
@ -226,8 +226,8 @@ func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context
// create a new secret as specified by the managed secret spec of CRD
newKubeSecretInstance := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
Name: managedSecretReference.SecretName,
Namespace: managedSecretReference.SecretNamespace,
Annotations: annotations,
Labels: labels,
},
@ -235,7 +235,7 @@ func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context
Data: plainProcessedSecrets,
}
if infisicalSecret.Spec.ManagedSecretReference.CreationPolicy == "Owner" {
if managedSecretReference.CreationPolicy == "Owner" {
// Set InfisicalSecret instance as the owner and controller of the managed secret
err := ctrl.SetControllerReference(&infisicalSecret, newKubeSecretInstance, r.Scheme)
if err != nil {
@ -252,8 +252,8 @@ func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context
return nil
}
func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
managedTemplateData := infisicalSecret.Spec.ManagedSecretReference.Template
func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, managedSecretReference v1alpha1.ManagedKubeSecretConfig, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
managedTemplateData := managedSecretReference.Template
plainProcessedSecrets := make(map[string][]byte)
if managedTemplateData == nil || managedTemplateData.IncludeAllSecrets {
@ -337,7 +337,7 @@ func (r *InfisicalSecretReconciler) updateResourceVariables(infisicalSecret v1al
infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables
}
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret) error {
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecretReferences []v1alpha1.ManagedKubeSecretConfig) error {
resourceVariables := r.getResourceVariables(infisicalSecret)
infisicalClient := resourceVariables.InfisicalClient
@ -361,10 +361,11 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
})
}
for _, managedSecretReference := range managedKubeSecretReferences {
// Look for managed secret by name and namespace
managedKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
Name: managedSecretReference.SecretName,
Namespace: managedSecretReference.SecretNamespace,
})
if err != nil && !k8Errors.IsNotFound(err) {
@ -420,13 +421,24 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
logger.Info(fmt.Sprintf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]", authDetails.AuthStrategy))
} else {
return errors.New("no authentication method provided yet. Please configure a authentication method then try again")
return errors.New("no authentication method provided. Please configure a authentication method then try again")
}
if !updateDetails.Modified {
logger.Info("ReconcileInfisicalSecret: No secrets modified so reconcile not needed")
continue
}
if managedKubeSecret == nil {
return r.createInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, plainTextSecretsFromApi, updateDetails.ETag)
if err := r.createInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, managedSecretReference, plainTextSecretsFromApi, updateDetails.ETag); err != nil {
return fmt.Errorf("failed to create managed secret [err=%s]", err)
}
} else {
return r.updateInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag)
if err := r.updateInfisicalManagedKubeSecret(ctx, logger, managedSecretReference, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag); err != nil {
return fmt.Errorf("failed to update managed secret [err=%s]", err)
}
}
}
return nil
}

View File

@ -59,6 +59,17 @@ func ReconcileDeploymentsWithManagedSecrets(ctx context.Context, client controll
return 0, nil
}
func ReconcileDeploymentsWithMultipleManagedSecrets(ctx context.Context, client controllerClient.Client, logger logr.Logger, managedSecrets []v1alpha1.ManagedKubeSecretConfig) (int, error) {
for _, managedSecret := range managedSecrets {
_, err := ReconcileDeploymentsWithManagedSecrets(ctx, client, logger, managedSecret)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to reconcile deployments with managed secret [name=%v]", managedSecret.SecretName))
return 0, err
}
}
return 0, nil
}
// Check if the deployment uses managed secrets
func IsDeploymentUsingManagedSecret(deployment v1.Deployment, managedSecret v1alpha1.ManagedKubeSecretConfig) bool {
managedSecretName := managedSecret.SecretName