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