Skip to content

Commit

Permalink
Merge branch 'master' into 2545-feat-community-hub-integration
Browse files Browse the repository at this point in the history
  • Loading branch information
shatfield4 committed Oct 29, 2024
2 parents de7866c + 4eeac27 commit 8c56b00
Show file tree
Hide file tree
Showing 19 changed files with 568 additions and 4 deletions.
6 changes: 5 additions & 1 deletion docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,8 @@ GID='1000'

# Disable viewing chat history from the UI and frontend APIs.
# See https://docs.anythingllm.com/configuration#disable-view-chat-history for more information.
# DISABLE_VIEW_CHAT_HISTORY=1
# DISABLE_VIEW_CHAT_HISTORY=1

# 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
3 changes: 3 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import PrivateRoute, {
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import Login from "@/pages/Login";
import SimpleSSOPassthrough from "@/pages/Login/SSO/simple";
import OnboardingFlow from "@/pages/OnboardingFlow";
import i18n from "./i18n";

Expand Down Expand Up @@ -80,6 +81,8 @@ export default function App() {
<Routes>
<Route path="/" element={<PrivateRoute Component={Main} />} />
<Route path="/login" element={<Login />} />
<Route path="/sso/simple" element={<SimpleSSOPassthrough />} />

<Route
path="/workspace/:slug/settings/:tab"
element={<ManagerRoute Component={WorkspaceSettings} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { ArrowSquareOut, Info } from "@phosphor-icons/react";
import { AWS_REGIONS } from "./regions";
import { useState } from "react";

export default function AwsBedrockLLMOptions({ settings }) {
const [useSessionToken, setUseSessionToken] = useState(
settings?.AwsBedrockLLMConnectionMethod === "sessionToken"
);

return (
<div className="w-full flex flex-col">
{!settings?.credentialsOnly && (
Expand All @@ -24,6 +29,43 @@ export default function AwsBedrockLLMOptions({ settings }) {
</div>
)}

<div className="flex flex-col gap-y-2">
<input
type="hidden"
name="AwsBedrockLLMConnectionMethod"
value={useSessionToken ? "sessionToken" : "iam"}
/>
<div className="flex flex-col w-full">
<label className="text-white text-sm font-semibold block mb-3">
Use session token
</label>
<p className="text-white/50 text-sm">
Select the method to authenticate with AWS Bedrock.
</p>
</div>
<div className="flex items-center justify-start gap-x-4 bg-zinc-900 p-2.5 rounded-lg w-fit">
<span
className={`text-sm ${!useSessionToken ? "text-white" : "text-white/50"}`}
>
IAM
</span>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
className="sr-only peer"
checked={useSessionToken}
onChange={(e) => setUseSessionToken(e.target.checked)}
/>
<div className="w-11 h-6 bg-zinc-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary-button"></div>
</label>
<span
className={`text-sm ${useSessionToken ? "text-white" : "text-white/50"}`}
>
Session Token
</span>
</div>
</div>

<div className="w-full flex items-center gap-[36px] my-1.5">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Expand Down Expand Up @@ -59,6 +101,25 @@ export default function AwsBedrockLLMOptions({ settings }) {
spellCheck={false}
/>
</div>
{useSessionToken && (
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
AWS Bedrock Session Token
</label>
<input
type="password"
name="AwsBedrockLLMSessionToken"
className="border-none bg-zinc-900 text-white placeholder:text-white/20 text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="AWS Bedrock Session Token"
defaultValue={
settings?.AwsBedrockLLMSessionToken ? "*".repeat(20) : ""
}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
)}
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
AWS region
Expand Down
24 changes: 24 additions & 0 deletions frontend/src/models/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,30 @@ const System = {
);
return { viewable: isViewable, error: null };
},

/**
* Validates a temporary auth token and logs in the user if the token is valid.
* @param {string} publicToken - the token to validate against
* @returns {Promise<{valid: boolean, user: import("@prisma/client").users | null, token: string | null, message: string | null}>}
*/
simpleSSOLogin: async function (publicToken) {
return fetch(`${API_BASE}/request-token/sso/simple?token=${publicToken}`, {
method: "GET",
})
.then(async (res) => {
if (!res.ok) {
const text = await res.text();
if (!text.startsWith("{")) throw new Error(text);
return JSON.parse(text);
}
return await res.json();
})
.catch((e) => {
console.error(e);
return { valid: false, user: null, token: null, message: e.message };
});
},

experimentalFeatures: {
liveSync: LiveDocumentSync,
agentPlugins: AgentPlugins,
Expand Down
54 changes: 54 additions & 0 deletions frontend/src/pages/Login/SSO/simple.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React, { useEffect, useState } from "react";
import { FullScreenLoader } from "@/components/Preloader";
import { Navigate } from "react-router-dom";
import paths from "@/utils/paths";
import useQuery from "@/hooks/useQuery";
import System from "@/models/system";
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants";

export default function SimpleSSOPassthrough() {
const query = useQuery();
const redirectPath = query.get("redirectTo") || paths.home();
const [ready, setReady] = useState(false);
const [error, setError] = useState(null);

useEffect(() => {
try {
if (!query.get("token")) throw new Error("No token provided.");

// Clear any existing auth data
window.localStorage.removeItem(AUTH_USER);
window.localStorage.removeItem(AUTH_TOKEN);
window.localStorage.removeItem(AUTH_TIMESTAMP);

System.simpleSSOLogin(query.get("token"))
.then((res) => {
if (!res.valid) throw new Error(res.message);

window.localStorage.setItem(AUTH_USER, JSON.stringify(res.user));
window.localStorage.setItem(AUTH_TOKEN, res.token);
window.localStorage.setItem(AUTH_TIMESTAMP, Number(new Date()));
setReady(res.valid);
})
.catch((e) => {
setError(e.message);
});
} catch (e) {
setError(e.message);
}
}, []);

if (error)
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex items-center justify-center flex-col gap-4">
<p className="text-white font-mono text-lg">{error}</p>
<p className="text-white/80 font-mono text-sm">
Please contact the system administrator about this error.
</p>
</div>
);
if (ready) return <Navigate to={redirectPath} />;

// Loading state by default
return <FullScreenLoader />;
}
6 changes: 5 additions & 1 deletion server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -280,4 +280,8 @@ TTS_PROVIDER="native"

# Disable viewing chat history from the UI and frontend APIs.
# See https://docs.anythingllm.com/configuration#disable-view-chat-history for more information.
# DISABLE_VIEW_CHAT_HISTORY=1
# DISABLE_VIEW_CHAT_HISTORY=1

# 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
60 changes: 60 additions & 0 deletions server/endpoints/api/userManagement/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
const { User } = require("../../../models/user");
const { TemporaryAuthToken } = require("../../../models/temporaryAuthToken");
const { multiUserMode } = require("../../../utils/http");
const {
simpleSSOEnabled,
} = require("../../../utils/middleware/simpleSSOEnabled");
const { validApiKey } = require("../../../utils/middleware/validApiKey");

function apiUserManagementEndpoints(app) {
Expand Down Expand Up @@ -59,6 +63,62 @@ function apiUserManagementEndpoints(app) {
response.sendStatus(500).end();
}
});

app.get(
"/v1/users/:id/issue-auth-token",
[validApiKey, simpleSSOEnabled],
async (request, response) => {
/*
#swagger.tags = ['User Management']
#swagger.description = 'Issue a temporary auth token for a user'
#swagger.parameters['id'] = {
in: 'path',
description: 'The ID of the user to issue a temporary auth token for',
required: true,
type: 'string'
}
#swagger.responses[200] = {
content: {
"application/json": {
schema: {
type: 'object',
example: {
token: "1234567890",
loginPath: "/sso/simple?token=1234567890"
}
}
}
}
}
}
#swagger.responses[403] = {
schema: {
"$ref": "#/definitions/InvalidAPIKey"
}
}
#swagger.responses[401] = {
description: "Instance is not in Multi-User mode. Permission denied.",
}
*/
try {
const { id: userId } = request.params;
const user = await User.get({ id: Number(userId) });
if (!user)
return response.status(404).json({ error: "User not found" });

const { token, error } = await TemporaryAuthToken.issue(userId);
if (error) return response.status(500).json({ error: error });

response.status(200).json({
token: String(token),
loginPath: `/sso/simple?token=${token}`,
});
} catch (e) {
console.error(e.message, e);
response.sendStatus(500).end();
}
}
);
}

module.exports = { apiUserManagementEndpoints };
45 changes: 45 additions & 0 deletions server/endpoints/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ const { BrowserExtensionApiKey } = require("../models/browserExtensionApiKey");
const {
chatHistoryViewable,
} = require("../utils/middleware/chatHistoryViewable");
const { simpleSSOEnabled } = require("../utils/middleware/simpleSSOEnabled");
const { TemporaryAuthToken } = require("../models/temporaryAuthToken");

function systemEndpoints(app) {
if (!app) return;
Expand Down Expand Up @@ -251,6 +253,49 @@ function systemEndpoints(app) {
}
});

app.get(
"/request-token/sso/simple",
[simpleSSOEnabled],
async (request, response) => {
const { token: tempAuthToken } = request.query;
const { sessionToken, token, error } =
await TemporaryAuthToken.validate(tempAuthToken);

if (error) {
await EventLogs.logEvent("failed_login_invalid_temporary_auth_token", {
ip: request.ip || "Unknown IP",
multiUserMode: true,
});
return response.status(401).json({
valid: false,
token: null,
message: `[001] An error occurred while validating the token: ${error}`,
});
}

await Telemetry.sendTelemetry(
"login_event",
{ multiUserMode: true },
token.user.id
);
await EventLogs.logEvent(
"login_event",
{
ip: request.ip || "Unknown IP",
username: token.user.username || "Unknown user",
},
token.user.id
);

response.status(200).json({
valid: true,
user: User.filterFields(token.user),
token: sessionToken,
message: null,
});
}
);

app.post(
"/system/recover-account",
[isMultiUserSetup],
Expand Down
3 changes: 3 additions & 0 deletions server/models/systemSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -512,8 +512,11 @@ const SystemSettings = {
GenericOpenAiKey: !!process.env.GENERIC_OPEN_AI_API_KEY,
GenericOpenAiMaxTokens: process.env.GENERIC_OPEN_AI_MAX_TOKENS,

AwsBedrockLLMConnectionMethod:
process.env.AWS_BEDROCK_LLM_CONNECTION_METHOD || "iam",
AwsBedrockLLMAccessKeyId: !!process.env.AWS_BEDROCK_LLM_ACCESS_KEY_ID,
AwsBedrockLLMAccessKey: !!process.env.AWS_BEDROCK_LLM_ACCESS_KEY,
AwsBedrockLLMSessionToken: !!process.env.AWS_BEDROCK_LLM_SESSION_TOKEN,
AwsBedrockLLMRegion: process.env.AWS_BEDROCK_LLM_REGION,
AwsBedrockLLMModel: process.env.AWS_BEDROCK_LLM_MODEL_PREFERENCE,
AwsBedrockLLMTokenLimit: process.env.AWS_BEDROCK_LLM_MODEL_TOKEN_LIMIT,
Expand Down
Loading

0 comments on commit 8c56b00

Please sign in to comment.