From be9b6f68a60bec0cda4b905e9311a9076f778976 Mon Sep 17 00:00:00 2001
From: Igor Pashev <pashev.igor@gmail.com>
Date: Sat, 26 Nov 2016 21:41:59 +0300
Subject: Populate permission database from a file

---
 src/Sproxy/Config.hs             |  2 ++
 src/Sproxy/Server.hs             | 29 ++++++++++-------
 src/Sproxy/Server/DB.hs          | 46 +++++++++++++++++++++++++--
 src/Sproxy/Server/DB/DataFile.hs | 69 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 133 insertions(+), 13 deletions(-)
 create mode 100644 src/Sproxy/Server/DB/DataFile.hs

(limited to 'src')

diff --git a/src/Sproxy/Config.hs b/src/Sproxy/Config.hs
index 30a8bae..e76b436 100644
--- a/src/Sproxy/Config.hs
+++ b/src/Sproxy/Config.hs
@@ -27,6 +27,7 @@ data ConfigFile = ConfigFile {
 , cfListen80     :: Maybe Bool
 , cfBackends     :: [BackendConf]
 , cfOAuth2       :: HashMap Text OAuth2Conf
+, cfDataFile     :: Maybe FilePath
 , cfDatabase     :: Maybe String
 , cfPgPassFile   :: Maybe FilePath
 , cfHTTP2        :: Bool
@@ -45,6 +46,7 @@ instance FromJSON ConfigFile where
     <*> m .:? "listen80"
     <*> m .:  "backends"
     <*> m .:  "oauth2"
+    <*> m .:? "datafile"
     <*> m .:? "database"
     <*> m .:? "pgpassfile"
     <*> m .:? "http2"          .!= True
diff --git a/src/Sproxy/Server.hs b/src/Sproxy/Server.hs
index 98e9d56..5c80e44 100644
--- a/src/Sproxy/Server.hs
+++ b/src/Sproxy/Server.hs
@@ -67,13 +67,8 @@ server configFile = do
     setGroupID $ userGroupID u
     setUserID $ userID u
 
-  case cfPgPassFile cf of
-    Nothing -> return ()
-    Just f  -> do
-      Log.info $ "pgpassfile: " ++ show f
-      setEnv "PGPASSFILE" f
-
-  db <- DB.start (cfHome cf) (newDataSource cf)
+  ds <- newDataSource cf
+  db <- DB.start (cfHome cf) ds
 
   key <- maybe
            (Log.info "using new random key" >> getEntropy 32)
@@ -112,11 +107,23 @@ server configFile = do
     (sproxy key db oauth2clients backends)
 
 
-newDataSource :: ConfigFile -> Maybe DB.DataSource
+newDataSource :: ConfigFile -> IO (Maybe DB.DataSource)
 newDataSource cf =
-  case cfDatabase cf of
-    Just str -> Just $ DB.PostgreSQL str
-    Nothing -> Nothing
+  case (cfDataFile cf, cfDatabase cf) of
+    (Nothing, Just str) -> do
+      case cfPgPassFile cf of
+        Nothing -> return ()
+        Just f  -> do
+          Log.info $ "pgpassfile: " ++ show f
+          setEnv "PGPASSFILE" f
+      return . Just $ DB.PostgreSQL str
+
+    (Just f, Nothing)   -> return . Just $ DB.File f
+
+    (Nothing, Nothing)  -> return Nothing
+    _ -> do
+      Log.error "only one data source can be used"
+      exitFailure
 
 
 newOAuth2Client :: (Text, OAuth2Conf) -> IO (Text, OAuth2Client)
diff --git a/src/Sproxy/Server/DB.hs b/src/Sproxy/Server/DB.hs
index 90e2abd..2823ba0 100644
--- a/src/Sproxy/Server/DB.hs
+++ b/src/Sproxy/Server/DB.hs
@@ -14,17 +14,20 @@ import Control.Monad (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 Database.PostgreSQL.Simple as PG
 import qualified Database.SQLite.Simple as SQLite
 
+import Sproxy.Server.DB.DataFile ( DataFile(..), GroupMember(..),
+  GroupPrivilege(..), PrivilegeRule(..) )
 import qualified Sproxy.Logging as Log
 
 
 type Database = Pool SQLite.Connection
 
-data DataSource = PostgreSQL String -- | File FilePath
+data DataSource = PostgreSQL String | File FilePath
 
 {- TODO:
  - Hash remote tables and update the local only when the remote change
@@ -77,6 +80,12 @@ userGroups db email domain path method =
      , ":method" := method -- XXX case-sensitive by RFC2616
      ]
 
+-- FIXME short-cut for https://github.com/nurpax/sqlite-simple/issues/50
+-- FIXME nextRow is the only way to execute a prepared statement
+-- FIXME with bound parameters, but we don't expect any results.
+submit :: SQLite.Statement -> IO ()
+submit st = void (SQLite.nextRow st :: IO (Maybe [Int]))
+
 
 populate :: Database -> Maybe DataSource -> IO ()
 
@@ -87,6 +96,40 @@ populate db Nothing = do
     createGroupPrivilege c
     createPrivilegeRule c
 
+populate db (Just (File f)) = do
+  Log.info $ "db: reading " ++ show f
+  r <- decodeFileEither f
+  case r of
+    Left e   -> Log.error $ f ++ ": " ++ show e
+    Right df ->
+      withResource db $ \c -> SQLite.withTransaction c $ do
+        dropGroupMember c
+        createGroupMember c
+        SQLite.withStatement c
+          [q|INSERT INTO group_member("group", email) VALUES (?, ?)|]
+            $ \st -> mapM_ (\gm -> SQLite.withBind st
+                                  (gmGroup gm, toLower $ gmEmail gm)
+                                  (submit st)
+                          ) (groupMember df)
+
+        dropGroupPrivilege c
+        createGroupPrivilege c
+        SQLite.withStatement c
+          [q|INSERT INTO group_privilege("group", domain, privilege) VALUES (?, ?, ?)|]
+            $ \st -> mapM_ (\gp -> SQLite.withBind st
+                                  (gpGroup gp, toLower $ gpDomain gp, gpPrivilege gp)
+                                  (submit st)
+                          ) (groupPrivilege df)
+
+        dropPrivilegeRule c
+        createPrivilegeRule c
+        SQLite.withStatement c
+          [q|INSERT INTO privilege_rule(domain, privilege, path, method) VALUES (?, ?, ?, ?)|]
+            $ \st -> mapM_ (\pr -> SQLite.withBind st
+                                  (toLower $ prDomain pr, prPrivilege pr, prPath pr, prMethod pr)
+                                  (submit st)
+                          ) (privilegeRule df)
+
 -- XXX We keep only required minimum of the data, without any integrity check.
 -- XXX Integrity check should be done somewhere else, e. g. in the master PostgreSQL database,
 -- XXX or during importing the config file.
@@ -155,7 +198,6 @@ createGroupPrivilege c = SQLite.execute_ c [q|
   )
 |]
 
-
 dropPrivilegeRule :: SQLite.Connection -> IO ()
 dropPrivilegeRule c = SQLite.execute_ c "DROP TABLE IF EXISTS privilege_rule"
 
diff --git a/src/Sproxy/Server/DB/DataFile.hs b/src/Sproxy/Server/DB/DataFile.hs
new file mode 100644
index 0000000..efac923
--- /dev/null
+++ b/src/Sproxy/Server/DB/DataFile.hs
@@ -0,0 +1,69 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Sproxy.Server.DB.DataFile (
+  DataFile(..)
+, GroupMember(..)
+, GroupPrivilege(..)
+, PrivilegeRule(..)
+) where
+
+import Control.Applicative (empty)
+import Data.Aeson (FromJSON, parseJSON)
+import Data.Text (Text)
+import Data.Yaml (Value(Object), (.:))
+
+
+data DataFile = DataFile {
+  groupMember    :: [GroupMember]
+, groupPrivilege :: [GroupPrivilege]
+, privilegeRule  :: [PrivilegeRule]
+} deriving (Show)
+
+instance FromJSON DataFile where
+  parseJSON (Object m) = DataFile <$>
+        m .: "group_member"
+    <*> m .: "group_privilege"
+    <*> m .: "privilege_rule"
+  parseJSON _ = empty
+
+
+data GroupMember = GroupMember {
+  gmGroup :: Text
+, gmEmail :: Text
+} deriving (Show)
+
+instance FromJSON GroupMember where
+  parseJSON (Object m) = GroupMember <$>
+        m .: "group"
+    <*> m .: "email"
+  parseJSON _ = empty
+
+
+data GroupPrivilege = GroupPrivilege {
+  gpGroup     :: Text
+, gpDomain    :: Text
+, gpPrivilege :: Text
+} deriving (Show)
+
+instance FromJSON GroupPrivilege where
+  parseJSON (Object m) = GroupPrivilege <$>
+        m .: "group"
+    <*> m .: "domain"
+    <*> m .: "privilege"
+  parseJSON _ = empty
+
+
+data PrivilegeRule = PrivilegeRule {
+  prDomain    :: Text
+, prPrivilege :: Text
+, prPath      :: Text
+, prMethod    :: Text
+} deriving (Show)
+
+instance FromJSON PrivilegeRule where
+  parseJSON (Object m) = PrivilegeRule <$>
+        m .: "domain"
+    <*> m .: "privilege"
+    <*> m .: "path"
+    <*> m .: "method"
+  parseJSON _ = empty
+
-- 
cgit v1.2.3