diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index dcd1ba569..18873afe4 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['2095-model-swap-in-chat'] # put your current branch to create a build. Core team only. + branches: ['3982-disable-login-simple-sso'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' diff --git a/docker/.env.example b/docker/.env.example index 755437c2e..9c65405c2 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -329,6 +329,7 @@ GID='1000' # Enable simple SSO passthrough to pre-authenticate users from a third party service. # See https://docs.anythingllm.com/configuration#simple-sso-passthrough for more information. # SIMPLE_SSO_ENABLED=1 +# SIMPLE_SSO_NO_LOGIN=1 # Allow scraping of any IP address in collector - must be string "true" to be enabled # See https://docs.anythingllm.com/configuration#local-ip-address-scraping for more information. diff --git a/frontend/src/hooks/useSimpleSSO.js b/frontend/src/hooks/useSimpleSSO.js new file mode 100644 index 000000000..35da97520 --- /dev/null +++ b/frontend/src/hooks/useSimpleSSO.js @@ -0,0 +1,33 @@ +import { useEffect, useState } from "react"; +import System from "@/models/system"; + +/** + * Checks if Simple SSO is enabled and if the user should be redirected to the SSO login page. + * @returns {{loading: boolean, ssoConfig: {enabled: boolean, noLogin: boolean}}} + */ +export default function useSimpleSSO() { + const [loading, setLoading] = useState(true); + const [ssoConfig, setSsoConfig] = useState({ + enabled: false, + noLogin: false, + }); + + useEffect(() => { + async function checkSsoConfig() { + try { + const settings = await System.keys(); + setSsoConfig({ + enabled: settings?.SimpleSSOEnabled, + noLogin: settings?.SimpleSSONoLogin, + }); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + } + checkSsoConfig(); + }, []); + + return { loading, ssoConfig }; +} diff --git a/frontend/src/pages/Login/SSO/simple.jsx b/frontend/src/pages/Login/SSO/simple.jsx index 48b9edf93..cfef761f9 100644 --- a/frontend/src/pages/Login/SSO/simple.jsx +++ b/frontend/src/pages/Login/SSO/simple.jsx @@ -39,9 +39,9 @@ export default function SimpleSSOPassthrough() { if (error) return ( -
-

{error}

-

+

+

{error}

+

Please contact the system administrator about this error.

