diff --git a/src/metabase/api/search.clj b/src/metabase/api/search.clj
index b1b9c42894b4d043be4e4ebb306b086418cccf87..2f8cfe422a0ea58c7b91386508d8dca13c0b5ee1 100644
--- a/src/metabase/api/search.clj
+++ b/src/metabase/api/search.clj
@@ -16,6 +16,7 @@
              [dashboard :refer [Dashboard]]
              [dashboard-favorite :refer [DashboardFavorite]]
              [metric :refer [Metric]]
+             [permissions :as perms]
              [pulse :refer [Pulse]]
              [segment :refer [Segment]]
              [table :refer [Table]]]
@@ -27,13 +28,12 @@
 
 (def ^:private SearchContext
   "Map with the various allowed search parameters, used to construct the SQL query"
-  {:search-string       (s/maybe su/NonBlankString)
-   :archived?           s/Bool
-   ;; either `:all` or a set of IDs and/or the string `root`
-   :visible-collections coll/VisibleCollections})
+  {:search-string      (s/maybe su/NonBlankString)
+   :archived?          s/Bool
+   :current-user-perms #{perms/UserPath}})
 
 (def ^:private searchable-models
-  [Card Dashboard Pulse Collection Segment Metric])
+  [Card Dashboard Pulse Collection Segment Metric Table])
 
 (def ^:private SearchableModel
   (apply s/enum searchable-models))
@@ -128,6 +128,17 @@
   [_]
   (into default-columns table-columns))
 
+(defmethod columns-for-model (class Table)
+  [_]
+  [:id
+   :name
+   :description
+   [:id :table_id]
+   [:db_id :database_id]
+   [:schema :table_schema]
+   [:name :table_name]
+   [:description :table_description]])
+
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                               Shared Query Logic                                               |
@@ -191,9 +202,22 @@
   [model :- SearchableModel]
   [[model (model->alias model)]])
 
