Utilities for working with permissions, particularly the permission paths which are stored in the DB. These should
typically not be used outside of permissions-related namespaces such as | (ns metabase.permissions.util (:require [clojure.string :as str] [metabase.api.common :as api] [metabase.premium-features.core :refer [defenterprise]] [metabase.util :as u] [metabase.util.i18n :refer [tru]] [metabase.util.log :as log] [metabase.util.malli :as mu] [metabase.util.malli.registry :as mr] [metabase.util.regex :as u.regex] [toucan2.core :as t2])) |
+----------------------------------------------------------------------------------------------------------------+ | API-level helpers | +----------------------------------------------------------------------------------------------------------------+ | |
Log changes to the permissions graph. | (defn log-permissions-changes
[old new]
(log/debug "Changing permissions"
"\n FROM:" (u/pprint-to-str :magenta old)
"\n TO:" (u/pprint-to-str :blue new))) |
Check that the revision number coming in as part of | (defn check-revision-numbers
[old-graph new-graph]
(when-not (:force new-graph)
(when (not= (:revision old-graph) (:revision new-graph))
(throw (ex-info (tru
(str "Looks like someone else edited the permissions and your data is out of date. "
"Please fetch new data and try again."))
{:status-code 409}))))) |
Save changes made to permission graph for logging/auditing purposes.
This doesn't do anything if | (defn save-perms-revision!
[model current-revision before changes]
(when api/*current-user-id*
(first (t2/insert-returning-instances! model
;; manually specify ID here so if one was somehow inserted in the meantime in the fraction of a second since we
;; called `check-revision-numbers` the PK constraint will fail and the transaction will abort
:id (inc current-revision)
:before before
:after changes
:user_id api/*current-user-id*)))) |
+----------------------------------------------------------------------------------------------------------------+ | PATH CLASSIFICATION + VALIDATION | +----------------------------------------------------------------------------------------------------------------+ | |
Regex for a valid character for a name that appears in a permissions path (e.g. a schema name or a Collection name).
Character is valid if it is either:
1. Any character other than a slash
2. A forward slash, escaped by a backslash: | (def path-char-rx
"Regex for a valid character for a name that appears in a permissions path (e.g. a schema name or a Collection name).
Character is valid if it is either:
1. Any character other than a slash
2. A forward slash, escaped by a backslash: `\\/`
3. A backslash escaped by a backslash: `\\\\`"
(u.regex/rx [:or #"[^\\/]" #"\\/" #"\\\\"])) |
(def ^:private data-rx->data-kind
{#"db/\d+/" :dk/db
[:and #"db/\d+/" "native" "/"] :dk/db-native
[:and #"db/\d+/" "schema" "/"] :dk/db-schema
[:and #"db/\d+/" "schema" "/" path-char-rx "*" "/"] :dk/db-schema-name
[:and #"db/\d+/" "schema" "/" path-char-rx "*" "/table/\\d+/"] :dk/db-schema-name-and-table
[:and #"db/\d+/" "schema" "/" path-char-rx "*" "/table/\\d+/" "read/"] :dk/db-schema-name-table-and-read
[:and #"db/\d+/" "schema" "/" path-char-rx "*" "/table/\\d+/" "query/"] :dk/db-schema-name-table-and-query
[:and #"db/\d+/" "schema" "/" path-char-rx "*" "/table/\\d+/" "query/" "segmented/"] :dk/db-schema-name-table-and-segmented}) | |
(def ^:private DataKind (into [:enum] (vals data-rx->data-kind))) | |
*-permissions-rx The *-permissions-rx do not have anchors, since they get combined (and anchors placed around them) below. Take care to use anchors where they make sense. | |
Paths starting with /db/ is a DATA ACCESS permissions path Paths that do not start with /db/ (e.g. /download/db/...) do not involve granting data access, and are not data-permissions. They are other kinds of paths, for example: see [[download-permissions-rx]]. | (def v1-data-permissions-rx (into [:or] (keys data-rx->data-kind))) |
(def ^:private v2-data-permissions-rx [:and "data/" v1-data-permissions-rx]) (def ^:private v2-query-permissions-rx [:and "query/" v1-data-permissions-rx]) | |
Any path starting with /download/ is a DOWNLOAD permissions path /download/db/:id/ -> permissions to download 1M rows in query results /download/limited/db/:id/ -> permissions to download 1k rows in query results | (def ^:private download-permissions-rx
[:and "download/" [:? "limited/"]
[:and #"db/\d+/"
[:? [:or "native/"
[:and "schema/"
[:? [:and path-char-rx "*/"
[:? #"table/\d+/"]]]]]]]]) |
Any path starting with /data-model/ is a DATA MODEL permissions path /download/db/:id/ -> permissions to access the data model for the DB | (def ^:private data-model-permissions-rx
[:and "data-model/"
[:and #"db/\d+/"
[:? [:and "schema/"
[:? [:and path-char-rx "*/"
[:? #"table/\d+/"]]]]]]]) |
any path starting with /details/ is a DATABASE CONNECTION DETAILS permissions path /details/db/:id/ -> permissions to edit the connection details and settings for the DB | (def ^:private db-conn-details-permissions-rx [:and "details/" #"db/\d+/"]) |
.../execute/ -> permissions to run query actions in the DB | (def ^:private execute-permissions-rx [:and "execute/" [:or "" #"db/\d+/"]]) |
(def ^:private collection-permissions-rx
[:and "collection/"
[:or ;; /collection/:id/ -> readwrite perms for a specific Collection
[:and #"\d+/"
;; /collection/:id/read/ -> read perms for a specific Collection
[:? "read/"]]
;; /collection/root/ -> readwrite perms for the Root Collection
[:and "root/"
;; /collection/root/read/ -> read perms for the Root Collection
[:? "read/"]]
;; /collection/namespace/:namespace/root/ -> readwrite perms for 'Root' Collection in non-default
;; namespace (only really used for EE)
[:and "namespace/" path-char-rx "+/root/"
;; /collection/namespace/:namespace/root/read/ -> read perms for 'Root' Collection in
;; non-default namespace
[:? "read/"]]]]) | |
Any path starting with /application is a permissions that is not scoped by database or collection /application/setting/ -> permissions to access /admin/settings page /application/monitoring/ -> permissions to access tools, audit and troubleshooting /application/subscription/ -> permisisons to create/edit subscriptions and alerts | (def ^:private non-scoped-permissions-rx [:and "application/" [:or "setting/" "monitoring/" "subscription/"]]) |
Any path starting with /block/ is for BLOCK aka anti-permissions. currently only supported at the DB level. e.g. /block/db/1/ => block collection-based access to Database 1 | (def ^:private block-permissions-rx #"block/db/\d+/") |
Root Permissions, i.e. for admin | (def ^:private admin-permissions-rx "") |
Regex for a valid permissions path. The [[metabase.util.regex/rx]] macro is used to make the big-and-hairy regex somewhat readable. | (def path-regex-v1
(u.regex/rx
"^/" [:or
collection-permissions-rx
non-scoped-permissions-rx
admin-permissions-rx]
"$")) |
(def ^:private rx->kind [[(u.regex/rx "^/" v1-data-permissions-rx "$") :data] [(u.regex/rx "^/" v2-data-permissions-rx "$") :data-v2] [(u.regex/rx "^/" v2-query-permissions-rx "$") :query-v2] [(u.regex/rx "^/" download-permissions-rx "$") :download] [(u.regex/rx "^/" data-model-permissions-rx "$") :data-model] [(u.regex/rx "^/" db-conn-details-permissions-rx "$") :db-conn-details] [(u.regex/rx "^/" execute-permissions-rx "$") :execute] [(u.regex/rx "^/" collection-permissions-rx "$") :collection] [(u.regex/rx "^/" non-scoped-permissions-rx "$") :non-scoped] [(u.regex/rx "^/" block-permissions-rx "$") :block] [(u.regex/rx "^/" admin-permissions-rx "$") :admin]]) | |
Regex for a valid permissions path. built with [[metabase.util.regex/rx]] to make the big-and-hairy regex somewhat readable. Will not match: - a v1 data path like "/db/1" or "/db/1/" - a block path like "block/db/2/" | (def path-regex-v2
(u.regex/rx
"^/" [:or
v2-query-permissions-rx
execute-permissions-rx
collection-permissions-rx
non-scoped-permissions-rx
admin-permissions-rx]
"$")) |
A permission path. | (def Path
[:or {:title "Path"} [:re path-regex-v1] [:re path-regex-v2]]) |
(def ^:private Kind
(into [:enum {:title "Kind"}] (map second rx->kind))) | |
(mu/defn classify-path :- Kind
"Classifies a permission [[metabase.permissions.models.permissions/Path]] into
a [[metabase.permissions.models.permissions/Kind]], or throws."
[path :- Path]
(let [result (keep (fn [[permission-rx kind]]
(when (re-matches (u.regex/rx permission-rx) path) kind))
rx->kind)]
(when-not (= 1 (count result))
(throw (ex-info (str "Unclassifiable path! " (pr-str {:path path :result result}))
{:path path :result result})))
(first result))) | |
A permissions path that's guaranteed to be a v1 data-permissions path | (def DataPath [:re (u.regex/rx "^/" v1-data-permissions-rx "$")]) |
(mu/defn classify-data-path :- DataKind
"Classifies data path permissions [[metabase.permissions.models.permissions/DataPath]] into
a [[metabase.permissions.models.permissions/DataKind]]"
[data-path :- DataPath]
(let [result (keep (fn [[data-rx kind]]
(when (re-matches (u.regex/rx [:and "^/" data-rx]) data-path) kind))
data-rx->data-kind)]
(when-not (= 1 (count result))
(throw (ex-info "Unclassified data path!!" {:data-path data-path :result result})))
(first result))) | |
Is | (let [path-validator (mr/validator Path)]
(defn valid-path?
^Boolean [^String path]
(path-validator path))) |
Schema for a permissions path with a valid format. | (def PathSchema
[:re
{:error/message "Valid permissions path"}
(re-pattern (str "^/(" path-char-rx "*/)*$"))]) |
Is | (let [path-format-validator (mr/validator PathSchema)]
(defn valid-path-format?
^Boolean [^String path]
(path-format-validator path))) |
+----------------------------------------------------------------------------------------------------------------+ | PATH UTILS | +----------------------------------------------------------------------------------------------------------------+ | |
Escape slashes in something that might be passed as a string part of a permissions path (e.g. DB schema name or Collection name). (escape-path-component "a/b") ;-> "a\/b" | (defn escape-path-component
"Escape slashes in something that might be passed as a string part of a permissions path (e.g. DB schema name or
Collection name).
(escape-path-component \"a/b\") ;-> \"a\\/b\""
[s]
(some-> s
(str/replace #"\\" "\\\\\\\\") ; \ -> \\
(str/replace #"/" "\\\\/"))) ; / -> \/ |
lookup table to generate v2 query + data permission from a v1 data permission. | (letfn [(delete [s to-delete] (str/replace s to-delete ""))
(data-query-split [path] [(str "/data" path) (str "/query" path)])]
(def ^:private data-kind->rewrite-fn
{:dk/db data-query-split
:dk/db-native (fn [path] (data-query-split (delete path "native/")))
:dk/db-schema (fn [path] [(str "/data" (delete path "schema/")) (str "/query" path)])
:dk/db-schema-name data-query-split
:dk/db-schema-name-and-table data-query-split
:dk/db-schema-name-table-and-read (constantly [])
:dk/db-schema-name-table-and-query (fn [path] (data-query-split (delete path "query/")))
:dk/db-schema-name-table-and-segmented (fn [path] (data-query-split (delete path "query/segmented/")))})) |
(mu/defn ->v2-path :- [:vector [:re path-regex-v2]]
"Takes either a v1 or v2 path, and translates it into one or more v2 paths."
[path :- [:or [:re path-regex-v1] [:re path-regex-v2]]]
(let [kind (classify-path path)]
(case kind
:data (let [data-permission-kind (classify-data-path path)
rewrite-fn (data-kind->rewrite-fn data-permission-kind)]
(rewrite-fn path))
:admin ["/"]
:block []
;; for sake of idempotency, v2 perm-paths should be unchanged.
(:data-v2 :query-v2) [path]
;; other paths should be unchanged too.
[path]))) | |
+----------------------------------------------------------------------------------------------------------------+ | EE UTILS | +----------------------------------------------------------------------------------------------------------------+ | |
Returns a boolean if the current user uses sandboxing for any database. In OSS this is always false. Will throw an error if [[api/current-user-id]] is not bound. | (defenterprise sandboxed-user?
metabase-enterprise.sandbox.api.util
[]
(when-not api/*current-user-id*
;; If no *current-user-id* is bound we can't check for sandboxes, so we should throw in this case to avoid
;; returning `false` for users who should actually be sandboxes.
(throw (ex-info (str (tru "No current user found"))
{:status-code 403})))
;; oss doesn't have sandboxing. But we throw if no current-user-id so the behavior doesn't change when ee version
;; becomes available
false) |
Returns a boolean if the current user uses connection impersonation for any database. In OSS this is always false. Will throw an error if [[api/current-user-id]] is not bound. | (defenterprise impersonated-user?
metabase-enterprise.impersonation.util
[]
(when-not api/*current-user-id*
;; If no *current-user-id* is bound we can't check for impersonations, so we should throw in this case to avoid
;; returning `false` for users who should actually be using impersonations.
(throw (ex-info (str (tru "No current user found"))
{:status-code 403})))
;; oss doesn't have connection impersonation. But we throw if no current-user-id so the behavior doesn't change when
;; ee version becomes available
false) |
Returns a boolean if the current user has an enforced connection impersonation policy for a provided database. In OSS this is always false. Will throw an error if [[api/current-user-id]] is not bound. | (defenterprise impersonation-enforced-for-db?
metabase-enterprise.impersonation.util
[_db-or-id]
(when-not api/*current-user-id*
;; If no *current-user-id* is bound we can't check for impersonations, so we should throw in this case to avoid
;; returning `false` for users who should actually be using impersonations.
(throw (ex-info (str (tru "No current user found"))
{:status-code 403})))
;; oss doesn't have connection impersonation. But we throw if no current-user-id so the behavior doesn't change when
;; ee version becomes available
false) |
Returns a boolean if the current user uses sandboxing or connection impersonation for any database. In OSS is always false. Will throw an error if [[api/current-user-id]] is not bound. | (defn sandboxed-or-impersonated-user?
[]
(or (sandboxed-user?)
(impersonated-user?))) |