Skip to content

Commit

Permalink
Implement total permission overhaul (#629)
Browse files Browse the repository at this point in the history
* Implement total permission overhaul
Add explicit permissions on each flex and strict route
Patch issues with role escalation and CRUD of users
Patch permissions on all routes for coverage
Improve middleware to accept role array for clarity

* update comments

* remove permissions to API-keys for manager. Manager could generate API-key and using high-privelege api-key give themselves admin

* update sidebar permissions for multi-user and single user

* update options for mobile sidebar
  • Loading branch information
timothycarambat committed Jan 22, 2024
1 parent 62cea07 commit 9a237db
Show file tree
Hide file tree
Showing 16 changed files with 611 additions and 374 deletions.
2 changes: 1 addition & 1 deletion frontend/src/App.jsx
Expand Up @@ -81,7 +81,7 @@ export default function App() {
/>
<Route
path="/settings/api-keys"
element={<ManagerRoute Component={GeneralApiKeys} />}
element={<AdminRoute Component={GeneralApiKeys} />}
/>
<Route
path="/settings/workspace-chats"
Expand Down
213 changes: 138 additions & 75 deletions frontend/src/components/SettingsSidebar/index.jsx
Expand Up @@ -62,79 +62,97 @@ export default function SettingsSidebar() {
<div className="h-[100%] flex flex-col w-full justify-between pt-4 overflow-y-hidden">
<div className="h-auto sidebar-items">
<div className="flex flex-col gap-y-2 h-[65vh] pb-8 overflow-y-scroll no-scroll">
{/* Admin/manager Multi-user Settings */}
{!!user && user?.role !== "default" && (
<>
<Option
href={paths.settings.system()}
btnText="System Preferences"
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.settings.invites()}
btnText="Invitation"
icon={
<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />
}
/>
<Option
href={paths.settings.users()}
btnText="Users"
icon={<Users className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.settings.workspaces()}
btnText="Workspaces"
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
/>
</>
)}

<Option
href={paths.settings.system()}
btnText="System Preferences"
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.invites()}
btnText="Invitation"
icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.users()}
btnText="Users"
icon={<Users className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.workspaces()}
btnText="Workspaces"
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.chats()}
btnText="Workspace Chat"
icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/>

<Option
href={paths.settings.appearance()}
btnText="Appearance"
icon={<Eye className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.apiKeys()}
btnText="API Keys"
icon={<Key className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.llmPreference()}
btnText="LLM Preference"
icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.embeddingPreference()}
btnText="Embedding Preference"
icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.vectorDatabase()}
btnText="Vector Database"
icon={<Database className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.dataConnectors.list()}
btnText="Data Connectors"
icon={<Plugs className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/>

{(!user || user?.role === "admin") && (
<>
<Option
href={paths.settings.llmPreference()}
btnText="LLM Preference"
icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.settings.embeddingPreference()}
btnText="Embedding Preference"
icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.settings.vectorDatabase()}
btnText="Vector Database"
icon={<Database className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.settings.dataConnectors.list()}
btnText="Data Connectors"
icon={<Plugs className="h-5 w-5 flex-shrink-0" />}
/>
</>
)}
<Option
href={paths.settings.security()}
btnText="Security"
icon={<Lock className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/>
</div>
</div>
Expand Down Expand Up @@ -265,63 +283,95 @@ export function SidebarMobileHeader() {
href={paths.settings.system()}
btnText="System Preferences"
icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.invites()}
btnText="Invitation"
icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.users()}
btnText="Users"
icon={<Users className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.workspaces()}
btnText="Workspaces"
icon={<BookOpen className="h-5 w-5 flex-shrink-0" />}
user={user}
allowedRole={["admin", "manager"]}
/>