+(defmulti ^:private archived-where-clause
+  {:arglists '([model archived?])}
+  (fn [model _] (class model)))
+
+(defmethod archived-where-clause :default
+  [model archived?]
+  [:= (hsql/qualify (model->alias model) :archived) archived?])
+
+;; Table has an `:active` flag, but no `:archived` flag; never return inactive Tables
+(defmethod archived-where-clause (class Table)
+  [model _]
+  [:= (hsql/qualify (model->alias model) :active) true])
+
 (s/defn ^:private base-where-clause-for-model :- [(s/one (s/enum :and :=) "type") s/Any]
   [model :- SearchableModel, {:keys [search-string archived?]} :- SearchContext]
-  (let [archived-clause      [:= (hsql/qualify (model->alias model) :archived) archived?]
+  (let [archived-clause      (archived-where-clause model archived?)
         search-string-clause (when (seq search-string)
                                [:like
                                 (hsql/call :lower (hsql/qualify (model->alias model) :name))
@@ -213,10 +237,12 @@
 (s/defn ^:private add-collection-join-and-where-clauses
   "Add a `WHERE` clause to the query to only return Collections the Current User has access to; join against Collection
   so we can return its `:name`."
-  [honeysql-query :- su/Map, collection-id-column :- s/Keyword, {:keys [visible-collections]} :- SearchContext]
-  (let [honeysql-query (h/merge-where
-                        honeysql-query
-                        (coll/visible-collection-ids->honeysql-filter-clause collection-id-column visible-collections))]
+  [honeysql-query :- su/Map, collection-id-column :- s/Keyword, {:keys [current-user-perms]} :- SearchContext]
+  (let [visible-collections      (coll/permissions-set->visible-collection-ids current-user-perms)
+        collection-filter-clause (coll/visible-collection-ids->honeysql-filter-clause
+                                  collection-id-column
+                                  visible-collections)
+        honeysql-query           (h/merge-where honeysql-query collection-filter-clause)]
     ;; add a JOIN against Collection *unless* the source table is already Collection
     (cond-> honeysql-query
       (not= collection-id-column :collection.id)
@@ -232,7 +258,7 @@
   {:arglists '([model search-context])}
   (fn [model _] (class model)))
 
-(s/defmethod ^:private search-query-for-model (class Card)
+(s/defmethod search-query-for-model (class Card)
   [_ search-ctx :- SearchContext]
   (-> (base-query-for-model Card search-ctx)
       (h/left-join [CardFavorite :fave]
@@ -241,12 +267,12 @@
                     [:= :fave.owner_id api/*current-user-id*]])
       (add-collection-join-and-where-clauses :card.collection_id search-ctx)))
 
-(s/defmethod ^:private search-query-for-model (class Collection)
+(s/defmethod search-query-for-model (class Collection)
   [_ search-ctx :- SearchContext]
   (-> (base-query-for-model Collection search-ctx)
       (add-collection-join-and-where-clauses :collection.id search-ctx)))
 
-(s/defmethod ^:private search-query-for-model (class Dashboard)
+(s/defmethod search-query-for-model (class Dashboard)
   [_ search-ctx :- SearchContext]
   (-> (base-query-for-model Dashboard search-ctx)
       (h/left-join [DashboardFavorite :fave]
@@ -255,7 +281,7 @@
                     [:= :fave.user_id api/*current-user-id*]])
       (add-collection-join-and-where-clauses :dashboard.collection_id search-ctx)))
 
-(s/defmethod ^:private search-query-for-model (class Pulse)
+(s/defmethod search-query-for-model (class Pulse)
   [_ search-ctx :- SearchContext]
   ;; Pulses don't currently support being archived, omit if archived is true
   (-> (base-query-for-model Pulse search-ctx)
@@ -263,21 +289,40 @@
       ;; We don't want alerts included in pulse results
       (h/merge-where [:= :alert_condition nil])))
 
-(s/defmethod ^:private search-query-for-model (class Metric)
+(s/defmethod search-query-for-model (class Metric)
   [_ search-ctx :- SearchContext]
   (-> (base-query-for-model Metric search-ctx)
       (h/left-join [Table :table] [:= :metric.table_id :table.id])))
 
-(s/defmethod ^:private search-query-for-model (class Segment)
+(s/defmethod search-query-for-model (class Segment)
   [_ search-ctx :- SearchContext]
   (-> (base-query-for-model Segment search-ctx)
       (h/left-join [Table :table] [:= :segment.table_id :table.id])))
 
+(s/defmethod search-query-for-model (class Table)
+  [_ {:keys [current-user-perms], :as search-ctx} :- SearchContext]
+  (when (seq current-user-perms)
+    (let [base-query (base-query-for-model Table search-ctx)]
+      (if (contains? current-user-perms "/")
+        base-query
+        {:select (:select base-query)
+         :from   [[(merge
+                    base-query
+                    {:select [:id :schema :db_id :name :description
+                              [(hx/concat "/db/" :db_id "/" :schema "/" :id "/") :path]]})
+                   :table]]
+         :where  (cons
+                  :or
+                  (for [path current-user-perms]
+                    [:like :path (str path "%")]))}))))
+
 (s/defn ^:private search
   "Builds a search query that includes all of the searchable entities and runs it"
   [search-ctx :- SearchContext]
-  (for [row (db/query {:union-all (for [model searchable-models]
-                                    (search-query-for-model model search-ctx))})]
+  (for [row (db/query {:union-all (for [model searchable-models
+                                        :let  [query (search-query-for-model model search-ctx)]
+                                        :when (seq query)]
+                                    query)})]
     ;; MySQL returns `:favorite` as `1` or `0` so convert those to boolean as needed
     (update row :favorite (fn [favorite]
                             (if (integer? favorite)
@@ -291,19 +336,15 @@
 
 (s/defn ^:private make-search-context :- SearchContext
   [search-string :- (s/maybe su/NonBlankString), archived-string :- (s/maybe su/BooleanString)]
-  {:search-string       search-string
-   :archived?           (Boolean/parseBoolean archived-string)
-   :visible-collections (coll/permissions-set->visible-collection-ids @api/*current-user-permissions-set*)})
+  {:search-string      search-string
+   :archived?          (Boolean/parseBoolean archived-string)
+   :current-user-perms @api/*current-user-permissions-set*})
 
 (api/defendpoint GET "/"
   "Search Cards, Dashboards, Collections and Pulses for the substring `q`."
   [q archived]
   {q        (s/maybe su/NonBlankString)
    archived (s/maybe su/BooleanString)}
-  (let [{:keys [visible-collections] :as search-ctx} (make-search-context q archived)]
-    ;; Throw if the user doesn't have access to any collections
-    (api/check-403 (or (= :all visible-collections)
-                       (seq visible-collections)))
-    (search search-ctx)))
+  (search (make-search-context q archived)))
 
 (api/define-routes)
diff --git a/src/metabase/models/permissions.clj b/src/metabase/models/permissions.clj
index 1bd0252a16559e1098579deadf3f9b41fe057905..6c0bb2d1d05a537345163543d92f741e09d8f860 100644
--- a/src/metabase/models/permissions.clj
+++ b/src/metabase/models/permissions.clj
@@ -41,27 +41,27 @@
 
 ;;; --------------------------------------------------- Validation ---------------------------------------------------
 
-(def ^:private ^:const valid-object-path-patterns
-  [#"^/db/(\d+)/$"                                              ; permissions for the entire DB -- native and all schemas
-   #"^/db/(\d+)/native/$"                                       ; permissions to create new native queries for the DB
-   #"^/db/(\d+)/schema/$"                                       ; permissions for all schemas in the DB
-   #"^/db/(\d+)/schema/([^/]*)/$"                               ; permissions for a specific schema
-   #"^/db/(\d+)/schema/([^/]*)/table/(\d+)/$"                   ; FULL permissions for a specific table
-   #"^/db/(\d+)/schema/([^/]*)/table/(\d+)/read/$"              ; Permissions to fetch the Metadata for a specific Table
-   #"^/db/(\d+)/schema/([^/]*)/table/(\d+)/query/$"             ; Permissions to run any sort of query against a Table
-   #"^/db/(\d+)/schema/([^\\/]*)/table/(\d+)/query/segmented/$" ; Permissions to run a query against a Table using GTAP
-   #"^/collection/(\d+)/$"                                      ; readwrite permissions for a collection
-   #"^/collection/(\d+)/read/$"                                 ; read permissions for a collection
-   #"^/collection/root/$"                                       ; readwrite permissions for the 'Root' Collection (things with `nil` collection_id)
-   #"^/collection/root/read/$"])                                ; read permissions for the 'Root' Collection
+(def ^:private valid-object-path-patterns
+  [#"^/db/\d+/$"                                          ; permissions for the entire DB -- native and all schemas
+   #"^/db/\d+/native/$"                                   ; permissions to create new native queries for the DB
+   #"^/db/\d+/schema/$"                                   ; permissions for all schemas in the DB
+   #"^/db/\d+/schema/[^/]*/$"                             ; permissions for a specific schema
+   #"^/db/\d+/schema/[^/]*/table/\d+/$"                   ; FULL permissions for a specific table
+   #"^/db/\d+/schema/[^/]*/table/\d+/read/$"              ; Permissions to fetch the Metadata for a specific Table
+   #"^/db/\d+/schema/[^/]*/table/\d+/query/$"             ; Permissions to run any sort of query against a Table
+   #"^/db/\d+/schema/[^/]*/table/\d+/query/segmented/$"   ; Permissions to run a query against a Table using GTAP
+   #"^/collection/\d+/$"                                  ; readwrite permissions for a collection
+   #"^/collection/\d+/read/$"                             ; read permissions for a collection
+   #"^/collection/root/$"                                 ; readwrite permissions for the 'Root' Collection (things with `nil` collection_id)
+   #"^/collection/root/read/$"])                          ; read permissions for the 'Root' Collection
 
 (defn valid-object-path?
-  "Does OBJECT-PATH follow a known, allowed format to an *object*?
-   (The root path, \"/\", is not considered an object; this returns `false` for it)."
+  "Does `object-path` follow a known, allowed format to an *object*? (The root path, \"/\", is not considered an object;
+  this returns `false` for it)."
   ^Boolean [^String object-path]
   (boolean (when (and (string? object-path)
                       (seq object-path))
-             (some (u/rpartial re-matches object-path)
+             (some #(re-matches % object-path)
                    valid-object-path-patterns))))
 
 (def ObjectPath
@@ -75,7 +75,7 @@
           "Valid user permissions path."))
 
 (defn- assert-not-admin-group
-  "Check to make sure the `:group_id` for PERMISSIONS entry isn't the admin group."
+  "Check to make sure the `:group_id` for `permissions` entry isn't the admin group."
   [{:keys [group_id]}]
   (when (and (= group_id (:id (group/admin)))
              (not *allow-admin-permissions-changes*))
@@ -83,14 +83,14 @@
              {:status-code 400}))))
 
 (defn- assert-valid-object
-  "Check to make sure the value of `:object` for PERMISSIONS entry is valid."
+  "Check to make sure the value of `:object` for `permissions` entry is valid."
   [{:keys [object]}]
   (when (and object
              (not (valid-object-path? object))
              (or (not= object "/")
                  (not *allow-root-entries*)))
-    (throw (ui18n/ex-info (tru "Invalid permissions object path: ''{0}''." object)
-             {:status-code 400}))))
+    (throw (ex-info (str (tru "Invalid permissions object path: ''{0}''." object))
+             {:status-code 400, :path object}))))
 
 (defn- assert-valid-metabot-permissions
   "MetaBot permissions can only be created for Collections, since MetaBot can only interact with objects that are always
@@ -98,11 +98,11 @@
   [{:keys [object group_id]}]
   (when (and (= group_id (:id (group/metabot)))
              (not (str/starts-with? object "/collection/")))
-    (throw (ui18n/ex-info (tru "MetaBot can only have Collection permissions.")
+    (throw (ex-info (str (tru "MetaBot can only have Collection permissions."))
              {:status-code 400}))))
 
 (defn- assert-valid
-  "Check to make sure this PERMISSIONS entry is something that's allowed to be saved (i.e. it has a valid `:object`
+  "Check to make sure this `permissions` entry is something that's allowed to be saved (i.e. it has a valid `:object`
    path and it's not for the admin group)."
   [permissions]
   (doseq [f [assert-not-admin-group
@@ -156,12 +156,12 @@
 ;;; -------------------------------------------- Permissions Checking Fns --------------------------------------------
 
 (defn is-permissions-for-object?
-  "Does PERMISSIONS-PATH grant *full* access for OBJECT-PATH?"
+  "Does `permissions`-PATH grant *full* access for OBJECT-PATH?"
   [permissions-path object-path]
   (str/starts-with? object-path permissions-path))
 
 (defn is-partial-permissions-for-object?
-  "Does PERMISSIONS-PATH grant access full access for OBJECT-PATH *or* for a descendant of OBJECT-PATH?"
+  "Does `permissions`-PATH grant access full access for OBJECT-PATH *or* for a descendant of OBJECT-PATH?"
   [permissions-path object-path]
   (or (is-permissions-for-object? permissions-path object-path)
       (str/starts-with? permissions-path object-path)))
diff --git a/src/metabase/models/table.clj b/src/metabase/models/table.clj
index df4c4f1e7d4af45f0d11d276f351e074b63defe9..b271565cae27e97f779e40c0965c4737e703bb19 100644
--- a/src/metabase/models/table.clj
+++ b/src/metabase/models/table.clj
@@ -64,7 +64,7 @@
 ;;; --------------------------------------------------- Hydration ----------------------------------------------------
 
 (defn fields
-  "Return the `FIELDS` belonging to a single TABLE."
+  "Return the Fields belonging to a single `table`."
   [{:keys [id]}]
   (db/select Field
     :table_id        id
@@ -73,17 +73,17 @@
     {:order-by [[:position :asc] [:name :asc]]}))
 
 (defn metrics
-  "Retrieve the `Metrics` for a single TABLE."
+  "Retrieve the Metrics for a single `table`."
   [{:keys [id]}]
   (retrieve-metrics id :all))
 
 (defn segments
-  "Retrieve the `Segments` for a single TABLE."
+  "Retrieve the Segments for a single `table`."
   [{:keys [id]}]
   (retrieve-segments id :all))
 
 (defn field-values
-  "Return the `FieldValues` for all `Fields` belonging to a single TABLE."
+  "Return the FieldValues for all Fields belonging to a single `table`."
   {:hydrate :field_values, :arglists '([table])}
   [{:keys [id]}]
   (let [field-ids (db/select-ids Field
@@ -94,7 +94,7 @@
       (db/select-field->field :field_id :values FieldValues, :field_id [:in field-ids]))))
 
 (defn pk-field-id
-  "Return the ID of the primary key `Field` for TABLE."
+  "Return the ID of the primary key `Field` for `table`."
   {:hydrate :pk_field, :arglists '([table])}
   [{:keys [id]}]
   (db/select-one-id Field
diff --git a/test/metabase/api/search_test.clj b/test/metabase/api/search_test.clj
index 7f4d4b75c4671f50681088510c800d975f2d0e7b..229441f5bf284b5cd99d0ab327bb320e8bd45f66 100644
--- a/test/metabase/api/search_test.clj
+++ b/test/metabase/api/search_test.clj
@@ -9,6 +9,7 @@
              [collection :as coll :refer [Collection]]
              [dashboard :refer [Dashboard]]
              [dashboard-favorite :refer [DashboardFavorite]]
+             [database :refer [Database]]
              [metric :refer [Metric]]
              [permissions :as perms]
              [permissions-group :as group :refer [PermissionsGroup]]
@@ -276,3 +277,29 @@
                 (and (= id (u/get-id pulse))
                      (= "pulse" model)))
               ((test-users/user->client :crowberto) :get 200 "search")))))
+
+;; You should see TABLES in the search results!
+(defn- default-table-search-row [table-name]
+  (merge
+   default-search-row
+   {:name table-name, :table_name table-name :table_id true, :archived nil, :model "table", :database_id true}))
+
+(let [table-name (tu/random-name)]
+  (expect
+    #{(default-table-search-row table-name)}
+    (tt/with-temp Table [table {:name table-name}]
+      (search-request :crowberto :q table-name))))
+
+(let [table-name (tu/random-name)]
+  (expect
+    #{(default-table-search-row table-name)}
+    (tt/with-temp Table [table {:name table-name}]
+      (search-request :rasta :q table-name))))
+
+;; you should not be able to see a Table if the current user doesn't have permissions for that Table
+(expect
+  #{}
+  (tt/with-temp* [Database [{db-id :id}]
+                  Table    [table {:db_id db-id}]]
+    (perms/revoke-permissions! (group/all-users) db-id)
+    (search-request :rasta :q (:name table))))
diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj
index f9fa12d4a0a07c1668c7b1a6dd8390e5cc28c409..6d5a1319d24dea28c97ab45fa84383db09aceeda 100644
--- a/test/metabase/test/util.clj
+++ b/test/metabase/test/util.clj
@@ -66,7 +66,7 @@
      ~actual))
 
 (defn- random-uppercase-letter []
-  (char (+ (rand-int 26) (int \A))))
+  (char (+ (int \A) (rand-int 26))))
 
 (defn random-name
   "Generate a random string of 20 uppercase letters."