WIP: change db roles based on HTTP basic auth headers
This commit is contained in:
@@ -31,6 +31,7 @@ executable dbapi
|
|||||||
, transformers
|
, transformers
|
||||||
, warp-tls
|
, warp-tls
|
||||||
, bcrypt
|
, bcrypt
|
||||||
|
, base64-string
|
||||||
Other-Modules: Dbapi
|
Other-Modules: Dbapi
|
||||||
, PgStructure
|
, PgStructure
|
||||||
, PgQuery
|
, PgQuery
|
||||||
@@ -62,3 +63,4 @@ Test-Suite spec
|
|||||||
, transformers
|
, transformers
|
||||||
, warp-tls
|
, warp-tls
|
||||||
, bcrypt
|
, bcrypt
|
||||||
|
, base64-string
|
||||||
|
|||||||
+23
-6
@@ -7,6 +7,7 @@ import Types (SqlRow, getRow)
|
|||||||
|
|
||||||
import Control.Exception (try)
|
import Control.Exception (try)
|
||||||
import Control.Monad (join)
|
import Control.Monad (join)
|
||||||
|
import Control.Exception.Base (bracket_)
|
||||||
import Control.Arrow ((***))
|
import Control.Arrow ((***))
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import Options.Applicative hiding (columns)
|
import Options.Applicative hiding (columns)
|
||||||
@@ -40,6 +41,7 @@ import qualified Data.Aeson as JSON
|
|||||||
import PgQuery
|
import PgQuery
|
||||||
import RangeQuery
|
import RangeQuery
|
||||||
import Data.Ranged.Ranges (emptyRange)
|
import Data.Ranged.Ranges (emptyRange)
|
||||||
|
import Codec.Binary.Base64.String (decode)
|
||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ data AppConfig = AppConfig {
|
|||||||
, configPort :: Int
|
, configPort :: Int
|
||||||
, configSslCert :: FilePath
|
, configSslCert :: FilePath
|
||||||
, configSslKey :: FilePath
|
, configSslKey :: FilePath
|
||||||
|
, configAnonRole :: String
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonContentType :: (HeaderName, BS.ByteString)
|
jsonContentType :: (HeaderName, BS.ByteString)
|
||||||
@@ -69,9 +72,22 @@ filterByKeys m keys =
|
|||||||
if null keys then m else
|
if null keys then m else
|
||||||
m `intersection` fromList (zip keys $ repeat undefined)
|
m `intersection` fromList (zip keys $ repeat undefined)
|
||||||
|
|
||||||
app :: Connection -> Application
|
httpRequesterRole :: RequestHeaders -> Connection -> IO(Maybe DbRole)
|
||||||
app conn req respond = do
|
httpRequesterRole hdrs conn = do
|
||||||
r <- try $
|
let auth = fromMaybe "" $ lookup hAuthorization hdrs
|
||||||
|
case BS.split ' ' (cs auth) of
|
||||||
|
("Basic " : b64 : _) ->
|
||||||
|
case BS.split ':' $ cs (decode $ cs b64) of
|
||||||
|
(u:p:_) -> signInRole u p conn
|
||||||
|
_ -> return Nothing
|
||||||
|
_ -> return Nothing
|
||||||
|
|
||||||
|
app :: Connection -> DbRole -> Application
|
||||||
|
app conn anonymous req respond = do
|
||||||
|
r <- try $ do
|
||||||
|
role <- fromMaybe anonymous <$> httpRequesterRole hdrs conn
|
||||||
|
|
||||||
|
bracket_ (pgSetRole conn role) (pgResetRole conn) $
|
||||||
case (path, verb) of
|
case (path, verb) of
|
||||||
([], _) ->
|
([], _) ->
|
||||||
responseLBS status200 [jsonContentType] <$> printTables ver conn
|
responseLBS status200 [jsonContentType] <$> printTables ver conn
|
||||||
@@ -142,9 +158,10 @@ app conn req respond = do
|
|||||||
path = pathInfo req
|
path = pathInfo req
|
||||||
verb = requestMethod req
|
verb = requestMethod req
|
||||||
qq = queryString req
|
qq = queryString req
|
||||||
ver = fromMaybe "1" $ requestedVersion (requestHeaders req)
|
hdrs = requestHeaders req
|
||||||
range = requestedRange (requestHeaders req)
|
ver = fromMaybe "1" $ requestedVersion hdrs
|
||||||
cRange = requestedContentRange (requestHeaders req)
|
range = requestedRange hdrs
|
||||||
|
cRange = requestedContentRange hdrs
|
||||||
|
|
||||||
respondWithRangedResult :: RangedResult -> Response
|
respondWithRangedResult :: RangedResult -> Response
|
||||||
respondWithRangedResult rr =
|
respondWithRangedResult rr =
|
||||||
|
|||||||
+4
-1
@@ -6,6 +6,7 @@ module Main where
|
|||||||
import Dbapi
|
import Dbapi
|
||||||
import Network.Wai.Handler.Warp hiding (Connection)
|
import Network.Wai.Handler.Warp hiding (Connection)
|
||||||
import Database.HDBC.PostgreSQL (connectPostgreSQL')
|
import Database.HDBC.PostgreSQL (connectPostgreSQL')
|
||||||
|
import Data.String.Conversions (cs)
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import Options.Applicative hiding (columns)
|
import Options.Applicative hiding (columns)
|
||||||
@@ -24,6 +25,8 @@ argParser = AppConfig
|
|||||||
<> help "path to SSL cert file")
|
<> help "path to SSL cert file")
|
||||||
<*> strOption (long "sslkey" <> short 'k' <> metavar "PATH" <> value "test/test.key"
|
<*> strOption (long "sslkey" <> short 'k' <> metavar "PATH" <> value "test/test.key"
|
||||||
<> help "path to SSL key file")
|
<> help "path to SSL key file")
|
||||||
|
<*> strOption (long "anonymous" <> short 'a' <> metavar "ROLE" <> value "dbapi_anon"
|
||||||
|
<> help "postgres role to use for non-authenticated requests")
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
@@ -36,7 +39,7 @@ main = do
|
|||||||
|
|
||||||
Prelude.putStrLn $ "Listening on port " ++ (show $ configPort conf :: String)
|
Prelude.putStrLn $ "Listening on port " ++ (show $ configPort conf :: String)
|
||||||
conn <- connectPostgreSQL' dburi
|
conn <- connectPostgreSQL' dburi
|
||||||
runTLS tls settings $ gzip def $ app conn
|
runTLS tls settings $ gzip def $ app conn (cs $ configAnonRole conf)
|
||||||
|
|
||||||
where
|
where
|
||||||
describe = progDesc "create a REST API to an existing Postgres database"
|
describe = progDesc "create a REST API to an existing Postgres database"
|
||||||
|
|||||||
+15
-4
@@ -8,7 +8,10 @@ module PgQuery (
|
|||||||
upsert,
|
upsert,
|
||||||
addUser,
|
addUser,
|
||||||
signInRole,
|
signInRole,
|
||||||
|
pgSetRole,
|
||||||
|
pgResetRole,
|
||||||
RangedResult(..),
|
RangedResult(..),
|
||||||
|
DbRole
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
@@ -19,7 +22,7 @@ import Data.List (intersperse, intercalate)
|
|||||||
import Data.Monoid ((<>), mconcat)
|
import Data.Monoid ((<>), mconcat)
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
|
|
||||||
import Control.Monad (join)
|
import Control.Monad (join, void)
|
||||||
|
|
||||||
import qualified RangeQuery as R
|
import qualified RangeQuery as R
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
@@ -44,7 +47,7 @@ data RangedResult = RangedResult {
|
|||||||
|
|
||||||
type QuotedSql = (String, [SqlValue])
|
type QuotedSql = (String, [SqlValue])
|
||||||
type Schema = String
|
type Schema = String
|
||||||
type DbRole = String
|
type DbRole = BS.ByteString
|
||||||
|
|
||||||
getRows :: Schema -> String -> Net.Query -> Maybe R.NonnegRange -> Connection -> IO RangedResult
|
getRows :: Schema -> String -> Net.Query -> Maybe R.NonnegRange -> Connection -> IO RangedResult
|
||||||
getRows schema table qq range conn = do
|
getRows schema table qq range conn = do
|
||||||
@@ -120,7 +123,7 @@ insert schema table row conn = do
|
|||||||
Just m <- fetchRowMap stmt
|
Just m <- fetchRowMap stmt
|
||||||
return m
|
return m
|
||||||
|
|
||||||
addUser :: String -> String -> String -> Connection -> IO ()
|
addUser :: BS.ByteString -> BS.ByteString -> BS.ByteString -> Connection -> IO ()
|
||||||
addUser identity pass role conn = do
|
addUser identity pass role conn = do
|
||||||
hashed <- hashPasswordUsingPolicy fastBcryptHashingPolicy $ cs pass
|
hashed <- hashPasswordUsingPolicy fastBcryptHashingPolicy $ cs pass
|
||||||
_ <- insert "dbapi" "auth" (SqlRow [
|
_ <- insert "dbapi" "auth" (SqlRow [
|
||||||
@@ -128,7 +131,7 @@ addUser identity pass role conn = do
|
|||||||
]) conn
|
]) conn
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
signInRole :: String -> String -> Connection -> IO(Maybe DbRole)
|
signInRole :: BS.ByteString -> BS.ByteString -> Connection -> IO(Maybe DbRole)
|
||||||
signInRole user pass conn = do
|
signInRole user pass conn = do
|
||||||
u <- quickQuery conn "select pass, rolname from dbapi.auth where id = ?" [toSql user]
|
u <- quickQuery conn "select pass, rolname from dbapi.auth where id = ?" [toSql user]
|
||||||
return $ case u of
|
return $ case u of
|
||||||
@@ -185,3 +188,11 @@ populateSql conn sql = do
|
|||||||
|
|
||||||
ph :: [a] -> String
|
ph :: [a] -> String
|
||||||
ph = intercalate ", " . map (const "?::varchar")
|
ph = intercalate ", " . map (const "?::varchar")
|
||||||
|
|
||||||
|
pgSetRole :: Connection -> DbRole -> IO ()
|
||||||
|
pgSetRole conn role = do
|
||||||
|
query <- populateSql conn ("set role %I", [toSql role])
|
||||||
|
void $ run conn query []
|
||||||
|
|
||||||
|
pgResetRole :: Connection -> IO ()
|
||||||
|
pgResetRole conn = void $ run conn "reset role" []
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@ import qualified Data.ByteString.Char8 as BS
|
|||||||
import Dbapi (app, AppConfig(..))
|
import Dbapi (app, AppConfig(..))
|
||||||
|
|
||||||
cfg :: AppConfig
|
cfg :: AppConfig
|
||||||
cfg = AppConfig "postgres://postgres:@localhost:5432/dbapi_test" 9000 "test/test.crt" "test/test.key"
|
cfg = AppConfig "postgres://postgres:@localhost:5432/dbapi_test" 9000 "test/test.crt" "test/test.key" "postgres"
|
||||||
|
|
||||||
openConnection :: IO Connection
|
openConnection :: IO Connection
|
||||||
openConnection = connectPostgreSQL' $ configDbUri cfg
|
openConnection = connectPostgreSQL' $ configDbUri cfg
|
||||||
@@ -40,7 +40,7 @@ dbWithSchema action = withDatabaseConnection $ \c -> do
|
|||||||
appWithFixture :: ActionWith Application -> IO ()
|
appWithFixture :: ActionWith Application -> IO ()
|
||||||
appWithFixture action = withDatabaseConnection $ \c -> do
|
appWithFixture action = withDatabaseConnection $ \c -> do
|
||||||
runRaw c "begin;"
|
runRaw c "begin;"
|
||||||
action $ app c
|
action $ app c "postgres"
|
||||||
rollback c
|
rollback c
|
||||||
|
|
||||||
rangeHdrs :: ByteRange -> [Header]
|
rangeHdrs :: ByteRange -> [Header]
|
||||||
|
|||||||
@@ -11,12 +11,16 @@ import PgQuery (insert, addUser, signInRole)
|
|||||||
import Types (SqlRow(SqlRow))
|
import Types (SqlRow(SqlRow))
|
||||||
import TestTypes (fromList, incStr, incNullableStr, incInsert, incId)
|
import TestTypes (fromList, incStr, incNullableStr, incInsert, incId)
|
||||||
import Data.Map (toList)
|
import Data.Map (toList)
|
||||||
|
import Data.Monoid ((<>))
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Control.Arrow
|
import Control.Arrow
|
||||||
|
|
||||||
import SpecHelper(dbWithSchema)
|
import SpecHelper(dbWithSchema)
|
||||||
|
|
||||||
quickALQuery :: IConnection conn => conn -> String -> [SqlValue] -> IO [[(String, SqlValue)]]
|
quickALQuery :: IConnection conn => conn ->
|
||||||
|
String ->
|
||||||
|
[SqlValue] ->
|
||||||
|
IO [[(String, SqlValue)]]
|
||||||
quickALQuery conn q bind = do
|
quickALQuery conn q bind = do
|
||||||
sth <- prepare conn q
|
sth <- prepare conn q
|
||||||
_ <- execute sth bind
|
_ <- execute sth bind
|
||||||
@@ -61,7 +65,7 @@ spec = around dbWithSchema $ do
|
|||||||
r2 <- signInRole user pass conn
|
r2 <- signInRole user pass conn
|
||||||
r2 `shouldBe` Just role
|
r2 `shouldBe` Just role
|
||||||
|
|
||||||
r3 <- signInRole user (pass++"crap") conn
|
r3 <- signInRole user (pass <> "crap") conn
|
||||||
r3 `shouldBe` Nothing
|
r3 `shouldBe` Nothing
|
||||||
|
|
||||||
it "will not add a user with an unknown role" $ \conn -> do
|
it "will not add a user with an unknown role" $ \conn -> do
|
||||||
|
|||||||
Reference in New Issue
Block a user