@@ -428,7 +428,7 @@ module TraceHelper = struct
428428end
429429
430430(* * Once the server functor has been instantiated, xapi sets this reference to the appropriate
431- "fake_rpc" (loopback non-HTTP) rpc function.
431+ "fake_rpc" (loopback non-HTTP) rpc function.
432432 This way, internally the coordinator can short-circuit API calls without having to go over the network. *)
433433let rpc_fun : (Http.Request.t -> Rpc.call -> Rpc.response) option ref = ref None
434434
@@ -2477,3 +2477,185 @@ module AuthenticationCache = struct
24772477 None
24782478 end
24792479end
2480+
2481+ (* Simple trie data structure that performs a favoured lookup to
2482+ implement a simple form of wildcard key matching. The trie is not
2483+ pruned during (or after) construction. *)
2484+ module MatchTrie = struct
2485+ type 'a node = {arrows : (string , 'a node ) Hashtbl .t ; mutable value : 'a option }
2486+
2487+ let create_node () =
2488+ let arrows = Hashtbl. create 16 in
2489+ let value = None in
2490+ {arrows; value}
2491+
2492+ let create = create_node
2493+
2494+ let insert root ~key ~value =
2495+ let parts = String. split_on_char '.' key in
2496+ let rec extend focused = function
2497+ | part :: parts ->
2498+ let next =
2499+ match Hashtbl. find_opt focused.arrows part with
2500+ | Some node ->
2501+ node
2502+ | _ ->
2503+ let next = create_node () in
2504+ Hashtbl. replace focused.arrows part next ;
2505+ next
2506+ in
2507+ extend next parts
2508+ | [] ->
2509+ focused
2510+ in
2511+ let final = extend root parts in
2512+ final.value < - Some value
2513+
2514+ let find root ~key =
2515+ let parts = String. split_on_char '.' key in
2516+ let rec find focused = function
2517+ | part :: parts -> (
2518+ (* Wildcard edges override other edges. *)
2519+ match Hashtbl. find_opt focused.arrows " *" with
2520+ | Some _ as sink ->
2521+ sink
2522+ | _ -> (
2523+ match Hashtbl. find_opt focused.arrows part with
2524+ | Some next ->
2525+ (find [@ tailcall]) next parts
2526+ | _ ->
2527+ None
2528+ )
2529+ )
2530+ | _ ->
2531+ Some focused
2532+ in
2533+ match find root parts with Some node -> node.value | _ -> None
2534+ end
2535+
2536+ (* Given an input key, compare against the protected keys of the
2537+ task.other_config field. If a protected key matches, return it.
2538+
2539+ For example, if the datamodel specifies "foo.bar.*" as a protected
2540+ key, then: match_protected_key ~key:"foo.bar.baz" = Some "foo.bar.*".
2541+
2542+ It must return the protected key as that is what key-related RBAC
2543+ entries are defined in terms of.
2544+ *)
2545+ let match_protected_key objname fieldname =
2546+ (* Attain the listing of protected keys from the datamodel at module
2547+ initialisation. Usually, this list is passed to Rbac.check by
2548+ handlers inside the auto-generated server.ml file. *)
2549+ let protected_keys =
2550+ let api = Datamodel. all_api in
2551+ let field = Dm_api. get_field_by_name api ~objname ~fieldname in
2552+ List. map fst field.field_map_keys_roles
2553+ in
2554+ (* Define the lookup function in terms of a simple trie data
2555+ structure - which is flexible to account for overlapping paths and
2556+ presence of wildcards. *)
2557+ let trie =
2558+ let root = MatchTrie. create () in
2559+ let add key = MatchTrie. insert root ~key ~value: key in
2560+ List. iter add protected_keys ;
2561+ root
2562+ in
2563+ MatchTrie. find trie
2564+
2565+ (* The behaviour of this function, with respect to RBAC checking, must
2566+ match serial "remove_from" and "add_to" operations (for only the keys
2567+ that are changing).
2568+
2569+ There is normally no key-related RBAC checking for
2570+ "set_X" (e.g. set_other_config) because the required writer role for the
2571+ entire field is usually higher than the role(s) required for
2572+ individually-protected keys.
2573+
2574+ {Task,VM}.set_other_config and VM.set_platform are special cases where
2575+ lower-privileged sessions must be able to manipulate a subset of entries
2576+ (those not protected by a more privileged role).
2577+ *)
2578+ let set_map_with_rbac ~__context ~self ~value ~get_fn ~set_fn ~match_protected
2579+ ~object_name ~field_name =
2580+ let match_protected = match_protected field_name in
2581+ let module S = Set. Make (String ) in
2582+ let create_lookup kvs =
2583+ let table = List. to_seq kvs |> Hashtbl. of_seq in
2584+ Hashtbl. find_opt table
2585+ in
2586+ let old_value = get_fn ~__context ~self in
2587+ let lookup_old, lookup_new = (create_lookup old_value, create_lookup value) in
2588+ let keys_before, keys_after =
2589+ let keys = List. map fst in
2590+ let before = keys old_value in
2591+ let after = keys value in
2592+ S. (of_list before, of_list after)
2593+ in
2594+ let keys_removed =
2595+ (* Keys no longer appearing in the map. The user must have the
2596+ "remove_from" role for each of the protected keys in the set. *)
2597+ S. diff keys_before keys_after
2598+ in
2599+ let keys_unchanged =
2600+ (* Keys that persist across the update. If any key in this set is
2601+ protected AND the value mapped to by the key would be changed by
2602+ the update, the session must have the "add_to" role. *)
2603+ let updated = S. inter keys_before keys_after in
2604+ let is_entry_unchanged key =
2605+ let is_same =
2606+ let ( let * ) = Option. bind in
2607+ let * old_value = lookup_old key in
2608+ let * new_value = lookup_new key in
2609+ Some (old_value = new_value)
2610+ in
2611+ Option. value ~default: false is_same
2612+ in
2613+ (* Filter out the unchanged entries, as you don't need any
2614+ extra privileges to maintain an entry that's already there. *)
2615+ S. filter is_entry_unchanged updated
2616+ in
2617+ let keys_added =
2618+ (* Treat all keys as new, unless they're referring to entries that
2619+ are unchanged across the update. *)
2620+ S. diff keys_after keys_unchanged
2621+ in
2622+ let permissions =
2623+ (* Map each of the added and removed keys to protected keys, if
2624+ such a key exists. *)
2625+ let filter keys =
2626+ S. filter_map (fun key -> match_protected ~key ) keys |> S. elements
2627+ in
2628+ let added = filter keys_added in
2629+ let removed = filter keys_removed in
2630+ let format operation key =
2631+ (* All the permissions are stored in lowercase. *)
2632+ let key = String. lowercase_ascii key in
2633+ Printf. sprintf " %s.%s_%s/key:%s" object_name operation field_name key
2634+ in
2635+ (* The required permissions are defined in terms of those
2636+ generated for "add_to" and "remove_from" (both implemented
2637+ above). They can be defined as custom AND use RBAC checking within
2638+ server.ml because their operation is purely destructive, so it's
2639+ sufficient to guard the entire action with Rbac.check. *)
2640+ let added_perms = List. map (format " add_to" ) added in
2641+ let removed_perms = List. map (format " remove_from" ) removed in
2642+ added_perms @ removed_perms
2643+ in
2644+ (* Find the first disallowed permission, indicating that we cannot
2645+ perform the action. *)
2646+ let session_id = Context. get_session_id __context in
2647+ match
2648+ Rbac. find_first_disallowed_permission ~__context ~session_id ~permissions
2649+ with
2650+ | None ->
2651+ (* No disallowed permission, perform the update. *)
2652+ set_fn ~__context ~self ~value
2653+ | Some disallowed ->
2654+ (* Report it as an RBAC error. *)
2655+ let action = Printf. sprintf " %s.set_%s" object_name field_name in
2656+ let extra_msg = " " in
2657+ let extra_dmsg = " " in
2658+ raise
2659+ (Rbac. disallowed_permission_exn ~extra_dmsg ~extra_msg ~__context
2660+ ~permission: disallowed ~action
2661+ )
0 commit comments