packages feed

sproxy2 1.94.1 → 1.95.0

raw patch · 6 files changed

+156/−40 lines, 6 files

Files

ChangeLog.md view
@@ -1,5 +1,13 @@ For differences with the original Sproxy scroll down. +1.95.0+======++  * Add end-point for checking access in a bunch (`/.sproxy/access`).++  * Respond with 502 (Bad Gateway) on any backend error.+    Previously it was 500 (Internal Server Error).+ 1.94.1 ====== @@ -122,5 +130,5 @@     This can be used in load-balancing multiple Sproxy2 instances.    * Configuration file has changed. It's still YAML, but some options are renamed, removed or added.-    Have a look at well-documented [sproxy.yml.example](./sproxy.yml.example)+    Have a look at well-documented [sproxy.example.yml](./sproxy.example.yml) 
README.md view
@@ -140,34 +140,38 @@ - SQL wildcards (`_` and `%`) are supported for emails, paths (this _will_ change in future versions).  -HTTP headers passed to the back-end server-------------------------------------------+Checking access in a bunch+-------------------------- -All Sproxy headers are UTF8-encoded.+There is an API end-point for checking access rights in a single POST query:+`/.sproxy/access`.  Users should be authenticated to use this end-point,+otherwise the respond will be HTTP 511. +The request body shall be a JSON object like this: -header               | value--------------------- | ------`From:`              | visitor's email address, lower case-`X-Groups:`          | all groups that granted access to this resource, separated by commas (see the note below)-`X-Given-Name:`      | the visitor's given (first) name-`X-Family-Name:`     | the visitor's family (last) name-`X-Forwarded-Proto:` | the visitor's protocol of an HTTP request, always `https`-`X-Forwarded-For`    | the visitor's IP address (added to the end of the list if header is already present in client request)+```json+{+  "tag1": {"path": "/foo", "method": "GET"},+  "tag2": {"path": "/bar", "method": "GET"}+}+``` +And the respond will contain a JSON array with tag matching path and method+pairs allowed to the user.  For example: -`X-Groups` denotes an intersection of the groups the visitor belongs to and the groups that granted access:+```sh+$ curl -d '{"foo": {"path":"/get", "method":"GET"}, "bar": {"path":"/post", "method":"POST"}}' -XPOST -k 'https://example.ru:8443/.sproxy/access' ...+["foo","bar"] -Visitor's groups | Granted groups | `X-Groups`----------------- | -------------- | ----------all              | all, devops    | all-all, devops      | all            | all-all, devops      | all, devops    | all,devops-all, devops      | devops         | devops-devops           | all, devops    | devops-devops           | all            | Access denied+$ curl -d '{"foo": {"path":"/get", "method":"POST"}, "bar": {"path":"/post", "method":"POST"}}' -XPOST -k 'https://example.ru:8443/.sproxy/access' ...+["bar"] +$ curl -d '{"foo": {"path":"/", "method":"POST"}, "bar": {"path":"/post", "method":"GET"}}' -XPOST -k 'https://example.ru:8443/.sproxy/access' ...+[] +```++ Logout ------ @@ -186,6 +190,34 @@ User-agent: * Disallow: / ```+++HTTP headers passed to the back-end server+------------------------------------------++All Sproxy headers are UTF8-encoded.+++header               | value+-------------------- | -----+`From:`              | visitor's email address, lower case+`X-Groups:`          | all groups that granted access to this resource, separated by commas (see the note below)+`X-Given-Name:`      | the visitor's given (first) name+`X-Family-Name:`     | the visitor's family (last) name+`X-Forwarded-Proto:` | the visitor's protocol of an HTTP request, always `https`+`X-Forwarded-For`    | the visitor's IP address (added to the end of the list if header is already present in client request)+++`X-Groups` denotes an intersection of the groups the visitor belongs to and the groups that granted access:++Visitor's groups | Granted groups | `X-Groups`+---------------- | -------------- | ---------+all              | all, devops    | all+all, devops      | all            | all+all, devops      | all, devops    | all,devops+all, devops      | devops         | devops+devops           | all, devops    | devops+devops           | all            | Access denied   Requirements
sproxy2.cabal view
@@ -1,5 +1,5 @@ name: sproxy2-version: 1.94.1+version: 1.95.0 synopsis: Secure HTTP proxy for authenticating users via OAuth2 description:   Sproxy is secure by default. No requests makes it to the backend@@ -32,6 +32,7 @@     main-is: Main.hs     other-modules:       Sproxy.Application+      Sproxy.Application.Access       Sproxy.Application.Cookie       Sproxy.Application.OAuth2       Sproxy.Application.OAuth2.Common
src/Sproxy/Application.hs view
@@ -27,11 +27,11 @@ import Foreign.C.Types (CTime(..)) import Network.HTTP.Client.Conduit (bodyReaderSource) import Network.HTTP.Conduit (requestBodySourceChunkedIO, requestBodySourceIO)-import Network.HTTP.Types (RequestHeaders, ResponseHeaders, methodGet)+import Network.HTTP.Types (RequestHeaders, ResponseHeaders, methodGet, methodPost) import Network.HTTP.Types.Header ( hConnection,   hContentLength, hContentType, hCookie, hLocation, hTransferEncoding )-import Network.HTTP.Types.Status ( Status(..), badRequest400, forbidden403, found302,-  internalServerError500, methodNotAllowed405, movedPermanently301,+import Network.HTTP.Types.Status ( Status(..), badGateway502, badRequest400, forbidden403,+  found302, internalServerError500, methodNotAllowed405, movedPermanently301,   networkAuthenticationRequired511, notFound404, ok200, seeOther303, temporaryRedirect307 ) import Network.Socket (NameInfoFlag(NI_NUMERICHOST), getNameInfo) import Network.Wai.Conduit (sourceRequestBody, responseSource)@@ -39,6 +39,7 @@ import System.Posix.Time (epochTime) import Text.InterpolatedString.Perl6 (qc) import Web.Cookie (Cookies, parseCookies, renderCookies)+import qualified Data.Aeson as JSON import qualified Network.HTTP.Client as BE import qualified Network.Wai as W import qualified Web.Cookie as WC@@ -48,7 +49,7 @@   getGivenNameUtf8 ) import Sproxy.Application.OAuth2.Common (OAuth2Client(..)) import Sproxy.Config(BackendConf(..))-import Sproxy.Server.DB (Database, userExists, userGroups)+import Sproxy.Server.DB (Database, userAccess, userExists, userGroups) import qualified Sproxy.Application.State as State import qualified Sproxy.Logging as Log @@ -81,12 +82,22 @@             ["robots.txt"] -> get robots req resp             (".sproxy":proxy) ->               case proxy of+                 ["logout"] -> get (logout key cookieName cookieDomain) req resp+                 ["oauth2", provider] ->                     case HM.lookup provider oa2 of                       Nothing -> notFound "OAuth2 provider" req resp                       Just oa2c -> get (oauth2callback key db (provider, oa2c) be) req resp++                ["access"] -> do+                  now <- Just <$> epochTime+                  case extractCookie key now cookieName req of+                    Nothing -> authenticationRequired key oa2 req resp+                    Just (authCookie, _) -> post (checkAccess db authCookie) req resp+                 _ -> notFound "proxy" req resp+             _ -> do               now <- Just <$> epochTime               case extractCookie key now cookieName req of@@ -195,6 +206,20 @@     setCookies cs = insert hCookie (toByteString . renderCookies $ cs)  +checkAccess :: Database -> AuthCookie -> W.Application+checkAccess db authCookie req resp = do+  let email = getEmail . acUser $ authCookie+      domain = decodeUtf8 . fromJust $ requestDomain req+  body <- W.strictRequestBody req+  case JSON.eitherDecode' body of+    Left err -> badRequest err req resp+    Right inq -> do+      Log.debug $ "access <<< " ++ show inq+      tags <- userAccess db email domain inq+      Log.debug $ "access >>> " ++ show tags+      resp $ W.responseLBS ok200 [(hContentType, "application/json")] (JSON.encode tags)++ -- XXX If something seems strange, think about HTTP/1.1 <-> HTTP/1.0. -- FIXME For HTTP/1.0 backends we might need an option -- FIXME in config file. HTTP Client does HTTP/1.1 by default.@@ -358,12 +383,16 @@ logException :: W.Middleware logException app req resp =   catches (app req resp) [+    Handler badGateway,     Handler internalError   ]   where     internalError :: SomeException -> IO W.ResponseReceived     internalError = response internalServerError500 +    badGateway :: BE.HttpException -> IO W.ResponseReceived+    badGateway = response badGateway502+     response :: Exception e => Status -> e -> IO W.ResponseReceived     response st e = do       Log.error $ show (statusCode st) ++ " " ++ unpack (statusMessage st)@@ -378,6 +407,14 @@   | otherwise = do     Log.warn $ "405 Method Not Allowed: " ++ showReq req     resp $ W.responseLBS methodNotAllowed405 [("Allow", "GET")] "Method Not Allowed"+++post :: W.Middleware+post app req resp+  | W.requestMethod req == methodPost = app req resp+  | otherwise = do+    Log.warn $ "405 Method Not Allowed: " ++ showReq req+    resp $ W.responseLBS methodNotAllowed405 [("Allow", "POST")] "Method Not Allowed"   redirectURL :: W.Request -> Text -> ByteString
+ src/Sproxy/Application/Access.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++module Sproxy.Application.Access (+  Inquiry+, Question(..)+) where++import Data.Aeson (FromJSON)+import Data.HashMap.Strict (HashMap)+import Data.Text (Text)+import GHC.Generics (Generic)+++data Question = Question {+  path :: Text+, method :: Text+} deriving (Generic, Show)++instance FromJSON Question++type Inquiry = HashMap Text Question+
src/Sproxy/Server/DB.hs view
@@ -3,6 +3,7 @@ module Sproxy.Server.DB (   Database , DataSource(..)+, userAccess , userExists , userGroups , start@@ -10,18 +11,20 @@  import Control.Concurrent (forkIO, threadDelay) import Control.Exception (SomeException, bracket, catch, finally)-import Control.Monad (forever, void)+import Control.Monad (filterM, forever, void) import Data.ByteString.Char8 (pack) import Data.Pool (Pool, createPool, withResource) import Data.Text (Text, toLower, unpack) import Data.Yaml (decodeFileEither) import Database.SQLite.Simple (NamedParam((:=))) import Text.InterpolatedString.Perl6 (q, qc)+import qualified Data.HashMap.Strict as HM import qualified Database.PostgreSQL.Simple as PG import qualified Database.SQLite.Simple as SQLite  import Sproxy.Server.DB.DataFile ( DataFile(..), GroupMember(..),   GroupPrivilege(..), PrivilegeRule(..) )+import qualified Sproxy.Application.Access as A import qualified Sproxy.Logging as Log  @@ -61,24 +64,36 @@   return $ head r  -userGroups :: Database -> Text -> Text -> Text -> Text -> IO [Text]-userGroups db email domain path method =-  withResource db $ \c -> fmap SQLite.fromOnly <$> SQLite.queryNamed c [q|-      SELECT gm."group" FROM group_privilege gp JOIN group_member gm ON gm."group"  = gp."group"-      WHERE :email LIKE gm.email-      AND gp.domain = :domain-      AND gp.privilege IN (-        SELECT privilege FROM privilege_rule-        WHERE domain = :domain-        AND :path LIKE path-        AND method = :method-        ORDER BY length(path) - length(replace(path, '/', '')) DESC LIMIT 1-      )+userGroups_ :: SQLite.Connection -> Text -> Text -> Text -> Text -> IO [Text]+userGroups_ c email domain path method =+  fmap SQLite.fromOnly <$> SQLite.queryNamed c [q|+    SELECT gm."group" FROM group_privilege gp JOIN group_member gm ON gm."group"  = gp."group"+    WHERE :email LIKE gm.email+    AND gp.domain = :domain+    AND gp.privilege IN (+      SELECT privilege FROM privilege_rule+      WHERE domain = :domain+      AND :path LIKE path+      AND method = :method+      ORDER BY length(path) - length(replace(path, '/', '')) DESC LIMIT 1+    )   |] [ ":email"  := email -- XXX always in lower case      , ":domain" := toLower domain      , ":path"   := path      , ":method" := method -- XXX case-sensitive by RFC2616      ]+++userAccess :: Database -> Text -> Text -> A.Inquiry -> IO [Text]+userAccess db email domain inq = do+  let permitted c (_, qn) =+        not . null <$> userGroups_ c email domain (A.path qn) (A.method qn)+  map fst <$> withResource db (\c -> filterM (permitted c) (HM.toList inq))+++userGroups :: Database -> Text -> Text -> Text -> Text -> IO [Text]+userGroups db email domain path method =+  withResource db $ \c -> userGroups_ c email domain path method   populate :: Database -> Maybe DataSource -> IO ()