Skip to content

Commit 1131ecb

Browse files
authored
CA-403744: Implement other_config operations for task (#6239)
Currently, users with read-only permissions can freely manipulate their own tasks (create, cancel, destroy, etc.). However, they cannot easily manipulate the `other_config` field. Some keys are specified in the datamodel as being writeable by subjects with the VM operator role. However, RBAC checking for keys was only ever implemented for the individual "add" and "remove" operations, not the `set_other_config` operation. This makes sense because the typical scenario is that the required writer role for an "other_config" field is more privileged than each of the individually-specified key-specific roles. In the case of tasks, the desired role for the `set_other_config` operation is the read-only role. However, we cannot simply demote the required "writer role" for the `other_config` field, because: 1) We must maintain key-based RBAC checks for the operation. For example, if a read-only user attempts to modify a task's `other_config` using `set_other_config`, the operation must fail if they've attempted to modify the value mapped to by some key that they are not privileged to use. 2) Key-based RBAC checking is special cased to `add_to` and `remove_from`. You can attempt to ascribe key-related role restrictions to arbitrary messages but these will all fail at runtime, when invoked - because `Rbac.check` is special cased to expect to be able to extract out the key name from a `key` argument it receives, which is emitted for `add_to` and `remove_from`. 3) There is an additional restriction that read-only users should not be able to modify arbitrary tasks, only their own. Both of these points require a custom implementation. To this end, we mark `other_config` as read-only (DynamicRO) and implement custom handlers for the `add_to`, `remove_from`, and `set` operations. In doing so, we implement a subset of the RBAC protection logic for keys. This custom implementation amounts to a relaxation of the usual RBAC rules: where `add_to` and `remove_from` (both purely destructive operations) cite a protected key (that they are not permitted to write), RBAC fails. In the custom implementation of `set_other_config`, an under-privileged session can cite any key so long as their change is not destructive (it must preserve what is already there). --- The motivation for this change is in fusing serial `add_to_other_config` and `remove_from_other_config` operations into a single `set_other_config` call. I've included (possibly excessive) commentary within the code as the RBAC checking stuff is not touched a lot. This change may not be desirable overall, as it has low impact.
2 parents 1f269dd + a3f2c6e commit 1131ecb

File tree

6 files changed

+366
-53
lines changed

6 files changed

+366
-53
lines changed

ocaml/idl/datamodel.ml

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,80 @@ module Task = struct
551551
let task_allowed_operations =
552552
Enum ("task_allowed_operations", List.map operation_enum [cancel; destroy])
553553

