<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Role on Tarragon</title><link>https://tarrragon.github.io/blog/tags/role/</link><description>Recent content in Role on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 31 Aug 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/role/index.xml" rel="self" type="application/rss+xml"/><item><title>SQL.16 SQL 的權限模型：角色、GRANT 的授權單位與最小權限</title><link>https://tarrragon.github.io/blog/backend/01-database/sql/privilege-model/</link><pubDate>Mon, 31 Aug 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/sql/privilege-model/</guid><description>&lt;p>&lt;code>GRANT&lt;/code> 與 &lt;code>REVOKE&lt;/code> 是 SQL 的一部分，與 &lt;code>SELECT&lt;/code> 一樣由標準定義。它們管的是誰被允許對哪個物件做哪件事，而預設是什麼都不允許——權限是一項一項加上去的，不是先給再收回來。這個預設之下，一個帳號能做什麼等於它被授予過什麼，兩者之間沒有隱含的推導。&lt;/p>
&lt;p>以下的輸出來自 PostgreSQL 18。SQLite 沒有使用者的概念，整個檔案的存取由檔案系統決定，所以本篇談的授權這一層在 SQLite 上不存在。&lt;/p>
&lt;h2 id="新建角色的預設權限">新建角色的預設權限&lt;/h2>
&lt;p>書店的資料裡有三位顧客與兩張訂單，現在要開一個只跑報表的帳號。PostgreSQL 用「角色」這一個概念同時涵蓋使用者與群組——帶 &lt;code>LOGIN&lt;/code> 屬性的角色可以連線，不帶的當群組用，兩者授權的方式相同。先建一個能登入的角色，什麼權限都還沒給：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ROLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">報表&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LOGIN&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">PASSWORD&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;...&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ROLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">報表&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- ERROR: permission denied for table 顧客&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個角色存在、登得進來、而它讀不到任何一張表。&lt;strong>能連線與能做事是兩件分開的事&lt;/strong>：能連線由角色的 &lt;code>LOGIN&lt;/code> 屬性決定，能做事由授權決定。&lt;/p>
&lt;p>給了讀取權之後才讀得到：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">RESET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ROLE&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- 切回表的擁有者；以報表的身分執行這條 GRANT 會回 permission denied
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">GRANT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">報表&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ROLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">報表&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">姓名&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 佳穎、宗翰、雅文&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="授權的單位角色表與動作">授權的單位：角色、表與動作&lt;/h2>
&lt;p>上面只給了 &lt;code>SELECT&lt;/code>。同一個角色去寫：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">INSERT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">INTO&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">VALUES&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">4&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="s1">&amp;#39;柏宇&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- permission denied for table 顧客
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">DELETE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客編號&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- permission denied for table 顧客&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個都被擋下來，錯誤訊息與完全沒授權時一模一樣。權限的單位是「哪個角色、對哪張表、做哪一種動作」，&lt;code>SELECT&lt;/code>、&lt;code>INSERT&lt;/code>、&lt;code>UPDATE&lt;/code>、&lt;code>DELETE&lt;/code> 各自獨立，給一項不會順帶給另一項。「哪張表」由識別字指認，而那個名字送進引擎之前會先被改寫一次——&lt;code>GRANT SELECT ON Orders&lt;/code> 在 PostgreSQL 上授權的對象是 &lt;code>orders&lt;/code>，這個改寫的規則在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/identifier-rules/" data-link-title="SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字" data-link-desc="表名與欄名送進引擎之後被怎麼改寫、PostgreSQL 與 SQLite、DuckDB 在引號上的差別、MySQL 的反引號，以及全部不加引號、全部加引號與混用各自在換引擎時的後果">SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字&lt;/a>。&lt;/p>
&lt;p>目前有哪些授權查得到，收回之後那個角色又回到 &lt;code>permission denied&lt;/code>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">RESET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ROLE&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">-- 查詢與收回都由表的擁有者執行
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">grantee&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">privilege_type&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">information_schema&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">role_table_grants&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">WHERE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">table_name&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;顧客&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- 表的擁有者：INSERT、SELECT、UPDATE、DELETE、TRUNCATE、REFERENCES、TRIGGER 各一列
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1">-- 報表 | SELECT
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">REVOKE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ON&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">報表&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SET&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">ROLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">報表&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="k">SELECT&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">FROM&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">顧客&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- ERROR: permission denied for table 顧客&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="建立物件的權限與-schema">建立物件的權限與 schema&lt;/h2>
&lt;p>&lt;code>GRANT SELECT&lt;/code> 給的是對既有表的存取，與「能不能建新的表」無關。報表角色去建表：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-sql" data-lang="sql">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">CREATE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">TABLE&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">偷建的&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">x&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nb">INT&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">-- ERROR: permission denied for schema public&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>被擋的層次不同——這次擋在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/schema-namespace/" data-link-title="Schema（命名空間義）" data-link-desc="看到 permission denied for schema、或想確認這個字指的是表結構還是命名空間時">schema&lt;/a> 而不是 table。PostgreSQL 15 之後 &lt;code>public&lt;/code> schema 預設不再開放給所有人建立物件，在那之前這個操作會成功。&lt;strong>同一段 SQL 在不同版本上的結果不同，而差別在預設值而不在語法。&lt;/strong>&lt;/p>
&lt;h2 id="最小權限應用程式帳號的授權範圍">最小權限：應用程式帳號的授權範圍&lt;/h2>
&lt;p>應用程式連資料庫用的帳號，開的權限涵蓋它實際會執行的動作就夠。一個只跑報表的程式從來不寫入，所以它的帳號有沒有 &lt;code>INSERT&lt;/code> 對它的功能沒有差別；處理訂單的程式不會刪顧客，所以它的帳號有沒有顧客表的 &lt;code>DELETE&lt;/code> 對它的功能也沒有差別。遷移是唯一需要 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/sql/knowledge-cards/ddl-dml/" data-link-title="DDL 與 DML（定義結構與動資料）" data-link-desc="看到「這個帳號只需要 DML 權限」或遷移文件把兩者分開講時，查這條界線切在哪裡">DDL&lt;/a> 的場合，而它不是常態運行的一部分——那些權限跟著它自己的帳號走，平常沒有任何一條應用程式的連線用它登入。遷移本身的流程（雙寫、回填、切流與回滾）在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同一個資料庫引擎內 schema 演進與資料變更的分段驗證流程：expand-contract、回填、驗證查詢、切流與回滾；換資料庫引擎的遷移不在範圍內">資料庫轉換實作&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p><code>GRANT</code> 與 <code>REVOKE</code> 是 SQL 的一部分，與 <code>SELECT</code> 一樣由標準定義。它們管的是誰被允許對哪個物件做哪件事，而預設是什麼都不允許——權限是一項一項加上去的，不是先給再收回來。這個預設之下，一個帳號能做什麼等於它被授予過什麼，兩者之間沒有隱含的推導。</p>
<p>以下的輸出來自 PostgreSQL 18。SQLite 沒有使用者的概念，整個檔案的存取由檔案系統決定，所以本篇談的授權這一層在 SQLite 上不存在。</p>
<h2 id="新建角色的預設權限">新建角色的預設權限</h2>
<p>書店的資料裡有三位顧客與兩張訂單，現在要開一個只跑報表的帳號。PostgreSQL 用「角色」這一個概念同時涵蓋使用者與群組——帶 <code>LOGIN</code> 屬性的角色可以連線，不帶的當群組用，兩者授權的方式相同。先建一個能登入的角色，什麼權限都還沒給：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">ROLE</span><span class="w"> </span><span class="err">報表</span><span class="w"> </span><span class="n">LOGIN</span><span class="w"> </span><span class="n">PASSWORD</span><span class="w"> </span><span class="s1">&#39;...&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="k">ROLE</span><span class="w"> </span><span class="err">報表</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- ERROR: permission denied for table 顧客</span></span></span></code></pre></div><p>這個角色存在、登得進來、而它讀不到任何一張表。<strong>能連線與能做事是兩件分開的事</strong>：能連線由角色的 <code>LOGIN</code> 屬性決定，能做事由授權決定。</p>
<p>給了讀取權之後才讀得到：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">RESET</span><span class="w"> </span><span class="k">ROLE</span><span class="p">;</span><span class="w">                     </span><span class="c1">-- 切回表的擁有者；以報表的身分執行這條 GRANT 會回 permission denied
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">GRANT</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">TO</span><span class="w"> </span><span class="err">報表</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="k">ROLE</span><span class="w"> </span><span class="err">報表</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="err">姓名</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="c1">-- 佳穎、宗翰、雅文</span></span></span></code></pre></div><h2 id="授權的單位角色表與動作">授權的單位：角色、表與動作</h2>
<p>上面只給了 <code>SELECT</code>。同一個角色去寫：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="mi">4</span><span class="p">,</span><span class="s1">&#39;柏宇&#39;</span><span class="p">);</span><span class="w">   </span><span class="c1">-- permission denied for table 顧客
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">DELETE</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="err">顧客編號</span><span class="o">=</span><span class="mi">1</span><span class="p">;</span><span class="w">    </span><span class="c1">-- permission denied for table 顧客</span></span></span></code></pre></div><p>兩個都被擋下來，錯誤訊息與完全沒授權時一模一樣。權限的單位是「哪個角色、對哪張表、做哪一種動作」，<code>SELECT</code>、<code>INSERT</code>、<code>UPDATE</code>、<code>DELETE</code> 各自獨立，給一項不會順帶給另一項。「哪張表」由識別字指認，而那個名字送進引擎之前會先被改寫一次——<code>GRANT SELECT ON Orders</code> 在 PostgreSQL 上授權的對象是 <code>orders</code>，這個改寫的規則在 <a href="/blog/backend/01-database/sql/identifier-rules/" data-link-title="SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字" data-link-desc="表名與欄名送進引擎之後被怎麼改寫、PostgreSQL 與 SQLite、DuckDB 在引號上的差別、MySQL 的反引號，以及全部不加引號、全部加引號與混用各自在換引擎時的後果">SQL.14 識別字的規則：各家的大小寫摺疊、引號與保留字</a>。</p>
<p>目前有哪些授權查得到，收回之後那個角色又回到 <code>permission denied</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">RESET</span><span class="w"> </span><span class="k">ROLE</span><span class="p">;</span><span class="w">                     </span><span class="c1">-- 查詢與收回都由表的擁有者執行
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">grantee</span><span class="p">,</span><span class="w"> </span><span class="n">privilege_type</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">information_schema</span><span class="p">.</span><span class="n">role_table_grants</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="k">table_name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;顧客&#39;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 表的擁有者：INSERT、SELECT、UPDATE、DELETE、TRUNCATE、REFERENCES、TRIGGER 各一列
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">-- 報表 | SELECT
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"></span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">REVOKE</span><span class="w"> </span><span class="k">SELECT</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="err">顧客</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">報表</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="k">ROLE</span><span class="w"> </span><span class="err">報表</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="err">顧客</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- ERROR: permission denied for table 顧客</span></span></span></code></pre></div><h2 id="建立物件的權限與-schema">建立物件的權限與 schema</h2>
<p><code>GRANT SELECT</code> 給的是對既有表的存取，與「能不能建新的表」無關。報表角色去建表：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="err">偷建的</span><span class="w"> </span><span class="p">(</span><span class="n">x</span><span class="w"> </span><span class="nb">INT</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="c1">-- ERROR: permission denied for schema public</span></span></span></code></pre></div><p>被擋的層次不同——這次擋在 <a href="/blog/backend/01-database/sql/knowledge-cards/schema-namespace/" data-link-title="Schema（命名空間義）" data-link-desc="看到 permission denied for schema、或想確認這個字指的是表結構還是命名空間時">schema</a> 而不是 table。PostgreSQL 15 之後 <code>public</code> schema 預設不再開放給所有人建立物件，在那之前這個操作會成功。<strong>同一段 SQL 在不同版本上的結果不同，而差別在預設值而不在語法。</strong></p>
<h2 id="最小權限應用程式帳號的授權範圍">最小權限：應用程式帳號的授權範圍</h2>
<p>應用程式連資料庫用的帳號，開的權限涵蓋它實際會執行的動作就夠。一個只跑報表的程式從來不寫入，所以它的帳號有沒有 <code>INSERT</code> 對它的功能沒有差別；處理訂單的程式不會刪顧客，所以它的帳號有沒有顧客表的 <code>DELETE</code> 對它的功能也沒有差別。遷移是唯一需要 <a href="/blog/backend/01-database/sql/knowledge-cards/ddl-dml/" data-link-title="DDL 與 DML（定義結構與動資料）" data-link-desc="看到「這個帳號只需要 DML 權限」或遷移文件把兩者分開講時，查這條界線切在哪裡">DDL</a> 的場合，而它不是常態運行的一部分——那些權限跟著它自己的帳號走，平常沒有任何一條應用程式的連線用它登入。遷移本身的流程（雙寫、回填、切流與回滾）在 <a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同一個資料庫引擎內 schema 演進與資料變更的分段驗證流程：expand-contract、回填、驗證查詢、切流與回滾；換資料庫引擎的遷移不在範圍內">資料庫轉換實作</a>。</p>
<p>每一種用途各用一個帳號、只開它用得到的權限，這樣分的效益在事故發生時才顯示出來。應用程式如果用超級使用者連線，一個注入漏洞——攻擊者讓自己寫的片段被當成 SQL 的一部分送出去——的可觸及範圍就是整個資料庫，包含刪表與讀取其他應用的資料；同一個漏洞在只有 <code>SELECT</code> 權限的帳號上，可觸及範圍縮到被授予 <code>SELECT</code> 的那幾張表的讀取。<strong>漏洞一樣，可觸及的範圍差很多，而差別在事發前就決定好了。</strong> 授權只是資料層攻擊面的其中一層，<a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">backend 模組一的攻擊者視角</a> 走注入、授權繞過、資料外洩、競態與資源耗盡五類，前三類各附真實事件對照。這個推論的前提是應用程式的每一種用途各用一個資料庫帳號；所有用途共用一個帳號、由應用層自己做授權的架構下，資料庫這一層的最小權限就使不上力，防線要落在別處。</p>
<p>授權查得到、也審計得到，所以「這個帳號能做什麼」是一個有明確答案的問題，不必靠讀應用程式的程式碼推測。</p>
]]></content:encoded></item></channel></rss>