2 Module : Gargantext.API.Admin.Auth
3 Description : Server API Auth Module
4 Copyright : (c) CNRS, 2017-Present
5 License : AGPL + CECILL v3
6 Maintainer : team@gargantext.org
7 Stability : experimental
10 Main authorization of Gargantext are managed in this module
12 -- 1: Implement the Server / Client JWT authentication
13 -> Client towards Python Backend
14 -> Server towards Purescript Front-End
16 -- 2: Implement the Auth API backend
17 https://github.com/haskell-servant/servant-auth
23 {-# LANGUAGE MonoLocalBinds #-}
24 {-# LANGUAGE ScopedTypeVariables #-}
25 {-# LANGUAGE TypeOperators #-}
27 module Gargantext.API.Admin.Auth
33 , ForgotPasswordAsyncParams
34 , ForgotPasswordAsyncAPI
38 import Control.Lens (view, (#))
39 --import Control.Monad.Logger.Aeson
41 import Data.Swagger (ToSchema(..))
42 import Data.Text (Text)
43 import Data.Text.Lazy (toStrict)
44 import qualified Data.Text.Lazy.Encoding as LE
45 import Data.UUID (UUID, fromText, toText)
46 import Data.UUID.V4 (nextRandom)
47 import GHC.Generics (Generic)
49 import Servant.Auth.Server
50 import Servant.Job.Async (JobFunction(..), serveJobsAPI)
51 import qualified Text.Blaze.Html.Renderer.Text as H
52 import qualified Text.Blaze.Html5 as H
53 --import qualified Text.Blaze.Html5.Attributes as HA
55 import qualified Gargantext.Prelude.Crypto.Auth as Auth
57 import Gargantext.API.Admin.Auth.Types
58 import Gargantext.API.Admin.Orchestrator.Types (JobLog(..), AsyncJobs)
59 import Gargantext.API.Admin.Types
60 import Gargantext.API.Job (jobLogSuccess)
61 import Gargantext.API.Prelude (HasJoseError(..), joseError, HasServerError, GargServerC, GargServer, _ServerError)
62 import Gargantext.API.Types
63 import Gargantext.Core.Mail (MailModel(..), mail)
64 import Gargantext.Core.Mail.Types (HasMail, mailSettings)
65 import Gargantext.Core.Types.Individu (User(..), Username, GargPassword(..))
66 import Gargantext.Database.Action.Flow.Types (FlowCmdM)
67 import Gargantext.Database.Admin.Types.Node (NodeId(..), UserId)
68 import Gargantext.Database.Prelude (Cmd', CmdM, HasConnectionPool, HasConfig)
69 import Gargantext.Database.Query.Table.User
70 import Gargantext.Database.Query.Tree (isDescendantOf, isIn)
71 import Gargantext.Database.Query.Tree.Root (getRoot)
72 import Gargantext.Database.Schema.Node (NodePoly(_node_id))
73 import Gargantext.Prelude hiding (reverse)
74 import Gargantext.Prelude.Crypto.Pass.User (gargPass)
76 ---------------------------------------------------
78 -- | Main functions of authorization
80 makeTokenForUser :: (HasSettings env, HasJoseError err)
81 => NodeId -> Cmd' env err Token
82 makeTokenForUser uid = do
83 jwtS <- view $ settings . jwtSettings
84 e <- liftBase $ makeJWT (AuthenticatedUser uid) jwtS Nothing
85 -- TODO-SECURITY here we can implement token expiration ^^.
86 either joseError (pure . toStrict . LE.decodeUtf8) e
87 -- TODO not sure about the encoding...
89 checkAuthRequest :: (HasSettings env, HasConnectionPool env, HasJoseError err, HasConfig env, HasMail env)
92 -> Cmd' env err CheckAuth
93 checkAuthRequest u (GargPassword p) = do
94 candidate <- head <$> getUsersWith u
96 Nothing -> pure InvalidUser
97 Just (UserLight { userLight_password = GargPassword h, .. }) ->
98 case Auth.checkPassword (Auth.mkPassword p) (Auth.PasswordHash h) of
99 Auth.PasswordCheckFail -> pure InvalidPassword
100 Auth.PasswordCheckSuccess -> do
101 muId <- head <$> getRoot (UserName u)
102 case _node_id <$> muId of
103 Nothing -> pure InvalidUser
105 token <- makeTokenForUser uid
106 pure $ Valid token uid userLight_id
108 auth :: (HasSettings env, HasConnectionPool env, HasJoseError err, HasConfig env, HasMail env)
109 => AuthRequest -> Cmd' env err AuthResponse
110 auth (AuthRequest u p) = do
111 checkAuthRequest' <- checkAuthRequest u p
112 case checkAuthRequest' of
113 InvalidUser -> pure $ AuthResponse Nothing (Just $ AuthInvalid "Invalid user")
114 InvalidPassword -> pure $ AuthResponse Nothing (Just $ AuthInvalid "Invalid password")
115 Valid to trId uId -> pure $ AuthResponse (Just $ AuthValid to trId uId) Nothing
117 --type instance BasicAuthCfg = BasicAuthData -> IO (AuthResult AuthenticatedUser)
120 instance FromBasicAuthData AuthenticatedUser where
121 fromBasicAuthData authData authCheckFunction = authCheckFunction authData
123 authCheck :: forall env. env
125 -> IO (AuthResult AuthenticatedUser)
126 authCheck _env (BasicAuthData login password) = pure $
127 maybe Indefinite Authenticated $ TODO
130 withAccessM :: (CmdM env err m, HasServerError err)
135 withAccessM uId (PathNode id) m = do
136 d <- id `isDescendantOf` NodeId uId
137 if d then m else m -- serverError err401
139 withAccessM uId (PathNodeNode cId docId) m = do
140 _a <- isIn cId docId -- TODO use one query for all ?
141 _d <- cId `isDescendantOf` NodeId uId
146 withAccess :: forall env err m api.
147 (GargServerC env err m, HasServer api '[]) =>
148 Proxy api -> Proxy m -> UserId -> PathId ->
149 ServerT api m -> ServerT api m
150 withAccess p _ uId id = hoistServer p f
152 f :: forall a. m a -> m a
153 f = withAccessM uId id
155 {- | Collaborative Schema
156 User at his root can create Teams Folder
157 User can create Team in Teams Folder.
158 User can invite User in Team as NodeNode only if Team in his parents.
159 All users can access to the Team folder as if they were owner.
162 newtype ForgotPasswordAsyncParams =
163 ForgotPasswordAsyncParams { email :: Text }
164 deriving (Generic, Show)
165 instance FromJSON ForgotPasswordAsyncParams where
166 parseJSON = genericParseJSON defaultOptions
167 instance ToJSON ForgotPasswordAsyncParams where
168 toJSON = genericToJSON defaultOptions
169 instance ToSchema ForgotPasswordAsyncParams
171 type ForgotPasswordAPI = Summary "Forgot password POST API"
172 :> ReqBody '[JSON] ForgotPasswordRequest
173 :> Post '[JSON] ForgotPasswordResponse
174 :<|> Summary "Forgot password GET API"
175 :> QueryParam "uuid" Text
179 forgotPassword :: GargServer ForgotPasswordAPI
180 -- => ForgotPasswordRequest -> Cmd' env err ForgotPasswordResponse
181 forgotPassword = forgotPasswordPost :<|> forgotPasswordGet
183 forgotPasswordPost :: ( HasConnectionPool env, HasConfig env, HasMail env)
184 => ForgotPasswordRequest -> Cmd' env err ForgotPasswordResponse
185 forgotPasswordPost (ForgotPasswordRequest email) = do
186 us <- getUsersWithEmail email
188 [u] -> forgotUserPassword u
191 -- NOTE Sending anything else here could leak information about
193 pure $ ForgotPasswordResponse "ok"
195 forgotPasswordGet :: (HasSettings env, HasConnectionPool env, HasJoseError err, HasConfig env, HasMail env, HasServerError err)
196 => Maybe Text -> Cmd' env err Text
197 forgotPasswordGet Nothing = pure ""
198 forgotPasswordGet (Just uuid) = do
199 let mUuid = fromText uuid
201 Nothing -> throwError $ _ServerError # err404 { errBody = "Not found" }
204 us <- getUsersWithForgotPasswordUUID uuid'
206 [u] -> forgotPasswordGetUser u
207 _ -> throwError $ _ServerError # err404 { errBody = "Not found" }
209 ---------------------
211 forgotPasswordGetUser :: (HasSettings env, HasConnectionPool env, HasJoseError err, HasConfig env, HasMail env, HasServerError err)
212 => UserLight -> Cmd' env err Text
213 forgotPasswordGetUser (UserLight { .. }) = do
214 -- pick some random password
215 password <- liftBase gargPass
217 -- set it as user's password
218 hashed <- liftBase $ Auth.hashPassword $ Auth.mkPassword password
219 let hashed' = Auth.unPasswordHash hashed
220 let userPassword = UserLight { userLight_password = GargPassword hashed', .. }
221 _ <- updateUserPassword userPassword
223 -- display this briefly in the html
225 -- clear the uuid so that the page can't be refreshed
226 _ <- updateUserForgotPasswordUUID $ UserLight { userLight_forgot_password_uuid = Nothing, .. }
228 pure $ toStrict $ H.renderHtml $
232 H.title "Gargantext - forgot password"
234 H.h1 "Forgot password"
236 H.span "Here is your password (will be shown only once): "
237 H.b $ H.toHtml password
239 forgotUserPassword :: (HasConnectionPool env, HasConfig env, HasMail env)
240 => UserLight -> Cmd' env err ()
241 forgotUserPassword (UserLight { .. }) = do
242 --printDebug "[forgotUserPassword] userLight_id" userLight_id
243 --logDebug $ "[forgotUserPassword]" :# ["userLight_id" .= userLight_id]
244 -- generate uuid for email
245 uuid <- generateForgotPasswordUUID
247 let userUUID = UserLight { userLight_forgot_password_uuid = Just $ toText uuid, .. }
249 -- save user with that uuid
250 _ <- updateUserForgotPasswordUUID userUUID
252 -- send email with uuid link
253 cfg <- view $ mailSettings
254 mail cfg (ForgotPassword { user = userUUID })
256 -- on uuid link enter: change user password and present it to the
261 --------------------------
263 -- Generate a unique (in whole DB) UUID for passwords.
264 generateForgotPasswordUUID :: (HasConnectionPool env, HasConfig env, HasMail env)
266 generateForgotPasswordUUID = do
267 uuid <- liftBase $ nextRandom
268 us <- getUsersWithForgotPasswordUUID uuid
271 _ -> generateForgotPasswordUUID
273 ----------------------------
275 -- NOTE THe async endpoint is better for the "forget password"
276 -- request, because the delay in email sending etc won't reveal to
277 -- malicious users emails of our users in the db
278 type ForgotPasswordAsyncAPI = Summary "Forgot password asnc"
279 :> AsyncJobs JobLog '[JSON] ForgotPasswordAsyncParams JobLog
281 forgotPasswordAsync :: GargServer ForgotPasswordAsyncAPI
282 forgotPasswordAsync =
284 JobFunction (\p log' ->
285 forgotPasswordAsync' p (liftBase . log')
288 forgotPasswordAsync' :: (FlowCmdM env err m)
289 => ForgotPasswordAsyncParams
292 forgotPasswordAsync' (ForgotPasswordAsyncParams { email }) logStatus = do
293 let jobLog = JobLog { _scst_succeeded = Just 1
294 , _scst_failed = Just 0
295 , _scst_remaining = Just 1
296 , _scst_events = Just []
300 printDebug "[forgotPasswordAsync'] email" email
302 _ <- forgotPasswordPost $ ForgotPasswordRequest { _fpReq_email = email }
304 pure $ jobLogSuccess jobLog