554+
module Special = struct
555+
(* These keys are usually ascribed to the field directly but,
556+
since we are providing custom implementations, we ascribe them
557+
to the messages themselves.
558+
559+
Note that only the "add_to" and "remove_from" messages are
560+
protected by these keys. This is because the current RBAC logic
561+
is special cased to those messages. The "set_other_config"
562+
message has a relaxed RBAC restriction by comparison, and its
563+
checking logic is defined in terms of the permissions created
564+
for the "add_to" and "remove_from" operations.
565+
566+
The difference is subtle: if a session attempts to perform
567+
"add_to"/"remove_from" upon "other_config", those operations
568+
are purely destructive and RBAC checking can be done by the
569+
current logic in Rbac.check (which guards the action). However,
570+
in the case of "set_other_config", we relax the restriction and
571+
must do the RBAC checking ourselves. The relaxed restriction is
572+
that the call may maintain entries that it cannot change
573+
itself. This means a read-only user can technically supply a
574+
map containing privileged entries, so long as those entries are
575+
already present. This allows read-only users to update a subset
576+
of the entries within "other_config".
577+
*)
578+
let protected_keys =
579+
[
580+
("applies_to", _R_VM_OP)
581+
; ("XenCenterUUID", _R_VM_OP)
582+
; ("XenCenterMeddlingActionTitle", _R_VM_OP)
583+
]
584+
585+
let call = call ~lifecycle:[] ~errs:[] ~allowed_roles:_R_READ_ONLY
586+
587+
let add_to_other_config =
588+
call ~name:"add_to_other_config"
589+
~doc:
590+
"Add the given key-value pair to the other_config field of the given \
591+
task."
592+
~params:
593+
[
594+
(Ref _task, "self", "Task object to modify")
595+
; (String, "key", "Key to add")
596+
; (String, "value", "Value to add")
597+
]
598+
~map_keys_roles:protected_keys ()
599+
600+
let remove_from_other_config =
601+
call ~name:"remove_from_other_config"
602+
~doc:
603+
"Remove the given key and its corresponding value from the \
604+
other_config field of the given task. If the key is not in that \
605+
Map, then do nothing."
606+
~params:
607+
[
608+
(Ref _task, "self", "Task object to modify")
609+
; (String, "key", "Key of entry to remove")
610+
]
611+
(* Privileged key permissions are generated for each of these protected keys. *)
612+
~map_keys_roles:protected_keys ()
613+
614+
(* We cannot cite the protected keys here as the current RBAC
615+
logic only works for "add_to" and "remove_from" and, even if it
616+
did, it is too strict. *)
617+
let set_other_config =
618+
call ~name:"set_other_config"
619+
~doc:"Set the other_config field of the given task."
620+
~params:
621+
[
622+
(Ref _task, "self", "Task object to modify")
623+
; (Map (String, String), "value", "New value to set")
624+
]
625+
()
626+
end
627+
554628
let t =
555629
create_obj ~in_db:true
556630
~lifecycle:[(Published, rel_rio, "A long-running asynchronous task")]
@@ -567,6 +641,9 @@ module Task = struct
567641
; set_progress
568642
; set_result
569643
; set_error_info
644+
; Special.add_to_other_config
645+
; Special.remove_from_other_config
646+
; Special.set_other_config
570647
]
571648
~contents:
572649
([
@@ -716,7 +793,7 @@ module Task = struct
716793
"error_info"
717794
"if the task has failed, this field contains the set of \
718795
associated error strings. Undefined otherwise."
719-
; field
796+
; field ~qualifier:DynamicRO
720797
~lifecycle:[(Published, rel_miami, "additional configuration")]
721798
~default_value:(Some (VMap []))
722799
~ty:(Map (String, String))

ocaml/idl/datamodel_lifecycle.ml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,5 +247,11 @@ let prototyped_of_message = function
247247
Some "22.27.0"
248248
| "pool", "set_custom_uefi_certificates" ->
249249
Some "24.0.0"
250+
| "task", "set_other_config" ->
251+
Some "25.2.0-next"
252+
| "task", "remove_from_other_config" ->
253+
Some "25.2.0-next"
254+
| "task", "add_to_other_config" ->
255+
Some "25.2.0-next"
250256
| _ ->
251257
None

ocaml/idl/schematest.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ let hash x = Digest.string x |> Digest.to_hex
33
(* BEWARE: if this changes, check that schema has been bumped accordingly in
44
ocaml/idl/datamodel_common.ml, usually schema_minor_vsn *)
55

6-
let last_known_schema_hash = "05ac9223f9c17b07b12e328d5dc3db52"
6+
let last_known_schema_hash = "e6b99e0d07ccf68df8f45c851e5d8dbf"
77

88
let current_schema_hash : string =
99
let open Datamodel_types in

ocaml/xapi/rbac.ml

Lines changed: 66 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -161,80 +161,95 @@ let is_permission_in_session ~session_id ~permission ~session =
161161

162162
open Db_actions
163163

164-
(* look up the list generated in xapi_session.get_permissions *)
165-
let is_access_allowed ~__context ~session_id ~permission =
166-
(* always allow local system access *)
167-
if Session_check.is_local_session __context session_id then
168-
true (* normal user session *)
164+
(* Given a list of permissions, determine if the given session is
165+
permitted to perform the related actions. If not, stop and return the
166+
first disallowed permission. This stops us doing redundant checks
167+
but also is consistent with the current RBAC error reporting, where
168+
a single action is usually reported. *)
169+
let find_first_disallowed_permission ~__context ~session_id ~permissions =
170+
let is_local_session () =
171+
Session_check.is_local_session __context session_id
172+
in
173+
let doesn't_have_permission session permission =
174+
is_permission_in_session ~session_id ~permission ~session = false
175+
in
176+
(* Test session properties before querying permission sets. *)
177+
if is_local_session () then
178+
None
169179
else
170180
let session = DB_Action.Session.get_record ~__context ~self:session_id in
171-
(* the root user can always execute anything *)
172181
if session.API.session_is_local_superuser then
173-
true
174-
(* not root user, so let's decide if permission is allowed or denied *)
182+
None
175183
else
176-
is_permission_in_session ~session_id ~permission ~session
184+
List.find_opt (doesn't_have_permission session) permissions
185+
186+
(* Determine if session has a given permission. *)
187+
let is_access_allowed ~__context ~session_id ~permission =
188+
find_first_disallowed_permission ~__context ~session_id
189+
~permissions:[permission]
190+
|> Option.is_none
191+
192+
let get_session_of_context ~__context ~permission =
193+
try Context.get_session_id __context
194+
with Failure _ ->
195+
let msg = "no session in context" in
196+
raise Api_errors.(Server_error (rbac_permission_denied, [permission; msg]))
197+
198+
let disallowed_permission_exn ?(extra_dmsg = "") ?(extra_msg = "") ~__context
199+
~permission ~action =
200+
let session_id = get_session_of_context ~__context ~permission in
201+
let allowed_roles =
202+
try
203+
Xapi_role.get_by_permission_name_label ~__context ~label:permission
204+
|> List.map (fun self -> Xapi_role.get_name_label ~__context ~self)
205+
|> String.concat ", "
206+
with e ->
207+
debug "Could not obtain allowed roles for %s (%s)" permission
208+
(ExnHelper.string_of_exn e) ;
209+
"<Could not obtain the list.>"
210+
in
211+
let msg =
212+
Printf.sprintf
213+
"No permission in user session. (Roles with this permission: %s)%s"
214+
allowed_roles extra_msg
215+
in
216+
debug "%s[%s]: %s %s %s" action permission msg (trackid session_id) extra_dmsg ;
217+
raise Api_errors.(Server_error (rbac_permission_denied, [permission; msg]))
177218

178-
(* Execute fn if rbac access is allowed for action, otherwise fails. *)
179219
let nofn () = ()
180220

181221
let check ?(extra_dmsg = "") ?(extra_msg = "") ?args ?(keys = []) ~__context ~fn
182222
session_id action =
183223
let permission = permission_of_action action ?args ~keys in
184-
if is_access_allowed ~__context ~session_id ~permission then (
185-
(* allow access to action *)
224+
let allow_access () =
225+
(* Allow access to action. *)
186226
let sexpr_of_args = Rbac_audit.allowed_pre_fn ~__context ~action ?args () in
187227
try
188-
let result = fn () (* call rbac-protected function *) in
228+
(* Call the RBAC-protected function. *)
229+
let result = fn () in
189230
Rbac_audit.allowed_post_fn_ok ~__context ~session_id ~action ~permission
190231
?sexpr_of_args ?args ~result () ;
191232
result
192233
with error ->
234+
(* Catch all exceptions and log to RBAC audit log. *)
193235
Backtrace.is_important error ;
194-
(* catch all exceptions *)
195236
Rbac_audit.allowed_post_fn_error ~__context ~session_id ~action
196237
~permission ?sexpr_of_args ?args ~error () ;
238+
(* Re-raise. *)
197239
raise error
198-
) else (* deny access to action *)
199-
let allowed_roles_string =
200-
try
201-
let allowed_roles =
202-
Xapi_role.get_by_permission_name_label ~__context ~label:permission
203-
in
204-
List.fold_left
205-
(fun acc allowed_role ->
206-
acc
207-
^ (if acc = "" then "" else ", ")
208-
^ Xapi_role.get_name_label ~__context ~self:allowed_role
209-
)
210-
"" allowed_roles
211-
with e ->
212-
debug "Could not obtain allowed roles for %s (%s)" permission
213-
(ExnHelper.string_of_exn e) ;
214-
"<Could not obtain the list.>"
215-
in
216-
let msg =
217-
Printf.sprintf
218-
"No permission in user session. (Roles with this permission: %s)%s"
219-
allowed_roles_string extra_msg
220-
in
221-
debug "%s[%s]: %s %s %s" action permission msg (trackid session_id)
222-
extra_dmsg ;
240+
in
241+
let deny_access () =
242+
(* Deny access to action, raising an exception. *)
223243
Rbac_audit.denied ~__context ~session_id ~action ~permission ?args () ;
224244
raise
225-
(Api_errors.Server_error
226-
(Api_errors.rbac_permission_denied, [permission; msg])
227-
)
228-
229-
let get_session_of_context ~__context ~permission =
230-
try Context.get_session_id __context
231-
with Failure _ ->
232-
raise
233-
(Api_errors.Server_error
234-
( Api_errors.rbac_permission_denied
235-
, [permission; "no session in context"]
236-
)
245+
(disallowed_permission_exn ~extra_dmsg ~extra_msg ~__context ~permission
246+
~action
237247
)
248+
in
249+
if is_access_allowed ~__context ~session_id ~permission then
250+
allow_access ()
251+
else
252+
deny_access ()
238253

239254
let assert_permission_name ~__context ~permission =
240255
let session_id = get_session_of_context ~__context ~permission in

ocaml/xapi/rbac.mli

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@ val is_access_allowed :
2222
(on the coordinator only) to benefit successive queries for the
2323
same session. *)
2424

25+
val find_first_disallowed_permission :
26+
__context:Context.t
27+
-> session_id:[`session] Ref.t
28+
-> permissions:string list
29+
-> string option
30+
(** Given a list of permissions, determine if the given session is
31+
permitted to perform the related actions. If not, stop and return
32+
the first disallowed permssion (without considering the remaining ones). *)
33+
34+
val disallowed_permission_exn :
35+
?extra_dmsg:string
36+
-> ?extra_msg:string
37+
-> __context:Context.t
38+
-> permission:string
39+
-> action:string
40+
-> exn
41+
(** Create an RBAC_PERMISSION_DENIED exception for the given
42+
permission. Attempts to report the role(s) which do have the given
43+
permission (if any). *)
44+
2545
val check :
2646
?extra_dmsg:string
2747
-> ?extra_msg:string

0 commit comments

Comments
 (0)