packages feed

espial 0.0.10 → 0.0.11

raw patch · 6 files changed

+91/−4 lines, 6 filesdep +base64dep +cryptohash-sha256

Dependencies added: base64, cryptohash-sha256

Files

app/migration/Main.hs view
@@ -51,8 +51,12 @@                , privateDefault :: Maybe Bool                , archiveDefault :: Maybe Bool                , privacyLock :: Maybe Bool }+  | CreateApiKey { conn :: Text+                 , userName :: Text }   | DeleteUser { conn :: Text                , userName :: Text }+  | DeleteApiKey { conn :: Text+                 , userName :: Text }   | ImportBookmarks { conn :: Text                     , userName :: Text                     , bookmarkFile :: FilePath }@@ -92,12 +96,32 @@           (UniqueUserName userName)           (User userName hash' Nothing False False False)           [ UserPasswordHash P.=. hash'-          , UserApiToken P.=. Nothing           , UserPrivateDefault P.=. fromMaybe False privateDefault           , UserArchiveDefault P.=. fromMaybe False archiveDefault           , UserPrivacyLock P.=. fromMaybe False privacyLock           ]         pure () :: DB ()++    CreateApiKey {..} ->+      P.runSqlite conn $ do+        apiKey@(ApiKey plainKey) <- liftIO generateApiKey+        muser <- P.getBy (UniqueUserName userName)+        case muser of+          Nothing -> liftIO (print (userName ++ " not found"))+          Just (P.Entity uid _) -> do+            -- API key is only displayed once after creation,+            -- since it is stored in hashed form.+            let hashedKey = hashApiKey apiKey+            P.update uid  [ UserApiToken P.=. Just hashedKey ]+            liftIO $ print plainKey++    DeleteApiKey {..} ->+      P.runSqlite conn $ do+        muser <- P.getBy (UniqueUserName userName)+        case muser of+          Nothing -> liftIO (print (userName ++ " not found"))+          Just (P.Entity uid _) -> do+            P.update uid  [ UserApiToken P.=. Nothing ]      DeleteUser {..} ->       P.runSqlite conn $ do
changelog.md view
@@ -1,3 +1,6 @@+__v0.0.11__+add api key auth. +add CreateApiKey/DeleteApiKey commands to executable 'migration'   __v0.0.10__ update purescript&package versions
espial.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           espial-version:        0.0.10+version:        0.0.11 synopsis:       Espial is an open-source, web-based bookmarking server. description:    .                 Espial is an open-source, web-based bookmarking server.@@ -163,6 +163,7 @@       aeson >=1.4     , attoparsec     , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5+    , base64     , bcrypt >=0.0.8     , blaze-html >=0.9 && <1.0     , bytestring >=0.9 && <0.11@@ -173,6 +174,7 @@     , conduit >=1.0 && <2.0     , connection     , containers+    , cryptohash-sha256     , data-default     , directory >=1.1 && <1.4     , entropy@@ -273,6 +275,7 @@       aeson >=1.4     , attoparsec     , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5+    , base64     , bcrypt >=0.0.8     , blaze-html >=0.9 && <1.0     , bytestring >=0.9 && <0.11@@ -283,6 +286,7 @@     , conduit >=1.0 && <2.0     , connection     , containers+    , cryptohash-sha256     , data-default     , directory >=1.1 && <1.4     , entropy@@ -380,6 +384,7 @@       aeson >=1.4     , attoparsec     , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5+    , base64     , bcrypt >=0.0.8     , blaze-html >=0.9 && <1.0     , bytestring >=0.9 && <0.11@@ -390,6 +395,7 @@     , conduit >=1.0 && <2.0     , connection     , containers+    , cryptohash-sha256     , data-default     , directory >=1.1 && <1.4     , entropy@@ -493,6 +499,7 @@       aeson >=1.4     , attoparsec     , base >=4.8.2.0 && <4.9 || >=4.9.1.0 && <5+    , base64     , bcrypt >=0.0.8     , blaze-html >=0.9 && <1.0     , bytestring >=0.9 && <0.11@@ -503,6 +510,7 @@     , conduit >=1.0 && <2.0     , connection     , containers+    , cryptohash-sha256     , data-default     , directory >=1.1 && <1.4     , entropy
src/Foundation.hs view
@@ -17,6 +17,7 @@ import qualified Data.CaseInsensitive as CI import qualified Data.Text.Encoding as TE import qualified Yesod.Core.Unsafe as Unsafe+import qualified Network.Wai as Wai  data App = App     { appSettings    :: AppSettings@@ -67,8 +68,20 @@             else id      yesodMiddleware :: HandlerFor App res -> HandlerFor App res-    yesodMiddleware = customMiddleware . defaultYesodMiddleware . defaultCsrfMiddleware+    yesodMiddleware = customMiddleware . defaultYesodMiddleware . customCsrfMiddleware       where+        customCsrfMiddleware handler = do+          maybeRoute <- getCurrentRoute+          dontCheckCsrf <- case maybeRoute of+            -- `maybeAuthId` checks for the validity of the Authorization+            -- header anyway, but it is still a good idea to limit this+            -- flexibility to designated routes.+            -- For the time being, `AddR` is the only route that accepts an+            -- authentication token.+            Just AddR -> isJust <$> lookupHeader "Authorization"+            _ -> pure False+          (if dontCheckCsrf then id else defaultCsrfMiddleware) handler+         customMiddleware handler = do           addHeader "X-Frame-Options" "DENY"           yesod <- getYesod@@ -167,6 +180,24 @@   onLogout =     deleteSession userNameKey   redirectToReferer = const True+  maybeAuthId = do+    req <- waiRequest+    let mAuthHeader = lookup "Authorization" (Wai.requestHeaders req)+        extractKey = stripPrefix "ApiKey " . TE.decodeUtf8+    case mAuthHeader of+      Just authHeader ->+        case extractKey authHeader of+          Just apiKey -> do+            user <- liftHandler $ runDB $ getApiKeyUser (ApiKey apiKey)+            let userId = entityKey <$> user+            pure userId+          -- Since we disable CSRF middleware in the presence of Authorization+          -- header, we need to explicitly check for the validity of the header+          -- content. Otherwise, a dummy Authorization header with garbage input+          -- could be provided to circumvent CSRF token requirement, making the app+          -- vulnerable to CSRF attacks.+          Nothing -> pure Nothing+      _ -> defaultMaybeAuthId  instance YesodAuthPersist App 
src/Model.hs view
@@ -34,7 +34,7 @@   Id Int64   name Text   passwordHash BCrypt-  apiToken Text Maybe+  apiToken HashedApiKey Maybe   privateDefault Bool   archiveDefault Bool   privacyLock Bool@@ -158,6 +158,10 @@ getUserByName :: UserNameP -> DB (Maybe (Entity User)) getUserByName (UserNameP uname) =   selectFirst [UserName CP.==. uname] []++getApiKeyUser :: ApiKey -> DB (Maybe (Entity User))+getApiKeyUser apiKey =+  selectFirst [UserApiToken CP.==. Just (hashApiKey apiKey)] []  -- returns a list of pair of bookmark with tags merged into a string bookmarksTagsQuery
src/ModelCustom.hs view
@@ -12,6 +12,8 @@ import System.Entropy (getEntropy) import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString.Base64.URL as Base64Url+import qualified Crypto.Hash.SHA256 as SHA256  mkSlug :: Int -> IO T.Text mkSlug size =@@ -58,3 +60,18 @@ validatePasswordHash :: BCrypt -> T.Text -> Bool validatePasswordHash hash' pass = do   validatePassword (TE.encodeUtf8 (unBCrypt hash')) (TE.encodeUtf8 pass)++newtype ApiKey = ApiKey { unApiKey :: T.Text }++newtype HashedApiKey+  = HashedApiKey T.Text+  deriving stock (Eq, Ord, Show)+  deriving newtype (PersistField, PersistFieldSql, A.FromJSON, A.ToJSON)++generateApiKey :: IO ApiKey+generateApiKey = do+  bytes <- getEntropy 32+  pure $ ApiKey $ Base64Url.encodeBase64 bytes++hashApiKey :: ApiKey -> HashedApiKey+hashApiKey = HashedApiKey . TE.decodeUtf8 . Base64Url.encodeBase64' . SHA256.hash . TE.encodeUtf8 . unApiKey