diff --git a/bin/core/src/api/read/mod.rs b/bin/core/src/api/read/mod.rs index c3e8996af..bcd424ab9 100644 --- a/bin/core/src/api/read/mod.rs +++ b/bin/core/src/api/read/mod.rs @@ -19,6 +19,7 @@ mod alerter; mod build; mod builder; mod deployment; +mod permission; mod procedure; mod repo; mod search; @@ -34,10 +35,13 @@ mod user; #[serde(tag = "type", content = "params")] enum ReadRequest { GetVersion(GetVersion), + GetCoreInfo(GetCoreInfo), + + // ==== USER ==== GetUsers(GetUsers), GetUsername(GetUsername), - GetCoreInfo(GetCoreInfo), ListApiKeys(ListApiKeys), + ListUserPermissions(ListUserPermissions), // ==== SEARCH ==== FindResources(FindResources), diff --git a/bin/core/src/api/read/permission.rs b/bin/core/src/api/read/permission.rs new file mode 100644 index 000000000..67d1845d3 --- /dev/null +++ b/bin/core/src/api/read/permission.rs @@ -0,0 +1,30 @@ +use anyhow::{anyhow, Context}; +use axum::async_trait; +use monitor_client::{ + api::read::{ListUserPermissions, ListUserPermissionsResponse}, + entities::user::User, +}; +use mungos::{find::find_collect, mongodb::bson::doc}; +use resolver_api::Resolve; + +use crate::{db::db_client, state::State}; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + ListUserPermissions { user_id }: ListUserPermissions, + user: User, + ) -> anyhow::Result { + if !user.admin { + return Err(anyhow!("this method is admin only")); + } + find_collect( + &db_client().await.permissions, + doc! { "user_id": user_id }, + None, + ) + .await + .context("failed to query db for permissions") + } +} diff --git a/client/core/rs/src/api/read/mod.rs b/client/core/rs/src/api/read/mod.rs index 29be37afd..c1ad7e6af 100644 --- a/client/core/rs/src/api/read/mod.rs +++ b/client/core/rs/src/api/read/mod.rs @@ -8,6 +8,7 @@ mod alerter; mod build; mod builder; mod deployment; +mod permission; mod procedure; mod repo; mod search; @@ -20,6 +21,7 @@ pub use alerter::*; pub use build::*; pub use builder::*; pub use deployment::*; +pub use permission::*; pub use procedure::*; pub use repo::*; pub use search::*; diff --git a/client/core/rs/src/api/read/permission.rs b/client/core/rs/src/api/read/permission.rs new file mode 100644 index 000000000..ced5ac15b --- /dev/null +++ b/client/core/rs/src/api/read/permission.rs @@ -0,0 +1,21 @@ +use derive_empty_traits::EmptyTraits; +use resolver_api::derive::Request; +use serde::{Deserialize, Serialize}; +use typeshare::typeshare; + +use crate::entities::permission::Permission; + +use super::MonitorReadRequest; + +#[typeshare] +#[derive( + Serialize, Deserialize, Debug, Clone, Request, EmptyTraits, +)] +#[empty_traits(MonitorReadRequest)] +#[response(ListUserPermissionsResponse)] +pub struct ListUserPermissions { + pub user_id: String, +} + +#[typeshare] +pub type ListUserPermissionsResponse = Vec; diff --git a/client/core/ts/src/responses.ts b/client/core/ts/src/responses.ts index 43a95d3f9..e8e68dbc9 100644 --- a/client/core/ts/src/responses.ts +++ b/client/core/ts/src/responses.ts @@ -10,10 +10,13 @@ export type AuthResponses = { export type ReadResponses = { GetVersion: Types.GetVersionResponse; + GetCoreInfo: Types.GetCoreInfoResponse; + + // ==== USER ==== GetUsers: Types.GetUsersResponse; GetUsername: Types.GetUsernameResponse; - GetCoreInfo: Types.GetCoreInfoResponse; ListApiKeys: Types.ListApiKeysResponse; + ListUserPermissions: Types.ListUserPermissionsResponse; // ==== SEARCH ==== FindResources: Types.FindResourcesResponse; diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index cffbbec77..23013006f 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -316,6 +316,22 @@ export type ListApiKeysResponse = ApiKey[]; export type GetUsersResponse = User[]; +export enum PermissionLevel { + None = "None", + Read = "Read", + Execute = "Execute", + Write = "Write", +} + +export interface Permission { + _id?: MongoId; + user_id: string; + target: ResourceTarget; + level?: PermissionLevel; +} + +export type ListUserPermissionsResponse = Permission[]; + export type ProcedureConfig = | { type: "Sequence", data: EnabledExecution[] } | { type: "Parallel", data: EnabledExecution[] }; @@ -1186,6 +1202,10 @@ export interface GetCoreInfoResponse { monitoring_interval: Timelength; } +export interface ListUserPermissions { + user_id: string; +} + export interface GetProcedure { /** Id or name */ procedure: string; @@ -1565,13 +1585,6 @@ export interface LaunchAwsServerConfig { use_public_ip: boolean; } -export enum PermissionLevel { - None = "None", - Read = "Read", - Execute = "Execute", - Write = "Write", -} - export interface UpdateUserPermissionsOnTarget { user_id: string; permission: PermissionLevel; @@ -1736,13 +1749,6 @@ export interface CloneArgs { github_account?: string; } -export interface Permission { - _id?: MongoId; - user_id: string; - target: ResourceTarget; - level?: PermissionLevel; -} - export type Execution = /** For new executions upon instantiation */ | { type: "None", params: None } @@ -1797,10 +1803,11 @@ export type ExecuteRequest = export type ReadRequest = | { type: "GetVersion", params: GetVersion } + | { type: "GetCoreInfo", params: GetCoreInfo } | { type: "GetUsers", params: GetUsers } | { type: "GetUsername", params: GetUsername } - | { type: "GetCoreInfo", params: GetCoreInfo } | { type: "ListApiKeys", params: ListApiKeys } + | { type: "ListUserPermissions", params: ListUserPermissions } | { type: "FindResources", params: FindResources } | { type: "GetProceduresSummary", params: GetProceduresSummary } | { type: "GetProcedure", params: GetProcedure } diff --git a/frontend/src/components/layouts.tsx b/frontend/src/components/layouts.tsx index 43a55007c..009fd5660 100644 --- a/frontend/src/components/layouts.tsx +++ b/frontend/src/components/layouts.tsx @@ -94,8 +94,8 @@ export const Page = ({ ); interface SectionProps { - title: ReactNode; - children: ReactNode; + title?: ReactNode; + children?: ReactNode; icon?: ReactNode; actions?: ReactNode; } @@ -103,10 +103,10 @@ interface SectionProps { export const Section = ({ title, icon, actions, children }: SectionProps) => (
-
+ {(title || icon) &&
{icon}

{title}

-
+
} {actions}
{children} diff --git a/frontend/src/pages/users.tsx b/frontend/src/pages/users.tsx index 92dfff300..2d7753e1c 100644 --- a/frontend/src/pages/users.tsx +++ b/frontend/src/pages/users.tsx @@ -1,8 +1,15 @@ -import { Page } from "@components/layouts"; -import { useRead } from "@lib/hooks"; +import { Page, Section } from "@components/layouts"; +import { ConfirmButton } from "@components/util"; +import { useInvalidate, useRead, useWrite } from "@lib/hooks"; import { DataTable } from "@ui/data-table"; +import { Label } from "@ui/label"; +import { Switch } from "@ui/switch"; +import { useToast } from "@ui/use-toast"; +import { UserCheck, UserMinus } from "lucide-react"; +import { useNavigate, useParams } from "react-router-dom"; export const UsersPage = () => { + const nav = useNavigate(); const users = useRead("GetUsers", {}).data; return ( @@ -10,10 +17,100 @@ export const UsersPage = () => { data={users ?? []} columns={[ { header: "Username", accessorKey: "username" }, - { header: "Admin", accessorKey: "admin" }, + { header: "Type", accessorKey: "config.type" }, + { + header: "Level", + accessorFn: (user) => (user.admin ? "Admin" : "User"), + }, { header: "Enabled", accessorKey: "enabled" }, ]} + onRowClick={(user) => nav(`/users/${user._id!.$oid}`)} /> ); }; + +export const UserPage = () => { + const { toast } = useToast(); + const inv = useInvalidate(); + const user_id = useParams().id as string; + const user = useRead("GetUsers", {}).data?.find( + (user) => user._id?.$oid === user_id + ); + const { mutate } = useWrite("UpdateUserPerimissions", { + onSuccess: () => inv(["GetUsers"]), + onError: (e) => { + console.log(e); + toast({ title: "Failed to update user permissions" }); + }, + }); + const enabledClass = user?.enabled ? "text-green-500" : "text-red-500"; + return ( + user && ( + +
+ {user?.enabled ? "Enabled" : "Disabled"} +
+ |
Level: {user?.admin ? "Admin" : "User"}
| +
Type: {user?.config.type}
+
+ } + actions={ + !user.admin && ( +
+ {(["Server", "Build"] as Array<"Server" | "Build">).map( + (item) => { + const key = `create_${item.toLowerCase()}_permissions` as + | "create_server_permissions" + | "create_build_permissions"; + const req_key = `create_${item.toLowerCase()}s`; + return ( +
+ + + mutate({ user_id, [req_key]: !user[key] }) + } + /> +
+ ); + } + )} + + ) : ( + + ) + } + variant={user.enabled ? "destructive" : "outline"} + onClick={() => mutate({ user_id, enabled: !user.enabled })} + /> +
+ ) + } + > + + + ) + ); +}; + +const PermissionsTable = () => { + const inv = useInvalidate(); + const user_id = useParams().id as string; + // const permissions = useRead("") + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 397f0fef8..442fe50aa 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -9,7 +9,7 @@ import { RouterProvider, createBrowserRouter } from "react-router-dom"; import { Tree } from "@pages/tree"; import { Tags } from "@pages/tags"; import { ResourceUpdates } from "@pages/resource_update"; -import { UsersPage } from "@pages/users"; +import { UserPage, UsersPage } from "@pages/users"; import { AllResources } from "@pages/all_resources"; import { UserDisabled } from "@pages/user_disabled"; @@ -19,7 +19,6 @@ const router = createBrowserRouter([ element: , children: [ { path: "", element: }, - { path: "users", element: }, { path: "keys", element: }, { path: "tags", element: }, { path: "tree", element: }, @@ -32,6 +31,13 @@ const router = createBrowserRouter([ { path: ":id/updates", element: }, ], }, + { + path: "users", + children: [ + { path: "", element: }, + { path: ":id", element: }, + ], + }, ], }, ]); diff --git a/frontend/src/ui/data-table.tsx b/frontend/src/ui/data-table.tsx index d50a59841..fa48286fb 100644 --- a/frontend/src/ui/data-table.tsx +++ b/frontend/src/ui/data-table.tsx @@ -19,15 +19,15 @@ import { import { ReactNode } from "react"; interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; + columns?: ColumnDef[]; + data?: TData[]; onRowClick?: (row: TData) => void; noResults?: ReactNode; } export function DataTable({ - columns, - data, + columns = [], + data = [], onRowClick, noResults, }: DataTableProps) {