[{"data":1,"prerenderedAt":1295},["ShallowReactive",2],{"blog-\u002Fblog\u002Fsql-permissions-roles-grants":3},{"id":4,"title":5,"body":6,"description":1281,"difficulty":1282,"extension":1283,"framework":1284,"frameworkSlug":47,"meta":1285,"navigation":87,"order":55,"path":1286,"qaPath":1287,"seo":1288,"stem":1289,"subtopic":1290,"topic":1291,"topicSlug":1292,"updated":1293,"__hash__":1294},"blog\u002Fblog\u002Fsql-permissions-roles-grants.md","SQL Permissions & Roles — GRANT, REVOKE, and Least Privilege",{"type":7,"value":8,"toc":1270},"minimark",[9,14,28,32,42,256,266,270,663,667,678,764,768,778,859,863,866,1010,1014,1194,1198,1253,1257,1266],[10,11,13],"h2",{"id":12},"why-database-permissions-matter","Why database permissions matter",[15,16,17,18,22,23,27],"p",{},"Application code bugs are inevitable. A logic error, a missing ",[19,20,21],"code",{},"WHERE"," clause,\nor an unintended admin route can trigger unintended writes or reads. Database\npermissions are the last line of defence — they limit what damage a bug or\ncompromised connection can do. The principle is ",[24,25,26],"strong",{},"least privilege",": each\ndatabase user gets only the permissions needed for its specific role, nothing\nmore.",[10,29,31],{"id":30},"grant-and-revoke","GRANT and REVOKE",[15,33,34,37,38,41],{},[19,35,36],{},"GRANT"," gives a privilege; ",[19,39,40],{},"REVOKE"," removes it.",[43,44,49],"pre",{"className":45,"code":46,"language":47,"meta":48,"style":48},"language-sql shiki shiki-themes github-light github-dark","-- Grant SELECT on one table\nGRANT SELECT ON orders TO reporting_role;\n\n-- Grant multiple privileges\nGRANT SELECT, INSERT, UPDATE, DELETE ON orders    TO app_role;\nGRANT SELECT, INSERT, UPDATE, DELETE ON customers TO app_role;\nGRANT SELECT                         ON products  TO app_role;\n\n-- Grant on all current tables in a schema (Postgres)\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;\n\n-- Revoke a privilege\nREVOKE INSERT ON orders FROM app_role;\nREVOKE ALL PRIVILEGES ON customers FROM analyst_user;\n","sql","",[19,50,51,60,82,89,95,128,156,173,178,184,210,215,221,238],{"__ignoreMap":48},[52,53,56],"span",{"class":54,"line":55},"line",1,[52,57,59],{"class":58},"sJ8bj","-- Grant SELECT on one table\n",[52,61,63,66,69,72,76,79],{"class":54,"line":62},2,[52,64,36],{"class":65},"szBVR",[52,67,68],{"class":65}," SELECT",[52,70,71],{"class":65}," ON",[52,73,75],{"class":74},"sVt8B"," orders ",[52,77,78],{"class":65},"TO",[52,80,81],{"class":74}," reporting_role;\n",[52,83,85],{"class":54,"line":84},3,[52,86,88],{"emptyLinePlaceholder":87},true,"\n",[52,90,92],{"class":54,"line":91},4,[52,93,94],{"class":58},"-- Grant multiple privileges\n",[52,96,98,100,102,105,108,110,113,115,118,120,123,125],{"class":54,"line":97},5,[52,99,36],{"class":65},[52,101,68],{"class":65},[52,103,104],{"class":74},", ",[52,106,107],{"class":65},"INSERT",[52,109,104],{"class":74},[52,111,112],{"class":65},"UPDATE",[52,114,104],{"class":74},[52,116,117],{"class":65},"DELETE",[52,119,71],{"class":65},[52,121,122],{"class":74}," orders    ",[52,124,78],{"class":65},[52,126,127],{"class":74}," app_role;\n",[52,129,131,133,135,137,139,141,143,145,147,149,152,154],{"class":54,"line":130},6,[52,132,36],{"class":65},[52,134,68],{"class":65},[52,136,104],{"class":74},[52,138,107],{"class":65},[52,140,104],{"class":74},[52,142,112],{"class":65},[52,144,104],{"class":74},[52,146,117],{"class":65},[52,148,71],{"class":65},[52,150,151],{"class":74}," customers ",[52,153,78],{"class":65},[52,155,127],{"class":74},[52,157,159,161,163,166,169,171],{"class":54,"line":158},7,[52,160,36],{"class":65},[52,162,68],{"class":65},[52,164,165],{"class":65},"                         ON",[52,167,168],{"class":74}," products  ",[52,170,78],{"class":65},[52,172,127],{"class":74},[52,174,176],{"class":54,"line":175},8,[52,177,88],{"emptyLinePlaceholder":87},[52,179,181],{"class":54,"line":180},9,[52,182,183],{"class":58},"-- Grant on all current tables in a schema (Postgres)\n",[52,185,187,189,191,193,196,199,202,205,207],{"class":54,"line":186},10,[52,188,36],{"class":65},[52,190,68],{"class":65},[52,192,71],{"class":65},[52,194,195],{"class":74}," ALL TABLES ",[52,197,198],{"class":65},"IN",[52,200,201],{"class":65}," SCHEMA",[52,203,204],{"class":74}," public ",[52,206,78],{"class":65},[52,208,209],{"class":74}," readonly_role;\n",[52,211,213],{"class":54,"line":212},11,[52,214,88],{"emptyLinePlaceholder":87},[52,216,218],{"class":54,"line":217},12,[52,219,220],{"class":58},"-- Revoke a privilege\n",[52,222,224,226,229,231,233,236],{"class":54,"line":223},13,[52,225,40],{"class":65},[52,227,228],{"class":65}," INSERT",[52,230,71],{"class":65},[52,232,75],{"class":74},[52,234,235],{"class":65},"FROM",[52,237,127],{"class":74},[52,239,241,243,246,249,251,253],{"class":54,"line":240},14,[52,242,40],{"class":65},[52,244,245],{"class":74}," ALL PRIVILEGES ",[52,247,248],{"class":65},"ON",[52,250,151],{"class":74},[52,252,235],{"class":65},[52,254,255],{"class":74}," analyst_user;\n",[15,257,258,259,262,263,265],{},"Grant to ",[24,260,261],{},"roles",", not directly to users. Then assign users to roles — you\ncan update access patterns by changing role memberships rather than rerunning\ndozens of ",[19,264,36],{}," statements.",[10,267,269],{"id":268},"designing-roles-for-a-typical-web-application","Designing roles for a typical web application",[43,271,273],{"className":45,"code":272,"language":47,"meta":48,"style":48},"-- Three roles matching the three access patterns\nCREATE ROLE readonly_role;    -- dashboards, reporting, read-only API\nCREATE ROLE app_role;         -- the web application's runtime connection\nCREATE ROLE migration_role;   -- only used during deployments\n\n-- readonly_role: read-only access to public schema\nGRANT USAGE ON SCHEMA public TO readonly_role;\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;\nALTER DEFAULT PRIVILEGES IN SCHEMA public\n    GRANT SELECT ON TABLES TO readonly_role;  -- also covers future tables\n\n-- app_role: read-write on specific tables, no DDL\nGRANT USAGE ON SCHEMA public TO app_role;\nGRANT SELECT, INSERT, UPDATE, DELETE ON customers TO app_role;\nGRANT SELECT, INSERT, UPDATE, DELETE ON orders     TO app_role;\nGRANT SELECT, INSERT, UPDATE, DELETE ON order_items TO app_role;\nGRANT SELECT ON products TO app_role;  -- read-only on catalogue\n\n-- migration_role: can create and alter tables (only used during deploys)\nGRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO migration_role;\nGRANT CREATE ON SCHEMA public TO migration_role;\n\n-- Users\nCREATE ROLE api_service  LOGIN PASSWORD '...' NOINHERIT;\nCREATE ROLE report_user  LOGIN PASSWORD '...' NOINHERIT;\n\nGRANT app_role      TO api_service;\nGRANT readonly_role TO report_user;\n",[19,274,275,280,294,306,318,322,327,344,364,382,402,406,411,427,453,481,509,529,534,540,562,580,585,591,614,632,637,650],{"__ignoreMap":48},[52,276,277],{"class":54,"line":55},[52,278,279],{"class":58},"-- Three roles matching the three access patterns\n",[52,281,282,285,288,291],{"class":54,"line":62},[52,283,284],{"class":65},"CREATE",[52,286,287],{"class":65}," ROLE",[52,289,290],{"class":74}," readonly_role;    ",[52,292,293],{"class":58},"-- dashboards, reporting, read-only API\n",[52,295,296,298,300,303],{"class":54,"line":84},[52,297,284],{"class":65},[52,299,287],{"class":65},[52,301,302],{"class":74}," app_role;         ",[52,304,305],{"class":58},"-- the web application's runtime connection\n",[52,307,308,310,312,315],{"class":54,"line":91},[52,309,284],{"class":65},[52,311,287],{"class":65},[52,313,314],{"class":74}," migration_role;   ",[52,316,317],{"class":58},"-- only used during deployments\n",[52,319,320],{"class":54,"line":97},[52,321,88],{"emptyLinePlaceholder":87},[52,323,324],{"class":54,"line":130},[52,325,326],{"class":58},"-- readonly_role: read-only access to public schema\n",[52,328,329,331,334,336,338,340,342],{"class":54,"line":158},[52,330,36],{"class":65},[52,332,333],{"class":74}," USAGE ",[52,335,248],{"class":65},[52,337,201],{"class":65},[52,339,204],{"class":74},[52,341,78],{"class":65},[52,343,209],{"class":74},[52,345,346,348,350,352,354,356,358,360,362],{"class":54,"line":175},[52,347,36],{"class":65},[52,349,68],{"class":65},[52,351,71],{"class":65},[52,353,195],{"class":74},[52,355,198],{"class":65},[52,357,201],{"class":65},[52,359,204],{"class":74},[52,361,78],{"class":65},[52,363,209],{"class":74},[52,365,366,369,372,375,377,379],{"class":54,"line":180},[52,367,368],{"class":65},"ALTER",[52,370,371],{"class":65}," DEFAULT",[52,373,374],{"class":74}," PRIVILEGES ",[52,376,198],{"class":65},[52,378,201],{"class":65},[52,380,381],{"class":74}," public\n",[52,383,384,387,389,391,394,396,399],{"class":54,"line":186},[52,385,386],{"class":65},"    GRANT",[52,388,68],{"class":65},[52,390,71],{"class":65},[52,392,393],{"class":74}," TABLES ",[52,395,78],{"class":65},[52,397,398],{"class":74}," readonly_role;  ",[52,400,401],{"class":58},"-- also covers future tables\n",[52,403,404],{"class":54,"line":212},[52,405,88],{"emptyLinePlaceholder":87},[52,407,408],{"class":54,"line":217},[52,409,410],{"class":58},"-- app_role: read-write on specific tables, no DDL\n",[52,412,413,415,417,419,421,423,425],{"class":54,"line":223},[52,414,36],{"class":65},[52,416,333],{"class":74},[52,418,248],{"class":65},[52,420,201],{"class":65},[52,422,204],{"class":74},[52,424,78],{"class":65},[52,426,127],{"class":74},[52,428,429,431,433,435,437,439,441,443,445,447,449,451],{"class":54,"line":240},[52,430,36],{"class":65},[52,432,68],{"class":65},[52,434,104],{"class":74},[52,436,107],{"class":65},[52,438,104],{"class":74},[52,440,112],{"class":65},[52,442,104],{"class":74},[52,444,117],{"class":65},[52,446,71],{"class":65},[52,448,151],{"class":74},[52,450,78],{"class":65},[52,452,127],{"class":74},[52,454,456,458,460,462,464,466,468,470,472,474,477,479],{"class":54,"line":455},15,[52,457,36],{"class":65},[52,459,68],{"class":65},[52,461,104],{"class":74},[52,463,107],{"class":65},[52,465,104],{"class":74},[52,467,112],{"class":65},[52,469,104],{"class":74},[52,471,117],{"class":65},[52,473,71],{"class":65},[52,475,476],{"class":74}," orders     ",[52,478,78],{"class":65},[52,480,127],{"class":74},[52,482,484,486,488,490,492,494,496,498,500,502,505,507],{"class":54,"line":483},16,[52,485,36],{"class":65},[52,487,68],{"class":65},[52,489,104],{"class":74},[52,491,107],{"class":65},[52,493,104],{"class":74},[52,495,112],{"class":65},[52,497,104],{"class":74},[52,499,117],{"class":65},[52,501,71],{"class":65},[52,503,504],{"class":74}," order_items ",[52,506,78],{"class":65},[52,508,127],{"class":74},[52,510,512,514,516,518,521,523,526],{"class":54,"line":511},17,[52,513,36],{"class":65},[52,515,68],{"class":65},[52,517,71],{"class":65},[52,519,520],{"class":74}," products ",[52,522,78],{"class":65},[52,524,525],{"class":74}," app_role;  ",[52,527,528],{"class":58},"-- read-only on catalogue\n",[52,530,532],{"class":54,"line":531},18,[52,533,88],{"emptyLinePlaceholder":87},[52,535,537],{"class":54,"line":536},19,[52,538,539],{"class":58},"-- migration_role: can create and alter tables (only used during deploys)\n",[52,541,543,545,547,549,551,553,555,557,559],{"class":54,"line":542},20,[52,544,36],{"class":65},[52,546,245],{"class":74},[52,548,248],{"class":65},[52,550,195],{"class":74},[52,552,198],{"class":65},[52,554,201],{"class":65},[52,556,204],{"class":74},[52,558,78],{"class":65},[52,560,561],{"class":74}," migration_role;\n",[52,563,565,567,570,572,574,576,578],{"class":54,"line":564},21,[52,566,36],{"class":65},[52,568,569],{"class":65}," CREATE",[52,571,71],{"class":65},[52,573,201],{"class":65},[52,575,204],{"class":74},[52,577,78],{"class":65},[52,579,561],{"class":74},[52,581,583],{"class":54,"line":582},22,[52,584,88],{"emptyLinePlaceholder":87},[52,586,588],{"class":54,"line":587},23,[52,589,590],{"class":58},"-- Users\n",[52,592,594,596,598,601,604,607,611],{"class":54,"line":593},24,[52,595,284],{"class":65},[52,597,287],{"class":65},[52,599,600],{"class":74}," api_service  ",[52,602,603],{"class":65},"LOGIN",[52,605,606],{"class":65}," PASSWORD",[52,608,610],{"class":609},"sZZnC"," '...'",[52,612,613],{"class":74}," NOINHERIT;\n",[52,615,617,619,621,624,626,628,630],{"class":54,"line":616},25,[52,618,284],{"class":65},[52,620,287],{"class":65},[52,622,623],{"class":74}," report_user  ",[52,625,603],{"class":65},[52,627,606],{"class":65},[52,629,610],{"class":609},[52,631,613],{"class":74},[52,633,635],{"class":54,"line":634},26,[52,636,88],{"emptyLinePlaceholder":87},[52,638,640,642,645,647],{"class":54,"line":639},27,[52,641,36],{"class":65},[52,643,644],{"class":74}," app_role      ",[52,646,78],{"class":65},[52,648,649],{"class":74}," api_service;\n",[52,651,653,655,658,660],{"class":54,"line":652},28,[52,654,36],{"class":65},[52,656,657],{"class":74}," readonly_role ",[52,659,78],{"class":65},[52,661,662],{"class":74}," report_user;\n",[10,664,666],{"id":665},"default-privileges-covering-future-tables","Default privileges — covering future tables",[15,668,669,670,673,674,677],{},"Without ",[19,671,672],{},"ALTER DEFAULT PRIVILEGES",", a new table added in a migration is not\nvisible to existing roles — their ",[19,675,676],{},"SELECT"," grant covered only tables that\nexisted at grant time.",[43,679,681],{"className":45,"code":680,"language":47,"meta":48,"style":48},"-- Ensure all future tables in the schema are accessible to app_role\nALTER DEFAULT PRIVILEGES IN SCHEMA public\n    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_role;\n\nALTER DEFAULT PRIVILEGES IN SCHEMA public\n    GRANT USAGE, SELECT ON SEQUENCES TO app_role;\n",[19,682,683,688,702,728,732,746],{"__ignoreMap":48},[52,684,685],{"class":54,"line":55},[52,686,687],{"class":58},"-- Ensure all future tables in the schema are accessible to app_role\n",[52,689,690,692,694,696,698,700],{"class":54,"line":62},[52,691,368],{"class":65},[52,693,371],{"class":65},[52,695,374],{"class":74},[52,697,198],{"class":65},[52,699,201],{"class":65},[52,701,381],{"class":74},[52,703,704,706,708,710,712,714,716,718,720,722,724,726],{"class":54,"line":84},[52,705,386],{"class":65},[52,707,68],{"class":65},[52,709,104],{"class":74},[52,711,107],{"class":65},[52,713,104],{"class":74},[52,715,112],{"class":65},[52,717,104],{"class":74},[52,719,117],{"class":65},[52,721,71],{"class":65},[52,723,393],{"class":74},[52,725,78],{"class":65},[52,727,127],{"class":74},[52,729,730],{"class":54,"line":91},[52,731,88],{"emptyLinePlaceholder":87},[52,733,734,736,738,740,742,744],{"class":54,"line":97},[52,735,368],{"class":65},[52,737,371],{"class":65},[52,739,374],{"class":74},[52,741,198],{"class":65},[52,743,201],{"class":65},[52,745,381],{"class":74},[52,747,748,750,753,755,757,760,762],{"class":54,"line":130},[52,749,386],{"class":65},[52,751,752],{"class":74}," USAGE, ",[52,754,676],{"class":65},[52,756,71],{"class":65},[52,758,759],{"class":74}," SEQUENCES ",[52,761,78],{"class":65},[52,763,127],{"class":74},[10,765,767],{"id":766},"schema-level-access","Schema-level access",[15,769,770,771,774,775,777],{},"In Postgres, a user must have ",[19,772,773],{},"USAGE"," on the schema before accessing any\nobject within it — even if they have ",[19,776,676],{}," on the table.",[43,779,781],{"className":45,"code":780,"language":47,"meta":48,"style":48},"-- Department-level schema separation\nCREATE SCHEMA finance;\nCREATE SCHEMA hr;\n\nGRANT USAGE ON SCHEMA finance TO finance_role;\nGRANT SELECT ON ALL TABLES IN SCHEMA finance TO finance_role;\n-- finance_role cannot see the hr schema at all\n",[19,782,783,788,801,812,816,834,854],{"__ignoreMap":48},[52,784,785],{"class":54,"line":55},[52,786,787],{"class":58},"-- Department-level schema separation\n",[52,789,790,792,794,798],{"class":54,"line":62},[52,791,284],{"class":65},[52,793,201],{"class":65},[52,795,797],{"class":796},"sScJk"," finance",[52,799,800],{"class":74},";\n",[52,802,803,805,807,810],{"class":54,"line":84},[52,804,284],{"class":65},[52,806,201],{"class":65},[52,808,809],{"class":796}," hr",[52,811,800],{"class":74},[52,813,814],{"class":54,"line":91},[52,815,88],{"emptyLinePlaceholder":87},[52,817,818,820,822,824,826,829,831],{"class":54,"line":97},[52,819,36],{"class":65},[52,821,333],{"class":74},[52,823,248],{"class":65},[52,825,201],{"class":65},[52,827,828],{"class":74}," finance ",[52,830,78],{"class":65},[52,832,833],{"class":74}," finance_role;\n",[52,835,836,838,840,842,844,846,848,850,852],{"class":54,"line":130},[52,837,36],{"class":65},[52,839,68],{"class":65},[52,841,71],{"class":65},[52,843,195],{"class":74},[52,845,198],{"class":65},[52,847,201],{"class":65},[52,849,828],{"class":74},[52,851,78],{"class":65},[52,853,833],{"class":74},[52,855,856],{"class":54,"line":158},[52,857,858],{"class":58},"-- finance_role cannot see the hr schema at all\n",[10,860,862],{"id":861},"row-level-security-rls-multi-tenant-isolation","Row-level security (RLS) — multi-tenant isolation",[15,864,865],{},"For multi-tenant applications, RLS ensures tenants cannot see each other's\ndata even if the application has a query bug.",[43,867,869],{"className":45,"code":868,"language":47,"meta":48,"style":48},"-- Enable RLS on the orders table\nALTER TABLE orders ENABLE ROW LEVEL SECURITY;\n\n-- Policy: the app sets a config variable; the policy filters rows by it\nCREATE POLICY tenant_isolation ON orders\n    FOR ALL TO app_role\n    USING (tenant_id = current_setting('app.tenant_id')::INT);\n\n-- Application sets the tenant before every query\nSET app.tenant_id = '42';\nSELECT * FROM orders;  -- automatically returns only tenant 42's orders\n",[19,870,871,876,899,903,908,923,936,962,966,971,994],{"__ignoreMap":48},[52,872,873],{"class":54,"line":55},[52,874,875],{"class":58},"-- Enable RLS on the orders table\n",[52,877,878,880,883,885,888,891,894,897],{"class":54,"line":62},[52,879,368],{"class":65},[52,881,882],{"class":65}," TABLE",[52,884,75],{"class":74},[52,886,887],{"class":65},"ENABLE",[52,889,890],{"class":65}," ROW",[52,892,893],{"class":65}," LEVEL",[52,895,896],{"class":65}," SECURITY",[52,898,800],{"class":74},[52,900,901],{"class":54,"line":84},[52,902,88],{"emptyLinePlaceholder":87},[52,904,905],{"class":54,"line":91},[52,906,907],{"class":58},"-- Policy: the app sets a config variable; the policy filters rows by it\n",[52,909,910,912,915,918,920],{"class":54,"line":97},[52,911,284],{"class":65},[52,913,914],{"class":65}," POLICY",[52,916,917],{"class":74}," tenant_isolation ",[52,919,248],{"class":65},[52,921,922],{"class":74}," orders\n",[52,924,925,928,931,933],{"class":54,"line":130},[52,926,927],{"class":65},"    FOR",[52,929,930],{"class":74}," ALL ",[52,932,78],{"class":65},[52,934,935],{"class":74}," app_role\n",[52,937,938,941,944,947,950,953,956,959],{"class":54,"line":158},[52,939,940],{"class":65},"    USING",[52,942,943],{"class":74}," (tenant_id ",[52,945,946],{"class":65},"=",[52,948,949],{"class":74}," current_setting(",[52,951,952],{"class":609},"'app.tenant_id'",[52,954,955],{"class":74},")::",[52,957,958],{"class":65},"INT",[52,960,961],{"class":74},");\n",[52,963,964],{"class":54,"line":175},[52,965,88],{"emptyLinePlaceholder":87},[52,967,968],{"class":54,"line":180},[52,969,970],{"class":58},"-- Application sets the tenant before every query\n",[52,972,973,976,980,983,986,989,992],{"class":54,"line":186},[52,974,975],{"class":65},"SET",[52,977,979],{"class":978},"sj4cs"," app",[52,981,982],{"class":74},".",[52,984,985],{"class":978},"tenant_id",[52,987,988],{"class":65}," =",[52,990,991],{"class":609}," '42'",[52,993,800],{"class":74},[52,995,996,998,1001,1004,1007],{"class":54,"line":212},[52,997,676],{"class":65},[52,999,1000],{"class":65}," *",[52,1002,1003],{"class":65}," FROM",[52,1005,1006],{"class":74}," orders;  ",[52,1008,1009],{"class":58},"-- automatically returns only tenant 42's orders\n",[10,1011,1013],{"id":1012},"audit-inspect-current-permissions","Audit: inspect current permissions",[43,1015,1017],{"className":45,"code":1016,"language":47,"meta":48,"style":48},"-- Postgres: what can a role do on a table?\nSELECT grantee, privilege_type\nFROM   information_schema.role_table_grants\nWHERE  table_name = 'orders'\nORDER  BY grantee, privilege_type;\n\n-- psql shorthand\n-- \\dp orders           → show ACL for the orders table\n-- \\du api_service      → show role memberships and attributes\n\n-- MySQL\nSHOW GRANTS FOR 'api_service'@'%';\n\n-- SQL Server\nSELECT permission_name, state_desc\nFROM   sys.database_permissions dp\nJOIN   sys.database_principals  pr ON dp.grantee_principal_id = pr.principal_id\nWHERE  pr.name = 'api_service';\n",[19,1018,1019,1024,1031,1043,1055,1063,1067,1072,1077,1082,1086,1091,1110,1114,1119,1126,1141,1176],{"__ignoreMap":48},[52,1020,1021],{"class":54,"line":55},[52,1022,1023],{"class":58},"-- Postgres: what can a role do on a table?\n",[52,1025,1026,1028],{"class":54,"line":62},[52,1027,676],{"class":65},[52,1029,1030],{"class":74}," grantee, privilege_type\n",[52,1032,1033,1035,1038,1040],{"class":54,"line":84},[52,1034,235],{"class":65},[52,1036,1037],{"class":978},"   information_schema",[52,1039,982],{"class":74},[52,1041,1042],{"class":978},"role_table_grants\n",[52,1044,1045,1047,1050,1052],{"class":54,"line":91},[52,1046,21],{"class":65},[52,1048,1049],{"class":74},"  table_name ",[52,1051,946],{"class":65},[52,1053,1054],{"class":609}," 'orders'\n",[52,1056,1057,1060],{"class":54,"line":97},[52,1058,1059],{"class":65},"ORDER  BY",[52,1061,1062],{"class":74}," grantee, privilege_type;\n",[52,1064,1065],{"class":54,"line":130},[52,1066,88],{"emptyLinePlaceholder":87},[52,1068,1069],{"class":54,"line":158},[52,1070,1071],{"class":58},"-- psql shorthand\n",[52,1073,1074],{"class":54,"line":175},[52,1075,1076],{"class":58},"-- \\dp orders           → show ACL for the orders table\n",[52,1078,1079],{"class":54,"line":180},[52,1080,1081],{"class":58},"-- \\du api_service      → show role memberships and attributes\n",[52,1083,1084],{"class":54,"line":186},[52,1085,88],{"emptyLinePlaceholder":87},[52,1087,1088],{"class":54,"line":212},[52,1089,1090],{"class":58},"-- MySQL\n",[52,1092,1093,1096,1099,1102,1105,1108],{"class":54,"line":217},[52,1094,1095],{"class":74},"SHOW GRANTS ",[52,1097,1098],{"class":65},"FOR",[52,1100,1101],{"class":609}," 'api_service'",[52,1103,1104],{"class":74},"@",[52,1106,1107],{"class":609},"'%'",[52,1109,800],{"class":74},[52,1111,1112],{"class":54,"line":223},[52,1113,88],{"emptyLinePlaceholder":87},[52,1115,1116],{"class":54,"line":240},[52,1117,1118],{"class":58},"-- SQL Server\n",[52,1120,1121,1123],{"class":54,"line":455},[52,1122,676],{"class":65},[52,1124,1125],{"class":74}," permission_name, state_desc\n",[52,1127,1128,1130,1133,1135,1138],{"class":54,"line":483},[52,1129,235],{"class":65},[52,1131,1132],{"class":978},"   sys",[52,1134,982],{"class":74},[52,1136,1137],{"class":978},"database_permissions",[52,1139,1140],{"class":74}," dp\n",[52,1142,1143,1146,1148,1150,1153,1156,1158,1161,1163,1166,1168,1171,1173],{"class":54,"line":511},[52,1144,1145],{"class":65},"JOIN",[52,1147,1132],{"class":978},[52,1149,982],{"class":74},[52,1151,1152],{"class":978},"database_principals",[52,1154,1155],{"class":74},"  pr ",[52,1157,248],{"class":65},[52,1159,1160],{"class":978}," dp",[52,1162,982],{"class":74},[52,1164,1165],{"class":978},"grantee_principal_id",[52,1167,988],{"class":65},[52,1169,1170],{"class":978}," pr",[52,1172,982],{"class":74},[52,1174,1175],{"class":978},"principal_id\n",[52,1177,1178,1180,1183,1185,1188,1190,1192],{"class":54,"line":531},[52,1179,21],{"class":65},[52,1181,1182],{"class":978},"  pr",[52,1184,982],{"class":74},[52,1186,1187],{"class":978},"name",[52,1189,988],{"class":65},[52,1191,1101],{"class":609},[52,1193,800],{"class":74},[10,1195,1197],{"id":1196},"never-use-a-superuser-for-application-connections","Never use a superuser for application connections",[43,1199,1201],{"className":45,"code":1200,"language":47,"meta":48,"style":48},"-- BAD: application connects as postgres \u002F root \u002F sa\n-- postgresql:\u002F\u002Fpostgres:password@localhost\u002Fmyapp\n-- A SQL injection in any query can now DROP TABLE, read pg_shadow, run shell commands\n\n-- GOOD: a minimal-privilege role with only what the app needs\n-- postgresql:\u002F\u002Fapi_service:password@localhost\u002Fmyapp\n\n-- Verify the connection user\nSELECT current_user, session_user;\n-- Should return: api_service | api_service (not postgres)\n",[19,1202,1203,1208,1213,1218,1222,1227,1232,1236,1241,1248],{"__ignoreMap":48},[52,1204,1205],{"class":54,"line":55},[52,1206,1207],{"class":58},"-- BAD: application connects as postgres \u002F root \u002F sa\n",[52,1209,1210],{"class":54,"line":62},[52,1211,1212],{"class":58},"-- postgresql:\u002F\u002Fpostgres:password@localhost\u002Fmyapp\n",[52,1214,1215],{"class":54,"line":84},[52,1216,1217],{"class":58},"-- A SQL injection in any query can now DROP TABLE, read pg_shadow, run shell commands\n",[52,1219,1220],{"class":54,"line":91},[52,1221,88],{"emptyLinePlaceholder":87},[52,1223,1224],{"class":54,"line":97},[52,1225,1226],{"class":58},"-- GOOD: a minimal-privilege role with only what the app needs\n",[52,1228,1229],{"class":54,"line":130},[52,1230,1231],{"class":58},"-- postgresql:\u002F\u002Fapi_service:password@localhost\u002Fmyapp\n",[52,1233,1234],{"class":54,"line":158},[52,1235,88],{"emptyLinePlaceholder":87},[52,1237,1238],{"class":54,"line":175},[52,1239,1240],{"class":58},"-- Verify the connection user\n",[52,1242,1243,1245],{"class":54,"line":180},[52,1244,676],{"class":65},[52,1246,1247],{"class":74}," current_user, session_user;\n",[52,1249,1250],{"class":54,"line":186},[52,1251,1252],{"class":58},"-- Should return: api_service | api_service (not postgres)\n",[10,1254,1256],{"id":1255},"recap","Recap",[15,1258,1259,1260,1262,1263,1265],{},"Grant privileges to roles, not users — users inherit them by role membership.\nDefine ",[19,1261,672],{}," so future tables are automatically covered.\nRequire ",[19,1264,773],{}," on schemas as an additional access layer. Use RLS for\nmulti-tenant isolation at the database level. Never use a superuser for\napplication connections — a least-privilege service account limits the blast\nradius of both bugs and SQL injection attacks.",[1267,1268,1269],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":48,"searchDepth":62,"depth":62,"links":1271},[1272,1273,1274,1275,1276,1277,1278,1279,1280],{"id":12,"depth":62,"text":13},{"id":30,"depth":62,"text":31},{"id":268,"depth":62,"text":269},{"id":665,"depth":62,"text":666},{"id":766,"depth":62,"text":767},{"id":861,"depth":62,"text":862},{"id":1012,"depth":62,"text":1013},{"id":1196,"depth":62,"text":1197},{"id":1255,"depth":62,"text":1256},"SQL access control explained — GRANT, REVOKE, roles, least privilege, row-level security, schema permissions, and securing a production database connection.","medium","md","SQL",{},"\u002Fblog\u002Fsql-permissions-roles-grants","\u002Fsql\u002Fsecurity\u002Fpermissions",{"title":5,"description":1281},"blog\u002Fsql-permissions-roles-grants","Permissions & Roles","Security & Integrity","security","2026-06-20","8LZAD6gvwd0R6o2E90JaRmXzfKghlsNzVAwM_gcsV_o",1782244088237]