packages feed

polysemy-account (empty) → 0.1.0.0

raw patch · 30 files changed

+1045/−0 lines, 30 filesdep +basedep +chronosdep +elocryptsetup-changed

Dependencies added: base, chronos, elocrypt, password, polysemy, polysemy-db, polysemy-plugin, prelate, random, sqel

Files

+ LICENSE view
@@ -0,0 +1,34 @@+Copyright (c) 2023 Torsten Schmits++Redistribution and use in source and binary forms, with or without modification, are permitted provided that the+following conditions are met:++  1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following+  disclaimer.+  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following+  disclaimer in the documentation and/or other materials provided with the distribution.++Subject to the terms and conditions of this license, each copyright holder and contributor hereby grants to those+receiving rights under this license a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except+for failure to satisfy the conditions of this license) patent license to make, have made, use, offer to sell, sell,+import, and otherwise transfer this software, where such license applies only to those patent claims, already acquired+or hereafter acquired, licensable by such copyright holder or contributor that are necessarily infringed by:++  (a) their Contribution(s) (the licensed copyrights of copyright holders and non-copyrightable additions of+  contributors, in source or binary form) alone; or+  (b) combination of their Contribution(s) with the work of authorship to which such Contribution(s) was added by such+  copyright holder or contributor, if, at the time the Contribution is added, such addition causes such combination to+  be necessarily infringed. The patent license shall not apply to any other combinations which include the Contribution.++Except as expressly stated above, no rights or licenses from any copyright holder or contributor is granted under this+license, whether expressly, by implication, estoppel or otherwise.++DISCLAIMER++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF+THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ changelog.md view
@@ -0,0 +1,1 @@+# Unreleased
+ lib/Polysemy/Account.hs view
@@ -0,0 +1,88 @@+-- | Description: Account management with Servant and Polysemy+module Polysemy.Account (+  -- * Effects+  Accounts,+  authenticate,+  generatePassword,+  create,+  finalizeCreate,+  addPassword,+  setStatus,+  byId,+  byName,+  update,+  Polysemy.Account.Effect.Accounts.privileges,+  updatePrivileges,+  all,+  allAuths,++  Password,+  hash,+  check,+  generate,++  -- * Interpreters+  interpretAccounts,+  interpretAccountsState,++  interpretPassword,+  interpretPasswordId,++  -- * Misc combinators+  register,+  login,+  unlockAccountName,++  -- * Data types+  Account (..),+  AuthedAccount (..),+  AccountsConfig (..),+  AccountsError (..),+  AccountsClientError (..),+  AccountCredentials (..),+  AccountName (..),+  RawPassword,+  rawPassword,+  GeneratedPassword (..),+  AccountStatus (..),+  Privilege (..),+  AccountP,+  AuthedAccountP,+  AuthToken (..),+  Port (Port),+) where++import Prelude hiding (all)++import Polysemy.Account.Accounts (login, register, unlockAccountName)+import Polysemy.Account.Data.Account (Account (..), AccountP)+import Polysemy.Account.Data.AccountCredentials (AccountCredentials (..))+import Polysemy.Account.Data.AccountName (AccountName (..))+import Polysemy.Account.Data.AccountStatus (AccountStatus (..))+import Polysemy.Account.Data.AccountsConfig (AccountsConfig (..))+import Polysemy.Account.Data.AccountsError (AccountsClientError (..), AccountsError (..))+import Polysemy.Account.Data.AuthToken (AuthToken (..))+import Polysemy.Account.Data.AuthedAccount (AuthedAccount (..), AuthedAccountP)+import Polysemy.Account.Data.GeneratedPassword (GeneratedPassword (..))+import Polysemy.Account.Data.Port (Port (..))+import Polysemy.Account.Data.Privilege (Privilege (..))+import Polysemy.Account.Data.RawPassword (RawPassword (..), rawPassword)+import Polysemy.Account.Effect.Accounts (+  Accounts,+  addPassword,+  all,+  allAuths,+  authenticate,+  byId,+  byName,+  create,+  finalizeCreate,+  generatePassword,+  privileges,+  setStatus,+  update,+  updatePrivileges,+  )+import Polysemy.Account.Effect.Password (Password, check, generate, hash)+import Polysemy.Account.Interpreter.Accounts (interpretAccounts, interpretAccountsState)+import Polysemy.Account.Interpreter.Password (interpretPassword, interpretPasswordId)
+ lib/Polysemy/Account/Accounts.hs view
@@ -0,0 +1,46 @@+-- | Description: Misc combinators+module Polysemy.Account.Accounts where++import Sqel (Uid (Uid))++import Polysemy.Account.Data.Account (Account (Account))+import Polysemy.Account.Data.AccountAuth (AccountAuth (AccountAuth))+import Polysemy.Account.Data.AccountCredentials (AccountCredentials (AccountCredentials))+import Polysemy.Account.Data.AccountName (AccountName)+import Polysemy.Account.Data.AccountStatus (AccountStatus (Active))+import Polysemy.Account.Data.AccountsError (AccountsError)+import Polysemy.Account.Data.AuthedAccount (AuthedAccount (AuthedAccount))+import qualified Polysemy.Account.Effect.Accounts as Accounts+import Polysemy.Account.Effect.Accounts (Accounts)++-- | Convenience function for unlocking the account matching the given name.+unlockAccountName ::+  Members [Accounts i p, Stop AccountsError] r =>+  AccountName ->+  Sem r ()+unlockAccountName name = do+  Uid i _ <- Accounts.byName name+  Accounts.setStatus i Active++-- | Authenticate the given credentials against the storage backend and return the matched account's information.+login ::+  Member (Accounts i p) r =>+  AccountCredentials ->+  Sem r (AuthedAccount i p)+login (AccountCredentials username password) = do+  Uid authId (AccountAuth accountId _ _ _) <- Accounts.authenticate username password+  Uid _ (Account name status privs) <- Accounts.byId accountId+  pure (AuthedAccount accountId authId name status privs)++-- | Register an account with the given credentials.+--+-- Create the account in the storage backend, hash the password and store it, then mark the account as created.+register ::+  Member (Accounts i p) r =>+  AccountCredentials ->+  Sem r (AuthedAccount i p)+register (AccountCredentials username password) = do+  Uid accountId _ <- Accounts.create username+  Uid authId (AccountAuth _ _ _ _) <- Accounts.addPassword accountId password Nothing+  Uid _ (Account name status privs) <- Accounts.finalizeCreate accountId+  pure (AuthedAccount accountId authId name status privs)
+ lib/Polysemy/Account/Data/Account.hs view
@@ -0,0 +1,20 @@+-- | Description: Account data type+module Polysemy.Account.Data.Account where++import Polysemy.Account.Data.AccountName (AccountName)+import Polysemy.Account.Data.AccountStatus (AccountStatus)+import Polysemy.Account.Data.Privilege (Privilege)++-- | A basic user account, consisting of a name, activation status, and an arbitrary privilege type.+data Account p =+  Account {+     name :: AccountName,+     status :: AccountStatus,+     privileges :: p+  }+  deriving stock (Eq, Show, Generic)++json ''Account++-- | Convenience alias for using the default privilege type with 'Account'.+type AccountP = Account [Privilege]
+ lib/Polysemy/Account/Data/AccountAuth.hs view
@@ -0,0 +1,21 @@+-- | Description: Account auth data type+module Polysemy.Account.Data.AccountAuth where++import Chronos (Datetime)++import Polysemy.Account.Data.AccountAuthDescription (AccountAuthDescription)+import Polysemy.Account.Data.HashedPassword (HashedPassword)++-- | A hashed password associated with an account.+data AccountAuth i =+  AccountAuth {+    -- | The account ID belonging to this password.+    account :: i,+    -- | A description of the password.+    description :: AccountAuthDescription,+    -- | A password hash.+    password :: HashedPassword,+    -- | The date at which the password expires.+    expiry :: Maybe Datetime+  }+  deriving stock (Eq, Show, Generic)
+ lib/Polysemy/Account/Data/AccountAuthDescription.hs view
@@ -0,0 +1,11 @@+-- | Description: Account auth description data type+module Polysemy.Account.Data.AccountAuthDescription where++-- | A freeform text used to describe the nature of an 'Polysemy.Account.AccountAuth', like whether it's a generated or+-- user-supplied password.+newtype AccountAuthDescription =+  AccountAuthDescription { unAccountAuthDescription :: Text }+  deriving stock (Eq, Show, Generic)+  deriving newtype (IsString, Ord)++json ''AccountAuthDescription
+ lib/Polysemy/Account/Data/AccountByName.hs view
@@ -0,0 +1,13 @@+-- | Description: Account by name query data type+module Polysemy.Account.Data.AccountByName where++import Polysemy.Account.Data.AccountName (AccountName)++-- | Query payload for looking up accounts by name.+data AccountByName =+  AccountByName { name :: AccountName }+  deriving stock (Eq, Show, Generic, Ord)++instance IsString AccountByName where+  fromString =+    AccountByName . fromString
+ lib/Polysemy/Account/Data/AccountCredentials.hs view
@@ -0,0 +1,17 @@+-- | Description: Account credentials data type+module Polysemy.Account.Data.AccountCredentials where++import Polysemy.Account.Data.AccountName (AccountName)+import Polysemy.Account.Data.RawPassword (RawPassword)++-- | User-supplied credentials for login or registration.+data AccountCredentials =+  AccountCredentials {+    -- | Account name.+    username :: AccountName,+    -- | Account password.+    password :: RawPassword+  }+  deriving stock (Eq, Show)++json ''AccountCredentials
+ lib/Polysemy/Account/Data/AccountName.hs view
@@ -0,0 +1,10 @@+-- | Description: Account name data type+module Polysemy.Account.Data.AccountName where++-- | The name of an account.+newtype AccountName =+  AccountName { unAccountName :: Text }+  deriving stock (Eq, Show, Generic)+  deriving newtype (IsString, Ord)++json ''AccountName
+ lib/Polysemy/Account/Data/AccountStatus.hs view
@@ -0,0 +1,19 @@+-- | Description: Account status data type+module Polysemy.Account.Data.AccountStatus where++-- | Basic account status.+data AccountStatus =+  -- | The account was added to storage, but not processed fully.+  Creating+  |+  -- | The account was fully created, but not approved by an admin.+  Pending+  |+  -- | The account is fully operational.+  Active+  |+  -- | An admin has disabled the account.+  Locked+  deriving stock (Eq, Show, Generic)++json ''AccountStatus
+ lib/Polysemy/Account/Data/AccountsConfig.hs view
@@ -0,0 +1,36 @@+{-# language NoFieldSelectors #-}++-- | Description: Config data type for the effect 'Polysemy.Account.Accounts'.+module Polysemy.Account.Data.AccountsConfig where++import Polysemy.Account.Data.Privilege (Privilege)++-- | The configuration for the interpreter for 'Polysemy.Account.Accounts'.+--+-- The defaults, when using 'Privilege', are:+--+-- - Length 20+-- - Don't activate accounts right away+-- - 'Polysemy.Account.Web' privileges+data AccountsConfig p =+  AccountsConfig {+    -- | Length of generated passwords.+    passwordLength :: Word,+    -- | Whether new accounts should immediately be marked as active rather than pending, allowing login.+    initActive :: Bool,+    -- | The privileges assigned to a new account.+    defaultPrivileges :: p+  }+  deriving stock (Eq, Show, Generic)++json ''AccountsConfig++-- | Convenience alias for using the default privilege type with 'AccountsConfig'.+type AccountsConfigP = AccountsConfig [Privilege]++instance Default p => Default (AccountsConfig p) where+  def = AccountsConfig {+    passwordLength = 20,+    initActive = False,+    defaultPrivileges = def+  }
+ lib/Polysemy/Account/Data/AccountsError.hs view
@@ -0,0 +1,28 @@+-- | Description: Error data types for the effect 'Polysemy.Account.Accounts'.+module Polysemy.Account.Data.AccountsError where++-- | Errors that indicate invalid client-supplied information.+data AccountsClientError =+  -- | No account was found for the given ID.+  NoAccountId+  |+  -- | Credentials did not match stored auth data.+  InvalidAuth+  |+  -- | No account was found for the given name.+  NoAccountName+  |+  -- | Name given for registration already exists in storage.+  Conflict+  deriving stock (Eq, Show)++json ''AccountsClientError++-- | Errors produced by the effect 'Polysemy.Account.Accounts'.+data AccountsError =+  -- | Errors that indicate invalid client-supplied information.+  Client AccountsClientError+  |+  -- | Error indicating storage backend failure.+  Internal Text+  deriving stock (Eq, Show)
+ lib/Polysemy/Account/Data/AuthForAccount.hs view
@@ -0,0 +1,7 @@+-- | Description: Account by name query data type+module Polysemy.Account.Data.AuthForAccount where++-- | Query payload for looking up auth info by account ID.+data AuthForAccount i =+  AuthForAccount { account :: i }+  deriving stock (Eq, Show, Generic, Ord)
+ lib/Polysemy/Account/Data/AuthToken.hs view
@@ -0,0 +1,9 @@+-- | Description: Auth token data type+module Polysemy.Account.Data.AuthToken where++-- | An auth token, used by the JWT tools in @polysemy-account-api@.+newtype AuthToken =+  AuthToken { unAuthToken :: Text }+  deriving stock (Eq, Show)++json ''AuthToken
+ lib/Polysemy/Account/Data/AuthedAccount.hs view
@@ -0,0 +1,22 @@+-- | Description: Authed account data type+module Polysemy.Account.Data.AuthedAccount where++import Polysemy.Account.Data.AccountName (AccountName)+import Polysemy.Account.Data.AccountStatus (AccountStatus)+import Polysemy.Account.Data.Privilege (Privilege)++-- | An account an the ID of the password used to authenticate it.+data AuthedAccount i p =+  AuthedAccount {+    id :: i,+    authId :: i,+    name :: AccountName,+    status :: AccountStatus,+    privileges :: p+  }+  deriving stock (Eq, Show, Generic)++json ''AuthedAccount++-- | Convenience alias for using the default privilege type with 'AuthedAccount'.+type AuthedAccountP i = AuthedAccount i [Privilege]
+ lib/Polysemy/Account/Data/GeneratedPassword.hs view
@@ -0,0 +1,11 @@+-- | Description: Generated password data type+module Polysemy.Account.Data.GeneratedPassword where++-- | A password that was generated, intended to be shown to the user, and therefore permitted to be 'show'n, as opposed+-- to 'Polysemy.Account.RawPassword'.+newtype GeneratedPassword =+  GeneratedPassword { unGeneratedPassword :: Text }+  deriving stock (Eq, Show, Generic)+  deriving newtype (IsString, Ord)++json ''GeneratedPassword
+ lib/Polysemy/Account/Data/HashedPassword.hs view
@@ -0,0 +1,10 @@+-- | Description: Hasned password data type+module Polysemy.Account.Data.HashedPassword where++-- | An internally used type containing a hash produced by "Data.Password".+newtype HashedPassword =+  HashedPassword { unAccountPassword :: Text }+  deriving stock (Eq, Show, Generic)+  deriving newtype (IsString, Ord)++json ''HashedPassword
+ lib/Polysemy/Account/Data/Port.hs view
@@ -0,0 +1,10 @@+-- | Description: Port data type+module Polysemy.Account.Data.Port where++-- | An API port, used by the Servant tools in @polysemy-account-api@.+newtype Port =+  Port { unPort :: Word }+  deriving stock (Eq, Show)+  deriving newtype (Num, Ord, Enum, Real, Integral, Read)++json ''Port
+ lib/Polysemy/Account/Data/Privilege.hs view
@@ -0,0 +1,18 @@+{-# options_haddock prune #-}++-- | Description: Privilege data type+module Polysemy.Account.Data.Privilege where++-- | The stock privilege type, used only for admin endpoint authorization in @polysemy-account-api@.+data Privilege =+  Web+  |+  Api+  |+  Admin+  deriving stock (Eq, Show, Generic)++json ''Privilege++instance {-# overlapping #-} Default [Privilege] where+  def = [Web]
+ lib/Polysemy/Account/Data/RawPassword.hs view
@@ -0,0 +1,19 @@+-- | Description: Raw password data type+module Polysemy.Account.Data.RawPassword where++import qualified Text.Show as Show++-- | A clear text password, supplied by the user or generated.+newtype RawPassword =+  UnsafeRawPassword { unRawPassword :: Text }+  deriving stock (Eq)++instance Show RawPassword where+  show _ =+    "--> raw password <--"++json ''RawPassword++-- | Construct a ''RawPassword'.+rawPassword :: Text -> RawPassword+rawPassword = UnsafeRawPassword
+ lib/Polysemy/Account/Effect/Accounts.hs view
@@ -0,0 +1,56 @@+-- | Description: Accounts effect+module Polysemy.Account.Effect.Accounts where++import Chronos (Datetime)+import Prelude hiding (all)+import Sqel (Uid)++import Polysemy.Account.Data.Account (Account)+import Polysemy.Account.Data.AccountAuth (AccountAuth)+import Polysemy.Account.Data.AccountName (AccountName)+import Polysemy.Account.Data.AccountStatus (AccountStatus)+import Polysemy.Account.Data.AuthedAccount (AuthedAccount)+import Polysemy.Account.Data.GeneratedPassword (GeneratedPassword)+import Polysemy.Account.Data.Privilege (Privilege)+import Polysemy.Account.Data.RawPassword (RawPassword)++-- | This effect provides common operations for account and password management.+--+-- The first parameter is the ID type for both accounts and authentication data, which might be 'Data.UUID.UUID' or+-- @Int@.+--+-- The second parameter encodes an accounts basic privileges, mainly used for API authorization.+data Accounts i p :: Effect where+  -- | Check credentials against the storage backend.+  Authenticate :: AccountName -> RawPassword -> Accounts i p m (Uid i (AccountAuth i))+  -- | Generate a fresh password.+  GeneratePassword :: i -> Maybe Datetime -> Accounts i p m GeneratedPassword+  -- | Add an account to the storage backend, without authentication.+  Create :: AccountName -> Accounts i p m (Uid i (Account p))+  -- | Mark an account as fully created.+  FinalizeCreate :: i -> Accounts i p m (Uid i (Account p))+  -- | Associate an account with a new password, with optional expiry time.+  AddPassword :: i -> RawPassword -> Maybe Datetime -> Accounts i p m (Uid i (AccountAuth i))+  -- | Update the status of an account.+  SetStatus :: i -> AccountStatus -> Accounts i p m ()+  -- | Look up an account by its ID.+  ById :: i -> Accounts i p m (Uid i (Account p))+  -- | Look up an account by its name.+  ByName :: AccountName -> Accounts i p m (Uid i (Account p))+  -- | Look up an account and return its auth info.+  Authed :: i -> Accounts i p m (AuthedAccount i p)+  -- | Overwrite an existing account.+  Update :: Uid i (Account p) -> Accounts i p m ()+  -- | Look up an account's privileges.+  Privileges :: i -> Accounts i p m p+  -- | Update an account's privileges.+  UpdatePrivileges :: i -> (p -> p) -> Accounts i p m ()+  -- | Fetch all accounts.+  All :: Accounts i p m [Uid i (Account p)]+  -- | Fetch all auth records.+  AllAuths :: Accounts i p m [Uid i (AccountAuth i)]++makeSem ''Accounts++-- | Convenience alias for 'Accounts' using the default 'Privilege' type.+type AccountsP i = Accounts i [Privilege]
+ lib/Polysemy/Account/Effect/Password.hs view
@@ -0,0 +1,17 @@+-- | Description: Password effect+module Polysemy.Account.Effect.Password where++import Polysemy.Account.Data.GeneratedPassword (GeneratedPassword)+import Polysemy.Account.Data.HashedPassword (HashedPassword)+import Polysemy.Account.Data.RawPassword (RawPassword)++-- | This effect provides password hashing, validation, and generation.+data Password :: Effect where+  -- | Hash a clear text password.+  Hash :: RawPassword -> Password m HashedPassword+  -- | Validate a password against a hash.+  Check :: RawPassword -> HashedPassword -> Password m Bool+  -- | Generate a new clear text password of the specified length.+  Generate :: Word -> Password m GeneratedPassword++makeSem ''Password
+ lib/Polysemy/Account/Interpreter/AccountByName.hs view
@@ -0,0 +1,36 @@+{-# options_haddock prune #-}++-- | Description: Interpreter for the query for an account by name+module Polysemy.Account.Interpreter.AccountByName where++import Polysemy.Db (DbError, PureStore, Query, Store, interpretQueryStoreConc)+import Sqel (Uid (Uid))++import Polysemy.Account.Data.Account (Account (Account))+import Polysemy.Account.Data.AccountByName (AccountByName (AccountByName))++match ::+  AccountByName ->+  Uid i (Account p) ->+  Maybe (Uid i (Account p))+match (AccountByName name) a@(Uid _ (Account accountName _ _))+  | name == accountName = Just a+  | otherwise = Nothing++type AccountQuery i p =+  [+    Query AccountByName (Maybe (Uid i (Account p))) !! DbError,+    Store i (Account p) !! DbError,+    AtomicState (PureStore i (Account p))+  ]++-- | Interpret @'Query' 'AccountByName'@ and the corresponding 'Store' in an 'AtomicState'.+interpretAccountByNameState ::+  ∀ i p r .+  Ord i =>+  Show i =>+  Member (Embed IO) r =>+  [Uid i (Account p)] ->+  InterpretersFor (AccountQuery i p) r+interpretAccountByNameState initial =+  interpretQueryStoreConc match initial
+ lib/Polysemy/Account/Interpreter/Accounts.hs view
@@ -0,0 +1,267 @@+{-# options_haddock prune #-}++-- | Description: Interpreters for 'Accounts' and 'Password'+module Polysemy.Account.Interpreter.Accounts where++import Chronos (Datetime)+import Polysemy.Db (Id, Query, Store, newId)+import qualified Polysemy.Db.Effect.Query as Query+import qualified Polysemy.Db.Effect.Store as Store+import Sqel (Uid (Uid))++import Polysemy.Account.Data.Account (Account (Account))+import Polysemy.Account.Data.AccountAuth (AccountAuth (AccountAuth))+import Polysemy.Account.Data.AccountAuthDescription (AccountAuthDescription)+import Polysemy.Account.Data.AccountByName (AccountByName (AccountByName))+import Polysemy.Account.Data.AccountName (AccountName)+import qualified Polysemy.Account.Data.AccountStatus as AccountStatus+import Polysemy.Account.Data.AccountStatus (AccountStatus)+import qualified Polysemy.Account.Data.AccountsConfig as AccountsConfig+import Polysemy.Account.Data.AccountsConfig (AccountsConfig (AccountsConfig))+import Polysemy.Account.Data.AccountsError (+  AccountsClientError (Conflict, InvalidAuth, NoAccountId, NoAccountName),+  AccountsError (Client, Internal),+  )+import Polysemy.Account.Data.AuthForAccount (AuthForAccount (AuthForAccount))+import Polysemy.Account.Data.AuthedAccount (AuthedAccount (AuthedAccount))+import Polysemy.Account.Data.GeneratedPassword (GeneratedPassword (GeneratedPassword))+import Polysemy.Account.Data.RawPassword (RawPassword (UnsafeRawPassword))+import Polysemy.Account.Effect.Accounts (Accounts (..))+import qualified Polysemy.Account.Effect.Password as Password+import Polysemy.Account.Effect.Password (Password)+import Polysemy.Account.Interpreter.AccountByName (interpretAccountByNameState)+import Polysemy.Account.Interpreter.AuthForAccount (interpretAuthForAccountState)+import Polysemy.Account.Interpreter.Password (interpretPasswordId)++dbError ::+  ∀ eff e r .+  Show e =>+  Members [eff !! e, Stop AccountsError] r =>+  InterpreterFor eff r+dbError =+  resumeHoist (Internal . show)++storeError ::+  ∀ a e i r .+  Show e =>+  Members [Store i a !! e, Stop AccountsError] r =>+  InterpreterFor (Store i a) r+storeError =+  resumeHoist (Internal . show)++queryError ::+  ∀ a q e r .+  Show e =>+  Members [Query q a !! e, Stop AccountsError] r =>+  InterpreterFor (Query q a) r+queryError =+  resumeHoist (Internal . show)++config ::+  Show e =>+  Members [Reader (AccountsConfig p) !! e, Stop AccountsError] r =>+  Sem r (AccountsConfig p)+config =+  dbError ask++byId ::+  ∀ i r a .+  Members [Store i a, Stop AccountsError] r =>+  i ->+  Sem r (Uid i a)+byId accountId =+  stopNote (Client NoAccountId) =<< Store.fetch accountId++byName ::+  ∀ i r a .+  Members [Query AccountByName (Maybe (Uid i a)), Stop AccountsError] r =>+  AccountName ->+  Sem r (Uid i a)+byName name =+  stopNote (Client NoAccountName) =<< Query.query (AccountByName name)++authedAccount ::+  ∀ i p r .+  Members [Store i (Account p), Store i (AccountAuth i), Stop AccountsError] r =>+  i ->+  Sem r (AuthedAccount i p)+authedAccount authId = do+  aa <- Store.fetch authId+  Uid _ (AccountAuth accountId _ _ _) <- stopNote (Client InvalidAuth) aa+  Uid _ (Account name status privs) <- byId accountId+  pure (AuthedAccount accountId authId name status privs)++-- TODO see if Query for AccountAuth can be used without Uid, extracting it in the interpreter+authenticate ::+  Show e =>+  Member (Query AccountByName (Maybe (Uid i a)) !! e) r =>+  Member (Query (AuthForAccount i) [Uid i (AccountAuth i)] !! e) r =>+  Members [Stop AccountsError, Password] r =>+  AccountName ->+  RawPassword ->+  Sem r (Uid i (AccountAuth i))+authenticate name password = do+  Uid id' _ <- notFound =<< queryError (Query.query (AccountByName name))+  auths <- queryError (Query.query (AuthForAccount id'))+  invalid =<< findM check auths+  where+    notFound =+      stopNote (Client NoAccountName)+    check (Uid _ (AccountAuth _ _ hash _)) =+      Password.check password hash+    invalid =+      stopNote (Client InvalidAuth)++privileges ::+  ∀ i p r .+  Members [Store i (Account p), Stop AccountsError] r =>+  i ->+  Sem r p+privileges i =+  Store.fetch i >>= \case+    Just (Uid _ (Account _ _ privs)) -> pure privs+    Nothing -> stop (Client NoAccountId)++addPassword ::+  Members [Password, Store i (AccountAuth i), Id i, Stop AccountsError] r =>+  AccountAuthDescription ->+  i ->+  RawPassword ->+  Maybe Datetime ->+  Sem r (Uid i (AccountAuth i))+addPassword desc accountId password expiry = do+  hashedPassword <- Password.hash password+  authId <- newId+  let auth = Uid authId (AccountAuth accountId desc hashedPassword expiry)+  auth <$ Store.insert auth++generatePassword ::+  Show e =>+  Members [Password, Store i (AccountAuth i), Reader (AccountsConfig p) !! e, Id i, Stop AccountsError] r =>+  i ->+  Maybe Datetime ->+  Sem r GeneratedPassword+generatePassword accountId expiry = do+  AccountsConfig {..} <- config+  pw@(GeneratedPassword raw) <- Password.generate passwordLength+  coerce pw <$ addPassword "auth token" accountId (UnsafeRawPassword raw) expiry++-- | Fail if the account name is already present in the store.+-- If the account status is `Creating', however, a previous attempt has failed critically and the account can be+-- overwritten.+deletePreviousFailure ::+  Members [Store i (Account p), Stop AccountsError] r =>+  Uid i (Account p) ->+  Sem r ()+deletePreviousFailure (Uid i (Account _ AccountStatus.Creating _)) =+  void (Store.delete i)+deletePreviousFailure _ =+  stop (Client Conflict)++create ::+  ∀ i p e r .+  Members [Store i (Account p), Query AccountByName (Maybe (Uid i (Account p))), Reader (AccountsConfig p) !! e] r =>+  Members [Id i, Stop AccountsError] r =>+  AccountName ->+  p ->+  Sem r (Uid i (Account p))+create name privs = do+  traverse_ deletePreviousFailure =<< Query.query (AccountByName name)+  accountId <- newId+  let account = Uid accountId (Account name AccountStatus.Creating privs)+  account <$ Store.upsert account++finishCreate ::+  ∀ i p r .+  Members [Store i (Account p), Stop AccountsError] r =>+  Bool ->+  i ->+  Sem r (Uid i (Account p))+finishCreate active accountId = do+  account :: Uid i (Account p) <- stopNote (Internal "Account absent after password creation") =<< Store.fetch accountId+  let updatedAccount = account & #payload . #status .~ status+  updatedAccount <$ Store.upsert (account & #payload . #status .~ status)+  where+    status = if active then AccountStatus.Active else AccountStatus.Pending++setStatus ::+  Members [Store i (Account p), Stop AccountsError] r =>+  i ->+  AccountStatus ->+  Sem r ()+setStatus accountId status = do+  account <- stopNote (Client NoAccountId) =<< Store.fetch accountId+  Store.upsert (account & #payload . #status .~ status)++updatePrivileges ::+  ∀ i p e r .+  Show e =>+  Members [Store i (Account p) !! e, Stop AccountsError] r =>+  i ->+  (p -> p) ->+  Sem r ()+updatePrivileges i f =+  dbError (Store.fetch i) >>= \case+    Just account ->+      dbError (Store.upsert (account & #payload . #privileges %~ f))+    Nothing ->+      stop (Client NoAccountId)++-- | Interpret 'Accounts' using 'Store' and 'Query' from [Polysemy.Db]("Polysemy.Db") as the storage backend.+interpretAccounts ::+  ∀ e i p r .+  Show e =>+  Member (Query AccountByName (Maybe (Uid i (Account p))) !! e) r =>+  Member (Query (AuthForAccount i) [Uid i (AccountAuth i)] !! e) r =>+  Members [Password, Store i (Account p) !! e, Store i (AccountAuth i) !! e, Reader (AccountsConfig p) !! e, Id i] r =>+  InterpreterFor (Accounts i p !! AccountsError) r+interpretAccounts =+  interpretResumable \case+    Authenticate name password ->+      authenticate name password+    GeneratePassword accountId expiry ->+      storeError (generatePassword accountId expiry)+    Create name -> do+      AccountsConfig {..} <- config+      queryError (storeError (create name defaultPrivileges))+    FinalizeCreate accountId -> do+      AccountsConfig {..} <- config+      storeError (finishCreate initActive accountId)+    AddPassword accountId password expiry ->+      storeError (addPassword "user login" accountId password expiry)+    SetStatus accountId status ->+      storeError (setStatus accountId status)+    ById accountId ->+      storeError (byId accountId)+    ByName name ->+      queryError (byName name)+    Authed authId ->+      storeError (storeError @(Account _) (authedAccount authId))+    Update account ->+      storeError (Store.upsert account)+    Privileges i ->+      storeError (privileges i)+    UpdatePrivileges i f ->+      updatePrivileges i f+    All ->+      storeError Store.fetchAll+    AllAuths ->+      storeError Store.fetchAll++-- | Interpret 'Accounts' and 'Password' using 'AtomicState' as storage backend.+interpretAccountsState ::+  ∀ i p r .+  Ord i =>+  Show i =>+  Members [Log, Id i, Embed IO] r =>+  AccountsConfig p ->+  [Uid i (Account p)] ->+  [Uid i (AccountAuth i)] ->+  InterpretersFor [Accounts i p !! AccountsError, Password] r+interpretAccountsState conf accounts auths =+  interpretPasswordId .+  raiseResumable (runReader conf) .+  interpretAccountByNameState accounts .+  interpretAuthForAccountState auths .+  interpretAccounts .+  insertAt @1
+ lib/Polysemy/Account/Interpreter/AuthForAccount.hs view
@@ -0,0 +1,35 @@+-- | Description: Interpreter for the query for auth info by account ID+module Polysemy.Account.Interpreter.AuthForAccount where++import Polysemy.Db (DbError, PureStore, Query, Store, interpretQueryStoreConc)+import Sqel (Uid (Uid))++import Polysemy.Account.Data.AccountAuth (AccountAuth (AccountAuth))+import Polysemy.Account.Data.AuthForAccount (AuthForAccount (AuthForAccount))++match ::+  Eq i =>+  AuthForAccount i ->+  Uid i (AccountAuth i) ->+  Maybe (Uid i (AccountAuth i))+match (AuthForAccount queryId) a@(Uid _ (AccountAuth accountId _ _ _))+  | queryId == accountId = Just a+  | otherwise = Nothing++type AuthQuery i p =+  [+    Query (AuthForAccount i) [Uid i (AccountAuth i)] !! DbError,+    Store i (AccountAuth i) !! DbError,+    AtomicState (PureStore i (AccountAuth i))+  ]++-- | Interpret @'Query' 'AccountAuth'@ and the corresponding 'Store' in an 'AtomicState'.+interpretAuthForAccountState ::+  ∀ i r p .+  Ord i =>+  Show i =>+  Member (Embed IO) r =>+  [Uid i (AccountAuth i)] ->+  InterpretersFor (AuthQuery i p) r+interpretAuthForAccountState initial =+  interpretQueryStoreConc match initial
+ lib/Polysemy/Account/Interpreter/Password.hs view
@@ -0,0 +1,43 @@+-- | Description: Interpreters for 'Password'+module Polysemy.Account.Interpreter.Password where++import Data.Elocrypt (genOptions, genPassword)+import Data.Password.Argon2 (+  PasswordCheck (PasswordCheckSuccess),+  PasswordHash (PasswordHash),+  checkPassword,+  hashPassword,+  mkPassword,+  )+import qualified Data.Text as Text+import System.Random (getStdGen)++import Polysemy.Account.Data.GeneratedPassword (GeneratedPassword (GeneratedPassword))+import Polysemy.Account.Data.HashedPassword (HashedPassword (HashedPassword))+import Polysemy.Account.Data.RawPassword (RawPassword (UnsafeRawPassword))+import Polysemy.Account.Effect.Password (Password (..))++-- | Interpret 'Password' trivially, not performing any hashing and generating sequences of asterisks.+interpretPasswordId ::+  InterpreterFor Password r+interpretPasswordId =+  interpret \case+    Hash (UnsafeRawPassword pw) ->+      pure (HashedPassword pw)+    Check (UnsafeRawPassword pw) (HashedPassword apw) ->+      pure (pw == apw)+    Generate len ->+      pure (GeneratedPassword (Text.replicate (fromIntegral len) "*"))++-- | Interpret 'Password' using the Argon2 algorithm and "Data.Elocrypt"-generated passwords.+interpretPassword ::+  Member (Embed IO) r =>+  InterpreterFor Password r+interpretPassword =+  interpret \case+    Hash (UnsafeRawPassword pw) ->+      coerce <$> hashPassword (mkPassword pw)+    Check (UnsafeRawPassword pw) (HashedPassword apw) ->+      pure (PasswordCheckSuccess == checkPassword (mkPassword pw) (PasswordHash apw))+    Generate len ->+      GeneratedPassword . toText . fst . genPassword (fromIntegral len) genOptions <$> embed getStdGen
+ polysemy-account.cabal view
@@ -0,0 +1,132 @@+cabal-version: 2.2++-- This file has been generated from package.yaml by hpack version 0.35.0.+--+-- see: https://github.com/sol/hpack++name:           polysemy-account+version:        0.1.0.0+synopsis:       Account management with Servant and Polysemy+description:    See https://hackage.haskell.org/package/polysemy-account/docs/Polysemy-Account.html+category:       Web+author:         Torsten Schmits+maintainer:     hackage@tryp.io+copyright:      2023 Torsten Schmits+license:        BSD-2-Clause-Patent+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    changelog.md+    readme.md++library+  exposed-modules:+      Polysemy.Account+      Polysemy.Account.Accounts+      Polysemy.Account.Data.Account+      Polysemy.Account.Data.AccountAuth+      Polysemy.Account.Data.AccountAuthDescription+      Polysemy.Account.Data.AccountByName+      Polysemy.Account.Data.AccountCredentials+      Polysemy.Account.Data.AccountName+      Polysemy.Account.Data.AccountsConfig+      Polysemy.Account.Data.AccountsError+      Polysemy.Account.Data.AccountStatus+      Polysemy.Account.Data.AuthedAccount+      Polysemy.Account.Data.AuthForAccount+      Polysemy.Account.Data.AuthToken+      Polysemy.Account.Data.GeneratedPassword+      Polysemy.Account.Data.HashedPassword+      Polysemy.Account.Data.Port+      Polysemy.Account.Data.Privilege+      Polysemy.Account.Data.RawPassword+      Polysemy.Account.Effect.Accounts+      Polysemy.Account.Effect.Password+      Polysemy.Account.Interpreter.AccountByName+      Polysemy.Account.Interpreter.Accounts+      Polysemy.Account.Interpreter.AuthForAccount+      Polysemy.Account.Interpreter.Password+  hs-source-dirs:+      lib+  default-extensions:+      StandaloneKindSignatures+      OverloadedRecordDot+      NoFieldSelectors+      AllowAmbiguousTypes+      ApplicativeDo+      BangPatterns+      BinaryLiterals+      BlockArguments+      ConstraintKinds+      DataKinds+      DefaultSignatures+      DeriveAnyClass+      DeriveDataTypeable+      DeriveFoldable+      DeriveFunctor+      DeriveGeneric+      DeriveLift+      DeriveTraversable+      DerivingStrategies+      DerivingVia+      DisambiguateRecordFields+      DoAndIfThenElse+      DuplicateRecordFields+      EmptyCase+      EmptyDataDecls+      ExistentialQuantification+      FlexibleContexts+      FlexibleInstances+      FunctionalDependencies+      GADTs+      GeneralizedNewtypeDeriving+      InstanceSigs+      KindSignatures+      LambdaCase+      LiberalTypeSynonyms+      MultiParamTypeClasses+      MultiWayIf+      NamedFieldPuns+      OverloadedLabels+      OverloadedLists+      OverloadedStrings+      PackageImports+      PartialTypeSignatures+      PatternGuards+      PatternSynonyms+      PolyKinds+      QuantifiedConstraints+      QuasiQuotes+      RankNTypes+      RecordWildCards+      RecursiveDo+      RoleAnnotations+      ScopedTypeVariables+      StandaloneDeriving+      TemplateHaskell+      TupleSections+      TypeApplications+      TypeFamilies+      TypeFamilyDependencies+      TypeOperators+      TypeSynonymInstances+      UndecidableInstances+      UnicodeSyntax+      ViewPatterns+  ghc-options: -Wall -Wredundant-constraints -Wincomplete-uni-patterns -Wmissing-deriving-strategies -Widentities -Wunused-packages -fplugin=Polysemy.Plugin+  build-depends:+      base >=4.12 && <5+    , chronos+    , elocrypt+    , password ==3.0.*+    , polysemy+    , polysemy-db+    , polysemy-plugin+    , prelate >=0.5+    , random+    , sqel+  mixins:+      base hiding (Prelude)+    , prelate (Prelate as Prelude)+    , prelate hiding (Prelate)+  default-language: Haskell2010
+ readme.md view
@@ -0,0 +1,7 @@+# About++This is a Haskell library for [Polysemy](https://hackage.haskell.org/package/polysemy) that provides effects for+managing accounts with Argon2-hashed password authentication.++Consult the [Haddocks](https://hackage.haskell.org/package/polysemy-account/docs/Polysemy-Account.html) for more+documentation.