Compare commits

..
9 Commits
Author SHA1 Message Date
steve-chavez 4197d2f739 bump version to 11.0.1 2023-04-27 21:30:40 -05:00
steve-chavez c10ba8e214 fix: skew of 30 seconds for JWT validation 2023-04-27 21:06:06 -05:00
steve-chavez 887948d259 fix: missing=default error msg on generated column 2023-04-27 19:27:09 -05:00
steve-chavez c63786733a fix: no schema privilege for schema cache
This was due to the usage of pg_get_serial_sequence
2023-04-27 19:27:09 -05:00
steve-chavez 4fe696dd96 nix: PGRST_DB_ANON_ROLE default for postgrest-run
Makes manually testing a feature easier
2023-04-27 19:27:09 -05:00
steve-chavez 67936b343f test: prove that authenticator is not a superuser 2023-04-27 19:27:09 -05:00
steve-chavez b0e395f495 nix: no SUPERUSER for connection role
Change :USER to :PGUSER in SQL scripts
2023-04-27 19:27:09 -05:00
dependabot[bot]andGitHub 3b55a27ef3 build(deps): bump codecov/codecov-action from 3.1.2 to 3.1.3 (#2765) 2023-04-24 11:21:49 -05:00
dependabot[bot]andGitHub 43da81c30c build(deps): bump codecov/codecov-action from 3.1.1 to 3.1.2 (#2757) 2023-04-18 12:25:50 -05:00
15 changed files with 129 additions and 29 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15) - name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
run: postgrest-coverage run: postgrest-coverage
- name: Upload coverage to codecov - name: Upload coverage to codecov
uses: codecov/codecov-action@v3.1.1 uses: codecov/codecov-action@v3.1.3
with: with:
files: ./coverage/codecov.json files: ./coverage/codecov.json
+9
View File
@@ -5,6 +5,15 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unreleased
## [11.0.1] - 2023-04-27
### Fixed
- #2762, Fixes "permission denied for schema" error during schema cache load - @steve-chavez
- #2756, Fix bad error message on generated columns when using `Prefer: missing=default` - @steve-chavez
- #1139, Allow a 30 second skew for JWT validation - @steve-chavez
+ It used to be 1 second, which was too strict
## [11.0.0] - 2023-04-16 ## [11.0.0] - 2023-04-16
### Added ### Added
+7 -1
View File
@@ -38,11 +38,17 @@ let
{ {
name = "postgrest-run"; name = "postgrest-run";
docs = "Run PostgREST after building it interactively with cabal-install"; docs = "Run PostgREST after building it interactively with cabal-install";
args = [ "ARG_LEFTOVERS([PostgREST arguments])" ]; args =
[
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])"
"ARG_LEFTOVERS([PostgREST arguments])"
];
inRootDir = true; inRootDir = true;
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
export PGRST_DB_ANON_ROLE
exec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \ exec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
postgrest "''${_arg_leftovers[@]}" postgrest "''${_arg_leftovers[@]}"
''; '';
+12 -5
View File
@@ -15,11 +15,14 @@
let let
withTmpDb = withTmpDb =
{ name, postgresql }: { name, postgresql }:
let commandName = "postgrest-with-${name}"; in let
commandName = "postgrest-with-${name}";
superuserRole = "postgres";
in
checkedShellScript checkedShellScript
{ {
name = commandName; name = commandName;
docs = "Run the given command in a temporary database with ${name}"; docs = "Run the given command in a temporary database with ${name}. If you wish to mutate the database, login with the '${superuserRole}' role.";
args = args =
[ [
"ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from], [test/spec/fixtures/load.sql])" "ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from], [test/spec/fixtures/load.sql])"
@@ -66,7 +69,8 @@ let
log "Initializing database cluster..." log "Initializing database cluster..."
# We try to make the database cluster as independent as possible from the host # We try to make the database cluster as independent as possible from the host
# by specifying the timezone, locale and encoding. # by specifying the timezone, locale and encoding.
PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "$PGUSER" --auth=trust \ # initdb -U creates a superuser(man initdb)
PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "${superuserRole}" --auth=trust \
>> "$setuplog" >> "$setuplog"
log "Starting the database cluster..." log "Starting the database cluster..."
@@ -82,8 +86,11 @@ let
} }
trap stop EXIT trap stop EXIT
log "Loading fixtures..." log "Creating a minimally privileged $PGUSER connection role..."
psql -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog" createuser "$PGUSER" -U "${superuserRole}" --host="$tmpdir/socket" --no-createdb --no-inherit --no-superuser --no-createrole --no-replication --login
log "Loading fixtures under the ${superuserRole} role..."
psql -U "${superuserRole}" -v PGUSER="$PGUSER" -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
log "Done. Running command..." log "Done. Running command..."
+2 -1
View File
@@ -1,5 +1,5 @@
name: postgrest name: postgrest
version: 11.0.0 version: 11.0.1
synopsis: REST API for any Postgres database synopsis: REST API for any Postgres database
description: Reads the schema of a PostgreSQL database and creates RESTful routes description: Reads the schema of a PostgreSQL database and creates RESTful routes
for tables, views, and functions, supporting all HTTP methods that security for tables, views, and functions, supporting all HTTP methods that security
@@ -187,6 +187,7 @@ test-suite spec
Feature.CorsSpec Feature.CorsSpec
Feature.ExtraSearchPathSpec Feature.ExtraSearchPathSpec
Feature.LegacyGucsSpec Feature.LegacyGucsSpec
Feature.NoSuperuserSpec
Feature.ObservabilitySpec Feature.ObservabilitySpec
Feature.OpenApi.DisabledOpenApiSpec Feature.OpenApi.DisabledOpenApiSpec
Feature.OpenApi.IgnorePrivOpenApiSpec Feature.OpenApi.IgnorePrivOpenApiSpec
+1 -1
View File
@@ -63,7 +63,7 @@ parseToken AppConfig{..} token time = do
liftEither . mapLeft jwtClaimsError $ JSON.toJSON <$> eitherClaims liftEither . mapLeft jwtClaimsError $ JSON.toJSON <$> eitherClaims
where where
validation = validation =
JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 1 JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 30
audienceCheck :: JWT.StringOrURI -> Bool audienceCheck :: JWT.StringOrURI -> Bool
audienceCheck = maybe (const True) (==) configJwtAudience audienceCheck = maybe (const True) (==) configJwtAudience
+22 -12
View File
@@ -40,7 +40,7 @@ import Text.InterpolatedString.Perl6 (q)
import PostgREST.Config.Database (pgVersionStatement) import PostgREST.Config.Database (pgVersionStatement)
import PostgREST.Config.PgVersion (PgVersion, pgVersion100, import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
pgVersion110) pgVersion110, pgVersion120)
import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName, import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName,
QualifiedIdentifier (..), QualifiedIdentifier (..),
Schema) Schema)
@@ -506,6 +506,8 @@ tablesSqlQuery pgVer =
-- the tbl_constraints/key_col_usage CTEs are based on the standard "information_schema.table_constraints"/"information_schema.key_column_usage" views, -- the tbl_constraints/key_col_usage CTEs are based on the standard "information_schema.table_constraints"/"information_schema.key_column_usage" views,
-- we cannot use those directly as they include the following privilege filter: -- we cannot use those directly as they include the following privilege filter:
-- (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text)); -- (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text));
-- on the "columns" CTE, left joining on pg_depend and pg_class is used to obtain the sequence name as a column default in case there are GENERATED .. AS IDENTITY,
-- generated columns are only available from pg >= 10 but the query is agnostic to versions. dep.deptype = 'i' is done because there are other 'a' dependencies on PKs
[q| [q|
WITH WITH
columns AS ( columns AS (
@@ -549,6 +551,12 @@ tablesSqlQuery pgVer =
ON t.typtype = 'd' AND t.typbasetype = bt.oid ON t.typtype = 'd' AND t.typbasetype = bt.oid
LEFT JOIN (pg_collation co JOIN pg_namespace nco ON co.collnamespace = nco.oid) LEFT JOIN (pg_collation co JOIN pg_namespace nco ON co.collnamespace = nco.oid)
ON a.attcollation = co.oid AND (nco.nspname <> 'pg_catalog'::name OR co.collname <> 'default'::name) ON a.attcollation = co.oid AND (nco.nspname <> 'pg_catalog'::name OR co.collname <> 'default'::name)
LEFT JOIN pg_depend dep
ON dep.refobjid = a.attrelid and dep.refobjsubid = a.attnum and dep.deptype = 'i'
LEFT JOIN pg_class seqclass
ON seqclass.oid = dep.objid
LEFT JOIN pg_namespace seqsch
ON seqsch.oid = seqclass.relnamespace
WHERE WHERE
NOT pg_is_other_temp_schema(nc.oid) NOT pg_is_other_temp_schema(nc.oid)
AND a.attnum > 0 AND a.attnum > 0
@@ -697,17 +705,19 @@ tablesSqlQuery pgVer =
"ORDER BY table_schema, table_name" "ORDER BY table_schema, table_name"
where where
relIsPartition = if pgVer >= pgVersion100 then " AND not c.relispartition " else mempty relIsPartition = if pgVer >= pgVersion100 then " AND not c.relispartition " else mempty
-- detect default values on columns that have GENERATED .. AS IDENTITY columnDefault
columnDefault = | pgVer >= pgVersion120 = [q|
if pgVer >= pgVersion100 CASE
then [q| WHEN a.attidentity = 'd' THEN format('nextval(%s)', quote_literal(seqsch.nspname || '.' || seqclass.relname))
CASE WHEN a.attgenerated = 's' THEN null
WHEN nullif(a.attidentity, '') is null ELSE pg_get_expr(ad.adbin, ad.adrelid)::text
THEN pg_get_expr(ad.adbin, ad.adrelid)::text END AS column_default,|]
ELSE format('nextval(%s)', quote_literal(pg_get_serial_sequence(a.attrelid::regclass::text, a.attname::text))) | pgVer >= pgVersion100 = [q|
END AS column_default,|] CASE
else "pg_get_expr(ad.adbin, ad.adrelid)::text AS column_default," WHEN a.attidentity = 'd' THEN format('nextval(%s)', quote_literal(seqsch.nspname || '.' || seqclass.relname))
ELSE pg_get_expr(ad.adbin, ad.adrelid)::text
END AS column_default,|]
| otherwise = "pg_get_expr(ad.adbin, ad.adrelid)::text as column_default,"
-- | Gets many-to-one relationships and one-to-one(O2O) relationships, which are a refinement of the many-to-one's -- | Gets many-to-one relationships and one-to-one(O2O) relationships, which are a refinement of the many-to-one's
allM2OandO2ORels :: PgVersion -> Bool -> SQL.Statement () [Relationship] allM2OandO2ORels :: PgVersion -> Bool -> SQL.Statement () [Relationship]
+2 -2
View File
@@ -4,7 +4,7 @@
set search_path to public; set search_path to public;
CREATE ROLE postgrest_test_anonymous; CREATE ROLE postgrest_test_anonymous;
ALTER ROLE :USER SET pgrst.db_anon_role = 'postgrest_test_anonymous'; ALTER ROLE :PGUSER SET pgrst.db_anon_role = 'postgrest_test_anonymous';
CREATE ROLE postgrest_test_author; CREATE ROLE postgrest_test_author;
@@ -14,7 +14,7 @@ alter role postgrest_test_serializable set default_transaction_isolation = 'seri
CREATE ROLE postgrest_test_repeatable_read; CREATE ROLE postgrest_test_repeatable_read;
alter role postgrest_test_repeatable_read set default_transaction_isolation = 'REPEATABLE READ'; alter role postgrest_test_repeatable_read set default_transaction_isolation = 'REPEATABLE READ';
GRANT postgrest_test_anonymous, postgrest_test_author, postgrest_test_serializable, postgrest_test_repeatable_read TO :USER; GRANT postgrest_test_anonymous, postgrest_test_author, postgrest_test_serializable, postgrest_test_repeatable_read TO :PGUSER;
CREATE SCHEMA v1; CREATE SCHEMA v1;
GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous; GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous;
+1 -1
View File
@@ -1,5 +1,5 @@
CREATE ROLE postgrest_test_anonymous; CREATE ROLE postgrest_test_anonymous;
GRANT postgrest_test_anonymous TO :USER; GRANT postgrest_test_anonymous TO :PGUSER;
CREATE SCHEMA test; CREATE SCHEMA test;
-- PUT+PATCH target needs one record and column to modify -- PUT+PATCH target needs one record and column to modify
+20
View File
@@ -0,0 +1,20 @@
module Feature.NoSuperuserSpec where
import Network.Wai (Application)
import Network.HTTP.Types
import Test.Hspec
import Test.Hspec.Wai
import Protolude
spec :: SpecWith ((), Application)
spec =
describe "No Superuser" $ do
it "proves that the authenticator role is not a superuser" $ do
request methodGet "/rpc/is_superuser"
mempty
""
`shouldRespondWith`
"false"
{ matchStatus = 200 }
+26 -2
View File
@@ -13,7 +13,8 @@ import Text.Heredoc
import PostgREST.Config.PgVersion (PgVersion, pgVersion100, import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
pgVersion110, pgVersion112, pgVersion110, pgVersion112,
pgVersion130) pgVersion120, pgVersion130,
pgVersion140)
import Protolude hiding (get) import Protolude hiding (get)
import SpecHelper import SpecHelper
@@ -496,7 +497,7 @@ spec actualPgVersion = do
} }
when (actualPgVersion >= pgVersion100) $ when (actualPgVersion >= pgVersion100) $
it "inserts a default on a generated by default as identity column" $ do it "inserts a default on a generated by default as identity column" $
request methodPost "/channels?columns=id,data,slug&select=data,slug" [("Prefer", "return=representation"), ("Prefer", "missing=default")] request methodPost "/channels?columns=id,data,slug&select=data,slug" [("Prefer", "return=representation"), ("Prefer", "missing=default")]
[json| { "slug": "foo" } |] [json| { "slug": "foo" } |]
`shouldRespondWith` `shouldRespondWith`
@@ -505,6 +506,29 @@ spec actualPgVersion = do
, matchHeaders = ["Preference-Applied" <:> "missing=default"] , matchHeaders = ["Preference-Applied" <:> "missing=default"]
} }
when (actualPgVersion >= pgVersion120) $
it "fails with a good error message on generated always columns" $
request methodPost "/foo?columns=a,b" [("Prefer", "return=representation"), ("Prefer", "missing=default")]
[json| [
{"a": "val"},
{"a": "val", "b": "val"}
]|]
`shouldRespondWith`
(if actualPgVersion < pgVersion140
then [json| {
"code": "42601",
"details": "Column \"b\" is a generated column.",
"hint": null,
"message": "cannot insert into column \"b\""
}|]
else [json| {
"code": "428C9",
"details": "Column \"b\" is a generated column.",
"hint": null,
"message": "cannot insert a non-DEFAULT value into column \"b\""
}|])
{ matchStatus = 400 }
it "inserts json that has duplicate keys" $ do it "inserts json that has duplicate keys" $ do
request methodPost "/tbl_w_json" [("Prefer", "return=representation")] request methodPost "/tbl_w_json" [("Prefer", "return=representation")]
[json| { "data": { "a": 1, "a": 2 }, "id": 3 } |] [json| { "data": { "a": 1, "a": 2 }, "id": 3 } |]
+2
View File
@@ -27,6 +27,7 @@ import qualified Feature.ConcurrentSpec
import qualified Feature.CorsSpec import qualified Feature.CorsSpec
import qualified Feature.ExtraSearchPathSpec import qualified Feature.ExtraSearchPathSpec
import qualified Feature.LegacyGucsSpec import qualified Feature.LegacyGucsSpec
import qualified Feature.NoSuperuserSpec
import qualified Feature.ObservabilitySpec import qualified Feature.ObservabilitySpec
import qualified Feature.OpenApi.DisabledOpenApiSpec import qualified Feature.OpenApi.DisabledOpenApiSpec
import qualified Feature.OpenApi.IgnorePrivOpenApiSpec import qualified Feature.OpenApi.IgnorePrivOpenApiSpec
@@ -149,6 +150,7 @@ main = do
, ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec) , ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec)
, ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec) , ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec)
, ("Feature.Query.SpreadQueriesSpec" , Feature.Query.SpreadQueriesSpec.spec) , ("Feature.Query.SpreadQueriesSpec" , Feature.Query.SpreadQueriesSpec.spec)
, ("Feature.NoSuperuserSpec" , Feature.NoSuperuserSpec.spec)
] ]
hspec $ do hspec $ do
+1 -1
View File
@@ -211,7 +211,7 @@ testObservabilityCfg = baseCfg { configServerTraceHeader = Just $ mk "X-Request-
analyzeTable :: Text -> IO () analyzeTable :: Text -> IO ()
analyzeTable tableName = analyzeTable tableName =
void $ readProcess "psql" ["--set", "ON_ERROR_STOP=1", "-a", "-c", toS $ "ANALYZE test.\"" <> tableName <> "\""] [] void $ readProcess "psql" ["-U", "postgres", "--set", "ON_ERROR_STOP=1", "-a", "-c", toS $ "ANALYZE test.\"" <> tableName <> "\""] []
rangeHdrs :: ByteRange -> [Header] rangeHdrs :: ByteRange -> [Header]
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)] rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
+1 -2
View File
@@ -1,7 +1,6 @@
\set AUTHENTICATOR current_user
DROP ROLE IF EXISTS postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author; DROP ROLE IF EXISTS postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author;
CREATE ROLE postgrest_test_anonymous; CREATE ROLE postgrest_test_anonymous;
CREATE ROLE postgrest_test_default_role; CREATE ROLE postgrest_test_default_role;
CREATE ROLE postgrest_test_author; CREATE ROLE postgrest_test_author;
GRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author TO :USER; GRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author TO :PGUSER;
+22
View File
@@ -3122,3 +3122,25 @@ BEGIN
END IF; END IF;
END END
$do$; $do$;
CREATE FUNCTION test.is_superuser() RETURNS boolean
LANGUAGE sql
AS $$
select current_setting('is_superuser')::boolean;
$$;
DO $do$
BEGIN
IF current_setting('server_version_num')::INT >= 120000 THEN
CREATE TABLE test.foo (
a text,
b text GENERATED ALWAYS AS (
case WHEN a = 'telegram' THEN 'im'
WHEN a = 'proton' THEN 'email'
WHEN a = 'infinity' THEN 'idea'
ELSE 'bad idea'
end) stored
);
END IF;
END
$do$;