codex: Allow for update of group archive permissions
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
namespace Oceanbox.Codex
|
||||
|
||||
type private Permission = {
|
||||
Tuple: Remoting.Tuple
|
||||
Relation: Remoting.ArchiveRelation
|
||||
}
|
||||
|
||||
module GroupArchive =
|
||||
open Browser
|
||||
open Fable.Core
|
||||
@@ -19,6 +24,18 @@ module GroupArchive =
|
||||
return res
|
||||
}
|
||||
|
||||
let private putPermissions (group: string) (archiveId: System.Guid) (permissions: Remoting.ArchiveRelation array) =
|
||||
promise {
|
||||
console.debug("Updating existing relations: %o", permissions)
|
||||
let req : Remoting.AddGroupPermissionsRequest = {
|
||||
Group = Groups.Utils.canonicalizeName group
|
||||
ArchiveId = archiveId
|
||||
Permissions = permissions
|
||||
}
|
||||
let! res = Remoting.adminApi.updateGroupPermissions req |> Async.StartAsPromise
|
||||
return res
|
||||
}
|
||||
|
||||
[<ReactComponent>]
|
||||
let private DeleteRelationButton onDelete (tuple: Remoting.Tuple) =
|
||||
let handleDelete (ev: Types.Event) =
|
||||
@@ -43,61 +60,127 @@ module GroupArchive =
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
let private ViewTerm
|
||||
(onDelete: Remoting.Tuple -> unit)
|
||||
(viewTerm: Remoting.ViewTerm)
|
||||
(tuple: Remoting.Tuple)
|
||||
=
|
||||
let private PermissionCard (title: string) onDelete (tuple: Remoting.Tuple) (children: ReactElement array) =
|
||||
Html.div [
|
||||
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
|
||||
prop.style [ style.flexBasis (length.px 320) ]
|
||||
prop.style [ style.flexBasis (length.px 384) ]
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.classes [ "flex-row-center" ]
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.classes [ "grow" ]
|
||||
prop.children [ Html.b "View Term" ]
|
||||
prop.children [
|
||||
Html.b [
|
||||
prop.style [
|
||||
style.fontSize(length.px 16)
|
||||
]
|
||||
prop.text title
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
DeleteRelationButton onDelete tuple
|
||||
]
|
||||
]
|
||||
|
||||
Html.div [
|
||||
prop.classes [ "ml-16" ]
|
||||
prop.children [
|
||||
Html.div (sprintf "Start time: %s" (Intl.shortDateTime viewTerm.StartTime))
|
||||
Html.div (sprintf "End time: %s" (Intl.shortDateTime viewTerm.EndTime))
|
||||
]
|
||||
prop.children children
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
let private ExecTicket onDelete (ticket: Remoting.ExecTicket) (tuple: Remoting.Tuple) =
|
||||
Html.div [
|
||||
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
|
||||
prop.style [ style.flexBasis (length.px 320) ]
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.classes [ "flex-row-center" ]
|
||||
prop.children [
|
||||
Html.div [ prop.classes [ "grow" ]; prop.children [ Html.b "Exec Ticket" ] ]
|
||||
let private ViewTerm
|
||||
key
|
||||
(onUpdate: Permission -> unit)
|
||||
(permission: Permission)
|
||||
(viewTerm: Remoting.ViewTerm)
|
||||
=
|
||||
let updateCond (newCond: Remoting.ViewTerm) =
|
||||
let updatedCond =
|
||||
permission.Tuple.Condition
|
||||
|> Option.map (fun cond ->
|
||||
{ cond with Context = JS.JSON.stringify newCond.JsonObj }
|
||||
)
|
||||
onUpdate {
|
||||
permission with
|
||||
Tuple.Condition = updatedCond
|
||||
Relation = Remoting.ArchiveRelation.ViewTerm newCond
|
||||
}
|
||||
|
||||
DeleteRelationButton onDelete tuple
|
||||
]
|
||||
]
|
||||
Html.div [
|
||||
prop.classes [ "ml-16" ]
|
||||
prop.children [
|
||||
Html.div (sprintf "Start time: %s" (Intl.shortDateTime ticket.StartTime))
|
||||
Html.div (sprintf "End time: %s" (Intl.shortDateTime ticket.EndTime))
|
||||
Html.div (sprintf "Quota: %.1f" ticket.Quota)
|
||||
Html.div [
|
||||
prop.children [
|
||||
Html.span "Tasks:"
|
||||
Html.ul [
|
||||
prop.children (ticket.Tasks |> Array.map (fun task -> Html.li task))
|
||||
let handleStartChange =
|
||||
React.useCallback (
|
||||
(fun (newStartOpt: System.DateTime option) ->
|
||||
match newStartOpt with
|
||||
| Some newStart ->
|
||||
let updated = { viewTerm with StartTime = newStart }
|
||||
updateCond updated
|
||||
| None ->
|
||||
console.error("Got no date from date picker")
|
||||
),
|
||||
[| permission |]
|
||||
)
|
||||
let handleEndChange =
|
||||
React.useCallback (
|
||||
(fun (newEndOpt: System.DateTime option) ->
|
||||
match newEndOpt with
|
||||
| Some newEnd ->
|
||||
let updated = { viewTerm with EndTime = newEnd }
|
||||
updateCond updated
|
||||
| None ->
|
||||
console.error("Got no date from date picker")
|
||||
),
|
||||
[| permission |]
|
||||
)
|
||||
|
||||
Fui.table [
|
||||
table.size.medium
|
||||
table.children [
|
||||
Fui.tableBody [
|
||||
tableBody.children [
|
||||
Fui.tableRow [
|
||||
tableRow.key "view-term-start-time"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "Start time"
|
||||
]
|
||||
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.datePicker [
|
||||
datePicker.size.small
|
||||
datePicker.onSelectDate handleStartChange
|
||||
datePicker.value (Some viewTerm.StartTime)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Fui.tableRow [
|
||||
tableRow.key "view-term-end-time"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "End time"
|
||||
]
|
||||
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.datePicker [
|
||||
datePicker.size.small
|
||||
datePicker.onSelectDate handleEndChange
|
||||
datePicker.value (Some viewTerm.EndTime)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -106,88 +189,279 @@ module GroupArchive =
|
||||
]
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
let private ExecTicket
|
||||
(key: string)
|
||||
(onUpdate: Permission -> unit)
|
||||
(permission: Permission)
|
||||
(ticket: Remoting.ExecTicket)
|
||||
=
|
||||
let updateCond (newCond: Remoting.ExecTicket) =
|
||||
let updatedCond =
|
||||
permission.Tuple.Condition
|
||||
|> Option.map (fun cond ->
|
||||
{ cond with Context = JS.JSON.stringify newCond.JsonObj }
|
||||
)
|
||||
onUpdate {
|
||||
permission with
|
||||
Tuple.Condition = updatedCond
|
||||
Relation = Remoting.ArchiveRelation.ExecTicket newCond
|
||||
}
|
||||
|
||||
let handleStartChange =
|
||||
React.useCallback (
|
||||
(fun (newStartOpt: System.DateTime option) ->
|
||||
match newStartOpt with
|
||||
| Some newStart ->
|
||||
let updated = { ticket with StartTime = newStart }
|
||||
updateCond updated
|
||||
| None ->
|
||||
console.error("Got no date from date picker")
|
||||
),
|
||||
[| ticket |]
|
||||
)
|
||||
let handleEndChange =
|
||||
React.useCallback (
|
||||
(fun (newEndOpt: System.DateTime option) ->
|
||||
match newEndOpt with
|
||||
| Some newEnd ->
|
||||
let updated = { ticket with EndTime = newEnd }
|
||||
updateCond updated
|
||||
| None ->
|
||||
console.error("Got no date from date picker")
|
||||
),
|
||||
[| ticket |]
|
||||
)
|
||||
|
||||
Fui.table [
|
||||
table.size.medium
|
||||
table.children [
|
||||
Fui.tableBody [
|
||||
tableBody.children [
|
||||
Fui.tableRow [
|
||||
tableRow.key "exec-ticket-start-time"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "Start time"
|
||||
]
|
||||
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.datePicker [
|
||||
datePicker.size.small
|
||||
datePicker.onSelectDate handleStartChange
|
||||
datePicker.value (Some ticket.StartTime)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Fui.tableRow [
|
||||
tableRow.key "exec-ticket-end-time"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "End time"
|
||||
]
|
||||
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.datePicker [
|
||||
datePicker.size.small
|
||||
datePicker.onSelectDate handleEndChange
|
||||
datePicker.value (Some ticket.EndTime)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Fui.tableRow [
|
||||
tableRow.key "exec-ticket-quota"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "Quota"
|
||||
]
|
||||
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.input [
|
||||
input.size.small
|
||||
input.type'.number
|
||||
input.value ticket.Quota
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Fui.tableRow [
|
||||
tableRow.key "exec-ticket-tasks"
|
||||
tableRow.children [
|
||||
Fui.tableCell [
|
||||
tableCell.text "Tasks"
|
||||
]
|
||||
|
||||
Fui.tableCell [
|
||||
tableCell.children (
|
||||
Fui.tableCellLayout [
|
||||
tableCellLayout.children (
|
||||
Fui.text (
|
||||
ticket.Tasks
|
||||
|> String.concat ", "
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
let private PermissionCreateCard group archiveId (onCreate: Permission -> unit) (defaultPermission: Permission) =
|
||||
let isLoading, setLoading = React.useState false
|
||||
let permission, setPermission = React.useState defaultPermission
|
||||
|
||||
let handleCreate (ev: Types.Event) =
|
||||
setLoading true
|
||||
postPermissions group archiveId [| permission.Relation |]
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok () ->
|
||||
console.info("Success.")
|
||||
onCreate permission
|
||||
| Error msg ->
|
||||
console.error("Error adding permissions %s.", msg)
|
||||
|
||||
setLoading false
|
||||
)
|
||||
|
||||
let handleUpdateRelation (permission: Permission) =
|
||||
setPermission permission
|
||||
|
||||
console.debug("Permission: %o", permission)
|
||||
|
||||
Html.div [
|
||||
prop.classes [ "flex-column"; "gap-8"; "shadow"; "brad-8"; "m-8"; "p-16" ]
|
||||
prop.style [ style.flexBasis (length.px 384) ]
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.classes [ "flex-row-center" ]
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.classes [ "grow" ]
|
||||
prop.children [
|
||||
Html.b [
|
||||
prop.style [
|
||||
style.fontSize(length.px 16)
|
||||
]
|
||||
prop.text (
|
||||
match permission.Relation with
|
||||
| Remoting.ArchiveRelation.ViewTerm _ -> "View Term"
|
||||
| Remoting.ArchiveRelation.ExecTicket _ -> "Exec Ticket"
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Fui.button [
|
||||
button.onClick handleCreate
|
||||
button.icon (
|
||||
if isLoading then
|
||||
Fui.spinner [ spinner.size.tiny ]
|
||||
else
|
||||
Fui.icon.addRegular []
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Html.div [
|
||||
prop.children [|
|
||||
match permission.Relation with
|
||||
| Remoting.ArchiveRelation.ViewTerm term ->
|
||||
ViewTerm
|
||||
"create-permission-view-term"
|
||||
handleUpdateRelation
|
||||
permission
|
||||
term
|
||||
| Remoting.ArchiveRelation.ExecTicket ticket ->
|
||||
ExecTicket
|
||||
"create-permission-exec-ticket"
|
||||
handleUpdateRelation
|
||||
permission
|
||||
ticket
|
||||
|]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
let private allRelations = [|
|
||||
Remoting.ArchiveRelation.ViewTerm Remoting.ViewTerm.empty
|
||||
Remoting.ArchiveRelation.ExecTicket Remoting.ExecTicket.empty
|
||||
|]
|
||||
|
||||
[<ReactComponent>]
|
||||
let private PermissionForm
|
||||
(group: string)
|
||||
(archiveId: System.Guid)
|
||||
(onUpdate: Remoting.Tuple array -> unit)
|
||||
(tuples: Remoting.Tuple array)
|
||||
(onAdd: Permission -> unit)
|
||||
(permissions: Permission array)
|
||||
=
|
||||
let adding, setAdding = React.useState false
|
||||
let loading, setLoading = React.useState false
|
||||
let success, setSuccess = React.useState false
|
||||
let newView, setNewView = React.useState<Remoting.ViewTerm option> None
|
||||
let newExec, setNewExec = React.useState<Remoting.ExecTicket option> None
|
||||
|
||||
let newPermissions = [|
|
||||
match newView with
|
||||
| Some view -> Remoting.ArchiveRelation.ViewTerm view
|
||||
| None -> ()
|
||||
// Create a list of permissions missing from the archive
|
||||
let availablePermissions : Permission array =
|
||||
allRelations
|
||||
|> Array.choose (fun relation ->
|
||||
let exists =
|
||||
permissions
|
||||
|> Array.exists (fun permission ->
|
||||
let name = Remoting.ArchiveRelation.ConditionName relation
|
||||
permission.Tuple.Condition
|
||||
|> Option.map (fun cond -> cond.Name = name)
|
||||
|> Option.defaultValue false
|
||||
)
|
||||
|
||||
match newExec with
|
||||
| Some exec -> Remoting.ArchiveRelation.ExecTicket exec
|
||||
| None -> ()
|
||||
|]
|
||||
|
||||
// TODO: Go back to using .Is* when we can
|
||||
let hasViewTerm =
|
||||
tuples
|
||||
|> Array.exists (fun tuple ->
|
||||
match tuple.Relation with
|
||||
| "view" -> true
|
||||
| _ -> false
|
||||
)
|
||||
let hasExecTicket =
|
||||
tuples
|
||||
|> Array.exists (fun tuple ->
|
||||
match tuple.Relation with
|
||||
| "exec" -> true
|
||||
| _ -> false
|
||||
if exists then
|
||||
None
|
||||
else
|
||||
let user = Groups.Utils.fgaMember group
|
||||
let object = sprintf "archive:%O" archiveId
|
||||
Some {
|
||||
Tuple = OpenFGA.Types.ArchiveRelation.toTuple user object relation
|
||||
Relation = relation
|
||||
}
|
||||
)
|
||||
|
||||
let handleAddClick (ev: Types.Event) =
|
||||
if not hasViewTerm && newView.IsNone then
|
||||
setNewView (Some Remoting.ViewTerm.empty)
|
||||
if not hasExecTicket && newExec.IsNone then
|
||||
setNewExec (Some Remoting.ExecTicket.empty)
|
||||
setAdding true
|
||||
|
||||
let handleSaveClick =
|
||||
React.useCallback (
|
||||
(fun (ev: Types.Event) ->
|
||||
setLoading true
|
||||
|
||||
postPermissions group archiveId newPermissions
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok () ->
|
||||
console.info("Success.")
|
||||
setAdding false
|
||||
setSuccess true
|
||||
setNewView None
|
||||
setNewExec None
|
||||
|
||||
let user = Groups.Utils.fgaMember group
|
||||
let object = sprintf "archive:%O" archiveId
|
||||
newPermissions
|
||||
|> Array.map (OpenFGA.Types.ArchiveRelation.toTuple user object)
|
||||
|> Array.append tuples
|
||||
|> onUpdate
|
||||
| Error msg ->
|
||||
console.error("Error adding permissions %s.", msg)
|
||||
|
||||
setLoading false
|
||||
)
|
||||
),
|
||||
[| newPermissions |]
|
||||
)
|
||||
let handleCancelClick (ev: Types.Event) =
|
||||
setAdding false
|
||||
|
||||
let handleUpdateView (updated) =
|
||||
setNewView (Some updated)
|
||||
let handleUpdateExec (updated) =
|
||||
setNewExec (Some updated)
|
||||
let handlePermissionAdd (newPermission: Permission) =
|
||||
console.debug("Added new permission: %o", newPermission)
|
||||
onAdd newPermission
|
||||
|
||||
React.fragment [
|
||||
Html.div [
|
||||
@@ -200,18 +474,13 @@ module GroupArchive =
|
||||
Html.p "Loading ..."
|
||||
else
|
||||
if adding then
|
||||
Html.button [
|
||||
prop.onClick handleSaveClick
|
||||
prop.text "Save"
|
||||
]
|
||||
|
||||
Html.button [
|
||||
prop.onClick handleCancelClick
|
||||
prop.text "Cancel"
|
||||
]
|
||||
else
|
||||
Html.button [
|
||||
prop.disabled (hasViewTerm && hasExecTicket)
|
||||
prop.disabled (Array.isEmpty availablePermissions)
|
||||
prop.onClick handleAddClick
|
||||
prop.text "Add"
|
||||
]
|
||||
@@ -229,46 +498,11 @@ module GroupArchive =
|
||||
"gap-32"
|
||||
]
|
||||
prop.children [
|
||||
if not hasViewTerm then
|
||||
match newView with
|
||||
| Some view ->
|
||||
Html.div [
|
||||
prop.classes [
|
||||
"flex-column"
|
||||
"gap-8"
|
||||
"shadow"
|
||||
"brad-8"
|
||||
"m-8"
|
||||
"p-16"
|
||||
]
|
||||
prop.children [
|
||||
Html.b "View"
|
||||
Groups.ViewForm (view, handleUpdateView)
|
||||
]
|
||||
]
|
||||
| None -> ()
|
||||
|
||||
if not hasExecTicket then
|
||||
match newExec with
|
||||
| Some exec ->
|
||||
Html.div [
|
||||
prop.classes [
|
||||
"flex-column"
|
||||
"gap-8"
|
||||
"shadow"
|
||||
"brad-8"
|
||||
"m-8"
|
||||
"p-16"
|
||||
]
|
||||
prop.style [
|
||||
style.flexBasis (length.px 512)
|
||||
]
|
||||
prop.children [
|
||||
Html.b "Exec"
|
||||
Groups.ExecForm (exec, handleUpdateExec)
|
||||
]
|
||||
]
|
||||
| None -> ()
|
||||
availablePermissions
|
||||
|> Array.map (fun permission ->
|
||||
PermissionCreateCard group archiveId handlePermissionAdd permission
|
||||
)
|
||||
|> unbox
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -277,15 +511,24 @@ module GroupArchive =
|
||||
let View (group: string) (archiveId: System.Guid) =
|
||||
let loading, setLoading = React.useState true
|
||||
let error, setError = React.useState<string option> None
|
||||
let archiveOpt, setArchive =
|
||||
React.useState<Archmaester.Dto.ArchiveProps option> None
|
||||
let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
|
||||
|
||||
let fgaUser = Groups.Utils.fgaMember group
|
||||
let tuples = OpenFGA.useReadTuples (fgaUser, object = sprintf "archive:%O" archiveId)
|
||||
|
||||
let relations: Remoting.ArchiveRelation array =
|
||||
let permissions: Permission array =
|
||||
tuples.Tuples
|
||||
|> Array.choose (fun tuple -> tuple.Condition |> Option.bind OpenFGA.Types.ArchiveRelation.tryOfCondition)
|
||||
|> Array.choose (fun tuple ->
|
||||
tuple.Condition
|
||||
|> Option.bind (fun cond ->
|
||||
cond
|
||||
|> OpenFGA.Types.ArchiveRelation.tryOfCondition
|
||||
|> Option.map (fun rel -> {
|
||||
Tuple = tuple
|
||||
Relation = rel
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
let handlePermissionDelete (tuple: Remoting.Tuple) =
|
||||
console.debug("Deleting %o from %o", tuple, tuples)
|
||||
@@ -293,6 +536,29 @@ module GroupArchive =
|
||||
|> Array.filter (fun existing -> existing.Relation <> tuple.Relation)
|
||||
|> tuples.SetTuples
|
||||
|
||||
let handlePermissionUpdate (updated: Permission) =
|
||||
console.debug("Updated permission tuple %o", updated)
|
||||
putPermissions group archiveId [| updated.Relation |]
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok () ->
|
||||
tuples.Tuples
|
||||
|> Array.map (fun existing ->
|
||||
let equal =
|
||||
existing.Object = updated.Tuple.Object
|
||||
&& existing.Relation = updated.Tuple.Relation
|
||||
&& existing.User = updated.Tuple.User
|
||||
|
||||
if equal then
|
||||
updated.Tuple
|
||||
else
|
||||
existing
|
||||
)
|
||||
|> tuples.SetTuples
|
||||
| Error msg ->
|
||||
setError (Some msg)
|
||||
)
|
||||
|
||||
let handleUpdateRelations =
|
||||
React.useCallback (
|
||||
(fun (updated: Remoting.Tuple array) ->
|
||||
@@ -303,6 +569,19 @@ module GroupArchive =
|
||||
[| box tuples |]
|
||||
)
|
||||
|
||||
let handleAddPermission =
|
||||
React.useCallback (
|
||||
(fun (newPermission: Permission) ->
|
||||
console.debug("New relation added: %o with current: %o", newPermission, tuples.Tuples)
|
||||
|
||||
newPermission.Tuple
|
||||
|> Array.singleton
|
||||
|> Array.append tuples.Tuples
|
||||
|> tuples.SetTuples
|
||||
),
|
||||
[| box tuples |]
|
||||
)
|
||||
|
||||
React.useEffect (
|
||||
(fun () ->
|
||||
setLoading true
|
||||
@@ -363,7 +642,7 @@ module GroupArchive =
|
||||
else
|
||||
Html.div [
|
||||
prop.children [
|
||||
PermissionForm group archive.archiveId handleUpdateRelations tuples.Tuples
|
||||
PermissionForm group archive.archiveId handleAddPermission permissions
|
||||
]
|
||||
]
|
||||
|
||||
@@ -374,26 +653,23 @@ module GroupArchive =
|
||||
]
|
||||
]
|
||||
|
||||
if Array.isEmpty relations then
|
||||
if Array.isEmpty permissions then
|
||||
Html.p "No permissions"
|
||||
else
|
||||
Html.div [
|
||||
prop.classes [ "flex-row-start"; "gap-32" ]
|
||||
prop.children (
|
||||
tuples.Tuples
|
||||
|> Array.choose (fun tuple ->
|
||||
tuple.Condition
|
||||
|> Option.bind (fun cond ->
|
||||
cond
|
||||
|> OpenFGA.Types.ArchiveRelation.tryOfCondition
|
||||
|> Option.map (fun rel ->
|
||||
match rel with
|
||||
| Remoting.ArchiveRelation.ViewTerm term ->
|
||||
ViewTerm handlePermissionDelete term tuple
|
||||
| Remoting.ArchiveRelation.ExecTicket ticket ->
|
||||
ExecTicket handlePermissionDelete ticket tuple
|
||||
)
|
||||
)
|
||||
permissions
|
||||
|> Array.map (fun permission ->
|
||||
match permission.Relation with
|
||||
| Remoting.ArchiveRelation.ViewTerm term ->
|
||||
PermissionCard "View Term" handlePermissionDelete permission.Tuple [|
|
||||
ViewTerm "view-term-table" handlePermissionUpdate permission term
|
||||
|]
|
||||
| Remoting.ArchiveRelation.ExecTicket ticket ->
|
||||
PermissionCard "Exec Ticket" handlePermissionDelete permission.Tuple [|
|
||||
ExecTicket "exec-ticket-table" handlePermissionUpdate permission ticket
|
||||
|]
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
@@ -575,6 +575,35 @@ module Admin =
|
||||
return Error (sprintf "Error setting user permissions: %s" e.Message)
|
||||
}
|
||||
|
||||
let updateGroupPermissions (ctx: HttpContext) (req: Remoting.AddGroupPermissionsRequest) =
|
||||
async {
|
||||
let user = ctx.User.Identity.Name
|
||||
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
|
||||
do logger.LogInformation ("updateGroupPermissions from {User}: {@Req}", user, req)
|
||||
try
|
||||
let fga = ctx.GetService<OpenFgaClient> ()
|
||||
|
||||
let deletes =
|
||||
req.Permissions
|
||||
|> Array.map (permissionToTuple req.ArchiveId req.Group)
|
||||
|> OpenFGA.Queries.deleteTuples'
|
||||
let! deleteResp = fga.DeleteTuples deletes |> Async.AwaitTask
|
||||
do logger.LogInformation ("OpenFGA delete responded with: {JSON}", deleteResp.ToJson ())
|
||||
|
||||
let writes =
|
||||
req.Permissions
|
||||
|> Array.map (permissionToTuple req.ArchiveId req.Group)
|
||||
|> ResizeArray
|
||||
let! writeResp = fga.WriteTuples writes |> Async.AwaitTask
|
||||
do logger.LogInformation ("OpenFGA write responded with: {JSON}", writeResp.ToJson ())
|
||||
|
||||
return Ok ()
|
||||
with e ->
|
||||
do logger.LogError (e, "Error updating group permissions")
|
||||
// TODO: Maybe do not send exn message
|
||||
return Error (sprintf "Error updating group permissions: %s" e.Message)
|
||||
}
|
||||
|
||||
let private impl (ctx: HttpContext) : Remoting.Api.Admin = {
|
||||
addArchive = Handler.addArchive ctx
|
||||
addArchiveGroups = Handler.addArchiveGroups ctx
|
||||
@@ -593,6 +622,7 @@ module Admin =
|
||||
removeUsers = Handler.removeUsers ctx
|
||||
setUserPermissions = Handler.setUserPermissions ctx
|
||||
updateArchive = Handler.updateArchive ctx
|
||||
updateGroupPermissions = Handler.updateGroupPermissions ctx
|
||||
}
|
||||
|
||||
let endpoints: HttpHandler =
|
||||
|
||||
@@ -126,6 +126,11 @@ module Remoting =
|
||||
type ArchiveRelation =
|
||||
| ViewTerm of ViewTerm
|
||||
| ExecTicket of ExecTicket
|
||||
with
|
||||
static member ConditionName (permission: ArchiveRelation) =
|
||||
match permission with
|
||||
| ViewTerm _ -> "term"
|
||||
| ExecTicket _ -> "ticket"
|
||||
|
||||
[<Struct>]
|
||||
type AddArchiveGroupsRequest = {
|
||||
@@ -230,6 +235,7 @@ module Remoting =
|
||||
removeUsers: string array -> Async<Result<unit, string>>
|
||||
setUserPermissions: UserPermissionRequest -> Async<Result<unit, string>>
|
||||
updateArchive: System.Guid -> EditArchiveRequest -> Async<Result<Archive, string>>
|
||||
updateGroupPermissions: AddGroupPermissionsRequest -> Async<Result<unit, string>>
|
||||
}
|
||||
|
||||
type OpenFGA = {
|
||||
|
||||
Reference in New Issue
Block a user