codex: Add ability to update user permissions

- Active
- Registered
- Disabled
This commit is contained in:
2026-01-14 15:37:19 +01:00
parent 0a543c7b21
commit 5d6fe5572b
7 changed files with 160 additions and 23 deletions

View File

@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.0",
"version": "10.0.100",
"rollForward": "latestMinor"
}
}

View File

@@ -9,6 +9,7 @@
<ItemGroup>
<Compile Include="../Shared/Remoting.fs" />
<Compile Include="Types.fs" />
<Compile Include="Utils.fs" />
<Compile Include="Extensions.fs" />
<Compile Include="Remoting.fs" />
<Compile Include="Map.fs" />

View File

@@ -6,17 +6,47 @@ open Feliz
[<Erase>]
type OpenFGA =
[<ReactComponent>]
static member private Spinner() =
JSX.html
$"""
import {{ Spinner }} from "@fluentui/react-components";
<Spinner size="tiny" />
"""
[<ReactComponent>]
static member Checkbox(key, label: string, user: string, relation: string, object: string) =
let isLoading, setLoading = React.useState false
let isChecked, setChecked = React.useState false
let handleChange (ev: Types.Event) =
console.debug("[OpenFGA.Checkbox] Checkbox %s changed to %o", key, not isChecked)
console.debug("[OpenFGA.Checkbox] Checkbox %s for user %s rel %s changed to %o", key, user, relation, not isChecked)
// TODO: Write to OpenFGA
setChecked(not isChecked)
let newChecked = not isChecked
// setChecked
setLoading true
Remoting.adminApi.setUserPermissions {
User = user
Permissions = [|
{ Name = relation; Enabled = newChecked }
|]
}
|> Async.StartAsPromise
|> Promise.iter (fun res ->
match res with
| Ok () ->
console.debug("Success.")
setChecked newChecked
| Error err ->
console.error("Error: %s", err)
setLoading false
)
React.useEffect (
(fun () ->
setLoading true
Remoting.openFgaApi.Check {
User = user
Relation = relation
@@ -30,6 +60,8 @@ type OpenFGA =
setChecked hasRelation
| Error err ->
console.error("[OpenFGA.Checkbox] Error checking user %s has relation %s to %s", user, relation, object)
setLoading false
)
),
[| |]
@@ -38,6 +70,9 @@ type OpenFGA =
Html.div [
prop.classes [ "flex-row-center" ]
prop.children [
if isLoading then
OpenFGA.Spinner() |> Utils.toReact
else
Html.input [
prop.id (sprintf "openfga-checkbox-%s" key)
prop.type'.checkbox
@@ -51,4 +86,3 @@ type OpenFGA =
]
]
]

View File

@@ -0,0 +1,7 @@
namespace Oceanbox.Codex
module Utils =
open Fable.Core
open Feliz
let toReact (el: JSX.Element) : ReactElement = unbox el

View File

@@ -528,22 +528,70 @@ module Admin =
return ()
}
let setUserPermissions (ctx: HttpContext) (req: Remoting.UserPermissionRequest) =
async {
let user = ctx.User.Identity.Name
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
do logger.LogInformation ("setUserPermissions from {User}: {@Req}", user, req)
// TODO(simkir): Sanitize/check the request, aka turn the dto to an internal type
try
let fga = ctx.GetService<OpenFgaClient> ()
let writes: Model.ClientWriteRequest =
req.Permissions
|> Array.choose (fun permission ->
if permission.Enabled then
Some {
Remoting.Tuple.empty with
User = req.User
Relation = permission.Name
Object = req.User
}
else
None
)
|> OpenFGA.Queries.write
if writes.Writes.Count > 0 then
let! fgaWriteResp = fga.Write writes |> Async.AwaitTask
do logger.LogInformation ("OpenFGA write responded with: {JSON}", fgaWriteResp.ToJson ())
let deletes =
req.Permissions
|> Array.choose (fun permission ->
if permission.Enabled then
None
else
Remoting.Tuple.delete(req.User, permission.Name, req.User)
|> Some
)
|> OpenFGA.Queries.deleteTuples
if deletes.Count > 0 then
let! fgaDeleteResp = fga.DeleteTuples deletes |> Async.AwaitTask
do logger.LogInformation ("OpenFGA delete responded with: {JSON}", fgaDeleteResp.ToJson ())
return Ok ()
with e ->
do logger.LogError (e, "Error setting user permissions")
// TODO: Maybe do not send exn message
return Error (sprintf "Error setting user permissions: %s" e.Message)
}
let private impl (ctx: HttpContext) : Remoting.Api.Admin = {
addUsers = Handler.addUsers ctx
addArchive = Handler.addArchive ctx
addArchiveGroups = Handler.addArchiveGroups ctx
addGroupPermissions = Handler.addGroupPermissions ctx
addUsers = Handler.addUsers ctx
deleteArchive = Handler.deleteArchive ctx
getAllGroups = Handler.getAllGroups ctx
getArchive = Handler.getArchive ctx
getArchiveCount = Handler.getArchiveCount ctx
getArchiveDataSet = Handler.getArchiveDataSet ctx
getArchiveRefs = Handler.getArchiveRefs ctx
getArchiveTypes = fun () -> Handler.getArchiveTypes ctx
getArchives = Handler.getArchives ctx
getDataSets = fun () -> Handler.getAllDataSets ctx
getGroupUsers = Handler.getGroupUsers ctx
removeUsers = Handler.removeUsers ctx
getDataSets = fun () -> Handler.getAllDataSets ctx
getArchiveDataSet = Handler.getArchiveDataSet ctx
addArchive = Handler.addArchive ctx
setUserPermissions = Handler.setUserPermissions ctx
updateArchive = Handler.updateArchive ctx
}

