Convert the permission graph's naive json conversion into the correct types.

The strategy here is to use s/conform to tag every value that needs to be converted with the conversion strategy, then postwalk to actually perform the conversion.

(ns metabase.api.permission-graph
  (:require
   [clojure.spec.alpha :as s]
   [clojure.spec.gen.alpha :as gen]
   [clojure.walk :as walk]
   [metabase.util :as u]
   [metabase.util.i18n :refer [trs]]))
(set! *warn-on-reflection* true)

convert values from the naively converted json to what we REALLY WANT

(defmulti ^:private convert
  first)
(defmethod convert :kw->int [[_ k]] (Integer/parseInt (name k)))
(defmethod convert :str->kw [[_ s]] (keyword s))

Convert a keyword to string without excluding the namespace. e.g: :schema/name => "schema/name". Primarily used for schema-name since schema are allowed to have "/" and calling (name s) returning a substring after "/".

(defmethod convert :kw->str [[_ s]] (u/qualified-name s))
(defmethod convert :nil->none [[_ _]] :none)
(defmethod convert :identity [[_ x]] x)
(defmethod convert :global-execute [[_ x]] x)
(defmethod convert :db-exeute [[_ x]] x)

--------------------------------------------------- Common ----------------------------------------------------

(defn- kw-int->int-decoder [kw-int]
  (if (int? kw-int)
    kw-int
    (parse-long (name kw-int))))

Integer malli schema that knows how to decode itself from the :123 sort of shape used in perm-graphs

(def DecodableKwInt
  [:int {:decode/perm-graph kw-int->int-decoder}])
(def ^:private Id DecodableKwInt)
(def ^:private GroupId DecodableKwInt)

ids come in as keywordized numbers

(s/def ::id (s/with-gen (s/or :kw->int (s/and keyword? #(re-find #"^\d+$" (name %))))
              #(gen/fmap (comp keyword str) (s/gen pos-int?))))

native permissions

(def ^:private Native
  [:maybe [:enum :write :none :full :limited]])

------------------------------------------------ Data Permissions ------------------------------------------------

Perms that get reused for TablePerms and SchemaPerms

(def ^:private Perms
  [:enum
   :all :segmented :none :full :limited :unrestricted :legacy-no-self-service :sandboxed :query-builder :no :blocked])
(def ^:private TablePerms
  [:or Perms [:map
              [:read {:optional true} [:enum :all :none]]
              [:query {:optional true} [:enum :all :none :segmented]]]])
(def ^:private SchemaPerms
  [:or Perms [:map-of Id TablePerms]])
(def ^:private SchemaGraph
  [:map-of
   [:string {:decode/perm-graph name}]
   SchemaPerms])
(def ^:private Schemas
  [:or
   [:enum
    :all
    :segmented
    :none
    :block
    :blocked
    :full
    :limited
    :impersonated
    :unrestricted
    :sandboxed
    :legacy-no-self-service
    :query-builder-and-native
    :query-builder
    :no]
   SchemaGraph])
(def ^:private DataPerms
  [:map
   [:native {:optional true} Native]
   [:schemas {:optional true} Schemas]])

Data perms that care about how view-data and make-queries are related to one another. If you have write access for native queries, you must have data access to all schemas.

(def StrictDataPerms
  [:and
   DataPerms
   [:fn {:error/fn (fn [_ _] (trs "Invalid DB permissions: If you have write access for native queries, you must have data access to all schemas."))}
    (fn [{:keys [native schemas]}]
      (not (and (= native :write) schemas (not (#{:all :impersonated} schemas)))))]])

like db-graph, but with added validations: - Ensures 'view-data' is not 'blocked' if 'create-queries' is 'query-builder-and-native'.

(def StrictDbGraph
  [:schema {:registry {"StrictDataPerms" StrictDataPerms}}
   [:map-of
    Id
    [:and
     [:map
      [:view-data {:optional true} Schemas]
      [:create-queries {:optional true} Schemas]
      [:data {:optional true} "StrictDataPerms"]
      [:download {:optional true} "StrictDataPerms"]
      [:data-model {:optional true} "StrictDataPerms"]
      [:details {:optional true} [:enum :yes :no]]]
     [:fn {:error/fn (fn [_ _] (trs "Invalid DB permissions: If you have write access for native queries, you must have data access to all schemas."))}
      (fn [db-entry]
        (let [{:keys [create-queries view-data]} db-entry]
          (not (and (= create-queries :query-builder-and-native) (= view-data :blocked)))))]]]])

Used to transform, and verify data permissions graph

(def DataPermissionsGraph
  [:map
   [:groups [:map-of GroupId [:maybe StrictDbGraph]]]])

Top level strict data graph schema expected over the API. Includes revision ID for avoiding concurrent updates.

(def StrictApiPermissionsGraph
  [:map
   [:revision {:optional true} [:maybe int?]]
   [:force {:optional true} [:maybe boolean?]]
   [:groups [:map-of GroupId [:maybe StrictDbGraph]]]])

--------------------------------------------- Execution Permissions ----------------------------------------------

(s/def ::execute (s/or :str->kw #{"all" "none"}))
(s/def ::execute-graph
  (s/or :global-execute ::execute
        :db-exeute      (s/map-of ::id ::execute
                                  :conform-keys true)))
(s/def :metabase.api.permission-graph.execution/groups
  (s/map-of ::id
            ::execute-graph
            :conform-keys true))
(s/def ::execution-permissions-graph
  (s/keys :req-un [:metabase.api.permission-graph.execution/groups]))

The permissions graph is received as JSON. That JSON is naively converted. This performs a further conversion to convert graph keys and values to the types we want to work with.

(defn converted-json->graph
  [spec kwj]
  (->> (s/conform spec kwj)
       (walk/postwalk (fn [x]
                        (if (and (vector? x) (get-method convert (first x)))
                          (convert x)
                          x)))))