Merge branch 'main' into feat/create-multiple-orgs-under-same-account

This commit is contained in:
vmatsiiako
2023-10-16 18:55:48 -07:00
committed by GitHub
29 changed files with 484 additions and 104 deletions

View File

@@ -133,7 +133,6 @@ Whether it's big or small, we love contributions. Check out our guide to see how
Not sure where to get started? You can:
- [Book a free, non-pressure pairing session / code walkthrough with one of our teammates](https://cal.com/tony-infisical/30-min-meeting-contributing)!
- Join our <a href="https://infisical.com/slack">Slack</a>, and ask us any questions there.
- Join our [community calls](https://us06web.zoom.us/j/82623506356) every Wednesday at 11am EST to ask any questions, provide feedback, hangout and more.

View File

@@ -9,7 +9,6 @@ import { Secret, ServiceTokenData } from "../../models";
import { Folder } from "../../models/folder";
import {
appendFolder,
generateFolderId,
getAllFolderIds,
getFolderByPath,
getFolderWithPathFromId,
@@ -132,9 +131,6 @@ export const createFolder = async (req: Request, res: Response) => {
// space has no folders initialized
if (!folders) {
if (directory !== "/") throw ERR_FOLDER_NOT_FOUND;
const id = generateFolderId();
const folder = new Folder({
workspace: workspaceId,
environment,
@@ -142,14 +138,15 @@ export const createFolder = async (req: Request, res: Response) => {
id: "root",
name: "root",
version: 1,
children: [{ id, name: folderName, children: [], version: 1 }]
children: []
}
});
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
await folder.save();
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: folder.nodes
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
@@ -163,9 +160,9 @@ export const createFolder = async (req: Request, res: Response) => {
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: id,
folderId: child.id,
folderName,
folderPath: `root/${folderName}`
folderPath: directory
}
},
{
@@ -173,26 +170,26 @@ export const createFolder = async (req: Request, res: Response) => {
}
);
return res.json({ folder: { id, name: folderName } });
return res.json({ folder: { id: child.id, name: folderName } });
}
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
if (!hasCreated) return res.json({ folder: child });
const folder = appendFolder(folders.nodes, { folderName, parentFolderId: parentFolder.id });
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
folderId: child.id
});
await EEAuditLogService.createAuditLog(
@@ -201,7 +198,7 @@ export const createFolder = async (req: Request, res: Response) => {
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: folder.id,
folderId: child.id,
folderName,
folderPath: directory
}
@@ -211,7 +208,7 @@ export const createFolder = async (req: Request, res: Response) => {
}
);
return res.json({ folder });
return res.json({ folder: child });
};
/**

View File

@@ -777,7 +777,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/
export const deleteSecretByName = async (req: Request, res: Response) => {
const {
body: { type, environment, secretPath, workspaceId },
body: { type, environment, secretPath, workspaceId, secretId },
params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
@@ -813,6 +813,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
const { secret } = await SecretService.deleteSecret({
secretName,
secretId,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,

View File

@@ -1,4 +1,4 @@
import mongoose, { Types } from "mongoose";
import mongoose, { Types, mongo } from "mongoose";
import {
Bot,
BotKey,
@@ -111,48 +111,78 @@ export const createOrganization = async ({
* @returns
*/
export const deleteOrganization = async ({
organizationId
organizationId,
existingSession
}: {
organizationId: Types.ObjectId;
existingSession?: mongo.ClientSession;
}) => {
const session = await mongoose.startSession();
session.startTransaction();
let session;
if (existingSession) {
session = existingSession;
} else {
session = await mongoose.startSession();
session.startTransaction();
}
try {
const organization = await Organization.findByIdAndDelete(organizationId);
const organization = await Organization.findByIdAndDelete(
organizationId,
{
session
}
);
if (!organization) throw ResourceNotFoundError();
await MembershipOrg.deleteMany({
organization: organization._id
}, {
session
});
await BotOrg.deleteMany({
organization: organization._id
}, {
session
});
await SSOConfig.deleteMany({
organization: organization._id
}, {
session
});
await Role.deleteMany({
organization: organization._id
}, {
session
});
await IncidentContactOrg.deleteMany({
organization: organization._id
}, {
session
});
await GitRisks.deleteMany({
organization: organization._id
}, {
session
});
await GitAppInstallationSession.deleteMany({
organization: organization._id
}, {
session
});
await GitAppOrganizationInstallation.deleteMany({
organization: organization._id
}, {
session
});
const workspaceIds = await Workspace.distinct("_id", {
@@ -161,167 +191,230 @@ export const deleteOrganization = async ({
await Workspace.deleteMany({
organization: organization._id
}, {
session
});
await Membership.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Key.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Bot.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await BotKey.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretBlindIndexData.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Secret.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretVersion.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretSnapshot.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretImport.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Folder.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await FolderVersion.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Webhook.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await TrustedIP.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Tag.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await IntegrationAuth.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Integration.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceToken.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceTokenData.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceTokenDataV3.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await ServiceTokenDataV3Key.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await AuditLog.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Log.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await Action.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretApprovalPolicy.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
await SecretApprovalRequest.deleteMany({
workspace: {
$in: workspaceIds
}
}, {
session
});
if (organization.customerId) {
// delete from stripe here
await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}`
);
}
return organization;
} catch (err) {
await session.abortTransaction();
if (!existingSession) {
await session.abortTransaction();
}
throw InternalServerError({
message: "Failed to delete organization"
});
} finally {
session.endSession();
if (!existingSession) {
await session.commitTransaction();
session.endSession();
}
}
}

View File

@@ -57,10 +57,10 @@ import { getAnImportedSecret } from "../services/SecretImportService";
/**
* Validate scope for service token v3
* @param authPayload
* @param environment
* @param secretPath
* @returns
* @param authPayload
* @param environment
* @param secretPath
* @returns
*/
export const isValidScopeV3 = ({
authPayload,
@@ -68,37 +68,40 @@ export const isValidScopeV3 = ({
secretPath,
requiredPermissions
}: {
authPayload: IServiceTokenDataV3,
environment: string,
secretPath: string,
requiredPermissions: Permission[]
authPayload: IServiceTokenDataV3;
environment: string;
secretPath: string;
requiredPermissions: Permission[];
}) => {
const { scopes } = authPayload;
const validScope = scopes.find(
(scope) =>
picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) &&
scope.environment === environment
);
if (validScope && !requiredPermissions.every(permission => validScope.permissions.includes(permission))) {
if (
validScope &&
!requiredPermissions.every((permission) => validScope.permissions.includes(permission))
) {
return false;
}
return Boolean(validScope);
}
};
/**
* Validate scope for service token v2
* @param authPayload
* @param environment
* @param secretPath
* @returns
* @param authPayload
* @param environment
* @param secretPath
* @returns
*/
export const isValidScope = (
authPayload: IServiceTokenData,
environment: string,
secretPath: string,
secretPath: string
) => {
const { scopes: tkScopes } = authPayload;
const validScope = tkScopes.find(
@@ -1000,12 +1003,22 @@ export const deleteSecretHelper = async ({
environment,
type,
authData,
secretPath = "/"
secretPath = "/",
// used for update corner case and blindIndex goes wrong way
secretId
}: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
let secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
if (secretId) {
const secret = await Secret.findOne({
workspace: workspaceId,
environment,
_id: secretId
}).select("secretBlindIndex");
if (secret && secret.secretBlindIndex) secretBlindIndex = secret.secretBlindIndex;
}
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);

View File

@@ -1,4 +1,4 @@
import mongoose, { Types } from "mongoose";
import mongoose, { Types, mongo } from "mongoose";
import {
APIKeyData,
BackupPrivateKey,
@@ -222,15 +222,26 @@ const checkDeleteUserConditions = async ({
* @returns {User} user - deleted user
*/
export const deleteUser = async ({
userId
userId,
existingSession
}: {
userId: Types.ObjectId
userId: Types.ObjectId;
existingSession?: mongo.ClientSession;
}) => {
const session = await mongoose.startSession();
session.startTransaction();
let session;
if (existingSession) {
session = existingSession;
} else {
session = await mongoose.startSession();
session.startTransaction();
}
try {
const user = await User.findByIdAndDelete(userId);
const user = await User.findByIdAndDelete(userId, {
session
});
if (!user) throw ResourceNotFoundError();
@@ -240,22 +251,32 @@ export const deleteUser = async ({
await UserAction.deleteMany({
user: user._id
}, {
session
});
await BackupPrivateKey.deleteMany({
user: user._id
}, {
session
});
await APIKeyData.deleteMany({
user: user._id
}, {
session
});
await Action.deleteMany({
user: user._id
}, {
session
});
await Log.deleteMany({
user: user._id
}, {
session
});
await TokenVersion.deleteMany({
@@ -264,10 +285,14 @@ export const deleteUser = async ({
await Key.deleteMany({
receiver: user._id
}, {
session
});
const membershipOrgs = await MembershipOrg.find({
user: userId
}, null, {
session
});
// delete organizations where user is only member
@@ -280,13 +305,16 @@ export const deleteUser = async ({
// organization only has 1 member (the current user)
await deleteOrganization({
organizationId: membershipOrg.organization
organizationId: membershipOrg.organization,
existingSession: session
});
}
}
const memberships = await Membership.find({
user: userId
}, null, {
session
});
// delete workspaces where user is only member
@@ -299,26 +327,36 @@ export const deleteUser = async ({
// workspace only has 1 member (the current user) -> delete workspace
await deleteWorkspace({
workspaceId: membership.workspace
workspaceId: membership.workspace,
existingSession: session
});
}
}
await MembershipOrg.deleteMany({
user: userId
}, {
session
});
await Membership.deleteMany({
user: userId
}, {
session
});
return user;
} catch (err) {
await session.abortTransaction();
if (!existingSession) {
await session.abortTransaction();
}
throw InternalServerError({
message: "Failed to delete account"
})
} finally {
session.endSession();
if (!existingSession) {
await session.commitTransaction();
session.endSession();
}
}
}

View File

@@ -1,4 +1,4 @@
import mongoose, { Types } from "mongoose";
import mongoose, { Types, mongo } from "mongoose";
import {
Bot,
BotKey,
@@ -102,125 +102,189 @@ export const createWorkspace = async ({
* @param {String} obj.id - id of workspace to delete
*/
export const deleteWorkspace = async ({
workspaceId
workspaceId,
existingSession
}: {
workspaceId: Types.ObjectId;
existingSession?: mongo.ClientSession;
}) => {
const session = await mongoose.startSession();
session.startTransaction();
let session;
if (existingSession) {
session = existingSession;
} else {
session = await mongoose.startSession();
session.startTransaction();
}
try {
const workspace = await Workspace.findByIdAndDelete(workspaceId);
const workspace = await Workspace.findByIdAndDelete(workspaceId, { session });
if (!workspace) throw ResourceNotFoundError();
await Membership.deleteMany({
workspace: workspace._id
}, {
session
});
await Key.deleteMany({
workspace: workspace._id
}, {
session
});
await Bot.deleteMany({
workspace: workspace._id
}, {
session
});
await BotKey.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretBlindIndexData.deleteMany({
workspace: workspace._id
}, {
session
});
await Secret.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretVersion.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretSnapshot.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretImport.deleteMany({
workspace: workspace._id
}, {
session
});
await Folder.deleteMany({
workspace: workspace._id
}, {
session
});
await FolderVersion.deleteMany({
workspace: workspace._id
}, {
session
});
await Webhook.deleteMany({
workspace: workspace._id
}, {
session
});
await TrustedIP.deleteMany({
workspace: workspace._id
}, {
session
});
await Tag.deleteMany({
workspace: workspace._id
}, {
session
});
await IntegrationAuth.deleteMany({
workspace: workspace._id
}, {
session
});
await Integration.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceToken.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceTokenData.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceTokenDataV3.deleteMany({
workspace: workspace._id
}, {
session
});
await ServiceTokenDataV3Key.deleteMany({
workspace: workspace._id
}, {
session
});
await AuditLog.deleteMany({
workspace: workspace._id
}, {
session
});
await Log.deleteMany({
workspace: workspace._id
}, {
session
});
await Action.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretApprovalPolicy.deleteMany({
workspace: workspace._id
}, {
session
});
await SecretApprovalRequest.deleteMany({
workspace: workspace._id
}, {
session
});
return workspace;
} catch (err) {
await session.abortTransaction();
if (!existingSession) {
await session.abortTransaction();
}
throw InternalServerError({
message: "Failed to delete organization"
});
} finally {
session.endSession();
if (!existingSession) {
await session.commitTransaction();
session.endSession();
}
}
};

View File

@@ -64,6 +64,7 @@ export interface UpdateSecretParams {
export interface DeleteSecretParams {
secretName: string;
secretId?: string;
workspaceId: Types.ObjectId;
environment: string;
type: "shared" | "personal";

View File

@@ -5,7 +5,7 @@ import path from "path";
type TAppendFolderDTO = {
folderName: string;
parentFolderId?: string;
directory: string;
};
type TRenameFolderDTO = {
@@ -50,9 +50,8 @@ export const folderBfsTraversal = async (
// bfs and then append to the folder
const appendChild = (folders: TFolderSchema, folderName: string) => {
const folder = folders.children.find(({ name }) => name === folderName);
if (folder) {
throw new Error("Folder already exists");
}
if (folder) return { folder, hasCreated: false };
const id = generateFolderId();
folders.version += 1;
folders.children.push({
@@ -61,24 +60,32 @@ const appendChild = (folders: TFolderSchema, folderName: string) => {
children: [],
version: 1
});
return { id, name: folderName };
// last element that is the new one
return { folder: folders.children[folders.children.length - 1], hasCreated: true };
};
// root of append child wrapper
export const appendFolder = (
folders: TFolderSchema,
{ folderName, parentFolderId }: TAppendFolderDTO
) => {
const isRoot = !parentFolderId;
{ folderName, directory }: TAppendFolderDTO
): { parent: TFolderSchema; child: TFolderSchema; hasCreated?: boolean } => {
if (directory === "/") {
const newFolder = appendChild(folders, folderName);
return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
}
if (isRoot) {
return appendChild(folders, folderName);
const segments = directory.split("/").filter(Boolean);
const segment = segments.shift();
if (segment) {
const nestedFolders = appendChild(folders, segment);
return appendFolder(nestedFolders.folder, {
folderName,
directory: path.join("/", ...segments)
});
}
const folder = searchByFolderId(folders, parentFolderId);
if (!folder) {
throw new Error("Parent Folder not found");
}
return appendChild(folder, folderName);
const newFolder = appendChild(folders, folderName);
return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
};
export const renameFolder = (

View File

@@ -379,7 +379,8 @@ export const DeleteSecretByNameV3 = z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]),
secretPath: z.string().trim().default("/")
secretPath: z.string().trim().default("/"),
secretId: z.string().trim().optional()
}),
params: z.object({
secretName: z.string()

View File

@@ -89,3 +89,13 @@ Back in Azure, navigate to the **Users and groups** tab and select **+ Add user/
Enabling SAML SSO enforces all members in your organization to only be able to log into Infisical via Azure.
![Azure SAML assignment](../../../images/sso/azure/enable-saml.png)
<Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to
set the `JWT_PROVIDER_AUTH_SECRET` and `SITE_URL` environment variable for it to work:
- `JWT_PROVIDER_AUTH_SECRET`: This is secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note>

View File

@@ -29,9 +29,24 @@ Obtain the **Client ID** and generate a new **Client Secret** for your GitHub OA
![GCP obtain OAuth2 credentials](../../../images/sso/github/credentials.png)
Back in your Infisical instance, add two new environment variables for the credentials of your GitHub OAuth application:
Back in your Infisical instance, make sure to set the following environment variables:
- `CLIENT_ID_GITHUB_LOGIN`: The **Client ID** of your GitHub OAuth application.
- `CLIENT_SECRET_GITHUB_LOGIN`: The **Client Secret** of your GitHub OAuth application.
- `JWT_PROVIDER_AUTH_SECRET`: A secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
Once added, restart your Infisical instance and log in with GitHub.
Once added, restart your Infisical instance and log in with GitHub.
## FAQ
<AccordionGroup>
<Accordion title="Why is GitHub SSO not working?">
It is likely that you have misconfigured your self-hosted instance of Infisical. You should:
- Check that you have set the `CLIENT_ID_GITHUB_LOGIN`, `CLIENT_SECRET_GITHUB_LOGIN`,
`JWT_PROVIDER_AUTH_SECRET`, and `SITE_URL` environment variables.
- Check that the **Authorization callback URL** specified in GitHub matches the `SITE_URL` environment variable.
For example, if the former is `https://app.infisical.com/api/v1/sso/github` then the latter should be `https://app.infisical.com`.
</Accordion>
</AccordionGroup>

View File

@@ -28,10 +28,25 @@ Obtain the **Application ID** and **Secret** for your GitLab application.
![sso gitlab config](/images/sso/gitlab/credentials.png)
Back in your Infisical instance, add 2-3 new environment variables for the credentials of your GitLab application:
Back in your Infisical instance, make sure to set the following environment variables:
- `CLIENT_ID_GITLAB_LOGIN`: The **Client ID** of your GitLab application.
- `CLIENT_SECRET_GITLAB_LOGIN`: The **Secret** of your GitLab application.
- (optional) `URL_GITLAB_LOGIN`: The URL of your self-hosted instance of GitLab where the OAuth application is registered. If no URL is passed in, this will default to `https://gitlab.com`.
- `JWT_PROVIDER_AUTH_SECRET`: A secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
Once added, restart your Infisical instance and log in with GitLab.
Once added, restart your Infisical instance and log in with GitLab.
## FAQ
<AccordionGroup>
<Accordion title="Why is GitLab SSO not working?">
It is likely that you have misconfigured your self-hosted instance of Infisical. You should:
- Check that you have set the `CLIENT_ID_GITLAB_LOGIN`, `CLIENT_SECRET_GITLAB_LOGIN`,
`JWT_PROVIDER_AUTH_SECRET`, and `SITE_URL` environment variables.
- Check that the **Redirect URI** specified in GitLab matches the `SITE_URL` environment variable.
For example, if the former is `https://app.infisical.com/api/v1/sso/gitlab` then the latter should be `https://app.infisical.com`.
</Accordion>
</AccordionGroup>

View File

@@ -22,9 +22,24 @@ Obtain the **Client ID** and **Client Secret** for your GCP OAuth2 application.
![GCP obtain OAuth2 credentials](../../../images/sso/google/credentials.png)
Back in your Infisical instance, add two new environment variables for the credentials of your GCP OAuth2 application:
Back in your Infisical instance, make sure to set the following environment variables:
- `CLIENT_ID_GOOGLE_LOGIN`: The **Client ID** of your GCP OAuth2 application.
- `CLIENT_SECRET_GOOGLE_LOGIN`: The **Client Secret** of your GCP OAuth2 application.
- `JWT_PROVIDER_AUTH_SECRET`: A secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
Once added, restart your Infisical instance and log in with Google
Once added, restart your Infisical instance and log in with Google
## FAQ
<AccordionGroup>
<Accordion title="Why is Google SSO not working?">
It is likely that you have misconfigured your self-hosted instance of Infisical. You should:
- Check that you have set the `CLIENT_ID_GOOGLE_LOGIN`, `CLIENT_SECRET_GOOGLE_LOGIN`,
`JWT_PROVIDER_AUTH_SECRET`, and `SITE_URL` environment variables.
- Check that the **Authorized redirect URI** specified in GCP matches the `SITE_URL` environment variable.
For example, if the former is `https://app.infisical.com/api/v1/sso/google` then the latter should be `https://app.infisical.com`.
</Accordion>
</AccordionGroup>

View File

@@ -72,3 +72,11 @@ Back in JumpCloud, navigate to the **User Groups** tab and assign users to the n
Enabling SAML SSO enforces all members in your organization to only be able to log into Infisical via JumpCloud.
![JumpCloud SAML assignment](../../../images/sso/jumpcloud/enable-saml.png)
<Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to
set the `JWT_PROVIDER_AUTH_SECRET` and `SITE_URL` environment variable for it to work:
- `JWT_PROVIDER_AUTH_SECRET`: This is secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note>

View File

@@ -77,3 +77,11 @@ At this point, you have configured everything you need within the context of the
Enabling SAML SSO enforces all members in your organization to only be able to log into Infisical via Okta.
![SAML Okta assignment](../../../images/sso/okta/enable-saml.png)
<Note>
If you're configuring SAML SSO on a self-hosted instance of Infisical, make sure to
set the `JWT_PROVIDER_AUTH_SECRET` and `SITE_URL` environment variable for it to work:
- `JWT_PROVIDER_AUTH_SECRET`: This is secret key used for signing and verifying JWT. This could be a randomly-generated 256-bit hex string.
- `SITE_URL`: The URL of your self-hosted instance of Infisical - should be an absolute URL including the protocol (e.g. https://app.infisical.com)
</Note>

View File

@@ -37,7 +37,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
) => (
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay
className="fixed inset-0 z-[70] h-full w-full"
className="fixed inset-0 z-20 h-full w-full"
style={{ backgroundColor: "rgba(0, 0, 0, 0.7)" }}
/>
<DialogPrimitive.Content

View File

@@ -189,12 +189,20 @@ export const useDeleteSecretV3 = ({
const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteSecretsV3DTO>({
mutationFn: async ({ secretPath = "/", type, environment, workspaceId, secretName }) => {
mutationFn: async ({
secretPath = "/",
type,
environment,
workspaceId,
secretName,
secretId
}) => {
const reqBody = {
workspaceId,
environment,
type,
secretPath
secretPath,
secretId
};
const { data } = await apiRequest.delete(`/api/v3/secrets/${secretName}`, {

View File

@@ -120,6 +120,7 @@ export type TDeleteSecretsV3DTO = {
type: "shared" | "personal";
secretPath: string;
secretName: string;
secretId?: string;
};
export type TCreateSecretBatchDTO = {

View File

@@ -51,6 +51,17 @@ export const useDeleteUser = () => {
return user;
},
onSuccess: () => {
localStorage.removeItem("protectedKey");
localStorage.removeItem("protectedKeyIV");
localStorage.removeItem("protectedKeyTag");
localStorage.removeItem("publicKey");
localStorage.removeItem("encryptedPrivateKey");
localStorage.removeItem("iv");
localStorage.removeItem("tag");
localStorage.removeItem("PRIVATE_KEY");
localStorage.removeItem("orgData.id");
localStorage.removeItem("projectData.id");
queryClient.clear();
}
});

View File

@@ -34,7 +34,6 @@ import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
import {
@@ -217,7 +216,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
// }
};
putUserInOrg();
onboardingCheck({});
}, [router.query.id]);
const onCreateProject = async ({ name, addMembers }: TAddProjectFormData) => {

View File

@@ -12,6 +12,7 @@ export const navigateUserToOrg = async (router: NextRouter) => {
router.push(`/org/${userOrg}/overview`);
} else {
// user is not part of any org
localStorage.removeItem("orgData.id");
router.push("/org/none");
}
}

View File

@@ -14,7 +14,42 @@ const schema = yup
export type FormData = yup.InferType<typeof schema>;
export const NonePage = () => {
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["createOrg"] as const);
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"createOrg",
] as const);
const { mutateAsync } = useCreateOrg();
const {
control,
handleSubmit,
reset,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: yupResolver(schema),
defaultValues: {
name: ""
}
});
useEffect(() => {
handlePopUpOpen("createOrg");
}, []);
const onFormSubmit = async ({ name }: FormData) => {
try {
const organization = await mutateAsync({
name
});
localStorage.setItem("orgData.id", organization._id);
createNotification({
text: "Successfully created organization",
type: "success"
});
useEffect(() => {
handlePopUpOpen("createOrg");

View File

@@ -124,13 +124,15 @@ export const SecretListView = ({
comment,
tags,
skipMultilineEncoding,
newKey
newKey,
secretId
}: Partial<{
value: string;
comment: string;
tags: string[];
skipMultilineEncoding: boolean;
newKey: string;
secretId: string;
}> = {}
) => {
if (operation === "delete") {
@@ -139,7 +141,8 @@ export const SecretListView = ({
workspaceId,
secretPath,
secretName: key,
type
type,
secretId
});
return;
}
@@ -249,9 +252,9 @@ export const SecretListView = ({
);
const handleSecretDelete = useCallback(async () => {
const { key } = popUp.deleteSecret?.data as DecryptedSecret;
const { key, _id: secretId } = popUp.deleteSecret?.data as DecryptedSecret;
try {
await handleSecretOperation("delete", "shared", key);
await handleSecretOperation("delete", "shared", key, { secretId });
queryClient.invalidateQueries(
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
);

View File

@@ -31,6 +31,7 @@ import {
} from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context";
import {
useCreateFolder,
useCreateSecretV3,
useDeleteSecretV3,
useGetFoldersByEnv,
@@ -104,9 +105,24 @@ export const SecretOverviewPage = () => {
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
const { mutateAsync: createFolder } = useCreateFolder();
const handleSecretCreate = async (env: string, key: string, value: string) => {
try {
// create folder if not existing
if (secretPath !== "/") {
const path = secretPath.split("/");
const directory = path.slice(0, -1).join("/");
const folderName = path.at(-1);
if (folderName && directory) {
await createFolder({
workspaceId,
environment: env,
directory,
folderName
});
}
}
await createSecretV3({
environment: env,
workspaceId,
@@ -154,13 +170,14 @@ export const SecretOverviewPage = () => {
}
};
const handleSecretDelete = async (env: string, key: string) => {
const handleSecretDelete = async (env: string, key: string, secretId?: string) => {
try {
await deleteSecretV3({
environment: env,
workspaceId,
secretPath,
secretName: key,
secretId,
type: "shared"
});
createNotification({
@@ -188,7 +205,20 @@ export const SecretOverviewPage = () => {
});
};
const handleExploreEnvClick = (slug: string) => {
const handleExploreEnvClick = async (slug: string) => {
if (secretPath !== "/") {
const path = secretPath.split("/");
const directory = path.slice(0, -1).join("/");
const folderName = path.at(-1);
if (folderName && directory) {
await createFolder({
workspaceId,
environment: slug,
directory,
folderName
});
}
}
const query: Record<string, string> = { ...router.query, env: slug };
const envIndex = userAvailableEnvs.findIndex((el) => slug === el.slug);
if (envIndex !== -1) {
@@ -335,7 +365,14 @@ export const SecretOverviewPage = () => {
query: { id: workspaceId, env: userAvailableEnvs?.[0]?.slug }
}}
>
<Button className="mt-4" variant="outline_bg" colorSchema="primary" size="md">Go to {userAvailableEnvs?.[0]?.name}</Button>
<Button
className="mt-4"
variant="outline_bg"
colorSchema="primary"
size="md"
>
Go to {userAvailableEnvs?.[0]?.name}
</Button>
</Link>
</EmptyState>
</Td>

View File

@@ -12,13 +12,14 @@ import { useToggle } from "@app/hooks";
type Props = {
defaultValue?: string | null;
secretName: string;
secretId?: string;
isCreatable?: boolean;
isVisible?: boolean;
environment: string;
secretPath: string;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretDelete: (env: string, key: string) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
};
export const SecretEditRow = ({
@@ -30,7 +31,8 @@ export const SecretEditRow = ({
onSecretDelete,
environment,
secretPath,
isVisible
isVisible,
secretId
}: Props) => {
const {
handleSubmit,
@@ -77,7 +79,7 @@ export const SecretEditRow = ({
const handleDeleteSecret = async () => {
setIsDeleting.on();
try {
await onSecretDelete(environment, secretName);
await onSecretDelete(environment, secretName, secretId);
reset({ value: undefined });
} finally {
setIsDeleting.off();

View File

@@ -24,7 +24,7 @@ type Props = {
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretDelete: (env: string, key: string) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
};
export const SecretOverviewTableRow = ({
@@ -149,6 +149,7 @@ export const SecretOverviewTableRow = ({
isVisible={isSecretVisible}
secretName={secretKey}
defaultValue={secret?.value}
secretId={secret?._id}
isCreatable={isCreatable}
onSecretDelete={onSecretDelete}
onSecretCreate={onSecretCreate}

View File

@@ -71,7 +71,7 @@ export const PreviewSection = () => {
console.error(err);
}
};
return (
<div>
{subscription &&

View File

@@ -54,9 +54,7 @@ export const SecretTagsSection = (): JSX.Element => {
colorSchema="secondary"
leftIcon={<FontAwesomeIcon icon={faPlus} />}
onClick={() => {
console.log("x");
handlePopUpOpen("CreateSecretTag");
console.log("x2");
}}
isDisabled={!isAllowed}
>