View File

@@ -71,7 +71,8 @@ module OpenFGA =
|> Seq.toArray
|> Array.map (fun t ->
let condition : Remoting.Condition option =
Option.ofObj t.Key.Condition
t.Key.Condition
|> Option.ofObj
|> Option.map (fun cond -> {
Name = cond.Name
Context = JsonSerializer.Serialize cond.Context
@@ -176,7 +177,23 @@ module OpenFGA =
result
let delete' (tuples: ClientTupleKey array) =
/// To be used with OpenFga.Sdk.Client.OpenFgaClient.DeleteTuples
let deleteTuples (tuples: Remoting.Tuple array) : ResizeArray<ClientTupleKeyWithoutCondition> =
let deletes: ClientTupleKeyWithoutCondition array =
tuples
|> Array.map (fun tuple ->
let result = ClientTupleKeyWithoutCondition ()
do result.Object <- tuple.Object
do result.Relation <- tuple.Relation
do result.User <- tuple.User
result
)
ResizeArray deletes
let delete' (tuples: ClientTupleKey array) : ClientWriteRequest =
let result = ClientWriteRequest ()
let deletes: ClientTupleKeyWithoutCondition array =
@@ -195,6 +212,22 @@ module OpenFGA =
result
/// To be used with OpenFga.Sdk.Client.OpenFgaClient.DeleteTuples
let deleteTuples' (tuples: ClientTupleKey array) : ResizeArray<ClientTupleKeyWithoutCondition> =
let deletes: ClientTupleKeyWithoutCondition array =
tuples
|> Array.map (fun tuple ->
let result = ClientTupleKeyWithoutCondition ()
do result.Object <- tuple.Object
do result.Relation <- tuple.Relation
do result.User <- tuple.User
result
)
ResizeArray deletes
let write (tuples: Remoting.Tuple array) =
let result = ClientWriteRequest ()
@@ -310,9 +343,10 @@ module OpenFGA =
let logger = ctx.GetLogger<Remoting.Api.OpenFGA> ()
let fga = ctx.GetService<OpenFgaClient> ()
try
let deleteRequest = Queries.delete [| tuple |]
do logger.LogInformation ("Delete req: {Request}", deleteRequest.ToJson ())
let! resp = fga.Write deleteRequest |> Async.AwaitTask
let deleteRequest = Queries.deleteTuples [| tuple |]
let json = JsonSerializer.Serialize deleteRequest
do logger.LogInformation ("Delete req: {Request}", json)
let! resp = fga.DeleteTuples deleteRequest |> Async.AwaitTask
do logger.LogInformation ("Delete resp: {Response}", resp.ToJson ())
return Ok (resp.Deletes.Count >= 1)
with e ->

View File

@@ -196,26 +196,39 @@ module Remoting =
Permissions: ArchiveRelation array
}
[<Struct>]
type Permission = {
Name: string
Enabled: bool
}
[<Struct>]
type UserPermissionRequest = {
User: string
Permissions: Permission array
}
[<RequireQualifiedAccess>]
module Api =
type Auth = { IsAuthenticated: Async<bool> }
type Admin = {
addUsers: AddUsersRequest -> Async<Result<unit, string>>
addArchive: AddArchiveRequest -> Async<Result<Archive, string>>
addArchiveGroups: AddArchiveGroupsRequest -> Async<Result<unit, string>>
addGroupPermissions: AddGroupPermissionsRequest -> Async<Result<unit, string>>
addUsers: AddUsersRequest -> Async<Result<unit, string>>
deleteArchive: Archmaester.Dto.ArchiveId -> Async<Result<bool, string>>
getAllGroups: Async<string array>
getArchive: Archmaester.Dto.ArchiveId -> Async<Result<Archmaester.Dto.ArchiveProps, string>>
getArchiveCount: Archmaester.Dto.ArchiveFilter -> Async<Result<int, string>>
getArchiveDataSet: System.Guid -> Async<Result<DataSet, string>>
getArchiveRefs: Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>>
getArchiveTypes: unit -> Async<Archmaester.Dto.ArchiveType array>
getArchives: int -> int -> Archmaester.Dto.ArchiveFilter -> Async<Result<Archmaester.Dto.ArchiveProps array, string>>
getDataSets: unit -> Async<Result<DataSet array, string>>
getGroupUsers: string -> Async<string array>
removeUsers: string array -> Async<Result<unit, string>>
getDataSets: unit -> Async<Result<DataSet array, string>>
getArchiveDataSet: System.Guid -> Async<Result<DataSet, string>>
addArchive: AddArchiveRequest -> Async<Result<Archive, string>>
setUserPermissions: UserPermissionRequest -> Async<Result<unit, string>>
updateArchive: System.Guid -> EditArchiveRequest -> Async<Result<Archive, string>>
}