Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4197d2f739 | ||
|
|
c10ba8e214 | ||
|
|
887948d259 | ||
|
|
c63786733a | ||
|
|
4fe696dd96 | ||
|
|
67936b343f | ||
|
|
b0e395f495 | ||
|
|
3b55a27ef3 | ||
|
|
43da81c30c |
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||
run: postgrest-coverage
|
||||
- name: Upload coverage to codecov
|
||||
uses: codecov/codecov-action@v3.1.1
|
||||
uses: codecov/codecov-action@v3.1.3
|
||||
with:
|
||||
files: ./coverage/codecov.json
|
||||
|
||||
|
||||
@@ -5,6 +5,15 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 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
|
||||
|
||||
### Added
|
||||
|
||||
@@ -38,11 +38,17 @@ let
|
||||
{
|
||||
name = "postgrest-run";
|
||||
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;
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
export PGRST_DB_ANON_ROLE
|
||||
|
||||
exec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
|
||||
postgrest "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
|
||||
+12
-5
@@ -15,11 +15,14 @@
|
||||
let
|
||||
withTmpDb =
|
||||
{ name, postgresql }:
|
||||
let commandName = "postgrest-with-${name}"; in
|
||||
let
|
||||
commandName = "postgrest-with-${name}";
|
||||
superuserRole = "postgres";
|
||||
in
|
||||
checkedShellScript
|
||||
{
|
||||
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 =
|
||||
[
|
||||
"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..."
|
||||
# We try to make the database cluster as independent as possible from the host
|
||||
# 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"
|
||||
|
||||
log "Starting the database cluster..."
|
||||
@@ -82,8 +86,11 @@ let
|
||||
}
|
||||
trap stop EXIT
|
||||
|
||||
log "Loading fixtures..."
|
||||
psql -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
|
||||
log "Creating a minimally privileged $PGUSER connection role..."
|
||||
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..."
|
||||
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
name: postgrest
|
||||
version: 11.0.0
|
||||
version: 11.0.1
|
||||
synopsis: REST API for any Postgres database
|
||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||
for tables, views, and functions, supporting all HTTP methods that security
|
||||
@@ -187,6 +187,7 @@ test-suite spec
|
||||
Feature.CorsSpec
|
||||
Feature.ExtraSearchPathSpec
|
||||
Feature.LegacyGucsSpec
|
||||
Feature.NoSuperuserSpec
|
||||
Feature.ObservabilitySpec
|
||||
Feature.OpenApi.DisabledOpenApiSpec
|
||||
Feature.OpenApi.IgnorePrivOpenApiSpec
|
||||
|
||||
@@ -63,7 +63,7 @@ parseToken AppConfig{..} token time = do
|
||||
liftEither . mapLeft jwtClaimsError $ JSON.toJSON <$> eitherClaims
|
||||
where
|
||||
validation =
|
||||
JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 1
|
||||
JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 30
|
||||
|
||||
audienceCheck :: JWT.StringOrURI -> Bool
|
||||
audienceCheck = maybe (const True) (==) configJwtAudience
|
||||
|
||||
@@ -40,7 +40,7 @@ import Text.InterpolatedString.Perl6 (q)
|
||||
|
||||
import PostgREST.Config.Database (pgVersionStatement)
|
||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||
pgVersion110)
|
||||
pgVersion110, pgVersion120)
|
||||
import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
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,
|
||||
-- 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));
|
||||
-- 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|
|
||||
WITH
|
||||
columns AS (
|
||||
@@ -549,6 +551,12 @@ tablesSqlQuery pgVer =
|
||||
ON t.typtype = 'd' AND t.typbasetype = bt.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)
|
||||
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
|
||||
NOT pg_is_other_temp_schema(nc.oid)
|
||||
AND a.attnum > 0
|
||||
@@ -697,17 +705,19 @@ tablesSqlQuery pgVer =
|
||||
"ORDER BY table_schema, table_name"
|
||||
where
|
||||
relIsPartition = if pgVer >= pgVersion100 then " AND not c.relispartition " else mempty
|
||||
-- detect default values on columns that have GENERATED .. AS IDENTITY
|
||||
columnDefault =
|
||||
if pgVer >= pgVersion100
|
||||
then [q|
|
||||
CASE
|
||||
WHEN nullif(a.attidentity, '') is null
|
||||
THEN pg_get_expr(ad.adbin, ad.adrelid)::text
|
||||
ELSE format('nextval(%s)', quote_literal(pg_get_serial_sequence(a.attrelid::regclass::text, a.attname::text)))
|
||||
END AS column_default,|]
|
||||
else "pg_get_expr(ad.adbin, ad.adrelid)::text AS column_default,"
|
||||
|
||||
columnDefault
|
||||
| pgVer >= pgVersion120 = [q|
|
||||
CASE
|
||||
WHEN a.attidentity = 'd' THEN format('nextval(%s)', quote_literal(seqsch.nspname || '.' || seqclass.relname))
|
||||
WHEN a.attgenerated = 's' THEN null
|
||||
ELSE pg_get_expr(ad.adbin, ad.adrelid)::text
|
||||
END AS column_default,|]
|
||||
| pgVer >= pgVersion100 = [q|
|
||||
CASE
|
||||
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
|
||||
allM2OandO2ORels :: PgVersion -> Bool -> SQL.Statement () [Relationship]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
set search_path to public;
|
||||
|
||||
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;
|
||||
|
||||
@@ -14,7 +14,7 @@ alter role postgrest_test_serializable set default_transaction_isolation = 'seri
|
||||
CREATE ROLE postgrest_test_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;
|
||||
GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
CREATE ROLE postgrest_test_anonymous;
|
||||
GRANT postgrest_test_anonymous TO :USER;
|
||||
GRANT postgrest_test_anonymous TO :PGUSER;
|
||||
CREATE SCHEMA test;
|
||||
|
||||
-- PUT+PATCH target needs one record and column to modify
|
||||
|
||||
@@ -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 }
|
||||
@@ -13,7 +13,8 @@ import Text.Heredoc
|
||||
|
||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||
pgVersion110, pgVersion112,
|
||||
pgVersion130)
|
||||
pgVersion120, pgVersion130,
|
||||
pgVersion140)
|
||||
|
||||
import Protolude hiding (get)
|
||||
import SpecHelper
|
||||
@@ -496,7 +497,7 @@ spec actualPgVersion = do
|
||||
}
|
||||
|
||||
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")]
|
||||
[json| { "slug": "foo" } |]
|
||||
`shouldRespondWith`
|
||||
@@ -505,6 +506,29 @@ spec actualPgVersion = do
|
||||
, 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
|
||||
request methodPost "/tbl_w_json" [("Prefer", "return=representation")]
|
||||
[json| { "data": { "a": 1, "a": 2 }, "id": 3 } |]
|
||||
|
||||
@@ -27,6 +27,7 @@ import qualified Feature.ConcurrentSpec
|
||||
import qualified Feature.CorsSpec
|
||||
import qualified Feature.ExtraSearchPathSpec
|
||||
import qualified Feature.LegacyGucsSpec
|
||||
import qualified Feature.NoSuperuserSpec
|
||||
import qualified Feature.ObservabilitySpec
|
||||
import qualified Feature.OpenApi.DisabledOpenApiSpec
|
||||
import qualified Feature.OpenApi.IgnorePrivOpenApiSpec
|
||||
@@ -149,6 +150,7 @@ main = do
|
||||
, ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec)
|
||||
, ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec)
|
||||
, ("Feature.Query.SpreadQueriesSpec" , Feature.Query.SpreadQueriesSpec.spec)
|
||||
, ("Feature.NoSuperuserSpec" , Feature.NoSuperuserSpec.spec)
|
||||
]
|
||||
|
||||
hspec $ do
|
||||
|
||||
@@ -211,7 +211,7 @@ testObservabilityCfg = baseCfg { configServerTraceHeader = Just $ mk "X-Request-
|
||||
|
||||
analyzeTable :: Text -> IO ()
|
||||
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 r = [rangeUnit, (hRange, renderByteRange r)]
|
||||
|
||||
Vendored
+1
-2
@@ -1,7 +1,6 @@
|
||||
\set AUTHENTICATOR current_user
|
||||
DROP ROLE IF EXISTS postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author;
|
||||
CREATE ROLE postgrest_test_anonymous;
|
||||
CREATE ROLE postgrest_test_default_role;
|
||||
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;
|
||||
|
||||
Vendored
+22
@@ -3122,3 +3122,25 @@ BEGIN
|
||||
END IF;
|
||||
END
|
||||
$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$;
|
||||
|
||||
Reference in New Issue
Block a user