diff --git a/frontend/src/pages/Login/index.jsx b/frontend/src/pages/Login/index.jsx index 4e77a5c66..0d4c2623c 100644 --- a/frontend/src/pages/Login/index.jsx +++ b/frontend/src/pages/Login/index.jsx @@ -4,11 +4,24 @@ import { FullScreenLoader } from "@/components/Preloader"; import { Navigate } from "react-router-dom"; import paths from "@/utils/paths"; import useQuery from "@/hooks/useQuery"; +import useSimpleSSO from "@/hooks/useSimpleSSO"; +/** + * Login page that handles both single and multi-user login. + * + * If Simple SSO is enabled and no login is allowed, the user will be redirected to the SSO login page + * which may not have a token so the login will fail. + * + * @returns {JSX.Element} + */ export default function Login() { const query = useQuery(); + const { loading: ssoLoading, ssoConfig } = useSimpleSSO(); const { loading, requiresAuth, mode } = usePasswordModal(!!query.get("nt")); - if (loading) return ; + + if (loading || ssoLoading) return ; + if (ssoConfig.enabled && ssoConfig.noLogin) + return ; if (requiresAuth === false) return ; return ; diff --git a/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx index 536029e82..1c9ad51bb 100644 --- a/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx @@ -284,7 +284,7 @@ const MyTeam = ({ setMultiUserLoginValid, myTeamSubmitRef, navigate }) => { htmlFor="name" className="block mb-3 text-sm font-medium text-white" > - {t("common.adminUsername")} + {t("onboarding.userSetup.adminUsername")} { return `/login${noTry ? "?nt=1" : ""}`; }, + sso: { + login: () => { + return "/sso/simple"; + }, + }, onboarding: { home: () => { return "/onboarding"; diff --git a/server/.env.example b/server/.env.example index ef6292c54..809e36ed8 100644 --- a/server/.env.example +++ b/server/.env.example @@ -326,6 +326,7 @@ TTS_PROVIDER="native" # Enable simple SSO passthrough to pre-authenticate users from a third party service. # See https://docs.anythingllm.com/configuration#simple-sso-passthrough for more information. # SIMPLE_SSO_ENABLED=1 +# SIMPLE_SSO_NO_LOGIN=1 # Allow scraping of any IP address in collector - must be string "true" to be enabled # See https://docs.anythingllm.com/configuration#local-ip-address-scraping for more information. diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js index 211f50465..4964c35a1 100644 --- a/server/endpoints/admin.js +++ b/server/endpoints/admin.js @@ -25,6 +25,9 @@ const { } = require("../utils/middleware/multiUserProtected"); const { validatedRequest } = require("../utils/middleware/validatedRequest"); const ImportedPlugin = require("../utils/agents/imported"); +const { + simpleSSOLoginDisabledMiddleware, +} = require("../utils/middleware/simpleSSOEnabled"); function adminEndpoints(app) { if (!app) return; @@ -168,7 +171,11 @@ function adminEndpoints(app) { app.post( "/admin/invite/new", - [validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])], + [ + validatedRequest, + strictMultiUserRoleValid([ROLES.admin, ROLES.manager]), + simpleSSOLoginDisabledMiddleware, + ], async (request, response) => { try { const user = await userFromSession(request, response); diff --git a/server/endpoints/invite.js b/server/endpoints/invite.js index 38eb71de8..9a05ed7cf 100644 --- a/server/endpoints/invite.js +++ b/server/endpoints/invite.js @@ -2,6 +2,9 @@ const { EventLogs } = require("../models/eventLogs"); const { Invite } = require("../models/invite"); const { User } = require("../models/user"); const { reqBody } = require("../utils/http"); +const { + simpleSSOLoginDisabledMiddleware, +} = require("../utils/middleware/simpleSSOEnabled"); function inviteEndpoints(app) { if (!app) return; @@ -31,46 +34,50 @@ function inviteEndpoints(app) { } }); - app.post("/invite/:code", async (request, response) => { - try { - const { code } = request.params; - const { username, password } = reqBody(request); - const invite = await Invite.get({ code }); - if (!invite || invite.status !== "pending") { - response - .status(200) - .json({ success: false, error: "Invite not found or is invalid." }); - return; + app.post( + "/invite/:code", + [simpleSSOLoginDisabledMiddleware], + async (request, response) => { + try { + const { code } = request.params; + const { username, password } = reqBody(request); + const invite = await Invite.get({ code }); + if (!invite || invite.status !== "pending") { + response + .status(200) + .json({ success: false, error: "Invite not found or is invalid." }); + return; + } + + const { user, error } = await User.create({ + username, + password, + role: "default", + }); + if (!user) { + console.error("Accepting invite:", error); + response + .status(200) + .json({ success: false, error: "Could not create user." }); + return; + } + + await Invite.markClaimed(invite.id, user); + await EventLogs.logEvent( + "invite_accepted", + { + username: user.username, + }, + user.id + ); + + response.status(200).json({ success: true, error: null }); + } catch (e) { + console.error(e); + response.sendStatus(500).end(); } - - const { user, error } = await User.create({ - username, - password, - role: "default", - }); - if (!user) { - console.error("Accepting invite:", error); - response - .status(200) - .json({ success: false, error: "Could not create user." }); - return; - } - - await Invite.markClaimed(invite.id, user); - await EventLogs.logEvent( - "invite_accepted", - { - username: user.username, - }, - user.id - ); - - response.status(200).json({ success: true, error: null }); - } catch (e) { - console.error(e); - response.sendStatus(500).end(); } - }); + ); } module.exports = { inviteEndpoints }; diff --git a/server/endpoints/system.js b/server/endpoints/system.js index 4077844e7..ee3778be0 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -54,7 +54,10 @@ const { BrowserExtensionApiKey } = require("../models/browserExtensionApiKey"); const { chatHistoryViewable, } = require("../utils/middleware/chatHistoryViewable"); -const { simpleSSOEnabled } = require("../utils/middleware/simpleSSOEnabled"); +const { + simpleSSOEnabled, + simpleSSOLoginDisabled, +} = require("../utils/middleware/simpleSSOEnabled"); const { TemporaryAuthToken } = require("../models/temporaryAuthToken"); const { SystemPromptVariables } = require("../models/systemPromptVariables"); const { VALID_COMMANDS } = require("../utils/chats"); @@ -116,6 +119,17 @@ function systemEndpoints(app) { const bcrypt = require("bcrypt"); if (await SystemSettings.isMultiUserMode()) { + if (simpleSSOLoginDisabled()) { + response.status(403).json({ + user: null, + valid: false, + token: null, + message: + "[005] Login via credentials has been disabled by the administrator.", + }); + return; + } + const { username, password } = reqBody(request); const existingUser = await User._get({ username: String(username) }); diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index a7a3e752c..bb7311fb8 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -279,6 +279,12 @@ const SystemSettings = { // Disable View Chat History for the whole instance. DisableViewChatHistory: "DISABLE_VIEW_CHAT_HISTORY" in process.env || false, + + // -------------------------------------------------------- + // Simple SSO Settings + // -------------------------------------------------------- + SimpleSSOEnabled: "SIMPLE_SSO_ENABLED" in process.env || false, + SimpleSSONoLogin: "SIMPLE_SSO_NO_LOGIN" in process.env || false, }; }, diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index b69c96417..f209ef450 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -1084,6 +1084,7 @@ function dumpENV() { "DISABLE_VIEW_CHAT_HISTORY", // Simple SSO "SIMPLE_SSO_ENABLED", + "SIMPLE_SSO_NO_LOGIN", // Community Hub "COMMUNITY_HUB_BUNDLE_DOWNLOADS_ENABLED", diff --git a/server/utils/middleware/simpleSSOEnabled.js b/server/utils/middleware/simpleSSOEnabled.js index 903200c03..2c6bb7220 100644 --- a/server/utils/middleware/simpleSSOEnabled.js +++ b/server/utils/middleware/simpleSSOEnabled.js @@ -34,6 +34,51 @@ async function simpleSSOEnabled(_, response, next) { next(); } +/** + * Checks if simple SSO login is disabled by checking if the + * SIMPLE_SSO_NO_LOGIN environment variable is set as well as + * SIMPLE_SSO_ENABLED is set. + * + * This check should only be run when in multi-user mode when used. + * @returns {boolean} + */ +function simpleSSOLoginDisabled() { + return ( + "SIMPLE_SSO_ENABLED" in process.env && "SIMPLE_SSO_NO_LOGIN" in process.env + ); +} + +/** + * Middleware that checks if simple SSO login is disabled by checking if the + * SIMPLE_SSO_NO_LOGIN environment variable is set as well as + * SIMPLE_SSO_ENABLED is set. + * + * This middleware will 403 if SSO is enabled and no login is allowed and + * the system is in multi-user mode. Otherwise, it will call next. + * + * @param {import("express").Request} request + * @param {import("express").Response} response + * @param {import("express").NextFunction} next + * @returns {void} + */ +async function simpleSSOLoginDisabledMiddleware(_, response, next) { + if (!("multiUserMode" in response.locals)) { + const multiUserMode = await SystemSettings.isMultiUserMode(); + response.locals.multiUserMode = multiUserMode; + } + + if (response.locals.multiUserMode && simpleSSOLoginDisabled()) { + response.status(403).json({ + success: false, + error: "Login via credentials has been disabled by the administrator.", + }); + return; + } + next(); +} + module.exports = { simpleSSOEnabled, + simpleSSOLoginDisabled, + simpleSSOLoginDisabledMiddleware, };