merge main and deconflict

This commit is contained in:
Scott Wilson
2025-08-06 18:50:02 -07:00
204 changed files with 5291 additions and 3396 deletions
+32 -111
View File
@@ -1,12 +1,8 @@
import crypto from "crypto";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import jsrp from "jsrp";
import { useServerConfig } from "@app/context";
import { initProjectHelper } from "@app/helpers/project";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
@@ -14,15 +10,9 @@ import { onRequestError } from "@app/hooks/api/reactQuery";
import InputField from "../basic/InputField";
import checkPassword from "../utilities/checks/password/checkPassword";
import Aes256Gcm from "../utilities/cryptography/aes-256-gcm";
import { deriveArgonKey, generateKeyPair } from "../utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage";
import SecurityClient from "../utilities/SecurityClient";
import { Button, Input } from "../v2";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface UserInfoStepProps {
incrementStep: () => void;
email: string;
@@ -76,7 +66,6 @@ export default function UserInfoStep({
}: UserInfoStepProps): JSX.Element {
const [nameError, setNameError] = useState(false);
const [organizationNameError, setOrganizationNameError] = useState(false);
const { config } = useServerConfig();
const [errors, setErrors] = useState<Errors>({});
@@ -108,109 +97,41 @@ export default function UserInfoStep({
});
if (!errorCheck) {
// Generate a random pair of a public and a private key
const pair = await generateKeyPair(config.fipsEnabled);
try {
const response = await completeAccountSignup({
email,
password,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
providerAuthToken,
organizationName,
attributionSource
});
localStorage.setItem("PRIVATE_KEY", pair.privateKey);
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(response.token);
SecurityClient.setProviderAuthToken("");
client.init(
{
username: email,
password
},
async () => {
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
try {
// TODO: moduralize into KeyService
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: pair.privateKey,
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)
});
const response = await completeAccountSignup({
email,
password,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey: pair.publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
providerAuthToken,
salt: result.salt,
verifier: result.verifier,
organizationName,
attributionSource
});
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(response.token);
SecurityClient.setProviderAuthToken("");
if (response.organizationId) {
await selectOrganization({ organizationId: response.organizationId });
}
saveTokenToLocalStorage({
publicKey: pair.publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey: pair.privateKey
});
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]?.id;
await initProjectHelper({
projectName: "Example Project"
});
localStorage.setItem("orgData.id", orgId);
incrementStep();
} catch (error) {
onRequestError(error);
setIsLoading(false);
console.error(error);
}
});
if (response.organizationId) {
await selectOrganization({ organizationId: response.organizationId });
}
);
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]?.id;
await initProjectHelper({
projectName: "Example Project"
});
localStorage.setItem("orgData.id", orgId);
incrementStep();
} catch (error) {
onRequestError(error);
setIsLoading(false);
console.error(error);
}
} else {
setIsLoading(false);
}
@@ -1,108 +0,0 @@
/* eslint-disable new-cap */
import crypto from "crypto";
import jsrp from "jsrp";
import { changePassword, srp1 } from "@app/hooks/api/auth/queries";
import Aes256Gcm from "./cryptography/aes-256-gcm";
import { deriveArgonKey } from "./cryptography/crypto";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
const clientOldPassword = new jsrp.client();
const clientNewPassword = new jsrp.client();
type Params = {
email: string;
currentPassword: string;
newPassword: string;
};
const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise<void> => {
return new Promise((resolve, reject) => {
clientOldPassword.init({ username: email, password: currentPassword }, async () => {
let serverPublicKey;
let salt;
try {
const clientPublicKey = clientOldPassword.getPublicKey();
const res = await srp1({ clientPublicKey });
serverPublicKey = res.serverPublicKey;
salt = res.salt;
clientOldPassword.setSalt(salt);
clientOldPassword.setServerPublicKey(serverPublicKey);
const clientProof = clientOldPassword.getProof();
clientNewPassword.init({ username: email, password: newPassword }, async () => {
clientNewPassword.createVerifier(async (_err, result) => {
try {
const derivedKey = await deriveArgonKey({
password: newPassword,
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);
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: localStorage.getItem("PRIVATE_KEY") as string,
secret: key
});
const {
ciphertext: protectedKey,
iv: protectedKeyIV,
tag: protectedKeyTag
} = Aes256Gcm.encrypt({
text: key.toString("hex"),
secret: Buffer.from(derivedKey.hash)
});
await changePassword({
password: newPassword,
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier
});
saveTokenToLocalStorage({
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
});
resolve();
} catch (err2) {
console.error(err2);
reject(err2);
}
});
});
} catch (err) {
console.error(err);
reject(err);
}
});
});
};
export default attemptChangePassword;
@@ -1,18 +1,18 @@
/* eslint-disable prefer-destructuring */
import axios from "axios";
import jsrp from "jsrp";
import { decryptPrivateKeyHelper } from "@app/helpers/key";
import { login1, login2 } from "@app/hooks/api/auth/queries";
import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries";
import { createNotification } from "../notifications";
import Telemetry from "./telemetry/Telemetry";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
import { LoginMode } from "./attemptLogin";
import SecurityClient from "./SecurityClient";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
export interface IsCliLoginSuccessful {
mfaEnabled: boolean;
loginResponse?: {
email: string;
privateKey: string;
@@ -31,14 +31,62 @@ const attemptLogin = async ({
email,
password,
providerAuthToken,
captchaToken
captchaToken,
loginMode = LoginMode.ServerSide
}: {
email: string;
password: string;
providerAuthToken?: string;
captchaToken?: string;
loginMode?: LoginMode;
}): Promise<IsCliLoginSuccessful> => {
const telemetry = new Telemetry().getInstance();
if (loginMode === LoginMode.ServerSide) {
console.log("Attempting login with server side...");
const data = await loginV3({
email,
password,
providerAuthToken,
captchaToken
}).catch((err) => {
if (axios.isAxiosError(err) && err.response?.status === 400) {
if (err.response?.data?.error === "LegacyEncryptionScheme") {
createNotification({
text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.",
type: "info"
});
return null;
}
}
throw err;
});
if (data === null) {
return attemptLogin({
email,
password,
providerAuthToken,
captchaToken,
loginMode: LoginMode.LegacySrp
});
}
SecurityClient.setProviderAuthToken("");
SecurityClient.setToken(data.accessToken);
return {
success: true,
loginResponse: {
email,
privateKey: "",
JTWToken: data.accessToken
}
};
}
return new Promise((resolve, reject) => {
client.init(
{
@@ -58,79 +106,26 @@ const attemptLogin = async ({
client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1
const {
mfaEnabled,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await login2({
const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({
email,
password,
clientProof,
providerAuthToken,
captchaToken
});
if (mfaEnabled) {
// case: MFA is enabled
// set temporary (MFA) JWT token
SecurityClient.setMfaToken(token);
resolve({
mfaEnabled,
success: true
});
} else if (
!mfaEnabled &&
encryptionVersion &&
encryptedPrivateKey &&
iv &&
tag &&
token
) {
// case: MFA is not enabled
// unset provider auth token in case it was used
if (encryptionVersion && encryptedPrivateKey && iv && tag && token) {
SecurityClient.setProviderAuthToken("");
// set JWT token
SecurityClient.setToken(token);
const privateKey = await decryptPrivateKeyHelper({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
if (email) {
telemetry.identify(email, email);
telemetry.capture("User Logged In");
}
resolve({
mfaEnabled: false,
loginResponse: {
email,
privateKey,
privateKey: "",
JTWToken: token
},
success: true
@@ -1,109 +0,0 @@
/* eslint-disable prefer-destructuring */
import jsrp from "jsrp";
import { decryptPrivateKeyHelper } from "@app/helpers/key";
import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
import SecurityClient from "./SecurityClient";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
interface IsMfaLoginSuccessful {
success: boolean;
loginResponse: {
privateKey: string;
JTWToken: string;
};
}
/**
* Return whether or not MFA-login is successful for user with email [email]
* and MFA token [mfaToken]
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {String} obj.mfaToken - MFA code/token
*/
const attemptLoginMfa = async ({
email,
password,
providerAuthToken,
mfaToken
}: {
email: string;
password: string;
providerAuthToken?: string;
mfaToken: string;
}): Promise<IsMfaLoginSuccessful> => {
return new Promise((resolve, reject) => {
client.init(
{
username: email,
password
},
async () => {
try {
const clientPublicKey = client.getPublicKey();
const { salt } = await login1({
email,
clientPublicKey,
providerAuthToken
});
const {
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await verifyMfaToken({
email,
mfaCode: mfaToken
});
// unset temporary (MFA) JWT token and set JWT token
SecurityClient.setMfaToken("");
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
const privateKey = await decryptPrivateKeyHelper({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
resolve({
success: true,
loginResponse: {
privateKey,
JTWToken: token
}
});
} catch (err) {
reject(err);
}
}
);
});
};
export default attemptLoginMfa;
@@ -1,15 +1,19 @@
/* eslint-disable prefer-destructuring */
import axios from "axios";
import jsrp from "jsrp";
import { decryptPrivateKeyHelper } from "@app/helpers/key";
import { login1, login2 } from "@app/hooks/api/auth/queries";
import { login1, login2, loginV3 } from "@app/hooks/api/auth/queries";
import { createNotification } from "../notifications";
import Telemetry from "./telemetry/Telemetry";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
import SecurityClient from "./SecurityClient";
export enum LoginMode {
LegacySrp = "legacy-srp",
ServerSide = "server-side"
}
interface IsLoginSuccessful {
mfaEnabled: boolean;
success: boolean;
}
@@ -23,14 +27,62 @@ const attemptLogin = async ({
email,
password,
providerAuthToken,
captchaToken
captchaToken,
loginMode = LoginMode.ServerSide
}: {
email: string;
password: string;
providerAuthToken?: string;
captchaToken?: string;
loginMode?: LoginMode;
}): Promise<IsLoginSuccessful> => {
const telemetry = new Telemetry().getInstance();
if (loginMode === LoginMode.ServerSide) {
console.log("Attempting login with server side...");
const data = await loginV3({
email,
password,
providerAuthToken,
captchaToken
}).catch((err) => {
if (axios.isAxiosError(err) && err.response?.status === 400) {
if (err.response?.data?.error === "LegacyEncryptionScheme") {
createNotification({
text: "Failed to login without SRP, attempting to authenticate with legacy SRP authentication.",
type: "info"
});
return null;
}
}
throw err;
});
if (data === null) {
return attemptLogin({
email,
password,
providerAuthToken,
captchaToken,
loginMode: LoginMode.LegacySrp
});
}
SecurityClient.setProviderAuthToken("");
SecurityClient.setToken(data.accessToken);
if (email) {
telemetry.identify(email, email);
telemetry.capture("User Logged In");
}
return {
success: true
};
}
// eslint-disable-next-line new-cap
const client = new jsrp.client();
await new Promise((resolve) => {
@@ -48,18 +100,7 @@ const attemptLogin = async ({
client.setServerPublicKey(serverPublicKey);
const clientProof = client.getProof(); // called M1
const {
mfaEnabled,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await login2({
const { encryptionVersion, token, encryptedPrivateKey, iv, tag } = await login2({
captchaToken,
email,
password,
@@ -67,56 +108,22 @@ const attemptLogin = async ({
providerAuthToken
});
if (mfaEnabled) {
// case: MFA is enabled
// set temporary (MFA) JWT token
SecurityClient.setMfaToken(token);
return {
mfaEnabled,
success: true
};
}
if (!mfaEnabled && encryptionVersion && encryptedPrivateKey && iv && tag && token) {
// case: MFA is not enabled
if (encryptionVersion && encryptedPrivateKey && iv && tag && token) {
// unset provider auth token in case it was used
SecurityClient.setProviderAuthToken("");
// set JWT token
SecurityClient.setToken(token);
const privateKey = await decryptPrivateKeyHelper({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
if (email) {
telemetry.identify(email, email);
telemetry.capture("User Logged In");
}
return {
mfaEnabled: false,
success: true
};
}
return { success: false, mfaEnabled: false };
return { success: false };
};
export default attemptLogin;
@@ -1,95 +0,0 @@
/* eslint-disable prefer-destructuring */
import jsrp from "jsrp";
import { decryptPrivateKeyHelper } from "@app/helpers/key";
import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries";
import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
import SecurityClient from "./SecurityClient";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
/**
* Return whether or not MFA-login is successful for user with email [email]
* and MFA token [mfaToken]
* @param {Object} obj
* @param {String} obj.email - email of user
* @param {String} obj.mfaToken - MFA code/token
*/
const attemptLoginMfa = async ({
email,
password,
providerAuthToken,
mfaToken
}: {
email: string;
password: string;
providerAuthToken?: string;
mfaToken: string;
}): Promise<boolean> => {
return new Promise((resolve, reject) => {
client.init(
{
username: email,
password
},
async () => {
try {
const clientPublicKey = client.getPublicKey();
const { salt } = await login1({
email,
clientPublicKey,
providerAuthToken
});
const {
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
token,
publicKey,
encryptedPrivateKey,
iv,
tag
} = await verifyMfaToken({
email,
mfaCode: mfaToken
});
// unset temporary (MFA) JWT token and set JWT token
SecurityClient.setMfaToken("");
SecurityClient.setToken(token);
SecurityClient.setProviderAuthToken("");
const privateKey = await decryptPrivateKeyHelper({
encryptionVersion,
encryptedPrivateKey,
iv,
tag,
password,
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag
});
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
});
resolve(true);
} catch (err) {
reject(err);
}
}
);
});
};
export default attemptLoginMfa;
@@ -1,114 +0,0 @@
/* eslint-disable new-cap */
import crypto from "crypto";
import jsrp from "jsrp";
import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries";
import generateBackupPDF from "../generateBackupPDF";
import Aes256Gcm from "./aes-256-gcm";
const clientPassword = new jsrp.client();
const clientKey = new jsrp.client();
interface BackupKeyProps {
email: string;
password: string;
personalName: string;
setBackupKeyError: (value: boolean) => void;
setBackupKeyIssued: (value: boolean) => void;
}
/**
* This function issue a backup key for a user
* @param {obkect} obj
* @param {string} obj.email - email of a user issuing a backup key
* @param {string} obj.password - password of a user issuing a backup key
* @param {string} obj.personalName - name of a user issuing a backup key
* @param {function} obj.setBackupKeyError - state function that turns true if there is an erorr with a backup key
* @param {function} obj.setBackupKeyIssued - state function that turns true if a backup key was issued correctly
* @returns
*/
const issueBackupKey = async ({
email,
password,
personalName,
setBackupKeyError,
setBackupKeyIssued
}: BackupKeyProps) => {
try {
setBackupKeyError(false);
setBackupKeyIssued(false);
clientPassword.init(
{
username: email,
password
},
async () => {
const clientPublicKey = clientPassword.getPublicKey();
let serverPublicKey;
let salt;
try {
const res = await srp1({
clientPublicKey
});
serverPublicKey = res.serverPublicKey;
salt = res.salt;
} catch (err) {
setBackupKeyError(true);
console.log("Wrong current password", err, 1);
}
clientPassword.setSalt(salt as string);
clientPassword.setServerPublicKey(serverPublicKey as string);
const clientProof = clientPassword.getProof(); // called M1
const generatedKey = crypto.randomBytes(16).toString("hex");
clientKey.init(
{
username: email,
password: generatedKey
},
async () => {
clientKey.createVerifier(
async (_err: any, result: { salt: string; verifier: string }) => {
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: String(localStorage.getItem("PRIVATE_KEY")),
secret: generatedKey
});
try {
await issueBackupPrivateKey({
encryptedPrivateKey: ciphertext,
iv,
tag,
salt: result.salt,
verifier: result.verifier,
clientProof
});
generateBackupPDF({
personalName,
personalEmail: email,
generatedKey
});
setBackupKeyIssued(true);
} catch {
setBackupKeyError(true);
}
}
);
}
);
}
);
} catch {
setBackupKeyError(true);
console.log("Failed to issue a backup key");
}
return true;
};
export default issueBackupKey;
@@ -1,67 +0,0 @@
interface Props {
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
privateKey?: string;
}
export const saveTokenToLocalStorage = ({
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv,
tag,
privateKey
}: Props) => {
try {
if (protectedKey) {
localStorage.removeItem("protectedKey");
localStorage.setItem("protectedKey", protectedKey);
}
if (protectedKeyIV) {
localStorage.removeItem("protectedKeyIV");
localStorage.setItem("protectedKeyIV", protectedKeyIV);
}
if (protectedKeyTag) {
localStorage.removeItem("protectedKeyTag");
localStorage.setItem("protectedKeyTag", protectedKeyTag);
}
if (publicKey) {
localStorage.removeItem("publicKey");
localStorage.setItem("publicKey", publicKey);
}
if (encryptedPrivateKey) {
localStorage.removeItem("encryptedPrivateKey");
localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
}
if (iv) {
localStorage.removeItem("iv");
localStorage.setItem("iv", iv);
}
if (tag) {
localStorage.removeItem("tag");
localStorage.setItem("tag", tag);
}
if (privateKey) {
localStorage.removeItem("PRIVATE_KEY");
localStorage.setItem("PRIVATE_KEY", privateKey);
}
} catch (err) {
if (err instanceof Error) {
throw new Error(`Unable to send the tokens in local storage:${err.message}`);
}
}
};
@@ -21,6 +21,15 @@ export enum OrgGatewayPermissionActions {
AttachGateways = "attach-gateways"
}
export enum OrgPermissionMachineIdentityAuthTemplateActions {
ListTemplates = "list-templates",
CreateTemplates = "create-templates",
EditTemplates = "edit-templates",
DeleteTemplates = "delete-templates",
UnlinkTemplates = "unlink-templates",
AttachTemplates = "attach-templates"
}
export enum OrgPermissionSubjects {
Workspace = "workspace",
Role = "role",
@@ -42,7 +51,8 @@ export enum OrgPermissionSubjects {
Kmip = "kmip",
Gateway = "gateway",
SecretShare = "secret-share",
GithubOrgSync = "github-org-sync"
GithubOrgSync = "github-org-sync",
MachineIdentityAuthTemplate = "machine-identity-auth-template"
}
export enum OrgPermissionAdminConsoleAction {
@@ -113,6 +123,10 @@ export type OrgPermissionSet =
| [OrgPermissionAppConnectionActions, OrgPermissionSubjects.AppConnections]
| [OrgPermissionIdentityActions, OrgPermissionSubjects.Identity]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSubjects.MachineIdentityAuthTemplate
]
| [OrgGatewayPermissionActions, OrgPermissionSubjects.Gateway]
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
// TODO(scott): add back once org UI refactored
+2 -8
View File
@@ -1,8 +1,7 @@
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { AxiosError } from "axios";
import { createNotification } from "@app/components/notifications";
import { apiRequest } from "@app/config/request";
import { onRequestError } from "@app/hooks/api/reactQuery";
import { TReactQueryOptions } from "@app/types/reactQuery";
import { Actor, AuditLog, TGetAuditLogsFilter } from "./types";
@@ -46,12 +45,7 @@ export const useGetAuditLogs = (
);
return data.auditLogs;
} catch (error) {
if (error instanceof AxiosError) {
createNotification({
type: "error",
text: error.response?.data.message
});
}
onRequestError(error);
return [];
}
},
+7 -48
View File
@@ -8,26 +8,24 @@ import { organizationKeys } from "../organization/queries";
import { setAuthToken } from "../reactQuery";
import { workspaceKeys } from "../workspace";
import {
ChangePasswordDTO,
CompleteAccountDTO,
CompleteAccountSignupDTO,
GetAuthTokenAPI,
GetBackupEncryptedPrivateKeyDTO,
IssueBackupPrivateKeyDTO,
Login1DTO,
Login1Res,
Login2DTO,
Login2Res,
LoginLDAPDTO,
LoginLDAPRes,
LoginV3DTO,
LoginV3Res,
MfaMethod,
ResetPasswordDTO,
ResetPasswordV2DTO,
ResetUserPasswordV2DTO,
SendMfaTokenDTO,
SetupPasswordDTO,
SRP1DTO,
SRPR1Res,
TOauthTokenExchangeDTO,
UserAgentType,
UserEncryptionVersion,
@@ -50,21 +48,14 @@ export const login2 = async (loginDetails: Login2DTO) => {
return data;
};
export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
const { data } = await apiRequest.post<LoginLDAPRes>("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token
export const loginV3 = async (loginDetails: LoginV3DTO) => {
const { data } = await apiRequest.post<LoginV3Res>("/api/v3/auth/login", loginDetails);
return data;
};
export const useLogin1 = () => {
return useMutation({
mutationFn: async (details: {
email: string;
clientPublicKey: string;
providerAuthToken?: string;
}) => {
return login1(details);
}
});
export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
const { data } = await apiRequest.post<LoginLDAPRes>("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token
return data;
};
export const selectOrganization = async (data: {
@@ -143,11 +134,6 @@ export const useOauthTokenExchange = () => {
});
};
export const srp1 = async (details: SRP1DTO) => {
const { data } = await apiRequest.post<SRPR1Res>("/api/v1/password/srp1", details);
return data;
};
export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => {
const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details);
return data;
@@ -158,14 +144,6 @@ export const completeAccountSignupInvite = async (details: CompleteAccountDTO) =
return data;
};
export const useCompleteAccountSignup = () => {
return useMutation({
mutationFn: async (details: CompleteAccountSignupDTO) => {
return completeAccountSignup(details);
}
});
};
export const useSendMfaToken = () => {
return useMutation<object, object, SendMfaTokenDTO>({
mutationFn: async ({ email }) => {
@@ -263,11 +241,6 @@ export const useVerifyPasswordResetCode = () => {
});
};
export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => {
const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details);
return data;
};
export const getBackupEncryptedPrivateKey = async ({
verificationToken
}: GetBackupEncryptedPrivateKeyDTO) => {
@@ -328,20 +301,6 @@ export const useResetUserPasswordV2 = () => {
});
};
export const changePassword = async (details: ChangePasswordDTO) => {
const { data } = await apiRequest.post("/api/v1/password/change-password", details);
return data;
};
export const useChangePassword = () => {
// note: use after srp1
return useMutation({
mutationFn: async (details: ChangePasswordDTO) => {
return changePassword(details);
}
});
};
// Refresh token is set as cookie when logged in
// Using that we fetch the auth bearer token needed for auth calls
export const fetchAuthToken = async () => {
+13 -49
View File
@@ -49,13 +49,19 @@ export type Login2DTO = {
password: string;
};
export type LoginV3DTO = {
email: string;
password: string;
providerAuthToken?: string;
captchaToken?: string;
};
export type Login1Res = {
serverPublicKey: string;
salt: string;
};
export type Login2Res = {
mfaEnabled: boolean;
token: string;
encryptionVersion?: number;
protectedKey?: string;
@@ -67,6 +73,11 @@ export type Login2Res = {
tag?: string;
};
export type LoginV3Res = {
accessToken: string;
mfaEnabled: boolean;
};
export type LoginLDAPDTO = {
organizationSlug: string;
username: string;
@@ -77,28 +88,10 @@ export type LoginLDAPRes = {
nextUrl: string;
};
export type SRP1DTO = {
clientPublicKey: string;
};
export type SRPR1Res = {
serverPublicKey: string;
salt: string;
};
export type CompleteAccountDTO = {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string;
tokenMetadata?: string;
};
@@ -116,19 +109,6 @@ export type VerifySignupInviteDTO = {
organizationId: string;
};
export type ChangePasswordDTO = {
password: string;
clientProof: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
};
export type ResetPasswordDTO = {
protectedKey: string;
protectedKeyIV: string;
@@ -153,27 +133,11 @@ export type ResetUserPasswordV2DTO = {
};
export type SetupPasswordDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
email: string;
token: string;
password: string;
};
export type IssueBackupPrivateKeyDTO = {
encryptedPrivateKey: string;
iv: string;
tag: string;
salt: string;
verifier: string;
clientProof: string;
};
export type GetBackupEncryptedPrivateKeyDTO = {
verificationToken: string;
};
@@ -1385,6 +1385,7 @@ export const useAddIdentityLdapAuth = () => {
return useMutation<IdentityLdapAuth, object, AddIdentityLdapAuthDTO>({
mutationFn: async ({
identityId,
templateId,
url,
bindDN,
bindPass,
@@ -1400,6 +1401,7 @@ export const useAddIdentityLdapAuth = () => {
const { data } = await apiRequest.post<{ identityLdapAuth: IdentityLdapAuth }>(
`/api/v1/auth/ldap-auth/identities/${identityId}`,
{
templateId,
url,
bindDN,
bindPass,
@@ -1432,6 +1434,7 @@ export const useUpdateIdentityLdapAuth = () => {
return useMutation<IdentityLdapAuth, object, UpdateIdentityLdapAuthDTO>({
mutationFn: async ({
identityId,
templateId,
url,
bindDN,
bindPass,
@@ -1447,6 +1450,7 @@ export const useUpdateIdentityLdapAuth = () => {
const { data } = await apiRequest.patch<{ identityLdapAuth: IdentityLdapAuth }>(
`/api/v1/auth/ldap-auth/identities/${identityId}`,
{
templateId,
url,
bindDN,
bindPass,
+11 -8
View File
@@ -567,10 +567,11 @@ export type IdentityTokenAuth = {
export type AddIdentityLdapAuthDTO = {
organizationId: string;
identityId: string;
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
templateId?: string;
url?: string;
bindDN?: string;
bindPass?: string;
searchBase?: string;
searchFilter: string;
ldapCaCertificate?: string;
allowedFields?: {
@@ -588,6 +589,7 @@ export type AddIdentityLdapAuthDTO = {
export type UpdateIdentityLdapAuthDTO = {
identityId: string;
organizationId: string;
templateId?: string;
url?: string;
bindDN?: string;
bindPass?: string;
@@ -612,10 +614,11 @@ export type DeleteIdentityLdapAuthDTO = {
};
export type IdentityLdapAuth = {
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
url?: string;
bindDN?: string;
templateId?: string;
bindPass?: string;
searchBase?: string;
searchFilter: string;
ldapCaCertificate?: string;
allowedFields?: {
@@ -0,0 +1,3 @@
export * from "./mutations";
export * from "./queries";
export * from "./types";
@@ -0,0 +1,97 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { identityAuthTemplatesKeys } from "./queries";
import {
CreateIdentityAuthTemplateDTO,
DeleteIdentityAuthTemplateDTO,
IdentityAuthTemplate,
MachineAuthTemplateUsage,
UnlinkTemplateUsageDTO,
UpdateIdentityAuthTemplateDTO
} from "./types";
export const useCreateIdentityAuthTemplate = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (dto: CreateIdentityAuthTemplateDTO) => {
const { data } = await apiRequest.post<{ template: IdentityAuthTemplate }>(
"/api/v1/identity-templates",
dto
);
return data.template;
},
onSuccess: (_, { organizationId }) => {
queryClient.invalidateQueries({
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
});
}
});
};
export const useUpdateIdentityAuthTemplate = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (dto: UpdateIdentityAuthTemplateDTO) => {
const { data } = await apiRequest.patch<{ template: IdentityAuthTemplate }>(
`/api/v1/identity-templates/${dto.templateId}`,
dto
);
return data.template;
},
onSuccess: (_, { organizationId, templateId }) => {
queryClient.invalidateQueries({
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
});
queryClient.invalidateQueries({
queryKey: identityAuthTemplatesKeys.getTemplate(templateId)
});
}
});
};
export const useDeleteIdentityAuthTemplate = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (dto: DeleteIdentityAuthTemplateDTO) => {
await apiRequest.delete(`/api/v1/identity-templates/${dto.templateId}`, {
params: { organizationId: dto.organizationId }
});
},
onSuccess: (_, { organizationId, templateId }) => {
queryClient.invalidateQueries({
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
});
queryClient.removeQueries({
queryKey: identityAuthTemplatesKeys.getTemplate(templateId)
});
}
});
};
export const useUnlinkTemplateUsage = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (dto: UnlinkTemplateUsageDTO) => {
const { data } = await apiRequest.post<MachineAuthTemplateUsage[]>(
`/api/v1/identity-templates/${dto.templateId}/delete-usage`,
{ identityIds: dto.identityIds },
{ params: { organizationId: dto.organizationId } }
);
return data;
},
onSuccess: (_, { templateId, organizationId }) => {
queryClient.invalidateQueries({
queryKey: identityAuthTemplatesKeys.getTemplateUsages(templateId)
});
queryClient.invalidateQueries({
queryKey: identityAuthTemplatesKeys.getTemplates({ organizationId })
});
}
});
};
@@ -0,0 +1,89 @@
import { useQuery } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import {
GetIdentityAuthTemplatesDTO,
GetTemplateUsagesDTO,
IdentityAuthTemplate,
MachineAuthTemplateUsage,
MachineIdentityAuthMethod
} from "./types";
export const identityAuthTemplatesKeys = {
all: ["identity-auth-templates"] as const,
getTemplates: (dto: GetIdentityAuthTemplatesDTO) =>
[...identityAuthTemplatesKeys.all, "list", dto] as const,
getTemplate: (templateId: string) =>
[...identityAuthTemplatesKeys.all, "single", templateId] as const,
getAvailableTemplates: (authMethod: MachineIdentityAuthMethod) =>
[...identityAuthTemplatesKeys.all, "available", authMethod] as const,
getTemplateUsages: (templateId: string) =>
[...identityAuthTemplatesKeys.all, "usages", templateId] as const
};
export const useGetIdentityAuthTemplates = (dto: GetIdentityAuthTemplatesDTO) => {
return useQuery({
queryKey: identityAuthTemplatesKeys.getTemplates(dto),
queryFn: async () => {
const { data } = await apiRequest.get<{
templates: IdentityAuthTemplate[];
totalCount: number;
}>("/api/v1/identity-templates/search", {
params: {
organizationId: dto.organizationId,
limit: dto.limit || 50,
offset: dto.offset || 0,
...(dto.search && { search: dto.search })
}
});
return data;
},
enabled: Boolean(dto.organizationId)
});
};
export const useGetIdentityAuthTemplate = (templateId: string, organizationId: string) => {
return useQuery({
queryKey: identityAuthTemplatesKeys.getTemplate(templateId),
queryFn: async () => {
const { data } = await apiRequest.get<IdentityAuthTemplate>(
`/api/v1/identity-templates/${templateId}`,
{
params: { organizationId }
}
);
return data;
},
enabled: Boolean(templateId) && Boolean(organizationId)
});
};
export const useGetAvailableTemplates = (authMethod: MachineIdentityAuthMethod) => {
return useQuery({
queryKey: identityAuthTemplatesKeys.getAvailableTemplates(authMethod),
queryFn: async () => {
const { data } = await apiRequest.get<IdentityAuthTemplate[]>("/api/v1/identity-templates", {
params: { authMethod }
});
return data;
},
enabled: Boolean(authMethod)
});
};
export const useGetTemplateUsages = (dto: GetTemplateUsagesDTO) => {
return useQuery({
queryKey: identityAuthTemplatesKeys.getTemplateUsages(dto.templateId),
queryFn: async () => {
const { data } = await apiRequest.get<MachineAuthTemplateUsage[]>(
`/api/v1/identity-templates/${dto.templateId}/usage`,
{
params: { organizationId: dto.organizationId }
}
);
return data;
},
enabled: Boolean(dto.templateId) && Boolean(dto.organizationId)
});
};
@@ -0,0 +1,78 @@
export enum MachineIdentityAuthMethod {
LDAP = "ldap"
}
export interface LdapTemplateFields {
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
ldapCaCertificate?: string;
}
export interface IdentityAuthTemplate {
id: string;
name: string;
authMethod: MachineIdentityAuthMethod;
organizationId: string;
templateFields: LdapTemplateFields;
createdAt: string;
updatedAt: string;
}
export interface CreateIdentityAuthTemplateDTO {
organizationId: string;
name: string;
authMethod: MachineIdentityAuthMethod;
templateFields: LdapTemplateFields;
}
export interface UpdateIdentityAuthTemplateDTO {
templateId: string;
organizationId: string;
name?: string;
templateFields?: Partial<LdapTemplateFields>;
}
export interface DeleteIdentityAuthTemplateDTO {
templateId: string;
organizationId: string;
}
export interface GetIdentityAuthTemplatesDTO {
organizationId: string;
limit?: number;
offset?: number;
search?: string;
}
export interface MachineAuthTemplateUsage {
identityId: string;
identityName: string;
}
export interface GetTemplateUsagesDTO {
templateId: string;
organizationId: string;
}
export interface UnlinkTemplateUsageDTO {
templateId: string;
identityIds: string[];
organizationId: string;
}
export const TEMPLATE_ERROR_MESSAGES = {
UNLINK_SUCCESS: "Successfully unlinked template usages",
UNLINK_FAILED: "Failed to unlink template usages",
SINGLE_UNLINK_SUCCESS: "Successfully unlinked template usage",
SINGLE_UNLINK_FAILED: "Failed to unlink template usage"
} as const;
export const TEMPLATE_UI_LABELS = {
VIEW_USAGES: "View Usages",
EDIT_TEMPLATE: "Edit Template",
DELETE_TEMPLATE: "Delete Template",
UNLINK: "Unlink",
UNSELECT_ALL: "Unselect All"
} as const;
+1
View File
@@ -15,6 +15,7 @@ export * from "./gateways";
export * from "./githubOrgSyncConfig";
export * from "./groups";
export * from "./identities";
export * from "./identityAuthTemplates";
export * from "./identityProjectAdditionalPrivilege";
export * from "./incidentContacts";
export * from "./integrationAuth";
@@ -53,4 +53,5 @@ export type SubscriptionPlan = {
secretScanning: boolean;
enterpriseSecretSyncs: boolean;
enterpriseAppConnections: boolean;
machineIdentityAuthTemplates: boolean;
};
-8
View File
@@ -495,14 +495,6 @@ export const useGetMyOrganizationProjects = (orgId: string) => {
});
};
export const fetchMyPrivateKey = async () => {
const {
data: { privateKey }
} = await apiRequest.get<{ privateKey: string }>("/api/v1/user/private-key");
return privateKey;
};
export const useListUserGroupMemberships = (username: string) => {
return useQuery({
queryKey: userKeys.listUserGroupMemberships(username),
-47
View File
@@ -4,53 +4,6 @@ import jsrp from "jsrp";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto";
import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries";
export const generateUserBackupKey = async (email: string, password: string) => {
// eslint-disable-next-line new-cap
const clientKey = new jsrp.client();
// eslint-disable-next-line new-cap
const clientPassword = new jsrp.client();
await new Promise((resolve) => {
clientPassword.init({ username: email, password }, () => resolve(null));
});
const clientPublicKey = clientPassword.getPublicKey();
const srpKeys = await srp1({ clientPublicKey });
clientPassword.setSalt(srpKeys.salt);
clientPassword.setServerPublicKey(srpKeys.serverPublicKey);
const clientProof = clientPassword.getProof(); // called M1
const generatedKey = crypto.randomBytes(16).toString("hex");
await new Promise((resolve) => {
clientKey.init({ username: email, password: generatedKey }, () => resolve(null));
});
const { salt, verifier } = await new Promise<{ salt: string; verifier: string }>(
(resolve, reject) => {
clientKey.createVerifier((err, res) => {
if (err) return reject(err);
return resolve(res);
});
}
);
const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
text: String(localStorage.getItem("PRIVATE_KEY")),
secret: generatedKey
});
await issueBackupPrivateKey({
encryptedPrivateKey: ciphertext,
iv,
tag,
salt,
verifier,
clientProof
});
return generatedKey;
};
export const generateUserPassKey = async (
email: string,
@@ -8,7 +8,6 @@ import { z } from "zod";
import { createNotification } from "@app/components/notifications";
// TODO(akhilmhdh): rewrite this into module functions in lib
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, ContentLoader, FormControl, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
@@ -63,13 +62,6 @@ export const SignUpPage = () => {
});
SecurityClient.setToken(res.token);
saveTokenToLocalStorage({
publicKey: userPass.publicKey,
encryptedPrivateKey: userPass.encryptedPrivateKey,
iv: userPass.encryptedPrivateKeyIV,
tag: userPass.encryptedPrivateKeyTag,
privateKey
});
await selectOrganization({ organizationId: res.organization.id });
// TODO(akhilmhdh): This is such a confusing pattern and too unreliable
@@ -18,7 +18,7 @@ import { useToggle } from "@app/hooks";
import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { fetchMyPrivateKey, fetchUserDuplicateAccounts } from "@app/hooks/api/users/queries";
import { fetchUserDuplicateAccounts } from "@app/hooks/api/users/queries";
import { EmailDuplicationConfirmation } from "@app/pages/auth/SelectOrgPage/EmailDuplicationConfirmation";
import { navigateUserToOrg, useNavigateToSelectOrganization } from "../../Login.utils";
@@ -70,9 +70,6 @@ export const PasswordStep = ({
// set JWT token
SecurityClient.setToken(oauthLogin.token);
const privateKey = await fetchMyPrivateKey();
localStorage.setItem("PRIVATE_KEY", privateKey);
// case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org
if (organizationId) {
const finishWithOrgWorkflow = async () => {
@@ -92,7 +89,7 @@ export const PasswordStep = ({
console.log("organization id was present. new JWT token to be used in CLI:", token);
const instance = axios.create();
const payload = {
privateKey,
privateKey: "", // note(daniel): no longer needed by the CLI, because the CLI only uses the private key to create service tokens, and the private key isn't used anymore when creating service tokens.
email,
JTWToken: token
};
@@ -1,22 +1,14 @@
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("");
@@ -65,83 +57,31 @@ export const PasswordSetupPage = () => {
setPasswordsMatch(true);
if (!errorCheck) {
client.init(
{
username: email,
try {
await setupPassword.mutateAsync({
email,
token,
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");
setIsRedirecting(true);
const key = crypto.randomBytes(32);
createNotification({
type: "success",
title: "Password successfully set",
text: "Redirecting to login..."
});
// 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" });
}
});
}
);
setTimeout(() => {
window.location.href = "/login";
}, 3000);
} catch (error) {
createNotification({
type: "error",
text: (error as Error).message ?? "Error setting password"
});
navigate({ to: "/personal-settings" });
}
}
};
@@ -123,11 +123,8 @@ export const SelectOrganizationSection = () => {
}
if (callbackPort) {
const privateKey = localStorage.getItem("PRIVATE_KEY");
let error: string | null = null;
if (!privateKey) error = "Private key not found";
if (!user?.email) error = "User email not found";
if (!token) error = "No token found";
@@ -142,7 +139,7 @@ export const SelectOrganizationSection = () => {
const payload = {
JTWToken: token,
email: user?.email,
privateKey
privateKey: ""
} as IsCliLoginSuccessful["loginResponse"];
// send request to server endpoint
@@ -1,23 +1,14 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable @typescript-eslint/no-unused-vars */
import crypto from "crypto";
import { useState } from "react";
import { Helmet } from "react-helmet";
import { faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Link, useNavigate, useSearch } from "@tanstack/react-router";
import jsrp from "jsrp";
import { Mfa } from "@app/components/auth/Mfa";
import InputField from "@app/components/basic/InputField";
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { useToggle } from "@app/hooks";
import {
completeAccountSignupInvite,
@@ -28,9 +19,6 @@ import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
import { isLoggedIn } from "@app/hooks/api/reactQuery";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
type Errors = {
tooShort?: string;
tooLong?: string;
@@ -67,7 +55,6 @@ export const SignupInvitePage = () => {
const metadata = queryParams.get("metadata") || undefined;
const { mutateAsync: selectOrganization } = useSelectOrganization();
const { config } = useServerConfig();
const loggedIn = isLoggedIn();
@@ -94,123 +81,56 @@ export const SignupInvitePage = () => {
}
if (!errorCheck) {
// Generate a random pair of a public and a private key
const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled);
try {
const { token: jwtToken } = await completeAccountSignupInvite({
email,
password,
firstName,
lastName,
tokenMetadata: metadata
});
localStorage.setItem("PRIVATE_KEY", privateKey);
// unset temporary signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(jwtToken);
client.init(
{
username: email,
password
},
async () => {
client.createVerifier(async (_err, result) => {
try {
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
const userOrgs = await fetchOrganizations();
if (!derivedKey) throw new Error("Failed to derive key from password");
const orgId = userOrgs[0].id;
const key = crypto.randomBytes(32);
if (!orgId) throw new Error("You are not part of any organization");
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
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)
});
const { token: jwtToken } = await completeAccountSignupInvite({
email,
password,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt: result.salt,
verifier: result.verifier,
tokenMetadata: metadata
});
// unset temporary signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(jwtToken);
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
});
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0].id;
if (!orgId) throw new Error("You are not part of any organization");
const completeSignupFlow = async () => {
const {
token: mfaToken,
isMfaEnabled,
mfaMethod
} = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(mfaToken);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => completeSignupFlow);
return;
}
localStorage.setItem("orgData.id", orgId);
navigate({
to: "/organization/projects"
});
};
await completeSignupFlow();
} catch (error) {
setIsLoading(false);
console.error(error);
}
const completeSignupFlow = async () => {
const {
token: mfaToken,
isMfaEnabled,
mfaMethod
} = await selectOrganization({
organizationId: orgId
});
}
);
if (isMfaEnabled) {
SecurityClient.setMfaToken(mfaToken);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => completeSignupFlow);
return;
}
localStorage.setItem("orgData.id", orgId);
navigate({
to: "/organization/projects"
});
};
await completeSignupFlow();
} catch (error) {
setIsLoading(false);
console.error(error);
}
} else {
setIsLoading(false);
}
@@ -3,24 +3,16 @@ import crypto from "crypto";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "@tanstack/react-router";
import jsrp from "jsrp";
import { Mfa } from "@app/components/auth/Mfa";
import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm";
import { deriveArgonKey, generateKeyPair } from "@app/components/utilities/cryptography/crypto";
import { saveTokenToLocalStorage } from "@app/components/utilities/saveTokenToLocalStorage";
import SecurityClient from "@app/components/utilities/SecurityClient";
import { Button, Input } from "@app/components/v2";
import { useServerConfig } from "@app/context";
import { initProjectHelper } from "@app/helpers/project";
import { useToggle } from "@app/hooks";
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
import { MfaMethod } from "@app/hooks/api/auth/types";
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
// eslint-disable-next-line new-cap
const client = new jsrp.client();
type Props = {
username: string;
password: string;
@@ -64,7 +56,6 @@ export const UserInfoSSOStep = ({
const { mutateAsync: selectOrganization } = useSelectOrganization();
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const navigate = useNavigate();
const { config } = useServerConfig();
useEffect(() => {
const randomPassword = crypto.randomBytes(32).toString("hex");
@@ -93,131 +84,64 @@ export const UserInfoSSOStep = ({
}
if (!errorCheck) {
// Generate a random pair of a public and a private key
const { publicKey, privateKey } = await generateKeyPair(config.fipsEnabled);
localStorage.setItem("PRIVATE_KEY", privateKey);
try {
const response = await completeAccountSignup({
email: username,
password,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
providerAuthToken,
organizationName,
attributionSource,
useDefaultOrg: forceDefaultOrg
});
client.init(
{
username,
password
},
async () => {
client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => {
try {
// TODO: moduralize into KeyService
const derivedKey = await deriveArgonKey({
password,
salt: result.salt,
mem: 65536,
time: 3,
parallelism: 1,
hashLen: 32
});
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(response.token);
SecurityClient.setProviderAuthToken("");
if (!derivedKey) throw new Error("Failed to derive key from password");
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]?.id;
const key = crypto.randomBytes(32);
const completeSignupFlow = async () => {
try {
const { isMfaEnabled, token, mfaMethod } = await selectOrganization({
organizationId: orgId
});
// create encrypted private key by encrypting the private
// key with the symmetric key [key]
const {
ciphertext: encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag
} = Aes256Gcm.encrypt({
text: privateKey,
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)
});
const response = await completeAccountSignup({
email: username,
password,
firstName: name.split(" ")[0],
lastName: name.split(" ").slice(1).join(" "),
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
providerAuthToken,
salt: result.salt,
verifier: result.verifier,
organizationName,
attributionSource,
useDefaultOrg: forceDefaultOrg
});
// unset signup JWT token and set JWT token
SecurityClient.setSignupToken("");
SecurityClient.setToken(response.token);
SecurityClient.setProviderAuthToken("");
saveTokenToLocalStorage({
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
privateKey
});
const userOrgs = await fetchOrganizations();
const orgId = userOrgs[0]?.id;
const completeSignupFlow = async () => {
try {
const { isMfaEnabled, token, mfaMethod } = await selectOrganization({
organizationId: orgId
});
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => completeSignupFlow);
return;
}
// only create example project if not joining existing org
if (!providerOrganizationName) {
await initProjectHelper({
projectName: "Example Project"
});
}
localStorage.setItem("orgData.id", orgId);
navigate({
to: "/organization/projects"
});
} catch (error) {
setIsLoading(false);
console.error(error);
}
};
await completeSignupFlow();
} catch (error) {
setIsLoading(false);
console.error(error);
if (isMfaEnabled) {
SecurityClient.setMfaToken(token);
if (mfaMethod) {
setRequiredMfaMethod(mfaMethod);
}
toggleShowMfa.on();
setMfaSuccessCallback(() => completeSignupFlow);
return;
}
});
}
);
// only create example project if not joining existing org
if (!providerOrganizationName) {
await initProjectHelper({
projectName: "Example Project"
});
}
localStorage.setItem("orgData.id", orgId);
navigate({
to: "/organization/projects"
});
} catch (error) {
setIsLoading(false);
console.error(error);
}
};
await completeSignupFlow();
} catch (error) {
setIsLoading(false);
console.error(error);
}
} else {
setIsLoading(false);
}
@@ -0,0 +1,317 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
Select,
SelectItem,
TextArea
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
MachineIdentityAuthMethod,
useCreateIdentityAuthTemplate,
useUpdateIdentityAuthTemplate
} from "@app/hooks/api/identityAuthTemplates";
import { UsePopUpState } from "@app/hooks/usePopUp";
const authMethods = [{ label: "LDAP Auth", value: MachineIdentityAuthMethod.LDAP }];
const schema = z.object({
name: z.string().min(1, "Template name is required"),
method: z.nativeEnum(MachineIdentityAuthMethod),
url: z.string().min(1, "LDAP URL is required"),
bindDN: z.string().min(1, "Bind DN is required"),
bindPass: z.string().min(1, "Bind Pass is required"),
searchBase: z.string().min(1, "Search Base / DN is required"),
ldapCaCertificate: z
.string()
.optional()
.transform((val) => val || undefined)
});
export type FormData = z.infer<typeof schema>;
type Props = {
popUp: UsePopUpState<["createTemplate", "editTemplate"]>;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["createTemplate", "editTemplate"]>,
state?: boolean
) => void;
};
export const IdentityAuthTemplateModal = ({ popUp, handlePopUpToggle }: Props) => {
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || "";
const { mutateAsync: createTemplate } = useCreateIdentityAuthTemplate();
const { mutateAsync: updateTemplate } = useUpdateIdentityAuthTemplate();
const isEdit = popUp.editTemplate.isOpen;
const template = popUp.editTemplate?.data?.template;
const {
control,
handleSubmit,
reset,
watch,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
name: "",
method: MachineIdentityAuthMethod.LDAP,
url: "",
bindDN: "",
bindPass: "",
searchBase: "",
ldapCaCertificate: ""
}
});
useEffect(() => {
if (isEdit && template) {
reset({
name: template.name || "",
method: MachineIdentityAuthMethod.LDAP,
url: template.templateFields?.url || "",
bindDN: template.templateFields?.bindDN || "",
bindPass: template.templateFields?.bindPass || "",
searchBase: template.templateFields?.searchBase || "",
ldapCaCertificate: template.templateFields?.ldapCaCertificate || ""
});
} else {
reset({
name: "",
method: MachineIdentityAuthMethod.LDAP,
url: "",
bindDN: "",
bindPass: "",
searchBase: "",
ldapCaCertificate: ""
});
}
}, [isEdit, template, reset]);
const selectedMethod = watch("method");
const onFormSubmit = async (data: FormData) => {
try {
if (isEdit && template) {
await updateTemplate({
templateId: template.id,
organizationId: orgId,
name: data.name,
templateFields: {
url: data.url,
bindDN: data.bindDN,
bindPass: data.bindPass,
searchBase: data.searchBase,
ldapCaCertificate: data.ldapCaCertificate
}
});
createNotification({
text: "Successfully updated auth template",
type: "success"
});
} else {
await createTemplate({
organizationId: orgId,
name: data.name,
authMethod: data.method,
templateFields: {
url: data.url,
bindDN: data.bindDN,
bindPass: data.bindPass,
searchBase: data.searchBase,
ldapCaCertificate: data.ldapCaCertificate
}
});
createNotification({
text: "Successfully created auth template",
type: "success"
});
}
handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false);
reset();
} catch (err) {
console.error(err);
const error = err as any;
const text =
error?.response?.data?.message ?? `Failed to ${isEdit ? "update" : "create"} auth template`;
createNotification({
text,
type: "error"
});
}
};
const handleClose = () => {
handlePopUpToggle(isEdit ? "editTemplate" : "createTemplate", false);
reset();
};
return (
<Modal
isOpen={popUp.createTemplate.isOpen || popUp.editTemplate.isOpen}
onOpenChange={handleClose}
>
<ModalContent
title={isEdit ? "Edit Identity Auth Template" : "Create Identity Auth Template"}
subTitle={
isEdit ? "Update the authentication template" : "Create a new authentication template"
}
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Template Name"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="My Template" />
</FormControl>
)}
/>
<Controller
control={control}
name="method"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Authentication Method"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Select
{...field}
className="w-full"
position="popper"
placeholder="Select auth method..."
dropdownContainerClassName="max-w-none"
onValueChange={(value) => field.onChange(value)}
>
{authMethods.map(({ label, value }) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
{/* LDAP Configuration Fields */}
{selectedMethod === "ldap" && (
<>
<Controller
control={control}
name="url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="LDAP URL"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="ldaps://domain-or-ip:636" type="text" />
</FormControl>
)}
/>
<Controller
control={control}
name="bindDN"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bind DN"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" />
</FormControl>
)}
/>
<Controller
control={control}
name="bindPass"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bind Pass"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="********" type="password" />
</FormControl>
)}
/>
<Controller
control={control}
name="searchBase"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Search Base / DN"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Input {...field} placeholder="ou=machines,dc=acme,dc=com" />
</FormControl>
)}
/>
<Controller
control={control}
name="ldapCaCertificate"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
isOptional
errorText={error?.message}
isError={Boolean(error)}
tooltipText="An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
</FormControl>
)}
/>
</>
)}
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={isSubmitting}
isDisabled={isSubmitting}
>
{isEdit ? "Update Template" : "Create Template"}
</Button>
<Button colorSchema="secondary" variant="plain" onClick={handleClose}>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1,281 @@
import {
faArrowDown,
faArrowUp,
faEdit,
faEllipsisV,
faEye,
faMagnifyingGlass,
faServer,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { OrgPermissionCan } from "@app/components/permissions";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
EmptyState,
IconButton,
Input,
Pagination,
Spinner,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { OrgPermissionSubjects, useOrganization } from "@app/context";
import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types";
import {
getUserTablePreference,
PreferenceKey,
setUserTablePreference
} from "@app/helpers/userTablePreferences";
import { usePagination, useResetPageHelper } from "@app/hooks";
import { OrderByDirection } from "@app/hooks/api/generic/types";
import {
TEMPLATE_UI_LABELS,
useGetIdentityAuthTemplates
} from "@app/hooks/api/identityAuthTemplates";
import { UsePopUpState } from "@app/hooks/usePopUp";
enum TemplatesOrderBy {
Name = "name",
AuthMethod = "authMethod"
}
type Props = {
handlePopUpOpen: (
popUpName: keyof UsePopUpState<
["deleteTemplate", "createTemplate", "editTemplate", "viewUsages"]
>,
data?: any
) => void;
};
export const IdentityAuthTemplatesTable = ({ handlePopUpOpen }: Props) => {
const { currentOrg } = useOrganization();
const {
offset,
limit,
orderBy,
setOrderBy,
orderDirection,
setOrderDirection,
search,
debouncedSearch,
setPage,
setSearch,
perPage,
page,
setPerPage
} = usePagination<TemplatesOrderBy>(TemplatesOrderBy.Name, {
initPerPage: getUserTablePreference("templatesTable", PreferenceKey.PerPage, 20)
});
const handlePerPageChange = (newPerPage: number) => {
setPerPage(newPerPage);
setUserTablePreference("templatesTable", PreferenceKey.PerPage, newPerPage);
};
const organizationId = currentOrg?.id || "";
const { data, isPending, isFetching } = useGetIdentityAuthTemplates({
organizationId,
limit,
offset,
search: debouncedSearch
});
const { templates = [], totalCount = 0 } = data ?? {};
useResetPageHelper({
totalCount,
offset,
setPage
});
const handleSort = (column: TemplatesOrderBy) => {
if (column === orderBy) {
setOrderDirection((prev) =>
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
);
return;
}
setOrderBy(column);
setOrderDirection(OrderByDirection.ASC);
};
return (
<div>
<div className="mb-4 flex items-center space-x-2">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
placeholder="Search templates by name..."
/>
</div>
<TableContainer>
<Table>
<THead>
<Tr className="h-14">
<Th className="w-1/6">
<div className="flex items-center">
Name
<IconButton
variant="plain"
className={`ml-2 ${orderBy === TemplatesOrderBy.Name ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(TemplatesOrderBy.Name)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC &&
orderBy === TemplatesOrderBy.Name
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th className="w-1/6">
<div className="flex items-center">
Method
<IconButton
variant="plain"
className={`ml-2 ${orderBy === TemplatesOrderBy.AuthMethod ? "" : "opacity-30"}`}
ariaLabel="sort"
onClick={() => handleSort(TemplatesOrderBy.AuthMethod)}
>
<FontAwesomeIcon
icon={
orderDirection === OrderByDirection.DESC &&
orderBy === TemplatesOrderBy.AuthMethod
? faArrowUp
: faArrowDown
}
/>
</IconButton>
</div>
</Th>
<Th className="w-2/3">URL</Th>
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={4} innerKey="identity-auth-templates" />}
{!isPending &&
templates?.map((template) => (
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`template-${template.id}`}
>
<Td>{template.name}</Td>
<Td>
<div className="flex items-center">
<span className="uppercase">{template.authMethod}</span>
</div>
</Td>
<Td>
<span className="text-sm text-mineshaft-400">
{template.templateFields.url}
</span>
</Td>
<Td>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
className="w-6"
colorSchema="secondary"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent sideOffset={2} align="end">
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faEye} />}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("viewUsages", { template });
}}
>
{TEMPLATE_UI_LABELS.VIEW_USAGES}
</DropdownMenuItem>
<OrgPermissionCan
I={OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates}
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
>
{(isAllowed) => (
<DropdownMenuItem
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("editTemplate", { template });
}}
isDisabled={!isAllowed}
>
{TEMPLATE_UI_LABELS.EDIT_TEMPLATE}
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates}
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
>
{(isAllowed) => (
<DropdownMenuItem
onClick={(e) => {
e.stopPropagation();
handlePopUpOpen("deleteTemplate", {
templateId: template.id,
name: template.name
});
}}
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
>
{TEMPLATE_UI_LABELS.DELETE_TEMPLATE}
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Td>
</Tr>
))}
</TBody>
</Table>
{!isPending && data && totalCount > 0 && (
<Pagination
count={totalCount}
page={page}
perPage={perPage}
onChangePage={(newPage) => setPage(newPage)}
onChangePerPage={handlePerPageChange}
/>
)}
{!isPending && templates.length === 0 && (
<EmptyState
title={
debouncedSearch.trim().length > 0
? "No templates match search filter"
: "No identity auth templates have been created"
}
icon={faServer}
/>
)}
</TableContainer>
</div>
);
};
@@ -11,6 +11,8 @@ import {
FormControl,
IconButton,
Input,
Select,
SelectItem,
Tab,
TabList,
TabPanel,
@@ -18,23 +20,31 @@ import {
TextArea,
Tooltip
} from "@app/components/v2";
import { useOrganization, useSubscription } from "@app/context";
import { useOrganization, useOrgPermission, useSubscription } from "@app/context";
import {
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSubjects
} from "@app/context/OrgPermissionContext/types";
import {
MachineIdentityAuthMethod,
useAddIdentityLdapAuth,
useGetIdentityLdapAuth,
useUpdateIdentityLdapAuth
} from "@app/hooks/api";
import { IdentityTrustedIp } from "@app/hooks/api/identities/types";
import { useGetAvailableTemplates } from "@app/hooks/api/identityAuthTemplates/queries";
import { UsePopUpState } from "@app/hooks/usePopUp";
import { IdentityFormTab } from "./types";
const schema = z
.object({
url: z.string().min(1),
bindDN: z.string(),
bindPass: z.string(),
searchBase: z.string(),
scope: z.enum(["template", "custom"]),
templateId: z.string().optional(),
url: z.string().optional(),
bindDN: z.string().optional(),
bindPass: z.string().optional(),
searchBase: z.string().optional(),
searchFilter: z.string(), // defaults to (uid={{username}})
ldapCaCertificate: z
.string()
@@ -66,7 +76,50 @@ const schema = z
)
.min(1)
})
.required();
.superRefine((data, ctx) => {
// Validation based on scope
if (data.scope === "template") {
if (!data.templateId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Template is required when using template scope",
path: ["templateId"]
});
}
return;
}
if (data.scope === "custom") {
if (!data.url) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "LDAP URL is required when using custom scope",
path: ["url"]
});
}
if (!data.bindDN) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Bind DN is required when using custom scope",
path: ["bindDN"]
});
}
if (!data.bindPass) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Bind Pass is required when using custom scope",
path: ["bindPass"]
});
}
if (!data.searchBase) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Search Base is required when using custom scope",
path: ["searchBase"]
});
}
}
});
export type FormData = z.infer<typeof schema>;
@@ -93,6 +146,13 @@ export const IdentityLdapAuthForm = ({
const { mutateAsync: addMutateAsync } = useAddIdentityLdapAuth();
const { mutateAsync: updateMutateAsync } = useUpdateIdentityLdapAuth();
const [tabValue, setTabValue] = useState<IdentityFormTab>(IdentityFormTab.Configuration);
const { data: templates } = useGetAvailableTemplates(MachineIdentityAuthMethod.LDAP);
const { permission } = useOrgPermission();
const canAttachTemplates = permission.can(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { data } = useGetIdentityLdapAuth(identityId ?? "", {
enabled: isUpdate
@@ -102,11 +162,14 @@ export const IdentityLdapAuthForm = ({
control,
handleSubmit,
reset,
watch,
setValue,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
defaultValues: {
scope: "custom",
templateId: "",
url: "",
bindDN: "",
bindPass: "",
@@ -119,6 +182,8 @@ export const IdentityLdapAuthForm = ({
}
});
const scope = watch("scope");
const {
fields: accessTokenTrustedIpsFields,
append: appendAccessTokenTrustedIp,
@@ -131,16 +196,30 @@ export const IdentityLdapAuthForm = ({
remove: removeAllowedField
} = useFieldArray({ control, name: "allowedFields" });
// Helper function to determine scope based on existing data
const determineScope = (authData: any) => {
// If templateId exists in the data, it's template scope
if (authData.templateId) {
return "template";
}
// Default to custom if we can't determine
return "custom";
};
useEffect(() => {
if (data) {
const detectedScope = determineScope(data);
reset({
url: data.url,
bindDN: data.bindDN,
bindPass: data.bindPass,
searchBase: data.searchBase,
scope: detectedScope,
templateId: data.templateId || "",
url: data.url || "",
bindDN: data.bindDN || "",
bindPass: data.bindPass || "",
searchBase: data.searchBase || "",
searchFilter: data.searchFilter,
ldapCaCertificate: data.ldapCaCertificate || undefined,
allowedFields: data.allowedFields,
allowedFields: data.allowedFields || [],
accessTokenTTL: String(data.accessTokenTTL),
accessTokenMaxTTL: String(data.accessTokenMaxTTL),
accessTokenNumUsesLimit: String(data.accessTokenNumUsesLimit),
@@ -152,78 +231,81 @@ export const IdentityLdapAuthForm = ({
}
)
});
} else {
reset({
url: "",
bindDN: "",
bindPass: "",
searchBase: "",
searchFilter: "(uid={{username}})",
ldapCaCertificate: undefined,
allowedFields: [],
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
return;
}
}, [data]);
reset({
scope: "custom",
templateId: "",
url: "",
bindDN: "",
bindPass: "",
searchBase: "",
searchFilter: "(uid={{username}})",
ldapCaCertificate: undefined,
allowedFields: [],
accessTokenTTL: "2592000",
accessTokenMaxTTL: "2592000",
accessTokenNumUsesLimit: "0",
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
}, [data, reset]);
useEffect(() => {
if (!subscription?.ldap) {
handlePopUpOpen("upgradePlan");
handlePopUpToggle("identityAuthMethod", false);
}
}, [subscription]);
}, [subscription, handlePopUpOpen, handlePopUpToggle]);
const onFormSubmit = async ({
url,
bindDN,
bindPass,
searchBase,
searchFilter,
ldapCaCertificate,
allowedFields,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
}: FormData) => {
const onFormSubmit = async (formData: FormData) => {
try {
if (!identityId) return;
const {
scope: submissionScope,
templateId: submissionTemplateId,
url: submissionUrl,
bindDN: submissionBindDN,
bindPass: submissionBindPass,
searchBase: submissionSearchBase,
searchFilter,
ldapCaCertificate,
allowedFields,
accessTokenTTL,
accessTokenMaxTTL,
accessTokenNumUsesLimit,
accessTokenTrustedIps
} = formData;
const basePayload = {
organizationId: orgId,
identityId,
searchFilter,
ldapCaCertificate,
allowedFields,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
};
// Add scope-specific fields
const payload =
submissionScope === "template"
? { ...basePayload, templateId: submissionTemplateId }
: {
...basePayload,
url: submissionUrl,
bindDN: submissionBindDN,
bindPass: submissionBindPass,
searchBase: submissionSearchBase
};
if (data) {
await updateMutateAsync({
organizationId: orgId,
identityId,
url,
bindDN,
bindPass,
searchBase,
searchFilter,
ldapCaCertificate,
allowedFields,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
await updateMutateAsync(payload);
} else {
await addMutateAsync({
organizationId: orgId,
identityId,
url,
bindDN,
bindPass,
searchBase,
searchFilter,
ldapCaCertificate,
allowedFields,
accessTokenTTL: Number(accessTokenTTL),
accessTokenMaxTTL: Number(accessTokenMaxTTL),
accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit),
accessTokenTrustedIps
});
await addMutateAsync(payload);
}
handlePopUpToggle("identityAuthMethod", false);
@@ -247,6 +329,8 @@ export const IdentityLdapAuthForm = ({
onSubmit={handleSubmit(onFormSubmit, (fields) => {
setTabValue(
[
"scope",
"templateId",
"url",
"bindDN",
"bindPass",
@@ -268,18 +352,102 @@ export const IdentityLdapAuthForm = ({
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
</TabList>
<TabPanel value={IdentityFormTab.Configuration}>
{canAttachTemplates && (
<Controller
control={control}
name="scope"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Configuration Type"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
value={value}
onValueChange={(val) => {
onChange(val);
setValue("templateId", data?.templateId || "");
setValue("url", data?.url || "");
setValue("bindDN", data?.bindDN || "");
setValue("bindPass", data?.bindPass || "");
setValue("searchBase", data?.searchBase || "");
setValue("ldapCaCertificate", data?.ldapCaCertificate || "");
}}
className="w-full"
position="popper"
dropdownContainerClassName="max-w-none"
>
<SelectItem value="template">Use Template</SelectItem>
<SelectItem value="custom">Custom Configuration</SelectItem>
</Select>
</FormControl>
)}
/>
)}
{scope === "template" && (
<Controller
control={control}
name="templateId"
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
label="Template"
isError={Boolean(error)}
errorText={error?.message}
isRequired
>
<Select
value={value}
onValueChange={(val) => {
onChange(val);
const tmp = templates?.find((t) => t.id === val);
if (!tmp) return;
setValue("url", tmp.templateFields.url);
setValue("bindDN", tmp.templateFields.bindDN);
setValue("bindPass", tmp.templateFields.bindPass);
setValue("searchBase", tmp.templateFields.searchBase);
setValue("ldapCaCertificate", tmp.templateFields.ldapCaCertificate);
}}
className="w-full"
position="popper"
dropdownContainerClassName="max-w-none"
placeholder="Select a template"
>
{templates?.map((template) => {
return (
<SelectItem value={template.id} key={template.id}>
{template.name}
</SelectItem>
);
})}
</Select>
</FormControl>
)}
/>
)}
<Controller
control={control}
defaultValue="2592000"
name="url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="LDAP URL"
isError={Boolean(error)}
errorText={error?.message}
tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: undefined
}
isRequired
>
<Input {...field} placeholder="ldaps://domain-or-ip:636" type="text" />
<Input
{...field}
placeholder="ldaps://domain-or-ip:636"
type="text"
isDisabled={scope === "template"}
containerClassName={scope === "template" ? "opacity-55" : ""}
/>
</FormControl>
)}
/>
@@ -292,14 +460,23 @@ export const IdentityLdapAuthForm = ({
label="Bind DN"
isError={Boolean(error)}
errorText={error?.message}
tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: undefined
}
>
<Input {...field} placeholder="cn=infisical,ou=Users,dc=example,dc=com" />
<Input
{...field}
containerClassName={scope === "template" ? "opacity-55" : ""}
placeholder="cn=infisical,ou=Users,dc=example,dc=com"
isDisabled={scope === "template"}
/>
</FormControl>
)}
/>
<Controller
control={control}
defaultValue=""
name="bindPass"
render={({ field, fieldState: { error } }) => (
<FormControl
@@ -307,8 +484,19 @@ export const IdentityLdapAuthForm = ({
label="Bind Pass"
isError={Boolean(error)}
errorText={error?.message}
tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: undefined
}
>
<Input {...field} placeholder="********" type="password" />
<Input
{...field}
placeholder="********"
type="password"
containerClassName={scope === "template" ? "opacity-55" : ""}
isDisabled={scope === "template"}
/>
</FormControl>
)}
/>
@@ -321,8 +509,18 @@ export const IdentityLdapAuthForm = ({
label="Search Base / DN"
isError={Boolean(error)}
errorText={error?.message}
tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: undefined
}
>
<Input {...field} placeholder="ou=machines,dc=acme,dc=com" />
<Input
{...field}
placeholder="ou=machines,dc=acme,dc=com"
containerClassName={scope === "template" ? "opacity-55" : ""}
isDisabled={scope === "template"}
/>
</FormControl>
)}
/>
@@ -452,7 +650,6 @@ export const IdentityLdapAuthForm = ({
<Controller
control={control}
defaultValue="2592000"
name="accessTokenTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
@@ -467,7 +664,6 @@ export const IdentityLdapAuthForm = ({
/>
<Controller
control={control}
defaultValue="2592000"
name="accessTokenMaxTTL"
render={({ field, fieldState: { error } }) => (
<FormControl
@@ -482,7 +678,6 @@ export const IdentityLdapAuthForm = ({
/>
<Controller
control={control}
defaultValue="0"
name="accessTokenNumUsesLimit"
render={({ field, fieldState: { error } }) => (
<FormControl
@@ -506,9 +701,18 @@ export const IdentityLdapAuthForm = ({
isOptional
errorText={error?.message}
isError={Boolean(error)}
tooltipText="An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
tooltipText={
scope === "template"
? "This field cannot be modified when using a template"
: "An optional PEM-encoded CA cert for the LDAP server. This is used by the TLS client for secure communication with the LDAP server."
}
>
<TextArea {...field} placeholder="-----BEGIN CERTIFICATE----- ..." />
<TextArea
{...field}
placeholder="-----BEGIN CERTIFICATE----- ..."
className={scope === "template" ? "opacity-55" : ""}
isDisabled={scope === "template"}
/>
</FormControl>
)}
/>
@@ -11,15 +11,18 @@ import {
useOrganization,
useSubscription
} from "@app/context";
import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types";
import { withPermission } from "@app/hoc";
import { useDeleteIdentity } from "@app/hooks/api";
import { useDeleteIdentityAuthTemplate } from "@app/hooks/api/identityAuthTemplates";
import { usePopUp } from "@app/hooks/usePopUp";
// import { IdentityAuthMethodModal } from "./IdentityAuthMethodModal";
import { IdentityAuthTemplateModal } from "./IdentityAuthTemplateModal";
import { IdentityAuthTemplatesTable } from "./IdentityAuthTemplatesTable";
import { IdentityModal } from "./IdentityModal";
import { IdentityTable } from "./IdentityTable";
import { IdentityTokenAuthTokenModal } from "./IdentityTokenAuthTokenModal";
// import { IdentityUniversalAuthClientSecretModal } from "./IdentityUniversalAuthClientSecretModal";
import { MachineAuthTemplateUsagesModal } from "./MachineAuthTemplateUsagesModal";
export const IdentitySection = withPermission(
() => {
@@ -28,6 +31,7 @@ export const IdentitySection = withPermission(
const orgId = currentOrg?.id || "";
const { mutateAsync: deleteMutateAsync } = useDeleteIdentity();
const { mutateAsync: deleteTemplateMutateAsync } = useDeleteIdentityAuthTemplate();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"identity",
"identityAuthMethod",
@@ -35,7 +39,11 @@ export const IdentitySection = withPermission(
"universalAuthClientSecret",
"deleteUniversalAuthClientSecret",
"upgradePlan",
"tokenAuthToken"
"tokenAuthToken",
"createTemplate",
"editTemplate",
"deleteTemplate",
"viewUsages"
] as const);
const isMoreIdentitiesAllowed = subscription?.identityLimit
@@ -69,53 +77,139 @@ export const IdentitySection = withPermission(
}
};
const onDeleteTemplateSubmit = async (templateId: string) => {
try {
await deleteTemplateMutateAsync({
templateId,
organizationId: orgId
});
createNotification({
text: "Successfully deleted template",
type: "success"
});
handlePopUpClose("deleteTemplate");
} catch (err) {
console.error(err);
const error = err as any;
const text = error?.response?.data?.message ?? "Failed to delete template";
createNotification({
text,
type: "error"
});
}
};
return (
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<OrgPermissionCan
I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", {
description: "You can add more identities if you upgrade your Infisical plan."
});
return;
}
handlePopUpOpen("identity");
}}
isDisabled={!isAllowed}
<div>
<div className="rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Identities</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/overview"
target="_blank"
rel="noopener noreferrer"
>
Create Identity
</Button>
)}
</OrgPermissionCan>
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<OrgPermissionCan
I={OrgPermissionIdentityActions.Create}
a={OrgPermissionSubjects.Identity}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (!isMoreIdentitiesAllowed && !isEnterprise) {
handlePopUpOpen("upgradePlan", {
description:
"You can add more identities if you upgrade your Infisical plan."
});
return;
}
handlePopUpOpen("identity");
}}
isDisabled={!isAllowed}
>
Create Identity
</Button>
)}
</OrgPermissionCan>
</div>
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
</div>
{/* Identity Auth Templates Section */}
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-1">
<p className="text-xl font-semibold text-mineshaft-100">Identity Auth Templates</p>
<a
href="https://infisical.com/docs/documentation/platform/identities/auth-templates"
target="_blank"
rel="noopener noreferrer"
>
<div className="ml-1 mt-[0.16rem] inline-block rounded-md bg-yellow/20 px-1.5 text-sm text-yellow opacity-80 hover:opacity-100">
<FontAwesomeIcon icon={faBookOpen} className="mr-1.5" />
<span>Docs</span>
<FontAwesomeIcon
icon={faArrowUpRightFromSquare}
className="mb-[0.07rem] ml-1.5 text-[10px]"
/>
</div>
</a>
</div>
<OrgPermissionCan
I={OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates}
a={OrgPermissionSubjects.MachineIdentityAuthTemplate}
>
{(isAllowed) => (
<Button
colorSchema="secondary"
type="submit"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
if (subscription && !subscription.kmip) {
handlePopUpOpen("upgradePlan");
return;
}
handlePopUpOpen("createTemplate");
}}
isDisabled={!isAllowed}
>
Create Template
</Button>
)}
</OrgPermissionCan>
</div>
<IdentityAuthTemplatesTable handlePopUpOpen={handlePopUpOpen} />
</div>
<IdentityTable handlePopUpOpen={handlePopUpOpen} />
<IdentityModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<IdentityAuthTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
<MachineAuthTemplateUsagesModal
isOpen={popUp.viewUsages.isOpen}
onClose={() => handlePopUpClose("viewUsages")}
templateId={
(popUp?.viewUsages?.data as { template: { id: string; name: string } })?.template?.id ||
""
}
templateName={
(popUp?.viewUsages?.data as { template: { id: string; name: string } })?.template
?.name || ""
}
/>
{/* <IdentityAuthMethodModal
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
@@ -140,10 +234,23 @@ export const IdentitySection = withPermission(
)
}
/>
<DeleteActionModal
isOpen={popUp.deleteTemplate.isOpen}
title={`Are you sure you want to delete ${
(popUp?.deleteTemplate?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteTemplate", isOpen)}
deleteKey="confirm"
onDeleteApproved={() =>
onDeleteTemplateSubmit(
(popUp?.deleteTemplate?.data as { templateId: string })?.templateId
)
}
/>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text={(popUp.upgradePlan?.data as { description: string })?.description}
text="You can use Identity Auth Templates if you switch to Infisical's Enterprise plan."
/>
</div>
);
@@ -0,0 +1,90 @@
import { faCertificate } from "@fortawesome/free-solid-svg-icons";
import { useNavigate } from "@tanstack/react-router";
import {
EmptyState,
Modal,
ModalContent,
Table,
TableContainer,
TableSkeleton,
TBody,
Td,
Th,
THead,
Tr
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import { useGetTemplateUsages } from "@app/hooks/api/identityAuthTemplates";
type Props = {
isOpen: boolean;
onClose: () => void;
templateId: string;
templateName: string;
};
export const MachineAuthTemplateUsagesModal = ({
isOpen,
onClose,
templateId,
templateName
}: Props) => {
const { currentOrg } = useOrganization();
const navigate = useNavigate();
const organizationId = currentOrg?.id || "";
const { data: usages = [], isPending } = useGetTemplateUsages({
templateId,
organizationId
});
return (
<Modal isOpen={isOpen} onOpenChange={onClose}>
<ModalContent title={`Usages for Identity Auth Template: ${templateName}`}>
<div>
<TableContainer>
<Table>
<THead>
<Tr className="h-14">
<Th>Identity Name</Th>
<Th>Identity ID</Th>
</Tr>
</THead>
<TBody>
{isPending && <TableSkeleton columns={3} innerKey="template-usages" />}
{!isPending &&
usages.map((usage) => (
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
key={`usage-${usage.identityId}`}
onClick={() =>
navigate({
to: "/organization/identities/$identityId",
params: {
identityId: usage.identityId
}
})
}
>
<Td>{usage.identityName}</Td>
<Td>
<span className="text-sm text-mineshaft-400">{usage.identityId}</span>
</Td>
</Tr>
))}
</TBody>
</Table>
{!isPending && usages.length === 0 && (
<EmptyState
title="This template is not currently being used by any identities"
icon={faCertificate}
/>
)}
</TableContainer>
</div>
</ModalContent>
</Modal>
);
};
@@ -119,7 +119,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="mt-4 py-4">
<DropdownMenuContent align="end" className="mt-4 overflow-visible py-4">
<form onSubmit={handleSubmit(setFilter)}>
<div className="flex min-w-64 flex-col font-inter">
<div className="mb-3 flex items-center border-b border-b-mineshaft-500 px-3 pb-2">
@@ -176,7 +176,8 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
</div>
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
align="end"
sideOffset={2}
className="thin-scrollbar z-[100] max-h-80 overflow-hidden"
>
<div className="max-h-80 overflow-y-auto">
@@ -258,6 +259,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
else setValue("userAgentType", e as UserAgentType, { shouldDirty: true });
}}
className={twMerge("w-full border border-mineshaft-500 bg-mineshaft-700")}
position="popper"
>
<SelectItem value="all" key="all">
All sources
@@ -319,7 +321,6 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
<AnimatePresence initial={false}>
{showSecretsSection && (
<motion.div
className="overflow-hidden"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
@@ -352,6 +353,7 @@ export const LogsFilter = ({ presets, setFilter, filter, project }: Props) => {
>
<FilterableSelect
value={value}
menuPlacement="top"
key={value?.name || "filter-environment"}
isClearable
isDisabled={!selectedProject}
@@ -9,6 +9,7 @@ import {
OrgPermissionGroupActions,
OrgPermissionIdentityActions,
OrgPermissionKmipActions,
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSecretShareAction
} from "@app/context/OrgPermissionContext/types";
import { TPermission } from "@app/hooks/api/roles/types";
@@ -82,6 +83,17 @@ const orgGatewayPermissionSchema = z
})
.optional();
const machineIdentityAuthTemplatePermissionSchema = z
.object({
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: z.boolean().optional(),
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: z.boolean().optional()
})
.optional();
const adminConsolePermissionSchmea = z
.object({
"access-all-projects": z.boolean().optional()
@@ -129,6 +141,7 @@ export const formSchema = z.object({
"app-connections": appConnectionsPermissionSchema,
kmip: kmipPermissionSchema,
gateway: orgGatewayPermissionSchema,
"machine-identity-auth-template": machineIdentityAuthTemplatePermissionSchema,
"secret-share": secretSharingPermissionSchema
})
.optional()
@@ -0,0 +1,211 @@
import { useEffect, useMemo } from "react";
import { Control, Controller, UseFormSetValue, useWatch } from "react-hook-form";
import { faChevronDown, faChevronRight } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { createNotification } from "@app/components/notifications";
import { Checkbox, Select, SelectItem, Td, Tr } from "@app/components/v2";
import { OrgPermissionMachineIdentityAuthTemplateActions } from "@app/context/OrgPermissionContext/types";
import { useToggle } from "@app/hooks";
import { TFormSchema } from "../OrgRoleModifySection.utils";
type Props = {
isEditable: boolean;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
};
enum Permission {
NoAccess = "no-access",
ReadOnly = "read-only",
FullAccess = "full-access",
Custom = "custom"
}
const PERMISSION_ACTIONS = [
{
action: OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
label: "List Templates"
},
{
action: OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
label: "Create Templates"
},
{
action: OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates,
label: "Edit Templates"
},
{
action: OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
label: "Delete Templates"
},
{
action: OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
label: "Unlink Templates"
},
{
action: OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
label: "Attach Templates"
}
] as const;
export const OrgPermissionMachineIdentityAuthTemplateRow = ({
isEditable,
control,
setValue
}: Props) => {
const [isRowExpanded, setIsRowExpanded] = useToggle();
const [isCustom, setIsCustom] = useToggle();
const rule = useWatch({
control,
name: "permissions.machine-identity-auth-template"
});
const selectedPermissionCategory = useMemo(() => {
const actions = Object.keys(rule || {}) as Array<keyof typeof rule>;
const totalActions = PERMISSION_ACTIONS.length;
const score = actions.map((key) => (rule?.[key] ? 1 : 0)).reduce((a, b) => a + b, 0 as number);
if (isCustom) return Permission.Custom;
if (score === 0) return Permission.NoAccess;
if (score === totalActions) return Permission.FullAccess;
if (score === 1 && rule?.[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates])
return Permission.ReadOnly;
return Permission.Custom;
}, [rule, isCustom]);
useEffect(() => {
if (selectedPermissionCategory === Permission.Custom) setIsCustom.on();
else setIsCustom.off();
}, [selectedPermissionCategory]);
useEffect(() => {
const isRowCustom = selectedPermissionCategory === Permission.Custom;
if (isRowCustom) {
setIsRowExpanded.on();
}
}, []);
const handlePermissionChange = (val: Permission) => {
if (!val) return;
if (val === Permission.Custom) {
setIsRowExpanded.on();
setIsCustom.on();
return;
}
setIsCustom.off();
switch (val) {
case Permission.FullAccess:
setValue(
"permissions.machine-identity-auth-template",
{
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: true
},
{ shouldDirty: true }
);
break;
case Permission.ReadOnly:
setValue(
"permissions.machine-identity-auth-template",
{
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: true,
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: true
},
{ shouldDirty: true }
);
break;
case Permission.NoAccess:
default:
setValue(
"permissions.machine-identity-auth-template",
{
[OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates]: false,
[OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates]: false
},
{ shouldDirty: true }
);
}
};
return (
<>
<Tr
className="h-10 cursor-pointer transition-colors duration-100 hover:bg-mineshaft-700"
onClick={() => setIsRowExpanded.toggle()}
>
<Td className="w-4">
<FontAwesomeIcon className="w-4" icon={isRowExpanded ? faChevronDown : faChevronRight} />
</Td>
<Td className="w-full select-none">Machine Identity Auth Templates</Td>
<Td>
<Select
value={selectedPermissionCategory}
className="h-8 w-40 bg-mineshaft-700"
dropdownContainerClassName="border text-left border-mineshaft-600 bg-mineshaft-800"
onValueChange={handlePermissionChange}
isDisabled={!isEditable}
position="popper"
>
<SelectItem value={Permission.NoAccess}>No Access</SelectItem>
<SelectItem value={Permission.ReadOnly}>Read Only</SelectItem>
<SelectItem value={Permission.FullAccess}>Full Access</SelectItem>
<SelectItem value={Permission.Custom}>Custom</SelectItem>
</Select>
</Td>
</Tr>
{isRowExpanded && (
<Tr>
<Td colSpan={3} className="border-mineshaft-500 bg-mineshaft-900 p-8">
<div className="flex flex-grow flex-wrap justify-start gap-x-8 gap-y-4">
{PERMISSION_ACTIONS.map(({ action, label }) => {
return (
<Controller
name={`permissions.machine-identity-auth-template.${action}`}
key={`permissions.machine-identity-auth-template.${action}`}
control={control}
render={({ field }) => (
<Checkbox
isChecked={Boolean(field.value)}
onCheckedChange={(e) => {
if (!isEditable) {
createNotification({
type: "error",
text: "Failed to update default role"
});
return;
}
field.onChange(e);
}}
id={`permissions.machine-identity-auth-template.${action}`}
>
{label}
</Checkbox>
)}
/>
);
})}
</div>
</Td>
</Tr>
)}
</>
);
};
@@ -65,7 +65,13 @@ type Props = {
title: string;
formName: keyof Omit<
Exclude<TFormSchema["permissions"], undefined>,
"workspace" | "organization-admin-console" | "kmip" | "gateway" | "secret-share" | "billing"
| "workspace"
| "organization-admin-console"
| "kmip"
| "gateway"
| "secret-share"
| "billing"
| "machine-identity-auth-template"
>;
setValue: UseFormSetValue<TFormSchema>;
control: Control<TFormSchema>;
@@ -21,6 +21,7 @@ import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow";
import { OrgPermissionGroupRow } from "./OrgPermissionGroupRow";
import { OrgPermissionIdentityRow } from "./OrgPermissionIdentityRow";
import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow";
import { OrgPermissionMachineIdentityAuthTemplateRow } from "./OrgPermissionMachineIdentityAuthTemplateRow";
import { OrgPermissionSecretShareRow } from "./OrgPermissionSecretShareRow";
import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow";
import { RolePermissionRow } from "./RolePermissionRow";
@@ -205,6 +206,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => {
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionMachineIdentityAuthTemplateRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgPermissionKmipRow
control={control}
setValue={setValue}
@@ -1207,13 +1207,16 @@ export const OverviewPage = () => {
secretsToDeleteKeys={secretsToDeleteKeys}
usedBySecretSyncs={usedBySecretSyncs}
/>
<div ref={tableRef} className="thin-scrollbar mt-4">
<div ref={tableRef} className="mt-4">
<TableContainer
onScroll={(e) => setScrollOffset(e.currentTarget.scrollLeft)}
className="thin-scrollbar rounded-b-none"
className="thin-scrollbar max-h-[66vh] overflow-y-auto rounded-b-none"
>
<Table>
<THead style={{ height: collapseEnvironments ? headerHeight : undefined }}>
<THead
className="sticky top-0 z-20"
style={{ height: collapseEnvironments ? headerHeight : undefined }}
>
<Tr
className="sticky top-0 z-20 border-0"
style={{ height: collapseEnvironments ? headerHeight : undefined }}
@@ -1370,7 +1373,7 @@ export const OverviewPage = () => {
})}
onClick={() => handleExploreEnvClick(slug)}
>
<p className="truncate font-medium">{name}</p>
<p className="truncate font-medium underline">{name}</p>
</button>
{!collapseEnvironments && missingKeyCount > 0 && (
<Tooltip
@@ -139,7 +139,7 @@ export const SecretItem = memo(
);
const getDefaultValue = () => {
if (secret.secretValueHidden) {
if (secret.secretValueHidden && !isPending) {
return canEditSecretValue ? HIDDEN_SECRET_VALUE : "";
}
return secret.valueOverride || secret.value || "";
@@ -219,7 +219,7 @@ export const SecretItem = memo(
}
if (isDirty && !isSubmitting && !isAutoSavingRef.current) {
const debounceTime = 600;
const debounceTime = 200;
autoSaveTimeoutRef.current = setTimeout(() => {
autoSaveChanges(formValues);
@@ -410,7 +410,7 @@ export const SecretItem = memo(
tabIndex={0}
role="button"
>
{secretValueHidden && !isOverridden && (
{secretValueHidden && !isOverridden && !isPending && (
<Tooltip
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
>
@@ -441,12 +441,14 @@ export const SecretItem = memo(
<InfisicalSecretInput
isReadOnly={isReadOnlySecret}
key="secret-value"
isVisible={isVisible && !secretValueHidden}
canEditButNotView={secretValueHidden && !isOverridden}
isVisible={isVisible && (!secretValueHidden || isPending)}
canEditButNotView={secretValueHidden && !isOverridden && !isPending}
environment={environment}
secretPath={secretPath}
{...field}
defaultValue={secretValueHidden ? HIDDEN_SECRET_VALUE : undefined}
defaultValue={
secretValueHidden && !isPending ? HIDDEN_SECRET_VALUE : undefined
}
containerClassName="py-1.5 rounded-md transition-all"
/>
)}
@@ -343,31 +343,18 @@ export const SecretListView = ({
id: orgSecret.id,
type: PendingAction.Update,
secretKey: trueOriginalSecret.key,
...(key !== trueOriginalSecret.key && { newSecretName: key }),
...(value !== trueOriginalSecret.value && {
originalValue: trueOriginalSecret.value,
secretValue: value
}),
...(comment !== trueOriginalSecret.comment && {
originalComment: trueOriginalSecret.comment,
secretComment: comment
}),
...(modSecret.skipMultilineEncoding !==
trueOriginalSecret.skipMultilineEncoding && {
originalSkipMultilineEncoding: trueOriginalSecret.skipMultilineEncoding,
skipMultilineEncoding: modSecret.skipMultilineEncoding
}),
...(!isSameTags && {
originalTags:
trueOriginalSecret.tags?.map((tag) => ({ id: tag.id, slug: tag.slug })) || [],
tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || []
}),
...(JSON.stringify(secretMetadata) !==
JSON.stringify(trueOriginalSecret.secretMetadata) && {
originalSecretMetadata: trueOriginalSecret.secretMetadata || [],
secretMetadata: secretMetadata || []
}),
newSecretName: key,
originalValue: trueOriginalSecret.value,
secretValue: value,
originalComment: trueOriginalSecret.comment,
secretComment: comment,
originalSkipMultilineEncoding: trueOriginalSecret.skipMultilineEncoding,
skipMultilineEncoding: modSecret.skipMultilineEncoding,
originalTags:
trueOriginalSecret.tags?.map((tag) => ({ id: tag.id, slug: tag.slug })) || [],
tags: tags?.map((tag) => ({ id: tag.id, slug: tag.name || tag.slug || "" })) || [],
originalSecretMetadata: trueOriginalSecret.secretMetadata || [],
secretMetadata: secretMetadata || [],
timestamp: Date.now(),
resourceType: "secret",
existingSecret: orgSecret
@@ -8,12 +8,12 @@ import { useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import attemptChangePassword from "@app/components/utilities/attemptChangePassword";
import checkPassword from "@app/components/utilities/checks/password/checkPassword";
import { Button, FormControl, Input } from "@app/components/v2";
import { useUser } from "@app/context";
import { useResetUserPasswordV2, useSendPasswordSetupEmail } from "@app/hooks/api/auth/queries";
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
import { clearSession } from "@app/hooks/api/users/queries";
type Errors = {
tooShort?: string;
@@ -62,19 +62,22 @@ export const ChangePasswordSection = () => {
if (errorCheck) return;
setIsLoading(true);
if (user.encryptionVersion === UserEncryptionVersion.V2) {
await resetPasswordV2({
oldPassword,
newPassword
});
} else {
await attemptChangePassword({
email: user.username,
currentPassword: oldPassword,
newPassword
if (user.encryptionVersion !== UserEncryptionVersion.V2) {
createNotification({
text: "Legacy encryption scheme not supported for changing password. Please contact support.",
type: "error"
});
setIsLoading(false);
return;
}
await resetPasswordV2({
oldPassword,
newPassword
});
clearSession();
setIsLoading(false);
createNotification({
text: "Successfully changed password",
@@ -1,80 +0,0 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import issueBackupKey from "@app/components/utilities/cryptography/issueBackupKey";
import { Button, FormControl, Input } from "@app/components/v2";
import { useUser } from "@app/context";
const schema = z
.object({
password: z.string().describe("Password is required")
})
.required();
export type FormData = z.infer<typeof schema>;
export const EmergencyKitSection = () => {
const { user } = useUser();
const { reset, control, handleSubmit } = useForm({
defaultValues: {
password: ""
},
resolver: zodResolver(schema)
});
const onFormSubmit = ({ password }: FormData) => {
try {
if (!user?.email) return;
issueBackupKey({
email: user.email,
password,
personalName: `${user.firstName} ${user.lastName}`,
setBackupKeyError: () => {},
setBackupKeyIssued: () => {}
});
reset();
} catch (err) {
console.error(err);
createNotification({
text: "Failed to download emergency kit",
type: "error"
});
}
};
return (
<form
onSubmit={handleSubmit(onFormSubmit)}
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
>
<h2 className="flex-1 text-xl font-semibold text-mineshaft-100">Emergency Kit</h2>
<p className="mb-8 text-gray-400">
The kit contains information you can use to recover your account.
</p>
<div className="max-w-md">
<Controller
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl isError={Boolean(error)} errorText={error?.message}>
<Input
placeholder="Password"
type="password"
{...field}
className="bg-mineshaft-800"
/>
</FormControl>
)}
control={control}
name="password"
/>
</div>
<Button type="submit" colorSchema="secondary" isLoading={false}>
Save
</Button>
</form>
);
};
@@ -1 +0,0 @@
export { EmergencyKitSection } from "./EmergencyKitSection";
@@ -1,20 +1,12 @@
import { useUser } from "@app/context";
import { UserEncryptionVersion } from "@app/hooks/api/auth/types";
import { DeleteAccountSection } from "../DeleteAccountSection";
import { EmergencyKitSection } from "../EmergencyKitSection";
import { SessionsSection } from "../SessionsSection";
import { UserNameSection } from "../UserNameSection";
export const PersonalGeneralTab = () => {
const { user } = useUser();
const encryptionVersion = user?.encryptionVersion ?? UserEncryptionVersion.V2;
return (
<div>
<UserNameSection />
<SessionsSection />
{encryptionVersion === UserEncryptionVersion.V1 && <EmergencyKitSection />}
<DeleteAccountSection />
</div>
);