fix: Make OPTIONS consider view instead of triggers (#1824)
This commit is contained in:
@@ -358,11 +358,18 @@ handleInfo identifier RequestContext{..} =
|
|||||||
allOrigins = ("Access-Control-Allow-Origin", "*")
|
allOrigins = ("Access-Control-Allow-Origin", "*")
|
||||||
allowH table =
|
allowH table =
|
||||||
( HTTP.hAllow
|
( HTTP.hAllow
|
||||||
, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET"
|
, BS8.intercalate "," $
|
||||||
|
["OPTIONS,GET,HEAD"]
|
||||||
|
++ ["POST" | tableInsertable table]
|
||||||
|
++ ["PUT" | tableInsertable table && tableUpdatable table && hasPK]
|
||||||
|
++ ["PATCH" | tableUpdatable table]
|
||||||
|
++ ["DELETE" | tableDeletable table]
|
||||||
)
|
)
|
||||||
tableMatches table =
|
tableMatches table =
|
||||||
tableName table == qiName identifier
|
tableName table == qiName identifier
|
||||||
&& tableSchema table == qiSchema identifier
|
&& tableSchema table == qiSchema identifier
|
||||||
|
hasPK =
|
||||||
|
not $ null $ tablePKCols ctxDbStructure (qiSchema identifier) (qiName identifier)
|
||||||
|
|
||||||
handleInvoke :: InvokeMethod -> ProcDescription -> RequestContext -> DbHandler Wai.Response
|
handleInvoke :: InvokeMethod -> ProcDescription -> RequestContext -> DbHandler Wai.Response
|
||||||
handleInvoke invMethod proc context@RequestContext{..} = do
|
handleInvoke invMethod proc context@RequestContext{..} = do
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ decodeTables =
|
|||||||
<*> column HD.text
|
<*> column HD.text
|
||||||
<*> nullableColumn HD.text
|
<*> nullableColumn HD.text
|
||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
|
<*> column HD.bool
|
||||||
|
<*> column HD.bool
|
||||||
|
|
||||||
decodeColumns :: [Table] -> HD.Result [Column]
|
decodeColumns :: [Table] -> HD.Result [Column]
|
||||||
decodeColumns tables =
|
decodeColumns tables =
|
||||||
@@ -329,22 +331,40 @@ accessibleTables =
|
|||||||
relname as table_name,
|
relname as table_name,
|
||||||
d.description as table_description,
|
d.description as table_description,
|
||||||
(
|
(
|
||||||
c.relkind in ('r', 'v', 'f')
|
c.relkind IN ('r', 'v','f')
|
||||||
and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
|
||||||
-- The function `pg_relation_is_updateable` returns a bitmask where 8
|
OR EXISTS (
|
||||||
-- corresponds to `1 << CMD_INSERT` in the PostgreSQL source code, i.e.
|
SELECT 1
|
||||||
-- it's possible to insert into the relation.
|
FROM pg_trigger
|
||||||
or (exists (
|
WHERE
|
||||||
select 1
|
|
||||||
from pg_trigger
|
|
||||||
where
|
|
||||||
pg_trigger.tgrelid = c.oid
|
pg_trigger.tgrelid = c.oid
|
||||||
and (pg_trigger.tgtype::integer & 69) = 69)
|
AND (pg_trigger.tgtype::integer & 69) = 69
|
||||||
-- The trigger type `tgtype` is a bitmask where 69 corresponds to
|
|
||||||
-- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_INSERT
|
|
||||||
-- in the PostgreSQL source code.
|
|
||||||
)
|
)
|
||||||
) as insertable
|
) AS insertable,
|
||||||
|
(
|
||||||
|
c.relkind IN ('r', 'v','f')
|
||||||
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 4) = 4
|
||||||
|
-- CMD_UPDATE
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_trigger
|
||||||
|
WHERE
|
||||||
|
pg_trigger.tgrelid = c.oid
|
||||||
|
and (pg_trigger.tgtype::integer & 81) = 81
|
||||||
|
)
|
||||||
|
) as updatable,
|
||||||
|
(
|
||||||
|
c.relkind IN ('r', 'v','f')
|
||||||
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 16) = 16
|
||||||
|
-- CMD_DELETE
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_trigger
|
||||||
|
WHERE
|
||||||
|
pg_trigger.tgrelid = c.oid
|
||||||
|
and (pg_trigger.tgtype::integer & 73) = 73
|
||||||
|
)
|
||||||
|
) as deletable
|
||||||
from
|
from
|
||||||
pg_class c
|
pg_class c
|
||||||
join pg_namespace n on n.oid = c.relnamespace
|
join pg_namespace n on n.oid = c.relnamespace
|
||||||
@@ -470,21 +490,61 @@ allTables =
|
|||||||
c.relname AS table_name,
|
c.relname AS table_name,
|
||||||
NULL AS table_description,
|
NULL AS table_description,
|
||||||
(
|
(
|
||||||
c.relkind IN ('r', 'v','f')
|
c.relkind = 'r'
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
|
OR (
|
||||||
OR EXISTS (
|
c.relkind in ('v','f')
|
||||||
SELECT 1
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
|
||||||
FROM pg_trigger
|
-- The function `pg_relation_is_updateable` returns a bitmask where 8
|
||||||
WHERE
|
-- corresponds to `1 << CMD_INSERT` in the PostgreSQL source code, i.e.
|
||||||
pg_trigger.tgrelid = c.oid
|
-- it's possible to insert into the relation.
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_trigger
|
||||||
|
WHERE
|
||||||
|
pg_trigger.tgrelid = c.oid
|
||||||
AND (pg_trigger.tgtype::integer & 69) = 69
|
AND (pg_trigger.tgtype::integer & 69) = 69
|
||||||
|
-- The trigger type `tgtype` is a bitmask where 69 corresponds to
|
||||||
|
-- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_INSERT
|
||||||
|
-- in the PostgreSQL source code.
|
||||||
|
)
|
||||||
)
|
)
|
||||||
) AS insertable
|
) AS insertable,
|
||||||
|
(
|
||||||
|
c.relkind = 'r'
|
||||||
|
OR (
|
||||||
|
c.relkind in ('v','f')
|
||||||
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 4) = 4
|
||||||
|
-- CMD_UPDATE
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_trigger
|
||||||
|
WHERE
|
||||||
|
pg_trigger.tgrelid = c.oid
|
||||||
|
and (pg_trigger.tgtype::integer & 81) = 81
|
||||||
|
-- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_UPDATE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) AS updatable,
|
||||||
|
(
|
||||||
|
c.relkind = 'r'
|
||||||
|
OR (
|
||||||
|
c.relkind in ('v','f')
|
||||||
|
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 16) = 16
|
||||||
|
-- CMD_DELETE
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_trigger
|
||||||
|
WHERE
|
||||||
|
pg_trigger.tgrelid = c.oid
|
||||||
|
and (pg_trigger.tgtype::integer & 73) = 73
|
||||||
|
-- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_DELETE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) AS deletable
|
||||||
FROM pg_class c
|
FROM pg_class c
|
||||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
WHERE c.relkind IN ('v','r','m','f')
|
WHERE c.relkind IN ('v','r','m','f')
|
||||||
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
||||||
GROUP BY table_schema, table_name, insertable
|
|
||||||
ORDER BY table_schema, table_name |]
|
ORDER BY table_schema, table_name |]
|
||||||
|
|
||||||
allColumns :: [Table] -> Bool -> H.Statement [Schema] [Column]
|
allColumns :: [Table] -> Bool -> H.Statement [Schema] [Column]
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ data Table = Table
|
|||||||
{ tableSchema :: Schema
|
{ tableSchema :: Schema
|
||||||
, tableName :: TableName
|
, tableName :: TableName
|
||||||
, tableDescription :: Maybe Text
|
, tableDescription :: Maybe Text
|
||||||
|
-- The following fields identify what can be done on the table/view, they're not related to the privileges granted to it
|
||||||
, tableInsertable :: Bool
|
, tableInsertable :: Bool
|
||||||
|
, tableUpdatable :: Bool
|
||||||
|
, tableDeletable :: Bool
|
||||||
}
|
}
|
||||||
deriving (Show, Ord, Generic, JSON.ToJSON)
|
deriving (Show, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ makeRowFilters :: Text -> [Column] -> [(Text, Param)]
|
|||||||
makeRowFilters tn = fmap (makeRowFilter tn)
|
makeRowFilters tn = fmap (makeRowFilter tn)
|
||||||
|
|
||||||
makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
|
makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
|
||||||
makePathItem (t, cs, _) = ("/" ++ T.unpack tn, p $ tableInsertable t)
|
makePathItem (t, cs, _) = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t || tableDeletable t)
|
||||||
where
|
where
|
||||||
-- Use first line of table description as summary; rest as description (if present)
|
-- Use first line of table description as summary; rest as description (if present)
|
||||||
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
||||||
|
|||||||
+55
-10
@@ -12,15 +12,60 @@ import SpecHelper
|
|||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
spec :: SpecWith ((), Application)
|
||||||
spec = describe "Allow header" $ do
|
spec = describe "Allow header" $ do
|
||||||
it "includes read/write verbs for writeable table" $ do
|
context "a table" $ do
|
||||||
r <- request methodOptions "/items" [] ""
|
it "includes read/write verbs for writeable table" $ do
|
||||||
liftIO $
|
r <- request methodOptions "/items" [] ""
|
||||||
simpleHeaders r `shouldSatisfy`
|
liftIO $
|
||||||
matchHeader "Allow" "GET,POST,PATCH,DELETE"
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
|
||||||
|
|
||||||
it "includes read verbs for read-only table" $ do
|
context "a view" $ do
|
||||||
r <- request methodOptions "/has_count_column" [] ""
|
context "auto updatable" $ do
|
||||||
liftIO $
|
it "includes read/write verbs for auto updatable views with pk" $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
r <- request methodOptions "/projects_auto_updatable_view_with_pk" [] ""
|
||||||
matchHeader "Allow" "GET"
|
liftIO $
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
|
||||||
|
|
||||||
|
it "includes read/write verbs for auto updatable views without pk" $ do
|
||||||
|
r <- request methodOptions "/projects_auto_updatable_view_without_pk" [] ""
|
||||||
|
liftIO $
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PATCH,DELETE"
|
||||||
|
|
||||||
|
context "non auto updatable" $ do
|
||||||
|
it "includes read verbs for non auto updatable views" $ do
|
||||||
|
r <- request methodOptions "/projects_view_without_triggers" [] ""
|
||||||
|
liftIO $
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Allow" "OPTIONS,GET,HEAD"
|
||||||
|
|
||||||
|
it "includes read/write verbs for insertable, updatable and deletable views with pk" $ do
|
||||||
|
r <- request methodOptions "/projects_view_with_all_triggers_with_pk" [] ""
|
||||||
|
liftIO $
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
|
||||||
|
|
||||||
|
it "includes read/write verbs for insertable, updatable and deletable views without pk" $ do
|
||||||
|
r <- request methodOptions "/projects_view_with_all_triggers_without_pk" [] ""
|
||||||
|
liftIO $
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PATCH,DELETE"
|
||||||
|
|
||||||
|
it "includes read and insert verbs for insertable views" $ do
|
||||||
|
r <- request methodOptions "/projects_view_with_insert_trigger" [] ""
|
||||||
|
liftIO $
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Allow" "OPTIONS,GET,HEAD,POST"
|
||||||
|
|
||||||
|
it "includes read and update verbs for updatable views" $ do
|
||||||
|
r <- request methodOptions "/projects_view_with_update_trigger" [] ""
|
||||||
|
liftIO $
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Allow" "OPTIONS,GET,HEAD,PATCH"
|
||||||
|
|
||||||
|
it "includes read and delete verbs for deletable views" $ do
|
||||||
|
r <- request methodOptions "/projects_view_with_delete_trigger" [] ""
|
||||||
|
liftIO $
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Allow" "OPTIONS,GET,HEAD,DELETE"
|
||||||
|
|||||||
Vendored
+67
@@ -1544,6 +1544,73 @@ $$A materialized view for projects
|
|||||||
|
|
||||||
Just a test for materialized views$$;
|
Just a test for materialized views$$;
|
||||||
|
|
||||||
|
-- Tests for updatable, insertable and deletable views
|
||||||
|
create view test.projects_auto_updatable_view_with_pk as
|
||||||
|
select id, name, client_id from test.projects;
|
||||||
|
|
||||||
|
create view test.projects_auto_updatable_view_without_pk as
|
||||||
|
select name, client_id from test.projects;
|
||||||
|
|
||||||
|
create view test.projects_view_without_triggers as
|
||||||
|
select distinct id, name, client_id from test.projects;
|
||||||
|
|
||||||
|
create or replace function test.test_for_views_with_triggers() returns trigger as $$
|
||||||
|
begin
|
||||||
|
return null;
|
||||||
|
end;
|
||||||
|
$$ language plpgsql;
|
||||||
|
|
||||||
|
create view test.projects_view_with_all_triggers_with_pk as
|
||||||
|
select distinct id, name, client_id from test.projects;
|
||||||
|
|
||||||
|
create trigger projects_view_with_all_triggers_with_pk_insert
|
||||||
|
instead of insert on test.projects_view_with_all_triggers_with_pk
|
||||||
|
for each row execute procedure test_for_views_with_triggers();
|
||||||
|
|
||||||
|
create trigger projects_view_with_all_triggers_with_pk_update
|
||||||
|
instead of update on test.projects_view_with_all_triggers_with_pk
|
||||||
|
for each row execute procedure test_for_views_with_triggers();
|
||||||
|
|
||||||
|
create trigger projects_view_with_all_triggers_with_pk_delete
|
||||||
|
instead of delete on test.projects_view_with_all_triggers_with_pk
|
||||||
|
for each row execute procedure test_for_views_with_triggers();
|
||||||
|
|
||||||
|
create view test.projects_view_with_all_triggers_without_pk as
|
||||||
|
select distinct name, client_id from test.projects;
|
||||||
|
|
||||||
|
create trigger projects_view_with_all_triggers_without_pk_insert
|
||||||
|
instead of insert on test.projects_view_with_all_triggers_without_pk
|
||||||
|
for each row execute procedure test_for_views_with_triggers();
|
||||||
|
|
||||||
|
create trigger projects_view_with_all_triggers_without_pk_update
|
||||||
|
instead of update on test.projects_view_with_all_triggers_without_pk
|
||||||
|
for each row execute procedure test_for_views_with_triggers();
|
||||||
|
|
||||||
|
create trigger projects_view_with_all_triggers_without_pk_delete
|
||||||
|
instead of delete on test.projects_view_with_all_triggers_without_pk
|
||||||
|
for each row execute procedure test_for_views_with_triggers();
|
||||||
|
|
||||||
|
create view test.projects_view_with_insert_trigger as
|
||||||
|
select distinct id, name, client_id from test.projects;
|
||||||
|
|
||||||
|
create trigger projects_view_with_insert_trigger_insert
|
||||||
|
instead of insert on test.projects_view_with_insert_trigger
|
||||||
|
for each row execute procedure test_for_views_with_triggers();
|
||||||
|
|
||||||
|
create view test.projects_view_with_update_trigger as
|
||||||
|
select distinct id, name, client_id from test.projects;
|
||||||
|
|
||||||
|
create trigger projects_view_with_update_trigger_update
|
||||||
|
instead of update on test.projects_view_with_update_trigger
|
||||||
|
for each row execute procedure test_for_views_with_triggers();
|
||||||
|
|
||||||
|
create view test.projects_view_with_delete_trigger as
|
||||||
|
select distinct id, name, client_id from test.projects;
|
||||||
|
|
||||||
|
create trigger projects_view_with_delete_trigger_delete
|
||||||
|
instead of delete on test.projects_view_with_delete_trigger
|
||||||
|
for each row execute procedure test_for_views_with_triggers();
|
||||||
|
|
||||||
create or replace function test."quotedFunction"("user" text, "fullName" text, "SSN" text)
|
create or replace function test."quotedFunction"("user" text, "fullName" text, "SSN" text)
|
||||||
returns jsonb AS $$
|
returns jsonb AS $$
|
||||||
select format('{"user": "%s", "fullName": "%s", "SSN": "%s"}', "user", "fullName", "SSN")::jsonb;
|
select format('{"user": "%s", "fullName": "%s", "SSN": "%s"}', "user", "fullName", "SSN")::jsonb;
|
||||||
|
|||||||
Reference in New Issue
Block a user