mirror of
https://github.com/Infisical/infisical.git
synced 2025-07-05 04:29:09 +00:00
Compare commits
44 Commits
daniel/upd
...
set-passwo
Author | SHA1 | Date | |
---|---|---|---|
d74b819f57 | |||
6af7c5c371 | |||
72468d5428 | |||
c7ec9ff816 | |||
554e268f88 | |||
a8a27c3045 | |||
1181c684db | |||
dda436bcd9 | |||
89124b18d2 | |||
a534a4975c | |||
79a616dc1c | |||
a93bfa69c9 | |||
598d14fc54 | |||
08a0550cd7 | |||
d7503573b1 | |||
b5a89edeed | |||
860eaae4c8 | |||
c7a4b6c4e9 | |||
c12c6dcc6e | |||
99c9b644df | |||
8741414cfa | |||
b8d29793ec | |||
92013dbfbc | |||
c5319588fe | |||
9efb8eaf78 | |||
dfc973c7f7 | |||
3013d1977c | |||
f358e8942d | |||
c3970d1ea2 | |||
2dc00a638a | |||
94aed485a5 | |||
e382941424 | |||
bab9c1f454 | |||
2bd4770fb4 | |||
31905fab6e | |||
784acf16d0 | |||
114b89c952 | |||
81420198cb | |||
0ff18e277f | |||
e093f70301 | |||
8e2ff18f35 | |||
3fbfecf7a9 | |||
9087def21c | |||
586dbd79b0 |
@ -203,7 +203,8 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
encryptedPrivateKeyIV: z.string().trim(),
|
encryptedPrivateKeyIV: z.string().trim(),
|
||||||
encryptedPrivateKeyTag: z.string().trim(),
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
salt: z.string().trim(),
|
salt: z.string().trim(),
|
||||||
verifier: z.string().trim()
|
verifier: z.string().trim(),
|
||||||
|
password: z.string().trim()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@ -218,7 +219,69 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
|
|||||||
userId: token.userId
|
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" };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -81,11 +81,14 @@ export const getAwsConnectionConfig = async (appConnection: TAwsConnectionConfig
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const validateAwsConnectionCredentials = async (appConnection: TAwsConnectionConfig) => {
|
export const validateAwsConnectionCredentials = async (appConnection: TAwsConnectionConfig) => {
|
||||||
const awsConfig = await getAwsConnectionConfig(appConnection);
|
let resp: AWS.STS.GetCallerIdentityResponse & {
|
||||||
const sts = new AWS.STS(awsConfig);
|
$response: AWS.Response<AWS.STS.GetCallerIdentityResponse, AWS.AWSError>;
|
||||||
let resp: Awaited<ReturnType<ReturnType<typeof sts.getCallerIdentity>["promise"]>>;
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const awsConfig = await getAwsConnectionConfig(appConnection);
|
||||||
|
const sts = new AWS.STS(awsConfig);
|
||||||
|
|
||||||
resp = await sts.getCallerIdentity().promise();
|
resp = await sts.getCallerIdentity().promise();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
throw new BadRequestError({
|
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({
|
throw new InternalServerError({
|
||||||
message: `Unable to validate credentials: ${
|
message: `Unable to validate credentials: ${
|
||||||
resp.$response.error?.message ??
|
resp.$response.error?.message ??
|
||||||
|
@ -57,6 +57,12 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
|||||||
const expiresAt = new Date(new Date().getTime() + 86400000);
|
const expiresAt = new Date(new Date().getTime() + 86400000);
|
||||||
return { token, expiresAt };
|
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: {
|
case TokenType.TOKEN_USER_UNLOCK: {
|
||||||
const token = crypto.randomBytes(16).toString("hex");
|
const token = crypto.randomBytes(16).toString("hex");
|
||||||
const expiresAt = new Date(new Date().getTime() + 259200000);
|
const expiresAt = new Date(new Date().getTime() + 259200000);
|
||||||
|
@ -6,6 +6,7 @@ export enum TokenType {
|
|||||||
TOKEN_EMAIL_MFA = "emailMfa",
|
TOKEN_EMAIL_MFA = "emailMfa",
|
||||||
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation",
|
||||||
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
|
TOKEN_EMAIL_PASSWORD_RESET = "passwordReset",
|
||||||
|
TOKEN_EMAIL_PASSWORD_SETUP = "passwordSetup",
|
||||||
TOKEN_USER_UNLOCK = "userUnlock"
|
TOKEN_USER_UNLOCK = "userUnlock"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,8 @@ import jwt from "jsonwebtoken";
|
|||||||
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
|
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 { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||||
import { TokenType } from "../auth-token/auth-token-types";
|
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 { TTotpConfigDALFactory } from "../totp/totp-config-dal";
|
||||||
import { TUserDALFactory } from "../user/user-dal";
|
import { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TAuthDALFactory } from "./auth-dal";
|
import { TAuthDALFactory } from "./auth-dal";
|
||||||
import { TChangePasswordDTO, TCreateBackupPrivateKeyDTO, TResetPasswordViaBackupKeyDTO } from "./auth-password-type";
|
import {
|
||||||
import { AuthTokenType } from "./auth-type";
|
TChangePasswordDTO,
|
||||||
|
TCreateBackupPrivateKeyDTO,
|
||||||
|
TResetPasswordViaBackupKeyDTO,
|
||||||
|
TSetupPasswordViaBackupKeyDTO
|
||||||
|
} from "./auth-password-type";
|
||||||
|
import { ActorType, AuthMethod, AuthTokenType } from "./auth-type";
|
||||||
|
|
||||||
type TAuthPasswordServiceFactoryDep = {
|
type TAuthPasswordServiceFactoryDep = {
|
||||||
authDAL: TAuthDALFactory;
|
authDAL: TAuthDALFactory;
|
||||||
@ -169,8 +176,13 @@ export const authPaswordServiceFactory = ({
|
|||||||
verifier,
|
verifier,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
userId
|
userId,
|
||||||
|
password
|
||||||
}: TResetPasswordViaBackupKeyDTO) => {
|
}: TResetPasswordViaBackupKeyDTO) => {
|
||||||
|
const cfg = getConfig();
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, cfg.BCRYPT_SALT_ROUND);
|
||||||
|
|
||||||
await userDAL.updateUserEncryptionByUserId(userId, {
|
await userDAL.updateUserEncryptionByUserId(userId, {
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
@ -180,7 +192,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag,
|
tag: encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier,
|
||||||
|
hashedPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
await userDAL.updateById(userId, {
|
await userDAL.updateById(userId, {
|
||||||
@ -267,6 +280,108 @@ export const authPaswordServiceFactory = ({
|
|||||||
return backupKey;
|
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 {
|
return {
|
||||||
generateServerPubKey,
|
generateServerPubKey,
|
||||||
changePassword,
|
changePassword,
|
||||||
@ -274,6 +389,8 @@ export const authPaswordServiceFactory = ({
|
|||||||
sendPasswordResetEmail,
|
sendPasswordResetEmail,
|
||||||
verifyPasswordResetEmail,
|
verifyPasswordResetEmail,
|
||||||
createBackupPrivateKey,
|
createBackupPrivateKey,
|
||||||
getBackupPrivateKeyOfUser
|
getBackupPrivateKeyOfUser,
|
||||||
|
sendPasswordSetupEmail,
|
||||||
|
setupPassword
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -23,6 +23,20 @@ export type TResetPasswordViaBackupKeyDTO = {
|
|||||||
encryptedPrivateKeyTag: string;
|
encryptedPrivateKeyTag: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
verifier: 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 = {
|
export type TCreateBackupPrivateKeyDTO = {
|
||||||
|
@ -30,6 +30,7 @@ export enum SmtpTemplates {
|
|||||||
NewDeviceJoin = "newDevice.handlebars",
|
NewDeviceJoin = "newDevice.handlebars",
|
||||||
OrgInvite = "organizationInvitation.handlebars",
|
OrgInvite = "organizationInvitation.handlebars",
|
||||||
ResetPassword = "passwordReset.handlebars",
|
ResetPassword = "passwordReset.handlebars",
|
||||||
|
SetupPassword = "passwordSetup.handlebars",
|
||||||
SecretLeakIncident = "secretLeakIncident.handlebars",
|
SecretLeakIncident = "secretLeakIncident.handlebars",
|
||||||
WorkspaceInvite = "workspaceInvitation.handlebars",
|
WorkspaceInvite = "workspaceInvitation.handlebars",
|
||||||
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
ScimUserProvisioned = "scimUserProvisioned.handlebars",
|
||||||
|
17
backend/src/services/smtp/templates/passwordSetup.handlebars
Normal file
17
backend/src/services/smtp/templates/passwordSetup.handlebars
Normal 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>
|
@ -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:
|
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.
|
- 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
|
## Project Setup
|
||||||
|
|
||||||
@ -46,29 +46,43 @@ Finally, create an index.js file containing the application code.
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { InfisicalClient } = require("@infisical/sdk");
|
const { InfisicalSDK } = require("@infisical/sdk");
|
||||||
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
|
|
||||||
const client = new InfisicalClient({
|
let client;
|
||||||
auth: {
|
|
||||||
universalAuth: {
|
const setupClient = () => {
|
||||||
clientId: "YOUR_CLIENT_ID",
|
|
||||||
clientSecret: "YOUR_CLIENT_SECRET",
|
if (client) {
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const infisicalSdk = new InfisicalSDK({
|
||||||
|
siteUrl: "your-infisical-instance.com" // Optional, defaults to https://app.infisical.com
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/", async (req, res) => {
|
await infisicalSdk.auth().universalAuth.login({
|
||||||
// access value
|
clientId: "<machine-identity-client-id>",
|
||||||
|
clientSecret: "<machine-identity-client-secret>"
|
||||||
|
});
|
||||||
|
|
||||||
const name = await client.getSecret({
|
// If authentication was successful, assign the client
|
||||||
environment: "dev",
|
client = infisicalSdk;
|
||||||
projectId: "PROJECT_ID",
|
}
|
||||||
path: "/",
|
|
||||||
type: "shared",
|
|
||||||
|
|
||||||
|
app.get("/", async (req, res) => {
|
||||||
|
|
||||||
|
|
||||||
|
const name = await client.secrets().getSecret({
|
||||||
|
environment: "dev", // dev, staging, prod, etc.
|
||||||
|
projectId: "<project-id>",
|
||||||
|
secretPath: "/",
|
||||||
secretName: "NAME"
|
secretName: "NAME"
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -76,13 +90,13 @@ app.get("/", async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.listen(PORT, async () => {
|
app.listen(PORT, async () => {
|
||||||
// initialize client
|
// initialize http server and Infisical
|
||||||
|
await setupClient();
|
||||||
console.log(`App listening on port ${PORT}`);
|
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
|
that we created earlier, giving access to the secrets in the development environment of the
|
||||||
project in Infisical that we created earlier.
|
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.
|
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
|
## FAQ
|
||||||
|
|
||||||
<AccordionGroup>
|
<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?">
|
<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
|
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`.
|
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:
|
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)
|
||||||
|
@ -3,7 +3,7 @@ title: "Role-based Access Controls"
|
|||||||
description: "Learn how to use RBAC to manage user permissions."
|
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).
|
In general, access controls can be split up across [projects](/documentation/platform/project) and [organizations](/documentation/platform/organization).
|
||||||
|
|
||||||
|
BIN
docs/images/app-connections/aws/access-key-create-policy.png
Normal file
BIN
docs/images/app-connections/aws/access-key-create-policy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 509 KiB |
BIN
docs/images/app-connections/aws/assume-role-create-policy.png
Normal file
BIN
docs/images/app-connections/aws/assume-role-create-policy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 522 KiB |
@ -9,10 +9,6 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
<Tab title="Assume Role (Recommended)">
|
<Tab title="Assume Role (Recommended)">
|
||||||
Infisical will assume the provided role in your AWS account securely, without the need to share any credentials.
|
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">
|
<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.
|
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.
|
|||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Set Up Connection Keys">
|
<Step title="Set Up Connection Keys">
|
||||||
1. Set the access key as **INF_APP_CONNECTION_AWS_CLIENT_ID**.
|
1. Set the access key as **INF_APP_CONNECTION_AWS_ACCESS_KEY_ID**.
|
||||||
2. Set the secret key as **INF_APP_CONNECTION_AWS_CLIENT_SECRET**.
|
2. Set the secret key as **INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY**.
|
||||||
</Step>
|
</Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
</Accordion>
|
</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.
|
4. Optionally, enable **Require external ID** and enter your **Organization ID** to further enhance security.
|
||||||
</Step>
|
</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**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
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 IAM Role:
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
@ -199,22 +199,13 @@ Infisical supports two methods for connecting to AWS.
|
|||||||
<Tab title="Access Key">
|
<Tab title="Access Key">
|
||||||
Infisical will use the provided **Access Key ID** and **Secret Key** to connect to your AWS instance.
|
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>
|
<Steps>
|
||||||
<Step title="Create the Managing User IAM Role for Infisical">
|
<Step title="Add Required Permissions to the IAM User">
|
||||||
1. Navigate to the [Create IAM Role](https://console.aws.amazon.com/iamv2/home#/roles/create?step=selectEntities) page in your AWS Console.
|
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>
|
|
||||||
|
|
||||||
<Step title="Add Required Permissions for the IAM Role">
|
Depending on your use case, add one or more of the following policies to your user:
|
||||||
Depending on your use case, add one or more of the following policies to your IAM Role:
|
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab title="Secret Sync">
|
<Tab title="Secret Sync">
|
||||||
|
@ -9,10 +9,6 @@ Infisical supports two methods for connecting to GitHub.
|
|||||||
<Tab title="GitHub App (Recommended)">
|
<Tab title="GitHub App (Recommended)">
|
||||||
Infisical will use a GitHub App with finely grained permissions to connect to GitHub.
|
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">
|
<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
|
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.
|
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_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_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_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_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_PRIVATE_KEY`: The **Private Key** of your GitHub application.
|
||||||
|
|
||||||
Once added, restart your Infisical instance and use the GitHub integration via app authentication.
|
Once added, restart your Infisical instance and use the GitHub integration via app authentication.
|
||||||
</Step>
|
</Step>
|
||||||
@ -100,10 +96,6 @@ Infisical supports two methods for connecting to GitHub.
|
|||||||
<Tab title="OAuth">
|
<Tab title="OAuth">
|
||||||
Infisical will use an OAuth App to connect to GitHub.
|
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">
|
<Accordion title="Self-Hosted Instance">
|
||||||
Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub
|
Using the GitHub integration on a self-hosted instance of Infisical requires configuring an OAuth application in GitHub
|
||||||
and registering your instance with it.
|
and registering your instance with it.
|
||||||
|
@ -26,8 +26,8 @@ spec:
|
|||||||
name: <service-account-name>
|
name: <service-account-name>
|
||||||
namespace: <service-account-namespace>
|
namespace: <service-account-namespace>
|
||||||
|
|
||||||
managedSecretReference:
|
managedKubeSecretReferences:
|
||||||
secretName: managed-secret
|
- secretName: managed-secret
|
||||||
secretNamespace: default
|
secretNamespace: default
|
||||||
creationPolicy: "Orphan"
|
creationPolicy: "Orphan"
|
||||||
template:
|
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.
|
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.
|
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>
|
||||||
<Accordion title="managedSecretReference.secretName">
|
<Accordion title="managedKubeSecretReferences[].secretName">
|
||||||
The name of the managed Kubernetes secret to be created
|
The name of the managed Kubernetes secret to be created
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="managedSecretReference.secretNamespace">
|
<Accordion title="managedKubeSecretReferences[].secretNamespace">
|
||||||
The namespace of the managed Kubernetes secret to be created.
|
The namespace of the managed Kubernetes secret to be created.
|
||||||
</Accordion>
|
</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.
|
Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets.
|
||||||
</Accordion>
|
</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.
|
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.
|
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.
|
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.
|
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>
|
||||||
<Accordion title="managedSecretReference.template.includeAllSecrets">
|
<Accordion title="managedKubeSecretReferences[].template.includeAllSecrets">
|
||||||
This property controls what secrets are included in your managed secret when using templates.
|
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.
|
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.**
|
**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.
|
Use this option when you would like to sync **only** a subset of secrets from Infisical to Kubernetes.
|
||||||
|
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="managedSecretReference.template.data">
|
<Accordion title="managedKubeSecretReferences[].template.data">
|
||||||
Define secret keys and their corresponding templates.
|
Define secret keys and their corresponding templates.
|
||||||
Each data value uses a Golang template with access to all secrets retrieved from the specified scope.
|
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:
|
#### Example template configuration:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
managedSecretReference:
|
managedKubeSecretReferences:
|
||||||
secretName: managed-secret
|
- secretName: managed-secret
|
||||||
secretNamespace: default
|
secretNamespace: default
|
||||||
template:
|
template:
|
||||||
includeAllSecrets: true
|
includeAllSecrets: true
|
||||||
@ -652,7 +666,7 @@ 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`.
|
The resulting managed secret will contain the decoded value of `BINARY_KEY_BASE64`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
managedSecretReference:
|
managedKubeSecretReferences:
|
||||||
secretName: managed-secret
|
secretName: managed-secret
|
||||||
secretNamespace: default
|
secretNamespace: default
|
||||||
template:
|
template:
|
||||||
@ -913,7 +927,7 @@ spec:
|
|||||||
..
|
..
|
||||||
authentication:
|
authentication:
|
||||||
...
|
...
|
||||||
managedSecretReference:
|
managedKubeSecretReferences:
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -347,17 +347,27 @@
|
|||||||
"group": "App Connections",
|
"group": "App Connections",
|
||||||
"pages": [
|
"pages": [
|
||||||
"integrations/app-connections/overview",
|
"integrations/app-connections/overview",
|
||||||
|
{
|
||||||
|
"group": "Connections",
|
||||||
|
"pages": [
|
||||||
"integrations/app-connections/aws",
|
"integrations/app-connections/aws",
|
||||||
"integrations/app-connections/github"
|
"integrations/app-connections/github"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Secret Syncs",
|
"group": "Secret Syncs",
|
||||||
"pages": [
|
"pages": [
|
||||||
"integrations/secret-syncs/overview",
|
"integrations/secret-syncs/overview",
|
||||||
|
{
|
||||||
|
"group": "Syncs",
|
||||||
|
"pages": [
|
||||||
"integrations/secret-syncs/aws-parameter-store",
|
"integrations/secret-syncs/aws-parameter-store",
|
||||||
"integrations/secret-syncs/github"
|
"integrations/secret-syncs/github"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"group": "Infrastructure Integrations",
|
"group": "Infrastructure Integrations",
|
||||||
|
@ -34,12 +34,6 @@ From local development to production, Infisical SDKs provide the easiest way for
|
|||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
<AccordionGroup>
|
<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?">
|
<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
|
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.
|
value ever-existed, the SDK falls back to whatever value is on the process environment.
|
||||||
|
@ -13,7 +13,8 @@ export const ROUTE_PATHS = Object.freeze({
|
|||||||
"/_restrict-login-signup/login/provider/success"
|
"/_restrict-login-signup/login/provider/success"
|
||||||
),
|
),
|
||||||
SignUpSsoPage: setRoute("/signup/sso", "/_restrict-login-signup/signup/sso"),
|
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: {
|
Organization: {
|
||||||
SecretScanning: setRoute(
|
SecretScanning: setRoute(
|
||||||
|
@ -23,6 +23,7 @@ import {
|
|||||||
MfaMethod,
|
MfaMethod,
|
||||||
ResetPasswordDTO,
|
ResetPasswordDTO,
|
||||||
SendMfaTokenDTO,
|
SendMfaTokenDTO,
|
||||||
|
SetupPasswordDTO,
|
||||||
SRP1DTO,
|
SRP1DTO,
|
||||||
SRPR1Res,
|
SRPR1Res,
|
||||||
TOauthTokenExchangeDTO,
|
TOauthTokenExchangeDTO,
|
||||||
@ -286,7 +287,8 @@ export const useResetPassword = () => {
|
|||||||
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
|
||||||
salt: details.salt,
|
salt: details.salt,
|
||||||
verifier: details.verifier
|
verifier: details.verifier,
|
||||||
|
password: details.password
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
@ -336,3 +338,23 @@ export const checkUserTotpMfa = async () => {
|
|||||||
|
|
||||||
return data.isVerified;
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -133,6 +133,20 @@ export type ResetPasswordDTO = {
|
|||||||
salt: string;
|
salt: string;
|
||||||
verifier: string;
|
verifier: string;
|
||||||
verificationToken: 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 = {
|
export type IssueBackupPrivateKeyDTO = {
|
||||||
|
@ -923,7 +923,7 @@ export const useAddIdentityTokenAuth = () => {
|
|||||||
});
|
});
|
||||||
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
queryClient.invalidateQueries({ queryKey: identitiesKeys.getIdentityById(identityId) });
|
||||||
queryClient.invalidateQueries({
|
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.getIdentityById(identityId) });
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: identitiesKeys.getIdentityUniversalAuth(identityId)
|
queryKey: identitiesKeys.getIdentityTokenAuth(identityId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -182,7 +182,7 @@ export const queryClient = new QueryClient({
|
|||||||
createNotification({
|
createNotification({
|
||||||
title: "Bad Request",
|
title: "Bad Request",
|
||||||
type: "error",
|
type: "error",
|
||||||
text: `${serverResponse.message}${serverResponse.message.endsWith(".") ? "" : "."}`,
|
text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`,
|
||||||
copyActions: [
|
copyActions: [
|
||||||
{
|
{
|
||||||
value: serverResponse.reqId,
|
value: serverResponse.reqId,
|
||||||
|
@ -136,7 +136,8 @@ export const PasswordResetPage = () => {
|
|||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
salt: result.salt,
|
salt: result.salt,
|
||||||
verifier: result.verifier,
|
verifier: result.verifier,
|
||||||
verificationToken
|
verificationToken,
|
||||||
|
password: newPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate({ to: "/login" });
|
navigate({ to: "/login" });
|
||||||
|
349
frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx
Normal file
349
frontend/src/pages/auth/PasswordSetupPage/PasswordSetupPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
15
frontend/src/pages/auth/PasswordSetupPage/route.tsx
Normal file
15
frontend/src/pages/auth/PasswordSetupPage/route.tsx
Normal 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)
|
||||||
|
});
|
@ -1,12 +1,13 @@
|
|||||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import { userKeys } from "@app/hooks/api";
|
import { userKeys } from "@app/hooks/api";
|
||||||
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
|
import { authKeys, fetchAuthToken } from "@app/hooks/api/auth/queries";
|
||||||
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
import { fetchUserDetails } from "@app/hooks/api/users/queries";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_authenticate")({
|
export const Route = createFileRoute("/_authenticate")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ context, location }) => {
|
||||||
if (!context.serverConfig.initialized) {
|
if (!context.serverConfig.initialized) {
|
||||||
throw redirect({ to: "/admin/signup" });
|
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" });
|
throw redirect({ to: "/login/select-organization" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,10 +7,10 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
import { IdentityAuthMethodModalContent } from "./IdentityAuthMethodModalContent";
|
import { IdentityAuthMethodModalContent } from "./IdentityAuthMethodModalContent";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
|
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>;
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
@ -34,7 +34,7 @@ export const IdentityAuthMethodModal = ({ popUp, handlePopUpOpen, handlePopUpTog
|
|||||||
title={
|
title={
|
||||||
isSelectedAuthAlreadyConfigured
|
isSelectedAuthAlreadyConfigured
|
||||||
? `Edit ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
? `Edit ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
||||||
: `Create new ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
: `Add ${identityAuthToNameMap[selectedAuthMethod!] ?? ""}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IdentityAuthMethodModalContent
|
<IdentityAuthMethodModalContent
|
||||||
|
@ -4,30 +4,8 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { Badge, FormControl, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
import {
|
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
||||||
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 { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
|
import { IdentityAwsAuthForm } from "./IdentityAwsAuthForm";
|
||||||
@ -40,10 +18,10 @@ import { IdentityTokenAuthForm } from "./IdentityTokenAuthForm";
|
|||||||
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
import { IdentityUniversalAuthForm } from "./IdentityUniversalAuthForm";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>;
|
popUp: UsePopUpState<["identityAuthMethod", "upgradePlan"]>;
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "upgradePlan"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
@ -56,13 +34,7 @@ type Props = {
|
|||||||
setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void;
|
setSelectedAuthMethod: (authMethod: IdentityAuthMethod) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TRevokeOptions = {
|
|
||||||
identityId: string;
|
|
||||||
organizationId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type TRevokeMethods = {
|
type TRevokeMethods = {
|
||||||
revokeMethod: (revokeOptions: TRevokeOptions) => Promise<any>;
|
|
||||||
render: () => JSX.Element;
|
render: () => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,18 +68,6 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
initialAuthMethod,
|
initialAuthMethod,
|
||||||
setSelectedAuthMethod
|
setSelectedAuthMethod
|
||||||
}: Props) => {
|
}: 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>({
|
const { control, watch } = useForm<FormData>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: async () => {
|
defaultValues: async () => {
|
||||||
@ -149,10 +109,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
|
|
||||||
const methodMap: Record<IdentityAuthMethod, TRevokeMethods | undefined> = {
|
const methodMap: Record<IdentityAuthMethod, TRevokeMethods | undefined> = {
|
||||||
[IdentityAuthMethod.UNIVERSAL_AUTH]: {
|
[IdentityAuthMethod.UNIVERSAL_AUTH]: {
|
||||||
revokeMethod: revokeUniversalAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityUniversalAuthForm
|
<IdentityUniversalAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -160,10 +119,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.OIDC_AUTH]: {
|
[IdentityAuthMethod.OIDC_AUTH]: {
|
||||||
revokeMethod: revokeOidcAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityOidcAuthForm
|
<IdentityOidcAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -171,10 +129,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.TOKEN_AUTH]: {
|
[IdentityAuthMethod.TOKEN_AUTH]: {
|
||||||
revokeMethod: revokeTokenAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityTokenAuthForm
|
<IdentityTokenAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -182,10 +139,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.AZURE_AUTH]: {
|
[IdentityAuthMethod.AZURE_AUTH]: {
|
||||||
revokeMethod: revokeAzureAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityAzureAuthForm
|
<IdentityAzureAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -193,10 +149,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.GCP_AUTH]: {
|
[IdentityAuthMethod.GCP_AUTH]: {
|
||||||
revokeMethod: revokeGcpAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityGcpAuthForm
|
<IdentityGcpAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -204,10 +159,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.KUBERNETES_AUTH]: {
|
[IdentityAuthMethod.KUBERNETES_AUTH]: {
|
||||||
revokeMethod: revokeKubernetesAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityKubernetesAuthForm
|
<IdentityKubernetesAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -215,10 +169,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.AWS_AUTH]: {
|
[IdentityAuthMethod.AWS_AUTH]: {
|
||||||
revokeMethod: revokeAwsAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityAwsAuthForm
|
<IdentityAwsAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -226,10 +179,9 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
[IdentityAuthMethod.JWT_AUTH]: {
|
[IdentityAuthMethod.JWT_AUTH]: {
|
||||||
revokeMethod: revokeJwtAuth,
|
|
||||||
render: () => (
|
render: () => (
|
||||||
<IdentityJwtAuthForm
|
<IdentityJwtAuthForm
|
||||||
identityAuthMethodData={identityAuthMethodData}
|
identityId={identityAuthMethodData.identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
/>
|
/>
|
||||||
@ -294,42 +246,6 @@ export const IdentityAuthMethodModalContent = ({
|
|||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
text="You can use IP allowlisting if you switch to Infisical's Pro plan."
|
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"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -6,17 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
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 { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityAwsAuth,
|
useAddIdentityAwsAuth,
|
||||||
useGetIdentityAwsAuth,
|
useGetIdentityAwsAuth,
|
||||||
useUpdateIdentityAwsAuth
|
useUpdateIdentityAwsAuth
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { IdentityFormTab } from "./types";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
stsEndpoint: z.string(),
|
stsEndpoint: z.string(),
|
||||||
@ -49,21 +59,18 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityAwsAuthForm = ({
|
export const IdentityAwsAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -71,11 +78,9 @@ export const IdentityAwsAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityAwsAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAwsAuth();
|
||||||
|
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||||
|
|
||||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityAwsAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
const { data } = useGetIdentityAwsAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -143,7 +148,7 @@ export const IdentityAwsAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
@ -151,7 +156,7 @@ export const IdentityAwsAuthForm = ({
|
|||||||
stsEndpoint,
|
stsEndpoint,
|
||||||
allowedPrincipalArns,
|
allowedPrincipalArns,
|
||||||
allowedAccountIds,
|
allowedAccountIds,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -160,7 +165,7 @@ export const IdentityAwsAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
stsEndpoint: stsEndpoint || "",
|
stsEndpoint: stsEndpoint || "",
|
||||||
allowedPrincipalArns: allowedPrincipalArns || "",
|
allowedPrincipalArns: allowedPrincipalArns || "",
|
||||||
allowedAccountIds: allowedAccountIds || "",
|
allowedAccountIds: allowedAccountIds || "",
|
||||||
@ -188,7 +193,21 @@ export const IdentityAwsAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue="2592000"
|
defaultValue="2592000"
|
||||||
@ -272,6 +291,8 @@ export const IdentityAwsAuthForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={IdentityFormTab.Advanced}>
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
@ -340,7 +361,8 @@ export const IdentityAwsAuthForm = ({
|
|||||||
Add IP Address
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
@ -349,7 +371,7 @@ export const IdentityAwsAuthForm = ({
|
|||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{!isUpdate ? "Create" : "Edit"}
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -360,18 +382,6 @@ export const IdentityAwsAuthForm = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -6,17 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
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 { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityAzureAuth,
|
useAddIdentityAzureAuth,
|
||||||
useGetIdentityAzureAuth,
|
useGetIdentityAzureAuth,
|
||||||
useUpdateIdentityAzureAuth
|
useUpdateIdentityAzureAuth
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { IdentityFormTab } from "./types";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
tenantId: z.string().min(1),
|
tenantId: z.string().min(1),
|
||||||
@ -44,21 +54,18 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityAzureAuthForm = ({
|
export const IdentityAzureAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -66,11 +73,9 @@ export const IdentityAzureAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityAzureAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityAzureAuth();
|
||||||
|
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||||
|
|
||||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityAzureAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
const { data } = useGetIdentityAzureAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -139,12 +144,12 @@ export const IdentityAzureAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
tenantId,
|
tenantId,
|
||||||
resource,
|
resource,
|
||||||
allowedServicePrincipalIds,
|
allowedServicePrincipalIds,
|
||||||
@ -156,7 +161,7 @@ export const IdentityAzureAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
tenantId: tenantId || "",
|
tenantId: tenantId || "",
|
||||||
resource: resource || "",
|
resource: resource || "",
|
||||||
allowedServicePrincipalIds: allowedServicePrincipalIds || "",
|
allowedServicePrincipalIds: allowedServicePrincipalIds || "",
|
||||||
@ -184,7 +189,21 @@ export const IdentityAzureAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue="2592000"
|
defaultValue="2592000"
|
||||||
@ -268,6 +287,8 @@ export const IdentityAzureAuthForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={IdentityFormTab.Advanced}>
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
@ -336,7 +357,8 @@ export const IdentityAzureAuthForm = ({
|
|||||||
Add IP Address
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
@ -345,7 +367,7 @@ export const IdentityAzureAuthForm = ({
|
|||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{!isUpdate ? "Create" : "Edit"}
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -356,18 +378,6 @@ export const IdentityAzureAuthForm = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -6,17 +6,29 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
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 { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityGcpAuth,
|
useAddIdentityGcpAuth,
|
||||||
useGetIdentityGcpAuth,
|
useGetIdentityGcpAuth,
|
||||||
useUpdateIdentityGcpAuth
|
useUpdateIdentityGcpAuth
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { IdentityFormTab } from "./types";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
type: z.enum(["iam", "gce"]),
|
type: z.enum(["iam", "gce"]),
|
||||||
@ -45,21 +57,18 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityGcpAuthForm = ({
|
export const IdentityGcpAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -67,11 +76,9 @@ export const IdentityGcpAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityGcpAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityGcpAuth();
|
||||||
|
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||||
|
|
||||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityGcpAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
const { data } = useGetIdentityGcpAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -146,11 +153,11 @@ export const IdentityGcpAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
type,
|
type,
|
||||||
allowedServiceAccounts,
|
allowedServiceAccounts,
|
||||||
@ -163,7 +170,7 @@ export const IdentityGcpAuthForm = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
type,
|
type,
|
||||||
allowedServiceAccounts: allowedServiceAccounts || "",
|
allowedServiceAccounts: allowedServiceAccounts || "",
|
||||||
@ -193,7 +200,21 @@ export const IdentityGcpAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="type"
|
name="type"
|
||||||
@ -253,7 +274,11 @@ export const IdentityGcpAuthForm = ({
|
|||||||
control={control}
|
control={control}
|
||||||
name="allowedZones"
|
name="allowedZones"
|
||||||
render={({ field, fieldState: { error } }) => (
|
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, ..." />
|
<Input {...field} placeholder="us-west2-a, us-central1-a, ..." />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@ -301,6 +326,8 @@ export const IdentityGcpAuthForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={IdentityFormTab.Advanced}>
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
@ -369,7 +396,8 @@ export const IdentityGcpAuthForm = ({
|
|||||||
Add IP Address
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
@ -378,7 +406,7 @@ export const IdentityGcpAuthForm = ({
|
|||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{!isUpdate ? "Create" : "Edit"}
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -389,18 +417,6 @@ export const IdentityGcpAuthForm = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
@ -14,17 +14,22 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs,
|
||||||
TextArea,
|
TextArea,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useSubscription } from "@app/context";
|
||||||
import { useAddIdentityJwtAuth, useUpdateIdentityJwtAuth } from "@app/hooks/api";
|
import { useAddIdentityJwtAuth, useUpdateIdentityJwtAuth } from "@app/hooks/api";
|
||||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
|
||||||
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
|
import { IdentityJwtConfigurationType } from "@app/hooks/api/identities/enums";
|
||||||
import { useGetIdentityJwtAuth } from "@app/hooks/api/identities/queries";
|
import { useGetIdentityJwtAuth } from "@app/hooks/api/identities/queries";
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { IdentityFormTab } from "./types";
|
||||||
|
|
||||||
const commonSchema = z.object({
|
const commonSchema = z.object({
|
||||||
accessTokenTrustedIps: z
|
accessTokenTrustedIps: z
|
||||||
.array(
|
.array(
|
||||||
@ -85,21 +90,18 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityJwtAuthForm = ({
|
export const IdentityJwtAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -107,11 +109,9 @@ export const IdentityJwtAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityJwtAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityJwtAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityJwtAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityJwtAuth();
|
||||||
|
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||||
|
|
||||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityJwtAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
const { data } = useGetIdentityJwtAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -218,13 +218,13 @@ export const IdentityJwtAuthForm = ({
|
|||||||
boundSubject
|
boundSubject
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) {
|
if (!identityId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
configurationType,
|
configurationType,
|
||||||
jwksUrl,
|
jwksUrl,
|
||||||
@ -241,7 +241,7 @@ export const IdentityJwtAuthForm = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
configurationType,
|
configurationType,
|
||||||
jwksUrl,
|
jwksUrl,
|
||||||
jwksCaCert,
|
jwksCaCert,
|
||||||
@ -275,7 +275,21 @@ export const IdentityJwtAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="configurationType"
|
name="configurationType"
|
||||||
@ -345,7 +359,6 @@ export const IdentityJwtAuthForm = ({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedConfigurationType === IdentityJwtConfigurationType.STATIC && (
|
{selectedConfigurationType === IdentityJwtConfigurationType.STATIC && (
|
||||||
<>
|
<>
|
||||||
{publicKeyFields.map(({ id }, index) => (
|
{publicKeyFields.map(({ id }, index) => (
|
||||||
@ -410,7 +423,6 @@ export const IdentityJwtAuthForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="boundIssuer"
|
name="boundIssuer"
|
||||||
@ -583,6 +595,8 @@ export const IdentityJwtAuthForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={IdentityFormTab.Advanced}>
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
@ -651,7 +665,8 @@ export const IdentityJwtAuthForm = ({
|
|||||||
Add IP Address
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
@ -671,18 +686,6 @@ export const IdentityJwtAuthForm = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -6,17 +6,28 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
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 { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityKubernetesAuth,
|
useAddIdentityKubernetesAuth,
|
||||||
useGetIdentityKubernetesAuth,
|
useGetIdentityKubernetesAuth,
|
||||||
useUpdateIdentityKubernetesAuth
|
useUpdateIdentityKubernetesAuth
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { IdentityFormTab } from "./types";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
kubernetesHost: z.string().min(1),
|
kubernetesHost: z.string().min(1),
|
||||||
@ -47,21 +58,18 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityKubernetesAuthForm = ({
|
export const IdentityKubernetesAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -69,11 +77,9 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityKubernetesAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityKubernetesAuth();
|
||||||
|
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||||
|
|
||||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityKubernetesAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
const { data } = useGetIdentityKubernetesAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -154,7 +160,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
@ -165,7 +171,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
allowedNamespaces,
|
allowedNamespaces,
|
||||||
allowedAudience,
|
allowedAudience,
|
||||||
caCert,
|
caCert,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -174,7 +180,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
kubernetesHost: kubernetesHost || "",
|
kubernetesHost: kubernetesHost || "",
|
||||||
tokenReviewerJwt,
|
tokenReviewerJwt,
|
||||||
allowedNames: allowedNames || "",
|
allowedNames: allowedNames || "",
|
||||||
@ -205,7 +211,29 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue="2592000"
|
defaultValue="2592000"
|
||||||
@ -237,20 +265,6 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
</FormControl>
|
</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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
@ -268,30 +282,15 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue=""
|
name="allowedNames"
|
||||||
name="allowedAudience"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field, fieldState: { error } }) => (
|
||||||
<FormControl
|
<FormControl
|
||||||
label="Allowed Audience"
|
label="Allowed Service Account Names"
|
||||||
isError={Boolean(error)}
|
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}
|
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" />
|
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
|
||||||
</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>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -340,6 +339,37 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
</FormControl>
|
</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) => (
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
@ -409,7 +439,8 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
Add IP Address
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
@ -418,7 +449,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{isUpdate ? "Update" : "Create"}
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -429,18 +460,6 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-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 { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
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 { useOrganization, useSubscription } from "@app/context";
|
||||||
import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api";
|
import { useAddIdentityOidcAuth, useUpdateIdentityOidcAuth } from "@app/hooks/api";
|
||||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
|
||||||
import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries";
|
import { useGetIdentityOidcAuth } from "@app/hooks/api/identities/queries";
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { IdentityFormTab } from "./types";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
accessTokenTrustedIps: z
|
accessTokenTrustedIps: z
|
||||||
.array(
|
.array(
|
||||||
@ -48,21 +60,18 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityOidcAuthForm = ({
|
export const IdentityOidcAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -70,11 +79,9 @@ export const IdentityOidcAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityOidcAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityOidcAuth();
|
||||||
|
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||||
|
|
||||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityOidcAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
const { data } = useGetIdentityOidcAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -160,13 +167,13 @@ export const IdentityOidcAuthForm = ({
|
|||||||
boundSubject
|
boundSubject
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) {
|
if (!identityId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
oidcDiscoveryUrl,
|
oidcDiscoveryUrl,
|
||||||
caCert,
|
caCert,
|
||||||
@ -181,7 +188,7 @@ export const IdentityOidcAuthForm = ({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
oidcDiscoveryUrl,
|
oidcDiscoveryUrl,
|
||||||
caCert,
|
caCert,
|
||||||
boundIssuer,
|
boundIssuer,
|
||||||
@ -213,7 +220,21 @@ export const IdentityOidcAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="oidcDiscoveryUrl"
|
name="oidcDiscoveryUrl"
|
||||||
@ -250,15 +271,6 @@ export const IdentityOidcAuthForm = ({
|
|||||||
</FormControl>
|
</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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="boundSubject"
|
name="boundSubject"
|
||||||
@ -301,6 +313,63 @@ export const IdentityOidcAuthForm = ({
|
|||||||
</FormControl>
|
</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) => (
|
{boundClaimsFields.map(({ id }, index) => (
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
@ -380,48 +449,6 @@ export const IdentityOidcAuthForm = ({
|
|||||||
Add Claims
|
Add Claims
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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) => (
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
@ -490,7 +517,8 @@ export const IdentityOidcAuthForm = ({
|
|||||||
Add IP Address
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
@ -499,7 +527,7 @@ export const IdentityOidcAuthForm = ({
|
|||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{isUpdate ? "Update" : "Create"}
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -510,18 +538,6 @@ export const IdentityOidcAuthForm = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -5,16 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
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 { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityTokenAuth,
|
useAddIdentityTokenAuth,
|
||||||
useGetIdentityTokenAuth,
|
useGetIdentityTokenAuth,
|
||||||
useUpdateIdentityTokenAuth
|
useUpdateIdentityTokenAuth
|
||||||
} from "@app/hooks/api";
|
} 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 { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { IdentityFormTab } from "./types";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
accessTokenTTL: z.string().refine((val) => Number(val) <= 315360000, {
|
||||||
@ -39,21 +51,18 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData?: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityTokenAuthForm = ({
|
export const IdentityTokenAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
@ -61,12 +70,9 @@ export const IdentityTokenAuthForm = ({
|
|||||||
|
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityTokenAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityTokenAuth();
|
||||||
|
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||||
|
|
||||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityTokenAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityTokenAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -91,6 +97,30 @@ export const IdentityTokenAuthForm = ({
|
|||||||
remove: removeAccessTokenTrustedIp
|
remove: removeAccessTokenTrustedIp
|
||||||
} = useFieldArray({ control, name: "accessTokenTrustedIps" });
|
} = 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 ({
|
const onFormSubmit = async ({
|
||||||
accessTokenTTL,
|
accessTokenTTL,
|
||||||
accessTokenMaxTTL,
|
accessTokenMaxTTL,
|
||||||
@ -98,12 +128,12 @@ export const IdentityTokenAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -112,7 +142,7 @@ export const IdentityTokenAuthForm = ({
|
|||||||
} else {
|
} else {
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
|
||||||
@ -137,7 +167,21 @@ export const IdentityTokenAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue="2592000"
|
defaultValue="2592000"
|
||||||
@ -180,6 +224,8 @@ export const IdentityTokenAuthForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={IdentityFormTab.Advanced}>
|
||||||
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
{accessTokenTrustedIpsFields.map(({ id }, index) => (
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
@ -248,7 +294,8 @@ export const IdentityTokenAuthForm = ({
|
|||||||
Add IP Address
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
@ -257,7 +304,7 @@ export const IdentityTokenAuthForm = ({
|
|||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{isUpdate ? "Update" : "Create"}
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@ -268,18 +315,6 @@ export const IdentityTokenAuthForm = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Remove Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
@ -6,17 +6,27 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
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 { useOrganization, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useAddIdentityUniversalAuth,
|
useAddIdentityUniversalAuth,
|
||||||
useGetIdentityUniversalAuth,
|
useGetIdentityUniversalAuth,
|
||||||
useUpdateIdentityUniversalAuth
|
useUpdateIdentityUniversalAuth
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { IdentityAuthMethod } from "@app/hooks/api/identities";
|
|
||||||
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
import { IdentityFormTab } from "./types";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
accessTokenTTL: z
|
accessTokenTTL: z
|
||||||
@ -52,33 +62,27 @@ export type FormData = z.infer<typeof schema>;
|
|||||||
type Props = {
|
type Props = {
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<["upgradePlan"]>) => void;
|
||||||
handlePopUpToggle: (
|
handlePopUpToggle: (
|
||||||
popUpName: keyof UsePopUpState<["identityAuthMethod", "revokeAuthMethod"]>,
|
popUpName: keyof UsePopUpState<["identityAuthMethod"]>,
|
||||||
state?: boolean
|
state?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
identityAuthMethodData?: {
|
identityId?: string;
|
||||||
identityId: string;
|
isUpdate?: boolean;
|
||||||
name: string;
|
|
||||||
configuredAuthMethods?: IdentityAuthMethod[];
|
|
||||||
authMethod?: IdentityAuthMethod;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityUniversalAuthForm = ({
|
export const IdentityUniversalAuthForm = ({
|
||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle,
|
handlePopUpToggle,
|
||||||
identityAuthMethodData
|
identityId,
|
||||||
|
isUpdate
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
const { mutateAsync: addMutateAsync } = useAddIdentityUniversalAuth();
|
||||||
const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth();
|
const { mutateAsync: updateMutateAsync } = useUpdateIdentityUniversalAuth();
|
||||||
|
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
|
||||||
|
|
||||||
const isUpdate = identityAuthMethodData?.configuredAuthMethods?.includes(
|
const { data } = useGetIdentityUniversalAuth(identityId ?? "", {
|
||||||
identityAuthMethodData.authMethod! || ""
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = useGetIdentityUniversalAuth(identityAuthMethodData?.identityId ?? "", {
|
|
||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -149,13 +153,13 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
accessTokenTrustedIps
|
accessTokenTrustedIps
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!identityAuthMethodData) return;
|
if (!identityId) return;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
// update universal auth configuration
|
// update universal auth configuration
|
||||||
await updateMutateAsync({
|
await updateMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
clientSecretTrustedIps,
|
clientSecretTrustedIps,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
@ -167,7 +171,7 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
|
|
||||||
await addMutateAsync({
|
await addMutateAsync({
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
identityId: identityAuthMethodData.identityId,
|
identityId,
|
||||||
clientSecretTrustedIps,
|
clientSecretTrustedIps,
|
||||||
accessTokenTTL: Number(accessTokenTTL),
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
accessTokenMaxTTL: Number(accessTokenMaxTTL),
|
||||||
@ -195,7 +199,21 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue="2592000"
|
defaultValue="2592000"
|
||||||
@ -238,6 +256,8 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={IdentityFormTab.Advanced}>
|
||||||
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
{clientSecretTrustedIpsFields.map(({ id }, index) => (
|
||||||
<div className="mb-3 flex items-end space-x-2" key={id}>
|
<div className="mb-3 flex items-end space-x-2" key={id}>
|
||||||
<Controller
|
<Controller
|
||||||
@ -374,7 +394,8 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
Add IP Address
|
Add IP Address
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
className="mr-4"
|
className="mr-4"
|
||||||
@ -383,7 +404,7 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
isDisabled={isSubmitting}
|
isDisabled={isSubmitting}
|
||||||
>
|
>
|
||||||
{isUpdate ? "Edit" : "Create"}
|
{isUpdate ? "Update" : "Add"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
colorSchema="secondary"
|
colorSchema="secondary"
|
||||||
@ -393,18 +414,6 @@ export const IdentityUniversalAuthForm = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{isUpdate && (
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
colorSchema="danger"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
onClick={() => handlePopUpToggle("revokeAuthMethod", true)}
|
|
||||||
>
|
|
||||||
Delete Auth Method
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
export enum IdentityFormTab {
|
||||||
|
Advanced = "advanced",
|
||||||
|
Configuration = "configuration"
|
||||||
|
}
|
@ -1,45 +1,24 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate, useParams } from "@tanstack/react-router";
|
import { useNavigate, useParams } from "@tanstack/react-router";
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import {
|
import { DeleteActionModal, PageHeader } from "@app/components/v2";
|
||||||
Button,
|
|
||||||
DeleteActionModal,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
PageHeader,
|
|
||||||
Tooltip
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import { ROUTE_PATHS } from "@app/const/routes";
|
import { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
|
||||||
import {
|
import { useDeleteIdentity, useGetIdentityById } from "@app/hooks/api";
|
||||||
IdentityAuthMethod,
|
|
||||||
useDeleteIdentity,
|
|
||||||
useGetIdentityById,
|
|
||||||
useRevokeIdentityTokenAuthToken,
|
|
||||||
useRevokeIdentityUniversalAuthClientSecret
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import { Identity } from "@app/hooks/api/identities/types";
|
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
import { ViewIdentityAuthModal } from "@app/pages/organization/IdentityDetailsByIDPage/components/ViewIdentityAuthModal/ViewIdentityAuthModal";
|
||||||
import { OrgAccessControlTabSections } from "@app/types/org";
|
import { OrgAccessControlTabSections } from "@app/types/org";
|
||||||
|
|
||||||
import { IdentityAuthMethodModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
|
import { IdentityAuthMethodModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityAuthMethodModal";
|
||||||
import { IdentityModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal";
|
import { IdentityModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityModal";
|
||||||
import { IdentityUniversalAuthClientSecretModal } from "../AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal";
|
|
||||||
import {
|
import {
|
||||||
IdentityAuthenticationSection,
|
IdentityAuthenticationSection,
|
||||||
IdentityClientSecretModal,
|
|
||||||
IdentityDetailsSection,
|
IdentityDetailsSection,
|
||||||
IdentityProjectsSection,
|
IdentityProjectsSection
|
||||||
IdentityTokenListModal,
|
|
||||||
IdentityTokenModal
|
|
||||||
} from "./components";
|
} from "./components";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
@ -52,25 +31,13 @@ const Page = () => {
|
|||||||
const orgId = currentOrg?.id || "";
|
const orgId = currentOrg?.id || "";
|
||||||
const { data } = useGetIdentityById(identityId);
|
const { data } = useGetIdentityById(identityId);
|
||||||
const { mutateAsync: deleteIdentity } = useDeleteIdentity();
|
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([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"identity",
|
"identity",
|
||||||
"deleteIdentity",
|
"deleteIdentity",
|
||||||
"identityAuthMethod",
|
"identityAuthMethod",
|
||||||
"revokeAuthMethod",
|
"upgradePlan",
|
||||||
"token",
|
"viewAuthMethod"
|
||||||
"tokenList",
|
|
||||||
"revokeToken",
|
|
||||||
"clientSecret",
|
|
||||||
"revokeClientSecret",
|
|
||||||
"universalAuthClientSecret", // list of client secrets
|
|
||||||
"upgradePlan"
|
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const onDeleteIdentitySubmit = async (id: string) => {
|
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 (
|
return (
|
||||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
{data && (
|
{data && (
|
||||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||||
<PageHeader title={data.identity.name}>
|
<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>
|
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="mr-4 w-96">
|
<div className="mr-4 w-96">
|
||||||
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
<IdentityDetailsSection identityId={identityId} handlePopUpOpen={handlePopUpOpen} />
|
||||||
<IdentityAuthenticationSection
|
<IdentityAuthenticationSection
|
||||||
selectedAuthMethod={selectedAuthMethod}
|
|
||||||
setSelectedAuthMethod={setSelectedAuthMethod}
|
|
||||||
identityId={identityId}
|
identityId={identityId}
|
||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
/>
|
/>
|
||||||
@ -260,18 +94,6 @@ const Page = () => {
|
|||||||
handlePopUpOpen={handlePopUpOpen}
|
handlePopUpOpen={handlePopUpOpen}
|
||||||
handlePopUpToggle={handlePopUpToggle}
|
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
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
@ -290,41 +112,11 @@ const Page = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<DeleteActionModal
|
<ViewIdentityAuthModal
|
||||||
isOpen={popUp.revokeToken.isOpen}
|
isOpen={popUp.viewAuthMethod.isOpen}
|
||||||
title={`Are you sure want to revoke ${
|
onOpenChange={(isOpen) => handlePopUpToggle("viewAuthMethod", isOpen)}
|
||||||
(popUp?.revokeToken?.data as { name: string })?.name || ""
|
authMethod={popUp.viewAuthMethod.data}
|
||||||
}?`}
|
identityId={identityId}
|
||||||
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
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,140 +1,56 @@
|
|||||||
import { useEffect } from "react";
|
import { faCog, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { faPencil, faPlus } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { Button, IconButton, Select, SelectItem, Tooltip } from "@app/components/v2";
|
import { Button } from "@app/components/v2";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { useGetIdentityById } from "@app/hooks/api";
|
import { IdentityAuthMethod, identityAuthToNameMap, useGetIdentityById } from "@app/hooks/api";
|
||||||
import { IdentityAuthMethod, identityAuthToNameMap } from "@app/hooks/api/identities";
|
|
||||||
import { Identity } from "@app/hooks/api/identities/types";
|
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityClientSecrets } from "./IdentityClientSecrets";
|
|
||||||
import { IdentityTokens } from "./IdentityTokens";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
setSelectedAuthMethod: (authMethod: Identity["authMethods"][number] | null) => void;
|
|
||||||
selectedAuthMethod: Identity["authMethods"][number] | null;
|
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<
|
popUpName: keyof UsePopUpState<["identityAuthMethod", "viewAuthMethod"]>,
|
||||||
[
|
data?: object | IdentityAuthMethod
|
||||||
"clientSecret",
|
|
||||||
"identityAuthMethod",
|
|
||||||
"revokeClientSecret",
|
|
||||||
"token",
|
|
||||||
"revokeToken",
|
|
||||||
"universalAuthClientSecret",
|
|
||||||
"tokenList"
|
|
||||||
]
|
|
||||||
>,
|
|
||||||
data?: object
|
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IdentityAuthenticationSection = ({
|
export const IdentityAuthenticationSection = ({ identityId, handlePopUpOpen }: Props) => {
|
||||||
identityId,
|
|
||||||
setSelectedAuthMethod,
|
|
||||||
selectedAuthMethod,
|
|
||||||
handlePopUpOpen
|
|
||||||
}: Props) => {
|
|
||||||
const { data } = useGetIdentityById(identityId);
|
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 ? (
|
return data ? (
|
||||||
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
<div className="flex items-center justify-between border-b border-mineshaft-400 pb-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>
|
<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>
|
</div>
|
||||||
{data.identity.authMethods.length > 0 ? (
|
{data.identity.authMethods.length > 0 ? (
|
||||||
<>
|
<div className="flex flex-col divide-y divide-mineshaft-400/50">
|
||||||
<div className="py-4">
|
{data.identity.authMethods.map((authMethod) => (
|
||||||
<div className="flex justify-between">
|
<button
|
||||||
<p className="mb-0.5 ml-px text-sm font-semibold text-mineshaft-300">Auth Method</p>
|
key={authMethod}
|
||||||
</div>
|
onClick={() => handlePopUpOpen("viewAuthMethod", authMethod)}
|
||||||
<div className="flex items-center gap-2">
|
type="button"
|
||||||
<div className="w-full">
|
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"
|
||||||
<Select
|
|
||||||
className="w-full"
|
|
||||||
value={selectedAuthMethod as string}
|
|
||||||
onValueChange={(value) => setSelectedAuthMethod(value as IdentityAuthMethod)}
|
|
||||||
>
|
>
|
||||||
{(data.identity?.authMethods || []).map((authMethod) => (
|
<span>{identityAuthToNameMap[authMethod]}</span>
|
||||||
<SelectItem key={authMethod || authMethod} value={authMethod}>
|
<FontAwesomeIcon icon={faCog} size="xs" className="text-mineshaft-400" />
|
||||||
{identityAuthToNameMap[authMethod]}
|
</button>
|
||||||
</SelectItem>
|
|
||||||
))}
|
))}
|
||||||
</Select>
|
|
||||||
</div>
|
</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">
|
<div className="w-full space-y-2 pt-2">
|
||||||
<p className="text-sm text-mineshaft-300">
|
<p className="text-sm text-mineshaft-300">
|
||||||
No authentication methods configured. Get started by creating a new auth method.
|
No authentication methods configured. Get started by creating a new auth method.
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!Object.values(IdentityAuthMethod).every((method) =>
|
||||||
|
data.identity.authMethods.includes(method)
|
||||||
|
) && (
|
||||||
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||||
|
{(isAllowed) => (
|
||||||
<Button
|
<Button
|
||||||
|
isDisabled={!isAllowed}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("identityAuthMethod", {
|
handlePopUpOpen("identityAuthMethod", {
|
||||||
identityId,
|
identityId,
|
||||||
@ -143,12 +59,14 @@ export const IdentityAuthenticationSection = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
className="w-full"
|
className="mt-3 w-full"
|
||||||
size="xs"
|
size="xs"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
>
|
>
|
||||||
Create Auth Method
|
{data.identity.authMethods.length ? "Add" : "Create"} Auth Method
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
|
IconButton,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useGetIdentityById, useGetIdentityTokensTokenAuth } from "@app/hooks/api";
|
import { useGetIdentityById, useGetIdentityTokensTokenAuth } from "@app/hooks/api";
|
||||||
@ -27,10 +28,13 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{tokens?.length ? (
|
{tokens?.length ? (
|
||||||
<div className="flex justify-between">
|
<div className="flex items-center justify-between border-b border-bunker-400 pb-1">
|
||||||
<p className="text-sm font-semibold text-mineshaft-300">{`Access Tokens (${tokens.length})`}</p>
|
<p className="text-sm font-medium text-bunker-300">{`Access Tokens (${tokens.length})`}</p>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
size="xs"
|
||||||
|
className="underline"
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("tokenList", {
|
handlePopUpOpen("tokenList", {
|
||||||
identityId,
|
identityId,
|
||||||
@ -50,16 +54,16 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<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}`}
|
key={`identity-token-${token.id}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<FontAwesomeIcon size="1x" icon={faKey} />
|
<FontAwesomeIcon size="xs" className="text-mineshaft-400" icon={faKey} />
|
||||||
<div className="ml-4">
|
<div className="ml-3">
|
||||||
<p className="text-sm font-semibold text-mineshaft-300">
|
<p className="text-sm font-medium text-mineshaft-300">
|
||||||
{token.name ? token.name : "-"}
|
{token.name ? token.name : "-"}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-mineshaft-300">
|
<p className="text-xs text-mineshaft-400">
|
||||||
{token.isAccessTokenRevoked
|
{token.isAccessTokenRevoked
|
||||||
? "Revoked"
|
? "Revoked"
|
||||||
: `Expires on ${format(expiresAt, "yyyy-MM-dd")}`}
|
: `Expires on ${format(expiresAt, "yyyy-MM-dd")}`}
|
||||||
@ -67,14 +71,19 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild className="rounded-lg">
|
<DropdownMenuTrigger asChild>
|
||||||
<div className="opacity-0 transition-opacity duration-300 hover:text-primary-400 group-hover:opacity-100 data-[state=open]:text-primary-400">
|
<Tooltip side="right" content="More options">
|
||||||
<Tooltip content="More options">
|
<IconButton
|
||||||
<FontAwesomeIcon size="sm" icon={faEllipsis} />
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
size="xs"
|
||||||
|
ariaLabel="More options"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEllipsisVertical} />
|
||||||
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="start" className="p-1">
|
<DropdownMenuContent align="start" className="z-[101] p-1">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
handlePopUpOpen("token", {
|
handlePopUpOpen("token", {
|
||||||
@ -106,8 +115,9 @@ export const IdentityTokens = ({ identityId, handlePopUpOpen }: Props) => {
|
|||||||
})}
|
})}
|
||||||
<Button
|
<Button
|
||||||
className="mr-4 mt-4 w-full"
|
className="mr-4 mt-4 w-full"
|
||||||
colorSchema="primary"
|
colorSchema="secondary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
size="xs"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handlePopUpOpen("token", {
|
handlePopUpOpen("token", {
|
||||||
identityId
|
identityId
|
||||||
|
@ -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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
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 { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
|
||||||
import { useTimedReset } from "@app/hooks";
|
import { useTimedReset } from "@app/hooks";
|
||||||
import { useGetIdentityById } from "@app/hooks/api";
|
import { useGetIdentityById } from "@app/hooks/api";
|
||||||
@ -11,7 +28,7 @@ import { UsePopUpState } from "@app/hooks/usePopUp";
|
|||||||
type Props = {
|
type Props = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
handlePopUpOpen: (
|
handlePopUpOpen: (
|
||||||
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "token", "clientSecret"]>,
|
popUpName: keyof UsePopUpState<["identity", "identityAuthMethod", "deleteIdentity"]>,
|
||||||
data?: object
|
data?: object
|
||||||
) => void;
|
) => 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="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">
|
<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>
|
<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}>
|
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Identity}>
|
||||||
{(isAllowed) => {
|
{(isAllowed) => (
|
||||||
return (
|
<DropdownMenuItem
|
||||||
<Tooltip content="Edit Identity">
|
className={twMerge(
|
||||||
<IconButton
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
isDisabled={!isAllowed}
|
)}
|
||||||
ariaLabel="copy icon"
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
variant="plain"
|
onClick={async () => {
|
||||||
className="group relative"
|
|
||||||
onClick={() => {
|
|
||||||
handlePopUpOpen("identity", {
|
handlePopUpOpen("identity", {
|
||||||
identityId,
|
identityId,
|
||||||
name: data.identity.name,
|
name: data.identity.name,
|
||||||
role: data.role,
|
role: data.role,
|
||||||
customRole: data.customRole,
|
customRole: data.customRole
|
||||||
metadata: data.metadata
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
disabled={!isAllowed}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPencil} />
|
Edit Identity
|
||||||
</IconButton>
|
</DropdownMenuItem>
|
||||||
</Tooltip>
|
)}
|
||||||
);
|
|
||||||
}}
|
|
||||||
</OrgPermissionCan>
|
</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>
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./ViewIdentityAuthModal";
|
@ -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"]>;
|
||||||
|
};
|
@ -2,5 +2,4 @@ export { IdentityAuthenticationSection } from "./IdentityAuthenticationSection/I
|
|||||||
export { IdentityClientSecretModal } from "./IdentityClientSecretModal";
|
export { IdentityClientSecretModal } from "./IdentityClientSecretModal";
|
||||||
export { IdentityDetailsSection } from "./IdentityDetailsSection";
|
export { IdentityDetailsSection } from "./IdentityDetailsSection";
|
||||||
export { IdentityProjectsSection } from "./IdentityProjectsSection/IdentityProjectsSection";
|
export { IdentityProjectsSection } from "./IdentityProjectsSection/IdentityProjectsSection";
|
||||||
export { IdentityTokenListModal } from "./IdentityTokenListModal";
|
|
||||||
export { IdentityTokenModal } from "./IdentityTokenModal";
|
export { IdentityTokenModal } from "./IdentityTokenModal";
|
||||||
|
@ -5,7 +5,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { useNavigate, useSearch } from "@tanstack/react-router";
|
import { useNavigate, useSearch } from "@tanstack/react-router";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
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 { ROUTE_PATHS } from "@app/const/routes";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
import { IntegrationsListPageTabs } from "@app/types/integrations";
|
||||||
@ -45,14 +45,12 @@ export const IntegrationsListPage = () => {
|
|||||||
<meta name="og:description" content={t("integrations.description") as string} />
|
<meta name="og:description" content={t("integrations.description") as string} />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="container relative mx-auto max-w-7xl pb-12 text-white">
|
<div className="container relative mx-auto max-w-7xl pb-12 text-white">
|
||||||
<div className="mx-6 mb-8">
|
<div className="mb-8">
|
||||||
<div className="mb-4 mt-6 flex flex-col items-start justify-between px-2 text-xl">
|
<PageHeader
|
||||||
<h1 className="text-3xl font-semibold">Integrations</h1>
|
title="Integrations"
|
||||||
<p className="text-base text-bunker-300">
|
description="Manage integrations with third-party services."
|
||||||
Manage integrations with third-party services.
|
/>
|
||||||
</p>
|
<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>
|
|
||||||
<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-1 flex items-center text-sm">
|
<div className="mb-1 flex items-center text-sm">
|
||||||
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1 text-primary" />
|
<FontAwesomeIcon icon={faInfoCircle} size="sm" className="mr-1 text-primary" />
|
||||||
Integrations Update
|
Integrations Update
|
||||||
|
@ -289,7 +289,8 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
<DropdownMenuContent className="thin-scrollbar max-h-[70vh] overflow-y-auto" align="end">
|
||||||
<DropdownMenuLabel>Status</DropdownMenuLabel>
|
<DropdownMenuLabel>Status</DropdownMenuLabel>
|
||||||
{Object.values(SecretSyncStatus).map((status) => (
|
{[SecretSyncStatus.Running, SecretSyncStatus.Succeeded, SecretSyncStatus.Failed].map(
|
||||||
|
(status) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -316,7 +317,8 @@ export const SecretSyncsTable = ({ secretSyncs }: Props) => {
|
|||||||
<span className="capitalize">{STATUS_ICON_MAP[status].name}</span>
|
<span className="capitalize">{STATUS_ICON_MAP[status].name}</span>
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
<DropdownMenuLabel>Service</DropdownMenuLabel>
|
<DropdownMenuLabel>Service</DropdownMenuLabel>
|
||||||
{secretSyncs.length ? (
|
{secretSyncs.length ? (
|
||||||
[...new Set(secretSyncs.map(({ destination }) => destination))].map((destination) => {
|
[...new Set(secretSyncs.map(({ destination }) => destination))].map((destination) => {
|
||||||
|
@ -180,10 +180,12 @@ export const SecretListView = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// personal secret change
|
// personal secret change
|
||||||
|
let personalAction = false;
|
||||||
if (overrideAction === "deleted") {
|
if (overrideAction === "deleted") {
|
||||||
await handleSecretOperation("delete", SecretType.Personal, oldKey, {
|
await handleSecretOperation("delete", SecretType.Personal, oldKey, {
|
||||||
secretId: orgSecret.idOverride
|
secretId: orgSecret.idOverride
|
||||||
});
|
});
|
||||||
|
personalAction = true;
|
||||||
} else if (overrideAction && idOverride) {
|
} else if (overrideAction && idOverride) {
|
||||||
await handleSecretOperation("update", SecretType.Personal, oldKey, {
|
await handleSecretOperation("update", SecretType.Personal, oldKey, {
|
||||||
value: valueOverride,
|
value: valueOverride,
|
||||||
@ -191,14 +193,16 @@ export const SecretListView = ({
|
|||||||
secretId: orgSecret.idOverride,
|
secretId: orgSecret.idOverride,
|
||||||
skipMultilineEncoding: modSecret.skipMultilineEncoding
|
skipMultilineEncoding: modSecret.skipMultilineEncoding
|
||||||
});
|
});
|
||||||
|
personalAction = true;
|
||||||
} else if (overrideAction) {
|
} else if (overrideAction) {
|
||||||
await handleSecretOperation("create", SecretType.Personal, oldKey, {
|
await handleSecretOperation("create", SecretType.Personal, oldKey, {
|
||||||
value: valueOverride
|
value: valueOverride
|
||||||
});
|
});
|
||||||
|
personalAction = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// shared secret change
|
// shared secret change
|
||||||
if (!isSharedSecUnchanged) {
|
if (!isSharedSecUnchanged && !personalAction) {
|
||||||
await handleSecretOperation("update", SecretType.Shared, oldKey, {
|
await handleSecretOperation("update", SecretType.Shared, oldKey, {
|
||||||
value,
|
value,
|
||||||
tags: tagIds,
|
tags: tagIds,
|
||||||
@ -232,8 +236,9 @@ export const SecretListView = ({
|
|||||||
});
|
});
|
||||||
handlePopUpClose("secretDetail");
|
handlePopUpClose("secretDetail");
|
||||||
createNotification({
|
createNotification({
|
||||||
type: isProtectedBranch ? "info" : "success",
|
type: isProtectedBranch && !personalAction ? "info" : "success",
|
||||||
text: isProtectedBranch
|
text:
|
||||||
|
isProtectedBranch && !personalAction
|
||||||
? "Requested changes have been sent for review"
|
? "Requested changes have been sent for review"
|
||||||
: "Successfully saved secrets"
|
: "Successfully saved secrets"
|
||||||
});
|
});
|
||||||
@ -283,7 +288,12 @@ export const SecretListView = ({
|
|||||||
text: "Failed to delete secret"
|
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
|
// for optimization on minimise re-rendering of secret items
|
||||||
const onCreateTag = useCallback(() => handlePopUpOpen("createTag"), []);
|
const onCreateTag = useCallback(() => handlePopUpOpen("createTag"), []);
|
||||||
|
@ -73,7 +73,7 @@ const PageContent = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 font-inter text-white">
|
<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
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
type="submit"
|
type="submit"
|
||||||
@ -89,7 +89,6 @@ const PageContent = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
className="mb-4"
|
|
||||||
>
|
>
|
||||||
Secret Syncs
|
Secret Syncs
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -4,7 +4,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
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 { IconButton } from "@app/components/v2";
|
||||||
import { ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionSub } from "@app/context";
|
||||||
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
|
import { ProjectPermissionSecretSyncActions } from "@app/context/ProjectPermissionContext/types";
|
||||||
@ -57,6 +57,11 @@ export const SecretSyncDetailsSection = ({ secretSync, onEditDetails }: Props) =
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<SecretSyncLabel label="Name">{name}</SecretSyncLabel>
|
<SecretSyncLabel label="Name">{name}</SecretSyncLabel>
|
||||||
<SecretSyncLabel label="Description">{description}</SecretSyncLabel>
|
<SecretSyncLabel label="Description">{description}</SecretSyncLabel>
|
||||||
|
{syncStatus && (
|
||||||
|
<SecretSyncLabel label="Status">
|
||||||
|
<SecretSyncStatusBadge status={syncStatus} />
|
||||||
|
</SecretSyncLabel>
|
||||||
|
)}
|
||||||
{lastSyncedAt && (
|
{lastSyncedAt && (
|
||||||
<SecretSyncLabel label="Last Synced">
|
<SecretSyncLabel label="Last Synced">
|
||||||
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}
|
{format(new Date(lastSyncedAt), "yyyy-MM-dd, hh:mm aaa")}
|
||||||
|
@ -19,7 +19,7 @@ const schema = z.object({
|
|||||||
environmentName: z
|
environmentName: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: "Environment Name field must be at least 1 character" }),
|
.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>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
@ -17,7 +17,7 @@ type Props = {
|
|||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
slug: slugSchema({ min: 1 })
|
slug: slugSchema({ min: 1, max: 64 })
|
||||||
});
|
});
|
||||||
|
|
||||||
export type FormData = z.infer<typeof schema>;
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
@ -18,7 +18,7 @@ export const Route = createFileRoute(
|
|||||||
beforeLoad: ({ context, params }) => {
|
beforeLoad: ({ context, params }) => {
|
||||||
return {
|
return {
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
...context.breadcrumbs,
|
...(context?.breadcrumbs || []),
|
||||||
{
|
{
|
||||||
label: "Integrations",
|
label: "Integrations",
|
||||||
link: linkOptions({
|
link: linkOptions({
|
||||||
|
@ -99,15 +99,10 @@ export const TeamcityConfigurePage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredBuildConfigs = targetBuildConfigs?.concat({
|
|
||||||
name: "",
|
|
||||||
buildConfigId: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
return integrationAuth &&
|
return integrationAuth &&
|
||||||
selectedSourceEnvironment &&
|
selectedSourceEnvironment &&
|
||||||
integrationAuthApps &&
|
integrationAuthApps &&
|
||||||
filteredBuildConfigs &&
|
targetBuildConfigs &&
|
||||||
targetAppId ? (
|
targetAppId ? (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -185,7 +180,7 @@ export const TeamcityConfigurePage = () => {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<SelectItem value="none" key="target-app-none">
|
<SelectItem value="none" key="target-app-none">
|
||||||
No project found
|
No projects found
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
@ -195,15 +190,22 @@ export const TeamcityConfigurePage = () => {
|
|||||||
value={targetBuildConfigId}
|
value={targetBuildConfigId}
|
||||||
onValueChange={(val) => setTargetBuildConfigId(val)}
|
onValueChange={(val) => setTargetBuildConfigId(val)}
|
||||||
className="w-full border border-mineshaft-500"
|
className="w-full border border-mineshaft-500"
|
||||||
|
isDisabled={targetBuildConfigs.length === 0}
|
||||||
>
|
>
|
||||||
{filteredBuildConfigs.map((buildConfig: any) => (
|
{targetBuildConfigs.length ? (
|
||||||
|
targetBuildConfigs.map((buildConfig: any) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
value={buildConfig.buildConfigId}
|
value={buildConfig.buildConfigId}
|
||||||
key={`target-build-config-${buildConfig.buildConfigId}`}
|
key={`target-build-config-${buildConfig.buildConfigId}`}
|
||||||
>
|
>
|
||||||
{buildConfig.name}
|
{buildConfig.name}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem value="none" key="target-app-none">
|
||||||
|
No build configs found
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Button
|
<Button
|
||||||
|
@ -11,6 +11,7 @@ import attemptChangePassword from "@app/components/utilities/attemptChangePasswo
|
|||||||
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
|
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
|
||||||
import { Button, FormControl, Input } from "@app/components/v2";
|
import { Button, FormControl, Input } from "@app/components/v2";
|
||||||
import { useUser } from "@app/context";
|
import { useUser } from "@app/context";
|
||||||
|
import { useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
|
||||||
|
|
||||||
type Errors = {
|
type Errors = {
|
||||||
tooShort?: string;
|
tooShort?: string;
|
||||||
@ -45,6 +46,7 @@ export const ChangePasswordSection = () => {
|
|||||||
});
|
});
|
||||||
const [errors, setErrors] = useState<Errors>({});
|
const [errors, setErrors] = useState<Errors>({});
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const sendSetupPasswordEmail = useSendPasswordSetupEmail();
|
||||||
|
|
||||||
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
|
const onFormSubmit = async ({ oldPassword, newPassword }: FormData) => {
|
||||||
try {
|
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 (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
@ -142,6 +162,16 @@ export const ChangePasswordSection = () => {
|
|||||||
<Button type="submit" colorSchema="secondary" isLoading={isLoading} isDisabled={isLoading}>
|
<Button type="submit" colorSchema="secondary" isLoading={isLoading} isDisabled={isLoading}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</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>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -24,6 +24,7 @@ import { Route as authSignUpInvitePageRouteImport } from './pages/auth/SignUpInv
|
|||||||
import { Route as authRequestNewInvitePageRouteImport } from './pages/auth/RequestNewInvitePage/route'
|
import { Route as authRequestNewInvitePageRouteImport } from './pages/auth/RequestNewInvitePage/route'
|
||||||
import { Route as authPasswordResetPageRouteImport } from './pages/auth/PasswordResetPage/route'
|
import { Route as authPasswordResetPageRouteImport } from './pages/auth/PasswordResetPage/route'
|
||||||
import { Route as authEmailNotVerifiedPageRouteImport } from './pages/auth/EmailNotVerifiedPage/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 userLayoutImport } from './pages/user/layout'
|
||||||
import { Route as organizationLayoutImport } from './pages/organization/layout'
|
import { Route as organizationLayoutImport } from './pages/organization/layout'
|
||||||
import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route'
|
import { Route as publicViewSharedSecretByIDPageRouteImport } from './pages/public/ViewSharedSecretByIDPage/route'
|
||||||
@ -310,6 +311,14 @@ const authEmailNotVerifiedPageRouteRoute =
|
|||||||
getParentRoute: () => middlewaresRestrictLoginSignupRoute,
|
getParentRoute: () => middlewaresRestrictLoginSignupRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
|
const authPasswordSetupPageRouteRoute = authPasswordSetupPageRouteImport.update(
|
||||||
|
{
|
||||||
|
id: '/password-setup',
|
||||||
|
path: '/password-setup',
|
||||||
|
getParentRoute: () => middlewaresAuthenticateRoute,
|
||||||
|
} as any,
|
||||||
|
)
|
||||||
|
|
||||||
const userLayoutRoute = userLayoutImport.update({
|
const userLayoutRoute = userLayoutImport.update({
|
||||||
id: '/_layout',
|
id: '/_layout',
|
||||||
getParentRoute: () => AuthenticatePersonalSettingsRoute,
|
getParentRoute: () => AuthenticatePersonalSettingsRoute,
|
||||||
@ -1577,6 +1586,13 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof middlewaresRestrictLoginSignupImport
|
preLoaderRoute: typeof middlewaresRestrictLoginSignupImport
|
||||||
parentRoute: typeof rootRoute
|
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': {
|
'/_restrict-login-signup/email-not-verified': {
|
||||||
id: '/_restrict-login-signup/email-not-verified'
|
id: '/_restrict-login-signup/email-not-verified'
|
||||||
path: '/email-not-verified'
|
path: '/email-not-verified'
|
||||||
@ -3397,12 +3413,14 @@ const AuthenticatePersonalSettingsRouteWithChildren =
|
|||||||
)
|
)
|
||||||
|
|
||||||
interface middlewaresAuthenticateRouteChildren {
|
interface middlewaresAuthenticateRouteChildren {
|
||||||
|
authPasswordSetupPageRouteRoute: typeof authPasswordSetupPageRouteRoute
|
||||||
middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren
|
middlewaresInjectOrgDetailsRoute: typeof middlewaresInjectOrgDetailsRouteWithChildren
|
||||||
AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren
|
AuthenticatePersonalSettingsRoute: typeof AuthenticatePersonalSettingsRouteWithChildren
|
||||||
}
|
}
|
||||||
|
|
||||||
const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren =
|
const middlewaresAuthenticateRouteChildren: middlewaresAuthenticateRouteChildren =
|
||||||
{
|
{
|
||||||
|
authPasswordSetupPageRouteRoute: authPasswordSetupPageRouteRoute,
|
||||||
middlewaresInjectOrgDetailsRoute:
|
middlewaresInjectOrgDetailsRoute:
|
||||||
middlewaresInjectOrgDetailsRouteWithChildren,
|
middlewaresInjectOrgDetailsRouteWithChildren,
|
||||||
AuthenticatePersonalSettingsRoute:
|
AuthenticatePersonalSettingsRoute:
|
||||||
@ -3487,6 +3505,7 @@ export interface FileRoutesByFullPath {
|
|||||||
'/cli-redirect': typeof authCliRedirectPageRouteRoute
|
'/cli-redirect': typeof authCliRedirectPageRouteRoute
|
||||||
'/share-secret': typeof publicShareSecretPageRouteRoute
|
'/share-secret': typeof publicShareSecretPageRouteRoute
|
||||||
'': typeof organizationLayoutRouteWithChildren
|
'': typeof organizationLayoutRouteWithChildren
|
||||||
|
'/password-setup': typeof authPasswordSetupPageRouteRoute
|
||||||
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
||||||
'/password-reset': typeof authPasswordResetPageRouteRoute
|
'/password-reset': typeof authPasswordResetPageRouteRoute
|
||||||
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
||||||
@ -3657,6 +3676,7 @@ export interface FileRoutesByTo {
|
|||||||
'/cli-redirect': typeof authCliRedirectPageRouteRoute
|
'/cli-redirect': typeof authCliRedirectPageRouteRoute
|
||||||
'/share-secret': typeof publicShareSecretPageRouteRoute
|
'/share-secret': typeof publicShareSecretPageRouteRoute
|
||||||
'': typeof organizationLayoutRouteWithChildren
|
'': typeof organizationLayoutRouteWithChildren
|
||||||
|
'/password-setup': typeof authPasswordSetupPageRouteRoute
|
||||||
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
'/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
||||||
'/password-reset': typeof authPasswordResetPageRouteRoute
|
'/password-reset': typeof authPasswordResetPageRouteRoute
|
||||||
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
'/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
||||||
@ -3824,6 +3844,7 @@ export interface FileRoutesById {
|
|||||||
'/share-secret': typeof publicShareSecretPageRouteRoute
|
'/share-secret': typeof publicShareSecretPageRouteRoute
|
||||||
'/_authenticate': typeof middlewaresAuthenticateRouteWithChildren
|
'/_authenticate': typeof middlewaresAuthenticateRouteWithChildren
|
||||||
'/_restrict-login-signup': typeof middlewaresRestrictLoginSignupRouteWithChildren
|
'/_restrict-login-signup': typeof middlewaresRestrictLoginSignupRouteWithChildren
|
||||||
|
'/_authenticate/password-setup': typeof authPasswordSetupPageRouteRoute
|
||||||
'/_restrict-login-signup/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
'/_restrict-login-signup/email-not-verified': typeof authEmailNotVerifiedPageRouteRoute
|
||||||
'/_restrict-login-signup/password-reset': typeof authPasswordResetPageRouteRoute
|
'/_restrict-login-signup/password-reset': typeof authPasswordResetPageRouteRoute
|
||||||
'/_restrict-login-signup/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
'/_restrict-login-signup/requestnewinvite': typeof authRequestNewInvitePageRouteRoute
|
||||||
@ -4004,6 +4025,7 @@ export interface FileRouteTypes {
|
|||||||
| '/cli-redirect'
|
| '/cli-redirect'
|
||||||
| '/share-secret'
|
| '/share-secret'
|
||||||
| ''
|
| ''
|
||||||
|
| '/password-setup'
|
||||||
| '/email-not-verified'
|
| '/email-not-verified'
|
||||||
| '/password-reset'
|
| '/password-reset'
|
||||||
| '/requestnewinvite'
|
| '/requestnewinvite'
|
||||||
@ -4173,6 +4195,7 @@ export interface FileRouteTypes {
|
|||||||
| '/cli-redirect'
|
| '/cli-redirect'
|
||||||
| '/share-secret'
|
| '/share-secret'
|
||||||
| ''
|
| ''
|
||||||
|
| '/password-setup'
|
||||||
| '/email-not-verified'
|
| '/email-not-verified'
|
||||||
| '/password-reset'
|
| '/password-reset'
|
||||||
| '/requestnewinvite'
|
| '/requestnewinvite'
|
||||||
@ -4338,6 +4361,7 @@ export interface FileRouteTypes {
|
|||||||
| '/share-secret'
|
| '/share-secret'
|
||||||
| '/_authenticate'
|
| '/_authenticate'
|
||||||
| '/_restrict-login-signup'
|
| '/_restrict-login-signup'
|
||||||
|
| '/_authenticate/password-setup'
|
||||||
| '/_restrict-login-signup/email-not-verified'
|
| '/_restrict-login-signup/email-not-verified'
|
||||||
| '/_restrict-login-signup/password-reset'
|
| '/_restrict-login-signup/password-reset'
|
||||||
| '/_restrict-login-signup/requestnewinvite'
|
| '/_restrict-login-signup/requestnewinvite'
|
||||||
@ -4562,6 +4586,7 @@ export const routeTree = rootRoute
|
|||||||
"/_authenticate": {
|
"/_authenticate": {
|
||||||
"filePath": "middlewares/authenticate.tsx",
|
"filePath": "middlewares/authenticate.tsx",
|
||||||
"children": [
|
"children": [
|
||||||
|
"/_authenticate/password-setup",
|
||||||
"/_authenticate/_inject-org-details",
|
"/_authenticate/_inject-org-details",
|
||||||
"/_authenticate/personal-settings"
|
"/_authenticate/personal-settings"
|
||||||
]
|
]
|
||||||
@ -4579,6 +4604,10 @@ export const routeTree = rootRoute
|
|||||||
"/_restrict-login-signup/admin/signup"
|
"/_restrict-login-signup/admin/signup"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"/_authenticate/password-setup": {
|
||||||
|
"filePath": "auth/PasswordSetupPage/route.tsx",
|
||||||
|
"parent": "/_authenticate"
|
||||||
|
},
|
||||||
"/_restrict-login-signup/email-not-verified": {
|
"/_restrict-login-signup/email-not-verified": {
|
||||||
"filePath": "auth/EmailNotVerifiedPage/route.tsx",
|
"filePath": "auth/EmailNotVerifiedPage/route.tsx",
|
||||||
"parent": "/_restrict-login-signup"
|
"parent": "/_restrict-login-signup"
|
||||||
|
@ -335,6 +335,7 @@ export const routes = rootRoute("root.tsx", [
|
|||||||
route("/verify-email", "auth/VerifyEmailPage/route.tsx")
|
route("/verify-email", "auth/VerifyEmailPage/route.tsx")
|
||||||
]),
|
]),
|
||||||
middleware("authenticate.tsx", [
|
middleware("authenticate.tsx", [
|
||||||
|
route("/password-setup", "auth/PasswordSetupPage/route.tsx"),
|
||||||
route("/personal-settings", [
|
route("/personal-settings", [
|
||||||
layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")])
|
layout("user/layout.tsx", [index("user/PersonalSettingsPage/route.tsx")])
|
||||||
]),
|
]),
|
||||||
|
@ -13,9 +13,9 @@ type: application
|
|||||||
# This is the chart version. This version number should be incremented each time you make changes
|
# 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.
|
# to the chart and its templates, including the app version.
|
||||||
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
# 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
|
# 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
|
# 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.
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
# It is recommended to use it with quotes.
|
# It is recommended to use it with quotes.
|
||||||
appVersion: "v0.8.5"
|
appVersion: "v0.8.7"
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
{{- if .Values.installCRDs }}
|
|
||||||
apiVersion: apiextensions.k8s.io/v1
|
apiVersion: apiextensions.k8s.io/v1
|
||||||
kind: CustomResourceDefinition
|
kind: CustomResourceDefinition
|
||||||
metadata:
|
metadata:
|
||||||
@ -262,6 +261,48 @@ spec:
|
|||||||
hostAPI:
|
hostAPI:
|
||||||
description: Infisical host to pull secrets from
|
description: Infisical host to pull secrets from
|
||||||
type: string
|
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:
|
managedSecretReference:
|
||||||
properties:
|
properties:
|
||||||
creationPolicy:
|
creationPolicy:
|
||||||
@ -338,7 +379,6 @@ spec:
|
|||||||
- secretNamespace
|
- secretNamespace
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- managedSecretReference
|
|
||||||
- resyncInterval
|
- resyncInterval
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
@ -426,4 +466,3 @@ status:
|
|||||||
plural: ""
|
plural: ""
|
||||||
conditions: []
|
conditions: []
|
||||||
storedVersions: []
|
storedVersions: []
|
||||||
{{- end }}
|
|
@ -32,7 +32,7 @@ controllerManager:
|
|||||||
- ALL
|
- ALL
|
||||||
image:
|
image:
|
||||||
repository: infisical/kubernetes-operator
|
repository: infisical/kubernetes-operator
|
||||||
tag: v0.8.5
|
tag: v0.8.7
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: 500m
|
cpu: 500m
|
||||||
|
@ -134,9 +134,12 @@ type InfisicalSecretSpec struct {
|
|||||||
// +kubebuilder:validation:Optional
|
// +kubebuilder:validation:Optional
|
||||||
Authentication Authentication `json:"authentication"`
|
Authentication Authentication `json:"authentication"`
|
||||||
|
|
||||||
// +kubebuilder:validation:Required
|
// +kubebuilder:validation:Optional
|
||||||
ManagedSecretReference ManagedKubeSecretConfig `json:"managedSecretReference"`
|
ManagedSecretReference ManagedKubeSecretConfig `json:"managedSecretReference"`
|
||||||
|
|
||||||
|
// +kubebuilder:validation:Optional
|
||||||
|
ManagedKubeSecretReferences []ManagedKubeSecretConfig `json:"managedKubeSecretReferences"`
|
||||||
|
|
||||||
// +kubebuilder:default:=60
|
// +kubebuilder:default:=60
|
||||||
ResyncInterval int `json:"resyncInterval"`
|
ResyncInterval int `json:"resyncInterval"`
|
||||||
|
|
||||||
|
@ -565,6 +565,13 @@ func (in *InfisicalSecretSpec) DeepCopyInto(out *InfisicalSecretSpec) {
|
|||||||
out.TokenSecretReference = in.TokenSecretReference
|
out.TokenSecretReference = in.TokenSecretReference
|
||||||
out.Authentication = in.Authentication
|
out.Authentication = in.Authentication
|
||||||
in.ManagedSecretReference.DeepCopyInto(&out.ManagedSecretReference)
|
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
|
out.TLS = in.TLS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -261,6 +261,48 @@ spec:
|
|||||||
hostAPI:
|
hostAPI:
|
||||||
description: Infisical host to pull secrets from
|
description: Infisical host to pull secrets from
|
||||||
type: string
|
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:
|
managedSecretReference:
|
||||||
properties:
|
properties:
|
||||||
creationPolicy:
|
creationPolicy:
|
||||||
@ -339,7 +381,6 @@ spec:
|
|||||||
- secretNamespace
|
- secretNamespace
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- managedSecretReference
|
|
||||||
- resyncInterval
|
- resyncInterval
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
|
@ -97,8 +97,8 @@ spec:
|
|||||||
secretsPath: "/path"
|
secretsPath: "/path"
|
||||||
recursive: true
|
recursive: true
|
||||||
|
|
||||||
managedSecretReference:
|
managedSecretReferences:
|
||||||
secretName: managed-secret
|
- secretName: managed-secret
|
||||||
secretNamespace: default
|
secretNamespace: default
|
||||||
creationPolicy: "Orphan" ## Owner | Orphan
|
creationPolicy: "Orphan" ## Owner | Orphan
|
||||||
# secretType: kubernetes.io/dockerconfigjson
|
# secretType: kubernetes.io/dockerconfigjson
|
||||||
|
@ -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
|
|
||||||
}
|
|
@ -21,14 +21,14 @@ func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.
|
|||||||
Type: "secrets.infisical.com/ReadyToSyncSecrets",
|
Type: "secrets.infisical.com/ReadyToSyncSecrets",
|
||||||
Status: metav1.ConditionFalse,
|
Status: metav1.ConditionFalse,
|
||||||
Reason: "Error",
|
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{
|
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
|
||||||
Type: "secrets.infisical.com/AutoRedeployReady",
|
Type: "secrets.infisical.com/AutoRedeployReady",
|
||||||
Status: metav1.ConditionFalse,
|
Status: metav1.ConditionFalse,
|
||||||
Reason: "Stopped",
|
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 {
|
} else {
|
||||||
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
|
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
|
||||||
|
@ -13,6 +13,8 @@ import (
|
|||||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||||
|
|
||||||
|
defaultErrors "errors"
|
||||||
|
|
||||||
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||||
"github.com/Infisical/infisical/k8-operator/packages/api"
|
"github.com/Infisical/infisical/k8-operator/packages/api"
|
||||||
controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerhelpers"
|
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)
|
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,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/status,verbs=get;update;patch
|
||||||
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/finalizers,verbs=update
|
//+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.
|
// 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
|
// 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 {
|
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 = ""
|
api.API_CA_CERTIFICATE = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCRD)
|
err = r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCRD, managedKubeSecretReferences)
|
||||||
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCRD, err)
|
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCRD, err)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -138,7 +162,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
|||||||
}, nil
|
}, 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)
|
r.SetInfisicalAutoRedeploymentReady(ctx, logger, &infisicalSecretCRD, numDeployments, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error(err, fmt.Sprintf("unable to reconcile auto redeployment. Will requeue after [requeueTime=%v]", requeueTime))
|
logger.Error(err, fmt.Sprintf("unable to reconcile auto redeployment. Will requeue after [requeueTime=%v]", requeueTime))
|
||||||
|
@ -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)
|
plainProcessedSecrets := make(map[string][]byte)
|
||||||
secretType := infisicalSecret.Spec.ManagedSecretReference.SecretType
|
secretType := managedSecretReference.SecretType
|
||||||
managedTemplateData := infisicalSecret.Spec.ManagedSecretReference.Template
|
managedTemplateData := managedSecretReference.Template
|
||||||
|
|
||||||
if managedTemplateData == nil || managedTemplateData.IncludeAllSecrets {
|
if managedTemplateData == nil || managedTemplateData.IncludeAllSecrets {
|
||||||
for _, secret := range secretsFromAPI {
|
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
|
// create a new secret as specified by the managed secret spec of CRD
|
||||||
newKubeSecretInstance := &corev1.Secret{
|
newKubeSecretInstance := &corev1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
|
Name: managedSecretReference.SecretName,
|
||||||
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
|
Namespace: managedSecretReference.SecretNamespace,
|
||||||
Annotations: annotations,
|
Annotations: annotations,
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
},
|
},
|
||||||
@ -235,7 +235,7 @@ func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context
|
|||||||
Data: plainProcessedSecrets,
|
Data: plainProcessedSecrets,
|
||||||
}
|
}
|
||||||
|
|
||||||
if infisicalSecret.Spec.ManagedSecretReference.CreationPolicy == "Owner" {
|
if managedSecretReference.CreationPolicy == "Owner" {
|
||||||
// Set InfisicalSecret instance as the owner and controller of the managed secret
|
// Set InfisicalSecret instance as the owner and controller of the managed secret
|
||||||
err := ctrl.SetControllerReference(&infisicalSecret, newKubeSecretInstance, r.Scheme)
|
err := ctrl.SetControllerReference(&infisicalSecret, newKubeSecretInstance, r.Scheme)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -252,8 +252,8 @@ func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
|
func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, managedSecretReference v1alpha1.ManagedKubeSecretConfig, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
|
||||||
managedTemplateData := infisicalSecret.Spec.ManagedSecretReference.Template
|
managedTemplateData := managedSecretReference.Template
|
||||||
|
|
||||||
plainProcessedSecrets := make(map[string][]byte)
|
plainProcessedSecrets := make(map[string][]byte)
|
||||||
if managedTemplateData == nil || managedTemplateData.IncludeAllSecrets {
|
if managedTemplateData == nil || managedTemplateData.IncludeAllSecrets {
|
||||||
@ -337,7 +337,7 @@ func (r *InfisicalSecretReconciler) updateResourceVariables(infisicalSecret v1al
|
|||||||
infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables
|
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)
|
resourceVariables := r.getResourceVariables(infisicalSecret)
|
||||||
infisicalClient := resourceVariables.InfisicalClient
|
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
|
// Look for managed secret by name and namespace
|
||||||
managedKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
managedKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
||||||
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
|
Name: managedSecretReference.SecretName,
|
||||||
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
|
Namespace: managedSecretReference.SecretNamespace,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil && !k8Errors.IsNotFound(err) {
|
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))
|
logger.Info(fmt.Sprintf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]", authDetails.AuthStrategy))
|
||||||
|
|
||||||
} else {
|
} 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 {
|
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 {
|
} 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
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,17 @@ func ReconcileDeploymentsWithManagedSecrets(ctx context.Context, client controll
|
|||||||
return 0, nil
|
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
|
// Check if the deployment uses managed secrets
|
||||||
func IsDeploymentUsingManagedSecret(deployment v1.Deployment, managedSecret v1alpha1.ManagedKubeSecretConfig) bool {
|
func IsDeploymentUsingManagedSecret(deployment v1.Deployment, managedSecret v1alpha1.ManagedKubeSecretConfig) bool {
|
||||||
managedSecretName := managedSecret.SecretName
|
managedSecretName := managedSecret.SecretName
|
||||||
|
Reference in New Issue
Block a user