Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd304c9fc3 | ||
|
|
3989aaa144 | ||
|
|
1d51a5f543 | ||
|
|
e4dafad64d | ||
|
|
3f31c60f1d | ||
|
|
83f48dcd15 | ||
|
|
1cc53245c5 | ||
|
|
f24ba048af | ||
|
|
a980db6d2d | ||
|
|
4277284a69 |
@@ -3,6 +3,15 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [0.2.10.0] - 2015-06-03
|
||||
### Added
|
||||
- Full text search, eg `/foo?text_vector=@@.bar`
|
||||
- Include auth id as well as db role to views (for row-level security)
|
||||
|
||||
## [0.2.9.1] - 2015-05-20
|
||||
### Fixed
|
||||
- Put -Werror behind a cabal flag (for CI) so Hackage accepts package
|
||||
|
||||
## [0.2.9.0] - 2015-05-20
|
||||
### Added
|
||||
- Return range headers in PATCH
|
||||
|
||||
@@ -20,7 +20,7 @@ your own projects.
|
||||
|
||||
### Usage
|
||||
|
||||
Download the binary ([OS X](http://bin.begriffs.com/dbapi/osx/postgrest-0.2.9.0.tar.xz) / [Linux](http://bin.begriffs.com/dbapi/heroku/postgrest-0.2.9.0.tar.xz)) and invoke like so:
|
||||
Download the binary ([latest release](https://github.com/begriffs/postgrest/releases/latest)) and invoke like so:
|
||||
|
||||
```bash
|
||||
postgrest --db-host localhost --db-port 5432 \
|
||||
@@ -75,12 +75,14 @@ Other optimizations are possible, and some are outlined in the
|
||||
|
||||
### Security
|
||||
|
||||
PostgREST handles authentication (HTTP Basic over SSL) and delegates
|
||||
authorization to the role information defined in the database. This
|
||||
ensures there is a single declarative source of truth for security.
|
||||
When dealing with the database the server assumes the identity of
|
||||
the currently authenticated user, and for the duration of the
|
||||
connection cannot do anything the user themselves couldn't.
|
||||
PostgREST handles authentication (HTTP Basic over SSL or [JSON Web
|
||||
Tokens](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions#json-web-tokens))
|
||||
and delegates authorization to the role information defined in the
|
||||
database. This ensures there is a single declarative source of truth
|
||||
for security. When dealing with the database the server assumes
|
||||
the identity of the currently authenticated user, and for the
|
||||
duration of the connection cannot do anything the user themselves
|
||||
couldn't.
|
||||
|
||||
Postgres 9.5 will soon support true [row-level
|
||||
security](http://michael.otacoo.com/postgresql-2/postgres-9-5-feature-highlight-row-level-security/).
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"POSTGREST_VER": {
|
||||
"description": "Version of PostgREST to deploy",
|
||||
"value": "0.2.9.0"
|
||||
"value": "0.2.10.0"
|
||||
},
|
||||
"DB_NAME": {
|
||||
"description": "Database name",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ name: postgrest
|
||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||
for the tables and views, supporting all HTTP verbs that security
|
||||
permits.
|
||||
version: 0.2.9.1
|
||||
version: 0.2.10.0
|
||||
synopsis: REST API for any Postgres database
|
||||
license: MIT
|
||||
license-file: LICENSE
|
||||
|
||||
@@ -120,9 +120,9 @@ app conf reqBody req =
|
||||
login <- signInRole (cs $ userId u)
|
||||
(cs $ userPass u)
|
||||
case login of
|
||||
LoginSuccess role ->
|
||||
LoginSuccess role uid ->
|
||||
return $ responseLBS status201 [ jsonH ] $
|
||||
encode . object $ [("token", String $ tokenJWT jwtSecret (cs $ userId u) role)]
|
||||
encode . object $ [("token", String $ tokenJWT jwtSecret uid role)]
|
||||
_ -> return $ responseLBS status401 [jsonH] $
|
||||
encode . object $ [("message", String "Failed authentication.")]
|
||||
|
||||
|
||||
+18
-5
@@ -40,12 +40,13 @@ instance ToJSON AuthUser where
|
||||
, "role" .= userRole u ]
|
||||
|
||||
type DbRole = Text
|
||||
type UserId = Text
|
||||
|
||||
data LoginAttempt =
|
||||
NoCredentials
|
||||
| MalformedAuth
|
||||
| LoginFailed
|
||||
| LoginSuccess DbRole
|
||||
| LoginSuccess DbRole UserId
|
||||
deriving (Eq, Show)
|
||||
|
||||
checkPass :: Text -> Text -> Bool
|
||||
@@ -57,6 +58,15 @@ setRole role = H.unitEx $ B.Stmt ("set role " <> cs (pgFmtLit role)) V.empty Tru
|
||||
resetRole :: H.Tx P.Postgres s ()
|
||||
resetRole = H.unitEx [H.stmt|reset role|]
|
||||
|
||||
setUserId :: Text -> H.Tx P.Postgres s ()
|
||||
setUserId uid = if uid /= "" then
|
||||
H.unitEx $ B.Stmt ("set user_vars.user_id = " <> cs (pgFmtLit uid)) V.empty True
|
||||
else
|
||||
resetUserId
|
||||
|
||||
resetUserId :: H.Tx P.Postgres s ()
|
||||
resetUserId = H.unitEx [H.stmt|reset user_vars.user_id|]
|
||||
|
||||
addUser :: Text -> Text -> Text -> H.Tx P.Postgres s ()
|
||||
addUser identity pass role = do
|
||||
let Just hashed = unsafePerformIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass)
|
||||
@@ -66,20 +76,23 @@ addUser identity pass role = do
|
||||
|
||||
signInRole :: Text -> Text -> H.Tx P.Postgres s LoginAttempt
|
||||
signInRole user pass = do
|
||||
u <- H.maybeEx $ [H.stmt|select pass, rolname from postgrest.auth where id = ?|] user
|
||||
u <- H.maybeEx $ [H.stmt|select id, pass, rolname from postgrest.auth where id = ?|] user
|
||||
return $ maybe LoginFailed (\r ->
|
||||
let (hashed, role) = r in
|
||||
let (uid, hashed, role) = r in
|
||||
if checkPass hashed pass
|
||||
then LoginSuccess role
|
||||
then LoginSuccess role uid
|
||||
else LoginFailed
|
||||
) u
|
||||
|
||||
signInWithJWT :: Text -> Text -> LoginAttempt
|
||||
signInWithJWT secret input = case maybeRole of
|
||||
Just (Just (String role)) -> LoginSuccess $ cs role
|
||||
Just (Just (String role)) -> case maybeUserId of
|
||||
Just (Just (String uid)) -> LoginSuccess (cs role) (cs uid)
|
||||
_ -> LoginFailed
|
||||
_ -> LoginFailed
|
||||
where
|
||||
maybeRole = (Data.Map.lookup "role" <$> claims) ::Maybe (Maybe Value)
|
||||
maybeUserId = (Data.Map.lookup "id" <$> claims) ::Maybe (Maybe Value)
|
||||
claims = JWT.unregisteredClaims <$> JWT.claims <$> decoded
|
||||
decoded = JWT.decodeAndVerifySignature (JWT.secret secret) input
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import Network.Wai (Application, requestHeaders, responseLBS, rawPathInfo,
|
||||
import Network.URI (URI(..), parseURI)
|
||||
|
||||
import PostgREST.Config (AppConfig(..))
|
||||
import PostgREST.Auth (LoginAttempt(..), signInRole, signInWithJWT, setRole, resetRole)
|
||||
import PostgREST.Auth (LoginAttempt(..), signInRole, signInWithJWT, setRole, resetRole, setUserId, resetUserId)
|
||||
import Codec.Binary.Base64.String (decode)
|
||||
|
||||
import Prelude
|
||||
@@ -35,8 +35,8 @@ authenticated conf app req = do
|
||||
return $ responseLBS status400 [] "Malformed basic auth header"
|
||||
LoginFailed ->
|
||||
return $ responseLBS status401 [] "Invalid username or password"
|
||||
LoginSuccess role -> if role /= currentRole then runInRole role else app req
|
||||
NoCredentials -> if anon /= currentRole then runInRole anon else app req
|
||||
LoginSuccess role uid -> if role /= currentRole then runInRole role uid else app req
|
||||
NoCredentials -> if anon /= currentRole then runInRole anon "" else app req
|
||||
|
||||
where
|
||||
jwtSecret = cs $ configJwtSecret conf
|
||||
@@ -54,11 +54,13 @@ authenticated conf app req = do
|
||||
return $ signInWithJWT jwtSecret jwt
|
||||
_ -> return NoCredentials
|
||||
|
||||
runInRole :: Text -> H.Tx P.Postgres s Response
|
||||
runInRole r = do
|
||||
runInRole :: Text -> Text -> H.Tx P.Postgres s Response
|
||||
runInRole r uid = do
|
||||
setUserId uid
|
||||
setRole r
|
||||
res <- app req
|
||||
resetRole
|
||||
resetUserId
|
||||
return res
|
||||
|
||||
|
||||
|
||||
@@ -175,6 +175,7 @@ wherePred (col, predicate) =
|
||||
"like" -> unknownLiteral $ T.map star value
|
||||
"ilike" -> unknownLiteral $ T.map star value
|
||||
"in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
|
||||
"@@" -> "to_tsquery(" <> unknownLiteral value <> ") "
|
||||
_ -> unknownLiteral value
|
||||
|
||||
op = case opCode of
|
||||
@@ -189,6 +190,7 @@ wherePred (col, predicate) =
|
||||
"in" -> "in"
|
||||
"is" -> "is"
|
||||
"isnot" -> "is not"
|
||||
"@@" -> "@@"
|
||||
_ -> "="
|
||||
|
||||
orderParse :: Net.Query -> [OrderTerm]
|
||||
|
||||
@@ -274,3 +274,23 @@ spec = afterAll_ resetDb $ around withApp $ do
|
||||
[("Prefer", "return=representation")]
|
||||
[json| { id: 99 } |]
|
||||
`shouldRespondWith` [json| [{id:99}] |]
|
||||
|
||||
describe "Row level permission" $
|
||||
it "set user_id when inserting rows" $ do
|
||||
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
|
||||
_ <- post "/postgrest/users" [json| { "id":"jroe", "pass": "1234", "role": "postgrest_test_author" } |]
|
||||
|
||||
p1 <- request methodPost "/authors_only"
|
||||
[ authHeaderBasic "jdoe" "1234", ("Prefer", "return=representation") ]
|
||||
[json| { "secret": "nyancat" } |]
|
||||
liftIO $ do
|
||||
simpleBody p1 `shouldBe` [json| { "owner":"jdoe", "secret":"nyancat" } |]
|
||||
simpleStatus p1 `shouldBe` created201
|
||||
|
||||
p2 <- request methodPost "/authors_only"
|
||||
-- jwt token for jroe
|
||||
[ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.YuF_VfmyIxWyuceT7crnNKEprIYXsJAyXid3rjPjIow", ("Prefer", "return=representation") ]
|
||||
[json| { "secret": "lolcat", "owner": "hacker" } |]
|
||||
liftIO $ do
|
||||
simpleBody p2 `shouldBe` [json| { "owner":"jroe", "secret":"lolcat" } |]
|
||||
simpleStatus p2 `shouldBe` created201
|
||||
|
||||
@@ -56,6 +56,10 @@ spec =
|
||||
get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith`
|
||||
"[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]"
|
||||
|
||||
it "matches with tsearch @@" $
|
||||
get "/tsearch?text_search_vector=@@.foo" `shouldRespondWith`
|
||||
"[{\"text_search_vector\":\"'bar':2 'foo':1\"}]"
|
||||
|
||||
describe "ordering response" $ do
|
||||
it "by a column asc" $
|
||||
get "/items?id=lte.2&order=id.asc"
|
||||
|
||||
@@ -22,6 +22,7 @@ spec = around withApp $ do
|
||||
, {"schema":"1","name":"menagerie","insertable":true}
|
||||
, {"schema":"1","name":"no_pk","insertable":true}
|
||||
, {"schema":"1","name":"simple_pk","insertable":true}
|
||||
, {"schema":"1","name":"tsearch","insertable":true}
|
||||
] |]
|
||||
{matchStatus = 200}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ spec = around dbWithSchema $ do
|
||||
addUser user pass role conn
|
||||
return conn) $ do
|
||||
it "accepts correct credentials and return the role" $ \conn ->
|
||||
signInRole user pass conn `shouldReturn` LoginSuccess role
|
||||
signInRole user pass conn `shouldReturn` LoginSuccess role user
|
||||
|
||||
it "returns nothing with bad creds" $ \conn -> do
|
||||
signInRole "not-a-user" pass conn `shouldReturn` LoginFailed
|
||||
|
||||
Vendored
+30
-9
@@ -72,6 +72,18 @@ $$;
|
||||
|
||||
ALTER FUNCTION postgrest.update_owner() OWNER TO postgrest_test;
|
||||
|
||||
|
||||
CREATE FUNCTION set_authors_only_owner() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
begin
|
||||
NEW.owner = current_setting('user_vars.user_id');
|
||||
RETURN NEW;
|
||||
end
|
||||
$$;
|
||||
|
||||
ALTER FUNCTION postgrest.set_authors_only_owner() OWNER TO postgrest_test;
|
||||
|
||||
SET search_path = "1", pg_catalog;
|
||||
|
||||
SET default_tablespace = '';
|
||||
@@ -80,6 +92,7 @@ SET default_with_oids = false;
|
||||
|
||||
|
||||
CREATE TABLE authors_only (
|
||||
owner character varying NOT NULL,
|
||||
secret character varying NOT NULL
|
||||
);
|
||||
|
||||
@@ -212,6 +225,12 @@ CREATE TABLE json
|
||||
ALTER TABLE "1".json OWNER TO postgrest_test;
|
||||
|
||||
|
||||
CREATE TABLE tsearch (
|
||||
text_search_vector tsvector
|
||||
);
|
||||
|
||||
ALTER TABLE "1".tsearch OWNER TO postgrest_test;
|
||||
|
||||
SET search_path = postgrest, pg_catalog;
|
||||
|
||||
|
||||
@@ -299,14 +318,8 @@ INSERT INTO items (id) VALUES (1);
|
||||
SELECT pg_catalog.setval('items_id_seq', 1, true);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
INSERT INTO tsearch (text_search_vector) VALUES ('''bar'':2 ''foo'':1');
|
||||
INSERT INTO tsearch (text_search_vector) VALUES ('''baz'':1 ''qux'':2');
|
||||
|
||||
SET search_path = postgrest, pg_catalog;
|
||||
|
||||
@@ -327,7 +340,8 @@ SET search_path = "1", pg_catalog;
|
||||
|
||||
ALTER TABLE ONLY authors_only
|
||||
ADD CONSTRAINT authors_only_pkey PRIMARY KEY (secret);
|
||||
|
||||
|
||||
CREATE TRIGGER secrets_owner_track BEFORE INSERT OR UPDATE ON authors_only FOR EACH ROW EXECUTE PROCEDURE postgrest.set_authors_only_owner();
|
||||
|
||||
|
||||
ALTER TABLE ONLY auto_incrementing_pk
|
||||
@@ -491,6 +505,13 @@ GRANT ALL ON TABLE json TO postgrest_test;
|
||||
GRANT ALL ON TABLE json TO postgrest_anonymous;
|
||||
|
||||
|
||||
|
||||
REVOKE ALL ON TABLE tsearch FROM PUBLIC;
|
||||
REVOKE ALL ON TABLE tsearch FROM postgrest_test;
|
||||
GRANT ALL ON TABLE tsearch TO postgrest_test;
|
||||
GRANT ALL ON TABLE tsearch TO postgrest_anonymous;
|
||||
|
||||
|
||||
SET search_path = postgrest, pg_catalog;
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user