diff --git a/app/src/auth/AppAuth.ts b/app/src/auth/AppAuth.ts index a0c6072..cead597 100644 --- a/app/src/auth/AppAuth.ts +++ b/app/src/auth/AppAuth.ts @@ -61,7 +61,7 @@ export class AppAuth extends Module { // register roles const roles = transformObject(this.config.roles ?? {}, (role, name) => { - return Role.create({ name, ...role }); + return Role.create(name, role); }); this.ctx.guard.setRoles(Object.values(roles)); this.ctx.guard.setConfig(this.config.guard ?? {}); @@ -210,10 +210,13 @@ export class AppAuth extends Module { } const strategies = this.authenticator.getStrategies(); + const roles = Object.fromEntries(this.ctx.guard.getRoles().map((r) => [r.name, r.toJSON()])); + console.log("roles", roles); return { ...this.config, ...this.authenticator.toJSON(secrets), + roles: secrets ? roles : undefined, strategies: transformObject(strategies, (strategy) => ({ enabled: this.isStrategyEnabled(strategy), ...strategy.toJSON(secrets), diff --git a/app/src/auth/auth-schema.ts b/app/src/auth/auth-schema.ts index 4fd40a4..e479ea1 100644 --- a/app/src/auth/auth-schema.ts +++ b/app/src/auth/auth-schema.ts @@ -1,6 +1,7 @@ import { cookieConfig, jwtConfig } from "auth/authenticate/Authenticator"; import { CustomOAuthStrategy, OAuthStrategy, PasswordStrategy } from "auth/authenticate/strategies"; -import { objectTransform, s } from "bknd/utils"; +import { roleSchema } from "auth/authorize/Role"; +import { objectTransform, omitKeys, pick, s } from "bknd/utils"; import { $object, $record } from "modules/mcp"; export const Strategies = { @@ -40,11 +41,8 @@ export type AppAuthCustomOAuthStrategy = s.Static; export type PermissionContext

> = P extends Permission< any, diff --git a/app/src/auth/authorize/Role.ts b/app/src/auth/authorize/Role.ts index 3f072a1..7506fc7 100644 --- a/app/src/auth/authorize/Role.ts +++ b/app/src/auth/authorize/Role.ts @@ -13,7 +13,7 @@ export const rolePermissionSchema = s.strictObject({ export type RolePermissionSchema = s.Static; export const roleSchema = s.strictObject({ - name: s.string(), + // @todo: remove anyOf, add migration permissions: s.anyOf([s.array(s.string()), s.array(rolePermissionSchema)]).optional(), is_default: s.boolean().optional(), implicit_allow: s.boolean().optional(), @@ -44,7 +44,7 @@ export class Role { public implicit_allow: boolean = false, ) {} - static create(config: RoleSchema) { + static create(name: string, config: RoleSchema) { const permissions = config.permissions?.map((p: string | RolePermissionSchema) => { if (typeof p === "string") { @@ -53,12 +53,11 @@ export class Role { const policies = p.policies?.map((policy) => new Policy(policy)); return new RolePermission(new Permission(p.permission), policies, p.effect); }) ?? []; - return new Role(config.name, permissions, config.is_default, config.implicit_allow); + return new Role(name, permissions, config.is_default, config.implicit_allow); } toJSON() { return { - name: this.name, permissions: this.permissions.map((p) => p.toJSON()), is_default: this.is_default, implicit_allow: this.implicit_allow, diff --git a/app/src/modules/server/SystemController.ts b/app/src/modules/server/SystemController.ts index 4469c7e..45787bf 100644 --- a/app/src/modules/server/SystemController.ts +++ b/app/src/modules/server/SystemController.ts @@ -32,6 +32,7 @@ import { getVersion } from "core/env"; import type { Module } from "modules/Module"; import { getSystemMcp } from "modules/mcp/system-mcp"; import type { DbModuleManager } from "modules/db/DbModuleManager"; +import type { TPermission } from "auth/authorize/Permission"; export type ConfigUpdate = { success: true; @@ -46,7 +47,8 @@ export type SchemaResponse = { schema: ModuleSchemas; readonly: boolean; config: ModuleConfigs; - permissions: string[]; + //permissions: string[]; + permissions: TPermission[]; }; export class SystemController extends Controller { @@ -412,11 +414,24 @@ export class SystemController extends Controller { readonly, schema, config: config ? this.app.toJSON(secrets) : undefined, - permissions: this.app.modules.ctx().guard.getPermissionNames(), + permissions: this.app.modules.ctx().guard.getPermissions(), + //permissions: this.app.modules.ctx().guard.getPermissionNames(), }); }, ); + hono.get( + "/permissions", + describeRoute({ + summary: "Get the permissions", + tags: ["system"], + }), + (c) => { + const permissions = this.app.modules.ctx().guard.getPermissions(); + return c.json({ permissions }); + }, + ); + hono.post( "/build", describeRoute({ diff --git a/app/src/ui/client/BkndProvider.tsx b/app/src/ui/client/BkndProvider.tsx index abb0020..c37d182 100644 --- a/app/src/ui/client/BkndProvider.tsx +++ b/app/src/ui/client/BkndProvider.tsx @@ -15,13 +15,14 @@ import { AppReduced } from "./utils/AppReduced"; import { Message } from "ui/components/display/Message"; import { useNavigate } from "ui/lib/routes"; import type { BkndAdminProps } from "ui/Admin"; +import type { TPermission } from "auth/authorize/Permission"; export type BkndContext = { version: number; readonly: boolean; schema: ModuleSchemas; config: ModuleConfigs; - permissions: string[]; + permissions: TPermission[]; hasSecrets: boolean; requireSecrets: () => Promise; actions: ReturnType; diff --git a/app/src/ui/components/form/json-schema-form/ObjectField.tsx b/app/src/ui/components/form/json-schema-form/ObjectField.tsx index 3cf920b..748bf23 100644 --- a/app/src/ui/components/form/json-schema-form/ObjectField.tsx +++ b/app/src/ui/components/form/json-schema-form/ObjectField.tsx @@ -2,7 +2,8 @@ import { isTypeSchema } from "ui/components/form/json-schema-form/utils"; import { AnyOfField } from "./AnyOfField"; import { Field } from "./Field"; import { FieldWrapper, type FieldwrapperProps } from "./FieldWrapper"; -import { type JSONSchema, useDerivedFieldContext } from "./Form"; +import { type JSONSchema, useDerivedFieldContext, useFormValue } from "./Form"; +import { JsonEditor } from "ui/components/code/JsonEditor"; export type ObjectFieldProps = { path?: string; diff --git a/app/src/ui/routes/auth/auth.roles.edit.$role.tsx b/app/src/ui/routes/auth/auth.roles.edit.$role.tsx index 7ee4ea6..ff7c39d 100644 --- a/app/src/ui/routes/auth/auth.roles.edit.$role.tsx +++ b/app/src/ui/routes/auth/auth.roles.edit.$role.tsx @@ -1,17 +1,38 @@ -import { useRef } from "react"; -import { TbDots } from "react-icons/tb"; import { useBknd } from "ui/client/bknd"; -import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth"; -import { Button } from "ui/components/buttons/Button"; -import { IconButton } from "ui/components/buttons/IconButton"; import { Message } from "ui/components/display/Message"; +import { useBkndAuth } from "ui/client/schema/auth/use-bknd-auth"; +import { useBrowserTitle } from "ui/hooks/use-browser-title"; +import { useRef, useState } from "react"; +import { useNavigate } from "ui/lib/routes"; +import { isDebug } from "core/env"; import { Dropdown } from "ui/components/overlay/Dropdown"; -import * as AppShell from "ui/layouts/AppShell/AppShell"; +import { IconButton } from "ui/components/buttons/IconButton"; +import { TbAdjustments, TbDots, TbLock, TbLockOpen, TbLockOpen2 } from "react-icons/tb"; +import { Button } from "ui/components/buttons/Button"; import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2"; -import { routes, useNavigate } from "ui/lib/routes"; -import { AuthRoleForm, type AuthRoleFormRef } from "ui/routes/auth/forms/role.form"; +import { routes } from "ui/lib/routes"; +import * as AppShell from "ui/layouts/AppShell/AppShell"; +import * as Formy from "ui/components/form/Formy"; + +import { ucFirst, type s } from "bknd/utils"; +import type { ModuleSchemas } from "bknd"; +import { + ArrayField, + Field, + Form, + FormDebug, + Subscribe, + useFormContext, + useFormValue, +} from "ui/components/form/json-schema-form"; +import type { TPermission } from "auth/authorize/Permission"; +import type { RoleSchema } from "auth/authorize/Role"; +import { SegmentedControl, Tooltip } from "@mantine/core"; +import { cn } from "ui/lib/utils"; export function AuthRolesEdit(props) { + useBrowserTitle(["Auth", "Roles", props.params.role]); + const { hasSecrets } = useBknd({ withSecrets: true }); if (!hasSecrets) { return ; @@ -20,32 +41,46 @@ export function AuthRolesEdit(props) { return ; } +// currently for backward compatibility +function getSchema(authSchema: ModuleSchemas["auth"]) { + const roles = authSchema.properties.roles.additionalProperties; + return { + ...roles, + properties: { + ...roles.properties, + permissions: { + ...roles.properties.permissions.anyOf[1], + }, + }, + }; +} + +const formConfig = { + options: { + debug: isDebug(), + }, +}; + function AuthRolesEditInternal({ params }) { const [navigate] = useNavigate(); - const { config, actions } = useBkndAuth(); + const { config, schema: authSchema, actions } = useBkndAuth(); const roleName = params.role; const role = config.roles?.[roleName]; - const formRef = useRef(null); const { readonly } = useBknd(); + const schema = getSchema(authSchema); - async function handleUpdate() { - console.log("data", formRef.current?.isValid()); - if (!formRef.current?.isValid()) return; - const data = formRef.current?.getData(); + async function handleDelete() {} + async function handleUpdate(data: any) { + console.log("data", data); const success = await actions.roles.patch(roleName, data); - if (success) { + console.log("success", success); + /* if (success) { navigate(routes.auth.roles.list()); - } - } - - async function handleDelete() { - if (await actions.roles.delete(roleName)) { - navigate(routes.auth.roles.list()); - } + } */ } return ( - <> +

@@ -69,9 +104,23 @@ function AuthRolesEditInternal({ params }) { {!readonly && ( - + ({ + dirty: state.dirty, + errors: state.errors.length > 0, + submitting: state.submitting, + })} + > + {({ dirty, errors, submitting }) => ( + + )} + )} } @@ -85,8 +134,120 @@ function AuthRolesEditInternal({ params }) { /> - +
+
+ +
+ +
+ + +
+
+
- +
); } + +type PermissionsData = Exclude; +type PermissionData = PermissionsData[number]; + +const Permissions = () => { + const { permissions } = useBknd(); + + const grouped = permissions.reduce( + (acc, permission, index) => { + const [group, name] = permission.name.split(".") as [string, string]; + if (!acc[group]) acc[group] = []; + acc[group].push({ index, permission }); + return acc; + }, + {} as Record, + ); + + return ( +
+ {Object.entries(grouped).map(([group, rows]) => { + return ( +
+

{ucFirst(group)} Permissions

+
+ {rows.map(({ index, permission }) => ( + + ))} +
+
+ ); + })} +
+ ); +}; + +const Permission = ({ permission, index }: { permission: TPermission; index?: number }) => { + const path = `permissions.${index}`; + const { value } = useFormValue(path); + const { setValue, deleteValue } = useFormContext(); + const [open, setOpen] = useState(false); + const data = value as PermissionData | undefined; + + async function handleSwitch() { + if (data) { + deleteValue(path); + } else { + setValue(path, { + permission: permission.name, + policies: [], + effect: "allow", + }); + } + } + + return ( + <> +
+
+
{permission.name}
+
+ + + setOpen((o) => !o)} + /> + +
+
+ {open && ( +
+ +
+ )} +
+ + ); +}; diff --git a/app/src/ui/routes/auth/auth.roles.tsx b/app/src/ui/routes/auth/auth.roles.tsx index 59c7e22..15596bf 100644 --- a/app/src/ui/routes/auth/auth.roles.tsx +++ b/app/src/ui/routes/auth/auth.roles.tsx @@ -12,8 +12,21 @@ import { CellValue, DataTable } from "../../components/table/DataTable"; import * as AppShell from "../../layouts/AppShell/AppShell"; import { routes, useNavigate } from "../../lib/routes"; import { useBknd } from "ui/client/bknd"; +import { useBrowserTitle } from "ui/hooks/use-browser-title"; +import { Message } from "ui/components/display/Message"; -export function AuthRolesList() { +export function AuthRolesList(props) { + useBrowserTitle(["Auth", "Roles"]); + + const { hasSecrets } = useBknd({ withSecrets: true }); + if (!hasSecrets) { + return ; + } + + return ; +} + +function AuthRolesListInternal() { const [navigate] = useNavigate(); const { config, actions } = useBkndAuth(); const { readonly } = useBknd(); @@ -21,7 +34,7 @@ export function AuthRolesList() { const data = Object.values( transformObject(config.roles ?? {}, (role, name) => ({ role: name, - permissions: role.permissions, + permissions: role.permissions?.map((p) => p.permission) as string[], is_default: role.is_default ?? false, implicit_allow: role.implicit_allow ?? false, })), diff --git a/app/src/ui/routes/auth/forms/role.form.tsx b/app/src/ui/routes/auth/forms/role.form.tsx index 0a16d2d..d1b7f51 100644 --- a/app/src/ui/routes/auth/forms/role.form.tsx +++ b/app/src/ui/routes/auth/forms/role.form.tsx @@ -34,7 +34,11 @@ export const AuthRoleForm = forwardRef< getValues, } = useForm({ resolver: standardSchemaResolver(schema), - defaultValues: role, + defaultValues: { + ...role, + // compat + permissions: role?.permissions?.map((p) => p.permission), + }, }); useImperativeHandle(ref, () => ({ @@ -47,7 +51,7 @@ export const AuthRoleForm = forwardRef<
{/*

Role Permissions

*/} - + p.name)} />
, ); - console.log("grouped", grouped); - //console.log("fieldState", fieldState, value); return (
{Object.entries(grouped).map(([group, permissions]) => { @@ -121,7 +123,7 @@ const Permissions = ({

{ucFirst(group)} Permissions

{permissions.map((permission) => { - const selected = data.includes(permission); + const selected = data.includes(permission as any); return (
diff --git a/app/src/ui/routes/settings/routes/auth.settings.tsx b/app/src/ui/routes/settings/routes/auth.settings.tsx index 6432570..a5faf5f 100644 --- a/app/src/ui/routes/settings/routes/auth.settings.tsx +++ b/app/src/ui/routes/settings/routes/auth.settings.tsx @@ -63,10 +63,10 @@ export const AuthSettings = ({ schema: _unsafe_copy, config }) => { } catch (e) {} console.log("_s", _s); const roleSchema = _schema.properties.roles?.additionalProperties ?? { type: "object" }; - if (_s.permissions) { + /* if (_s.permissions) { roleSchema.properties.permissions.items.enum = _s.permissions; roleSchema.properties.permissions.uniqueItems = true; - } + } */ return ( diff --git a/app/src/ui/routes/test/index.tsx b/app/src/ui/routes/test/index.tsx index 71bb87f..95681fd 100644 --- a/app/src/ui/routes/test/index.tsx +++ b/app/src/ui/routes/test/index.tsx @@ -27,6 +27,7 @@ import SortableTest from "./tests/sortable-test"; import { SqlAiTest } from "./tests/sql-ai-test"; import Themes from "./tests/themes"; import ErrorBoundary from "ui/components/display/ErrorBoundary"; +import CodeEditorTest from "./tests/code-editor-test"; const tests = { DropdownTest, @@ -52,6 +53,7 @@ const tests = { JsonSchemaForm3, FormyTest, HtmlFormTest, + CodeEditorTest, } as const; export default function TestRoutes() {