diff --git a/README.md b/README.md index 3696da483..a48b4934e 100644 --- a/README.md +++ b/README.md @@ -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 Slack, 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. diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index d030bce9a..835551e76 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -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 }); }; /** diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index 31a727e02..4db0dca94 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -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, diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 3e8598de3..2e5ee6ebb 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -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(); + } } } diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 6f718f5fe..ea6f4a254 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -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); diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index 263c5c736..b78f471f1 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -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(); + } } } \ No newline at end of file diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 73f83b061..71d945e78 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -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(); + } } }; diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index f42bbadf6..203f0e178 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -64,6 +64,7 @@ export interface UpdateSecretParams { export interface DeleteSecretParams { secretName: string; + secretId?: string; workspaceId: Types.ObjectId; environment: string; type: "shared" | "personal"; diff --git a/backend/src/services/FolderService.ts b/backend/src/services/FolderService.ts index cb4ef4286..65aa5b0f3 100644 --- a/backend/src/services/FolderService.ts +++ b/backend/src/services/FolderService.ts @@ -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 = ( diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index ea9a24e6c..264eb7271 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -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() diff --git a/docs/documentation/platform/sso/azure.mdx b/docs/documentation/platform/sso/azure.mdx index 7012e9d48..7c082b971 100644 --- a/docs/documentation/platform/sso/azure.mdx +++ b/docs/documentation/platform/sso/azure.mdx @@ -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) + + + 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) + + + diff --git a/docs/documentation/platform/sso/github.mdx b/docs/documentation/platform/sso/github.mdx index 2b4dd8c85..faab5a074 100644 --- a/docs/documentation/platform/sso/github.mdx +++ b/docs/documentation/platform/sso/github.mdx @@ -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. \ No newline at end of file +Once added, restart your Infisical instance and log in with GitHub. + +## FAQ + + + + 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`. + + \ No newline at end of file diff --git a/docs/documentation/platform/sso/gitlab.mdx b/docs/documentation/platform/sso/gitlab.mdx index cac946280..7ae6e4a95 100644 --- a/docs/documentation/platform/sso/gitlab.mdx +++ b/docs/documentation/platform/sso/gitlab.mdx @@ -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. \ No newline at end of file +Once added, restart your Infisical instance and log in with GitLab. + +## FAQ + + + + 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`. + + \ No newline at end of file diff --git a/docs/documentation/platform/sso/google.mdx b/docs/documentation/platform/sso/google.mdx index 61da9188c..2546a5be8 100644 --- a/docs/documentation/platform/sso/google.mdx +++ b/docs/documentation/platform/sso/google.mdx @@ -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 \ No newline at end of file +Once added, restart your Infisical instance and log in with Google + +## FAQ + + + + 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`. + + \ No newline at end of file diff --git a/docs/documentation/platform/sso/jumpcloud.mdx b/docs/documentation/platform/sso/jumpcloud.mdx index ef17156da..4c2908416 100644 --- a/docs/documentation/platform/sso/jumpcloud.mdx +++ b/docs/documentation/platform/sso/jumpcloud.mdx @@ -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) + + + 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) + diff --git a/docs/documentation/platform/sso/okta.mdx b/docs/documentation/platform/sso/okta.mdx index 4595de019..f5db2529c 100644 --- a/docs/documentation/platform/sso/okta.mdx +++ b/docs/documentation/platform/sso/okta.mdx @@ -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) + + + 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) + \ No newline at end of file diff --git a/frontend/src/components/v2/Drawer/Drawer.tsx b/frontend/src/components/v2/Drawer/Drawer.tsx index 3bfcbe527..3a92c14db 100644 --- a/frontend/src/components/v2/Drawer/Drawer.tsx +++ b/frontend/src/components/v2/Drawer/Drawer.tsx @@ -37,7 +37,7 @@ export const DrawerContent = forwardRef( ) => ( ({ - 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}`, { diff --git a/frontend/src/hooks/api/secrets/types.ts b/frontend/src/hooks/api/secrets/types.ts index 253ce77a6..ceccf286c 100644 --- a/frontend/src/hooks/api/secrets/types.ts +++ b/frontend/src/hooks/api/secrets/types.ts @@ -120,6 +120,7 @@ export type TDeleteSecretsV3DTO = { type: "shared" | "personal"; secretPath: string; secretName: string; + secretId?: string; }; export type TCreateSecretBatchDTO = { diff --git a/frontend/src/hooks/api/users/queries.tsx b/frontend/src/hooks/api/users/queries.tsx index 7949a59e5..adf0ccd09 100644 --- a/frontend/src/hooks/api/users/queries.tsx +++ b/frontend/src/hooks/api/users/queries.tsx @@ -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(); } }); diff --git a/frontend/src/layouts/AppLayout/AppLayout.tsx b/frontend/src/layouts/AppLayout/AppLayout.tsx index 7200f92c9..ac7be7d4c 100644 --- a/frontend/src/layouts/AppLayout/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout/AppLayout.tsx @@ -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) => { diff --git a/frontend/src/views/Login/Login.utils.tsx b/frontend/src/views/Login/Login.utils.tsx index 44bf24ce8..c8bf2f6c1 100644 --- a/frontend/src/views/Login/Login.utils.tsx +++ b/frontend/src/views/Login/Login.utils.tsx @@ -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"); } } diff --git a/frontend/src/views/Org/NonePage/NonePage.tsx b/frontend/src/views/Org/NonePage/NonePage.tsx index 26294a36b..e2fd70fd2 100644 --- a/frontend/src/views/Org/NonePage/NonePage.tsx +++ b/frontend/src/views/Org/NonePage/NonePage.tsx @@ -14,7 +14,42 @@ const schema = yup export type FormData = yup.InferType; 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({ + 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"); diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx index 3863e55bf..1a09fe796 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretListView.tsx @@ -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 }) ); diff --git a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx index 34bbc7a6d..58e3f35a7 100644 --- a/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx +++ b/frontend/src/views/SecretOverviewPage/SecretOverviewPage.tsx @@ -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 = { ...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 } }} > - + diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index 38db3e6b4..06a58172d 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -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; onSecretUpdate: (env: string, key: string, value: string) => Promise; - onSecretDelete: (env: string, key: string) => Promise; + onSecretDelete: (env: string, key: string, secretId?: string) => Promise; }; 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(); diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx index ec74dd00a..35a0e734f 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretOverviewTableRow.tsx @@ -24,7 +24,7 @@ type Props = { getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined; onSecretCreate: (env: string, key: string, value: string) => Promise; onSecretUpdate: (env: string, key: string, value: string) => Promise; - onSecretDelete: (env: string, key: string) => Promise; + onSecretDelete: (env: string, key: string, secretId?: string) => Promise; }; 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} diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx index 04d878b67..579730ec3 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx @@ -71,7 +71,7 @@ export const PreviewSection = () => { console.error(err); } }; - + return (
{subscription && diff --git a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx index fb5b2c7bb..5219624ec 100644 --- a/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx +++ b/frontend/src/views/Settings/ProjectSettingsPage/components/SecretTagsSection/SecretTagsSection.tsx @@ -54,9 +54,7 @@ export const SecretTagsSection = (): JSX.Element => { colorSchema="secondary" leftIcon={} onClick={() => { - console.log("x"); handlePopUpOpen("CreateSecretTag"); - console.log("x2"); }} isDisabled={!isAllowed} >