From 83f48dcd158b8362e4ce072d4abb7ba606bf1c02 Mon Sep 17 00:00:00 2001 From: Federico Rampazzo Date: Sun, 24 May 2015 02:41:14 +0100 Subject: [PATCH] User_id support (via user_vars) --- src/PostgREST/App.hs | 4 ++-- src/PostgREST/Auth.hs | 23 ++++++++++++++++++----- src/PostgREST/Middleware.hs | 12 +++++++----- test/Feature/InsertSpec.hs | 20 ++++++++++++++++++++ test/Unit/PgQuerySpec.hx | 2 +- test/fixtures/schema.sql | 16 +++++++++++++++- 6 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index d52994416..6ee55dedc 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -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.")] diff --git a/src/PostgREST/Auth.hs b/src/PostgREST/Auth.hs index 3b0334749..597dae421 100644 --- a/src/PostgREST/Auth.hs +++ b/src/PostgREST/Auth.hs @@ -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 diff --git a/src/PostgREST/Middleware.hs b/src/PostgREST/Middleware.hs index 7f1ca4079..568cfff9a 100644 --- a/src/PostgREST/Middleware.hs +++ b/src/PostgREST/Middleware.hs @@ -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 diff --git a/test/Feature/InsertSpec.hs b/test/Feature/InsertSpec.hs index 02c917158..9bb0f6a2d 100644 --- a/test/Feature/InsertSpec.hs +++ b/test/Feature/InsertSpec.hs @@ -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 diff --git a/test/Unit/PgQuerySpec.hx b/test/Unit/PgQuerySpec.hx index 283ea2a62..0cc3de225 100644 --- a/test/Unit/PgQuerySpec.hx +++ b/test/Unit/PgQuerySpec.hx @@ -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 diff --git a/test/fixtures/schema.sql b/test/fixtures/schema.sql index 6168133bf..3201a76fa 100755 --- a/test/fixtures/schema.sql +++ b/test/fixtures/schema.sql @@ -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 ); @@ -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