<Option
href={paths.settings.chats()}
btnText="Workspace Chat"
icon={
<ChatCenteredText className="h-5 w-5 flex-shrink-0" />
}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.appearance()}
btnText="Appearance"
icon={<Eye className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/>
<Option
href={paths.settings.apiKeys()}
btnText="API Keys"
icon={<Key className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.llmPreference()}
btnText="LLM Preference"
icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.embeddingPreference()}
btnText="Embedding Preference"
icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.vectorDatabase()}
btnText="Vector Database"
icon={<Database className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin"]}
/>
<Option
href={paths.settings.dataConnectors.list()}
btnText="Data Connectors"
icon={<Plugs className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/>
{(!user || user?.role === "admin") && (
<>
<Option
href={paths.settings.llmPreference()}
btnText="LLM Preference"
icon={<ChatText className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.settings.embeddingPreference()}
btnText="Embedding Preference"
icon={<FileCode className="h-5 w-5 flex-shrink-0" />}
/>
<Option
href={paths.settings.vectorDatabase()}
btnText="Vector Database"
icon={<Database className="h-5 w-5 flex-shrink-0" />}
/>
</>
)}
<Option
href={paths.settings.security()}
btnText="Security"
icon={<Lock className="h-5 w-5 flex-shrink-0" />}
user={user}
flex={true}
allowedRole={["admin", "manager"]}
/>
</div>
</div>
Expand Down Expand Up @@ -364,8 +414,21 @@ export function SidebarMobileHeader() {
);
}

const Option = ({ btnText, icon, href }) => {
const Option = ({
btnText,
icon,
href,
flex = false,
user = null,
allowedRole = [],
}) => {
const isActive = window.location.pathname === href;

// Option only for multi-user
if (!flex && !allowedRole.includes(user?.role)) return null;

// Option is dual-mode, but user exists, we need to check permissions
if (flex && !!user && !allowedRole.includes(user?.role)) return null;
return (
<div className="flex gap-x-2 items-center justify-between text-white">
<a
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/models/system.js
Expand Up @@ -209,6 +209,7 @@ const System = {
return await fetch(`${API_BASE}/system/pfp/${id}`, {
method: "GET",
cache: "no-cache",
headers: baseHeaders(),
})
.then((res) => {
if (res.ok && res.status !== 204) return res.blob();
Expand Down Expand Up @@ -283,6 +284,7 @@ const System = {
return await fetch(`${API_BASE}/system/welcome-messages`, {
method: "GET",
cache: "no-cache",
headers: baseHeaders(),
})
.then((res) => {
if (!res.ok) throw new Error("Could not fetch welcome messages.");
Expand Down
35 changes: 29 additions & 6 deletions frontend/src/pages/Admin/Users/UserRow/index.jsx
Expand Up @@ -3,9 +3,17 @@ import { titleCase } from "text-case";
import Admin from "@/models/admin";
import EditUserModal, { EditUserModalId } from "./EditUserModal";
import { DotsThreeOutline } from "@phosphor-icons/react";
import showToast from "@/utils/toast";

const ModMap = {
admin: ["admin", "manager", "default"],
manager: ["manager", "default"],
default: [],
};

export default function UserRow({ currUser, user }) {
const rowRef = useRef(null);
const canModify = ModMap[currUser?.role || "default"].includes(user.role);
const [suspended, setSuspended] = useState(user.suspended === 1);
const handleSuspend = async () => {
if (
Expand All @@ -14,8 +22,19 @@ export default function UserRow({ currUser, user }) {
)
)
return false;
setSuspended(!suspended);
await Admin.updateUser(user.id, { suspended: suspended ? 0 : 1 });

const { success, error } = await Admin.updateUser(user.id, {
suspended: suspended ? 0 : 1,
});
if (!success) showToast(error, "error", { clear: true });
if (success) {
showToast(
`User ${!suspended ? "has been suspended" : "is no longer suspended"}.`,
"success",
{ clear: true }
);
setSuspended(!suspended);
}
};
const handleDelete = async () => {
if (
Expand All @@ -24,8 +43,12 @@ export default function UserRow({ currUser, user }) {
)
)
return false;
rowRef?.current?.remove();
await Admin.deleteUser(user.id);
const { success, error } = await Admin.deleteUser(user.id);
if (!success) showToast(error, "error", { clear: true });
if (success) {
rowRef?.current?.remove();
showToast("User deleted from system.", "success", { clear: true });
}
};

return (
Expand All @@ -40,7 +63,7 @@ export default function UserRow({ currUser, user }) {
<td className="px-6 py-4">{titleCase(user.role)}</td>
<td className="px-6 py-4">{user.createdAt}</td>
<td className="px-6 py-4 flex items-center gap-x-6">
{currUser?.role !== "default" && (
{canModify && (
<button
onClick={() =>
document?.getElementById(EditUserModalId(user))?.showModal()
Expand All @@ -50,7 +73,7 @@ export default function UserRow({ currUser, user }) {
<DotsThreeOutline weight="fill" className="h-5 w-5" />
</button>
)}
{currUser?.id !== user.id && currUser?.role !== "default" && (
{currUser?.id !== user.id && canModify && (
<>
<button
onClick={handleSuspend}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/pages/Admin/Users/index.jsx
Expand Up @@ -105,7 +105,8 @@ const ROLE_HINT = {
"Cannot modify any settings at all.",
],
manager: [
"Can view all workspaces and modify all settings.",
"Can view, create, and delete any workspaces and modify workspace-specific settings.",
"Can create, update and invite new users to the instance.",
"Cannot modify LLM, vectorDB, embedding, or other connections.",
],
admin: [
Expand Down

0 comments on commit 9a237db

Please sign in to comment.