Compare commits

...

14 Commits

21 changed files with 308 additions and 201 deletions

4
.gitignore vendored
View File

@ -1,2 +1,6 @@
**/db/schema.sql **/db/schema.sql
**/client_session_key.aes **/client_session_key.aes
**/*.o
**/*.hi
**/*.dyn_o
**/*.dyn_hi

View File

@ -7,6 +7,8 @@
#:use-module ((guix licenses) #:prefix license:) #:use-module ((guix licenses) #:prefix license:)
#:use-module (guix packages) #:use-module (guix packages)
#:use-module (gnu packages golang) #:use-module (gnu packages golang)
#:use-module (gnu packages golang-xyz)
#:use-module (gnu packages golang-compression)
#:use-module (gnu packages golang-check) #:use-module (gnu packages golang-check)
#:use-module (gnu packages syncthing) #:use-module (gnu packages syncthing)
#:use-module (gnu packages haskell) #:use-module (gnu packages haskell)
@ -759,7 +761,7 @@ README for more information.")
(define-public go-github-com-jmoiron-sqlx (define-public go-github-com-jmoiron-sqlx
(package (package
(name "go-github-com-jmoiron-sqlx") (name "go-github-com-jmoiron-sqlx")
(version "1.3.5") (version "1.4.0")
(source (origin (source (origin
(method git-fetch) (method git-fetch)
(uri (git-reference (uri (git-reference
@ -768,7 +770,7 @@ README for more information.")
(file-name (git-file-name name version)) (file-name (git-file-name name version))
(sha256 (sha256
(base32 (base32
"09snd3gfi3sm3gib7jdc6p8zxpn2ah0isqnibbag6f63k473yj14")))) "10rg9b6cl1j7jjr6z95xa1k45016mhicii3cmz0pkwrxw3dpfzfh"))))
(build-system go-build-system) (build-system go-build-system)
(arguments (arguments
'(#:import-path "github.com/jmoiron/sqlx")) '(#:import-path "github.com/jmoiron/sqlx"))
@ -782,29 +784,29 @@ README for more information.")
"Package sqlx provides general purpose extensions to database/sql.") "Package sqlx provides general purpose extensions to database/sql.")
(license license:expat))) (license license:expat)))
(define-public go-github-com-pierrec-lz4 (define-public go-github-com-bkaradzic-go-lz4
(package (package
(name "go-github-com-pierrec-lz4") (name "go-github-com-bkaradzic-go-lz4")
(version "v2.0.5+incompatible") (version "1.0.0")
(source (origin (source
(method git-fetch) (origin
(uri (git-reference (method git-fetch)
(url "https://github.com/pierrec/lz4") (uri (git-reference
(commit (go-version->git-ref version)))) (url "https://github.com/bkaradzic/go-lz4")
(file-name (git-file-name name version)) (commit (string-append "v" version))))
(sha256 (file-name (git-file-name name version))
(base32 (sha256
"0y5rh7z01zycd59nnjpkqq0ydyjmcg9j1xw15q1i600l9j9g617p")))) (base32 "1vdid8v0c2v2qhrg9rzn3l7ya1h34jirrxfnir7gv7w6s4ivdvc1"))))
(build-system go-build-system) (build-system go-build-system)
(arguments (arguments
'(#:import-path "github.com/pierrec/lz4")) (list
(home-page "https://github.com/pierrec/lz4") #:import-path "github.com/bkaradzic/go-lz4"))
(synopsis "lz4 : LZ4 compression in pure Go") (home-page "https://github.com/bkaradzic/go-lz4")
(synopsis "go-lz4")
(description (description
"Package lz4 implements reading and writing lz4 compressed data (a frame), as "go-lz4 is port of LZ4 lossless compression algorithm to Go. The original C code
specified in is located at:.")
@url{http://fastcompression.blogspot.fr/2013/04/lz4-streaming-format-final.html,http://fastcompression.blogspot.fr/2013/04/lz4-streaming-format-final.html}.") (license license:bsd-2)))
(license license:bsd-3)))
(define-public go-github-com-clickhouse-clickhouse-go (define-public go-github-com-clickhouse-clickhouse-go
(package (package
@ -839,7 +841,7 @@ ClickHouse}")
(define-public go-github-com-joho-godotenv (define-public go-github-com-joho-godotenv
(package (package
(name "go-github-com-joho-godotenv") (name "go-github-com-joho-godotenv")
(version "1.4.0") (version "1.5.1")
(source (origin (source (origin
(method git-fetch) (method git-fetch)
(uri (git-reference (uri (git-reference
@ -848,7 +850,7 @@ ClickHouse}")
(file-name (git-file-name name version)) (file-name (git-file-name name version))
(sha256 (sha256
(base32 (base32
"1036h59vyhb58n817az6yg0zw5wa87yb86i7fnbdq8cw46mnjgw8")))) "03vijs05k31jdf24pzj3vlk6b5jxf894v1kvzals4wzclyq2h3ch"))))
(build-system go-build-system) (build-system go-build-system)
(arguments (arguments
'(#:import-path "github.com/joho/godotenv")) '(#:import-path "github.com/joho/godotenv"))
@ -859,9 +861,9 @@ ClickHouse}")
(@url{https://github.com/bkeepers/dotenv,https://github.com/bkeepers/dotenv})") (@url{https://github.com/bkeepers/dotenv,https://github.com/bkeepers/dotenv})")
(license license:expat))) (license license:expat)))
(define-public go-github-com-kami-zh-go-capturer (define-public go-github-com-zenizh-go-capturer
(package (package
(name "go-github-com-kami-zh-go-capturer") (name "go-github-com-zenizh-go-capturer")
(version "0.0.0-20211219060012-52ea6c8fed04") (version "0.0.0-20211219060012-52ea6c8fed04")
(source (origin (source (origin
(method git-fetch) (method git-fetch)
@ -874,8 +876,8 @@ ClickHouse}")
"0zwz9gr1863z32gz9nyysg66mg124w6nql4m99g2dg6fbq2klda4")))) "0zwz9gr1863z32gz9nyysg66mg124w6nql4m99g2dg6fbq2klda4"))))
(build-system go-build-system) (build-system go-build-system)
(arguments (arguments
'(#:import-path "github.com/kami-zh/go-capturer")) '(#:import-path "github.com/zenizh/go-capturer"))
(home-page "https://github.com/kami-zh/go-capturer") (home-page "https://github.com/zenizh/go-capturer")
(synopsis "go-capturer") (synopsis "go-capturer")
(description (description
"Capture @code{os.Stdout} and/or @code{os.Stderr} in Go. This package is useful "Capture @code{os.Stdout} and/or @code{os.Stderr} in Go. This package is useful
@ -885,7 +887,7 @@ for writing tests which print some outputs using @code{fmt} package.")
(define-public go-github-com-cloudflare-golz4 (define-public go-github-com-cloudflare-golz4
(package (package
(name "go-github-com-cloudflare-golz4") (name "go-github-com-cloudflare-golz4")
(version "0.0.0-20150217214814-ef862a3cdc58") (version "0.0.0-20240916140612-caecf3c00c06")
(source (origin (source (origin
(method git-fetch) (method git-fetch)
(uri (git-reference (uri (git-reference
@ -894,7 +896,7 @@ for writing tests which print some outputs using @code{fmt} package.")
(file-name (git-file-name name version)) (file-name (git-file-name name version))
(sha256 (sha256
(base32 (base32
"0ckiwp3aa010sdnki6vd32f0n08768ppnggc8d7syzh3kkn9zvn1")))) "15nppvbav7kj3hh9qv9qbn15pd0c9lpljs5syl004cz6mif43as3"))))
(build-system go-build-system) (build-system go-build-system)
(arguments (arguments
'(#:import-path "github.com/cloudflare/golz4" '(#:import-path "github.com/cloudflare/golz4"
@ -904,53 +906,12 @@ for writing tests which print some outputs using @code{fmt} package.")
(description "Package lz4 implements compression using lz4.c and lz4hc.c") (description "Package lz4 implements compression using lz4.c and lz4hc.c")
(license license:bsd-3))) (license license:bsd-3)))
(define-public go-github-com-cpuguy83-go-md2man-v2
(package
(name "go-github-com-cpuguy83-go-md2man-v2")
(version "2.0.2")
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/cpuguy83/go-md2man")
(commit (string-append "v" version))))
(file-name (git-file-name name version))
(sha256
(base32
"19qri18cinpzxblkid6ngz2vcxslv73s1aid900q0gfzvc71mqqb"))))
(build-system go-build-system)
(arguments
'(#:import-path "github.com/cpuguy83/go-md2man/v2"))
(propagated-inputs (list go-github-com-russross-blackfriday-v2))
(home-page "https://github.com/cpuguy83/go-md2man")
(synopsis "go-md2man")
(description "Converts markdown into roff (man pages).")
(license license:expat)))
(define-public go-github-com-russross-blackfriday-v2
(package
(name "go-github-com-russross-blackfriday-v2")
(version "2.1.0")
(source (origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/russross/blackfriday")
(commit (string-append "v" version))))
(file-name (git-file-name name version))
(sha256
(base32
"0d1rg1drrfmabilqjjayklsz5d0n3hkf979sr3wsrw92bfbkivs7"))))
(build-system go-build-system)
(arguments
'(#:import-path "github.com/russross/blackfriday/v2"))
(home-page "https://github.com/russross/blackfriday")
(synopsis "Blackfriday")
(description "Package blackfriday is a markdown processor.")
(license license:bsd-2)))
(define-public dbmate (define-public dbmate
(package (package
(name "dbmate") (name "dbmate")
(version "1.15.0") (version "1.16.2")
(source (origin (source (origin
(method git-fetch) (method git-fetch)
(uri (git-reference (uri (git-reference
@ -959,22 +920,16 @@ for writing tests which print some outputs using @code{fmt} package.")
(file-name (git-file-name name version)) (file-name (git-file-name name version))
(sha256 (sha256
(base32 (base32
"1dbhm2aqppn4m55xnx18017shsy109hqv2nhksxb4ix83bjaq5vq")))) "0mp06dg8x19pkbl51k0b5xj7bamaj6f61fyi0cfbd1dldwzw0676"))))
(build-system go-build-system) (build-system go-build-system)
(arguments (arguments
'(#:import-path "github.com/amacneil/dbmate")) '(#:import-path "github.com/amacneil/dbmate"))
(propagated-inputs (propagated-inputs
(list go-gopkg-in-yaml-v3 (list go-github-com-zenizh-go-capturer
go-github-com-russross-blackfriday-v2
go-github-com-pmezard-go-difflib
go-github-com-davecgh-go-spew
go-github-com-cpuguy83-go-md2man-v2
go-github-com-cloudflare-golz4
go-github-com-urfave-cli-v2 go-github-com-urfave-cli-v2
go-github-com-stretchr-testify go-github-com-stretchr-testify
go-github-com-mattn-go-sqlite3 go-github-com-mattn-go-sqlite3
go-github-com-lib-pq go-github-com-lib-pq
go-github-com-kami-zh-go-capturer
go-github-com-joho-godotenv go-github-com-joho-godotenv
go-github-com-go-sql-driver-mysql go-github-com-go-sql-driver-mysql
go-github-com-clickhouse-clickhouse-go)) go-github-com-clickhouse-clickhouse-go))

View File

@ -114,7 +114,7 @@ authBearer Nothing m = m
authBearer (Just (BearerToken bearer)) m = do authBearer (Just (BearerToken bearer)) m = do
let getUserPermissions = do let getUserPermissions = do
Right DBToken {..} <- lift $ dbGetTokenBySecret bearer Right DBToken {..} <- lift $ dbGetTokenBySecret bearer
permissions' <- fromMaybe mempty . readPermission <$> lift dbTokenPermissions permissions' <- lift dbTokenPermissions
DBUser {..} <- lift dbTokenUser DBUser {..} <- lift dbTokenUser
userID <- lift dbUserId userID <- lift dbUserId
pure (Just userID, permissions') pure (Just userID, permissions')
@ -140,8 +140,7 @@ authBasic :: Maybe BasicAuth -> ActionT LText APIM a -> ActionT LText APIM a
authBasic Nothing m = m authBasic Nothing m = m
authBasic (Just basic) m = do authBasic (Just basic) m = do
DBUser {..} <- verifyBasic basic DBUser {..} <- verifyBasic basic
permissions <- readPermission <$> lift dbUserPermissions >>= permissions <- lift dbUserPermissions
fromMaybeFail status500 "Internal server error"
userID <- lift dbUserId userID <- lift dbUserId
flip local m $ \state -> state flip local m $ \state -> state
{ stateCurrentUser = Just userID { stateCurrentUser = Just userID
@ -166,7 +165,7 @@ newtype APIM a = APIM (ReaderT RequestState IO a)
data RequestState = RequestState data RequestState = RequestState
{ stateCurrentUser :: Maybe UserID { stateCurrentUser :: Maybe UserID
, statePermissions :: Map Scope Permission , statePermissions :: Permissions
, stateConfig :: Config , stateConfig :: Config
} }
@ -214,28 +213,15 @@ instance MonadRandom APIM where
getRandomBytes = liftIO . getRandomBytes getRandomBytes = liftIO . getRandomBytes
instance MonadPermissions APIM where instance MonadPermissions APIM where
currentPermissions = show <$> asks statePermissions currentPermissions = asks statePermissions
defaultPermissions = pure $ show $ (fromList [(OwnProfile, ReadWrite)] :: Map Scope Permission) defaultPermissions =
toPermissions = pure . fmap show . readPermission maybe mempty (\user -> Permissions $ fromList [(Profile user, ReadWrite)]) <$>
hasPermission scope permission = (>= permission) <$> findPermission scope asks stateCurrentUser
where findPermission :: Scope -> APIM Permission
findPermission scope@(Profile user) = selfPermissions scope user OwnProfile
findPermission scope@(Tokens user) = selfPermissions scope user OwnTokens
findPermission scope = findPermission' scope <$> asks statePermissions
findPermission' :: Scope -> Map Scope Permission -> Permission
findPermission' = findWithDefault None
selfPermissions :: Scope -> UserID -> Scope -> APIM Permission
selfPermissions scope user own = do
isSelf <- (Just user ==) <$> currentUser
let f = if isSelf then max <$> findPermission' own <*> findPermission' scope
else findPermission' scope
f <$> asks statePermissions
runAPIM :: Config -> APIM a -> IO a runAPIM :: Config -> APIM a -> IO a
runAPIM config (APIM m) = runReaderT m RequestState runAPIM config (APIM m) = runReaderT m RequestState
{ stateCurrentUser = Nothing { stateCurrentUser = Nothing
, statePermissions = fromList [] , statePermissions = Permissions $ fromList []
, stateConfig = config , stateConfig = config
} }

View File

@ -24,6 +24,7 @@ import "cryptonite" Crypto.Random (getRandomBytes, MonadRandom)
import qualified "base64" Data.ByteString.Base64 as B64 import qualified "base64" Data.ByteString.Base64 as B64
import Control.Monad.Except (MonadError, throwError, catchError) import Control.Monad.Except (MonadError, throwError, catchError)
import qualified Data.Map as Map
import Data.Morpheus.Server (deriveApp, runApp) import Data.Morpheus.Server (deriveApp, runApp)
import Data.Morpheus.Server.Types (defaultRootResolver, RootResolver(..), Undefined) import Data.Morpheus.Server.Types (defaultRootResolver, RootResolver(..), Undefined)
import Data.Morpheus.Types (Arg(..), GQLType, GQLError, App) import Data.Morpheus.Types (Arg(..), GQLType, GQLError, App)
@ -82,18 +83,24 @@ sendVerificationSecret DBUser {..} = do
sendVerificationEmail secret' pendingEmail' >> pure True sendVerificationEmail secret' pendingEmail' >> pure True
_ -> pure False _ -> pure False
updateArgsToData :: (MonadTime m, MonadRandom m, MonadError GQLError m, MonadDB m) => updateArgsToData :: (MonadTime m, MonadRandom m, MonadError GQLError m, MonadDB m, MonadPermissions m) =>
UpdateArgs -> UserID -> m UpdateData UpdateArgs -> UserID -> m UpdateData
updateArgsToData UpdateArgs {..} user = do updateArgsToData UpdateArgs {..} user = do
when (maybe False T.null name) $ throwError "Name must not be empty" when (maybe False T.null name) $ throwError "Name must not be empty"
when (maybe False T.null password) $ throwError "Password must not be empty" when (maybe False T.null password) $ throwError "Password must not be empty"
when (maybe False T.null homeplace) $ throwError "Homeplace must not be empty" when (maybe False T.null homeplace) $ throwError "Homeplace must not be empty"
isMember <- dbGetUser user >>= liftDBEither >>= fmap isJust . dbUserAccepted requestPermissions <- currentPermissions
let permissions' = inputPermissionsToPermissions <$> permissions
when (maybe False (> requestPermissions) permissions') $ throwError "Permissions must not be greater than the request's"
dbUser <- dbGetUser user >>= liftDBEither
isMember <- fmap isJust $ dbUserAccepted $ dbUser
when (isMember && isJust application) $ throwError "Members can't update their applications" when (isMember && isJust application) $ throwError "Members can't update their applications"
passwordHash <- sequence $ hashPassword <$> password passwordHash <- sequence $ hashPassword <$> password
updateTime <- currentTime updateTime <- currentTime
verificationSecret <- genVerificationSecret verificationSecret <- genVerificationSecret
pure UpdateData {..} oldPermissions <- dbUserPermissions dbUser
pure UpdateData {permissions = (<> oldPermissions) <$> permissions', ..}
-- Map's (and thus Permissions') <> prefers values from the left operand
updateUser :: (MonadRandom m, MonadDB m, MonadEmail m, updateUser :: (MonadRandom m, MonadDB m, MonadEmail m,
MonadError GQLError m, MonadTime m, MonadPermissions m) => UserID -> UpdateArgs -> m (User m) MonadError GQLError m, MonadTime m, MonadPermissions m) => UserID -> UpdateArgs -> m (User m)
@ -108,7 +115,7 @@ newTokenArgsToData :: (MonadRandom m, MonadTime m, MonadPermissions m) =>
newTokenArgsToData NewTokenArgs {..} user = do newTokenArgsToData NewTokenArgs {..} user = do
tokenData <- B64.encodeBase64 <$> getRandomBytes 128 tokenData <- B64.encodeBase64 <$> getRandomBytes 128
issued <- currentTime issued <- currentTime
permissions <- maybe currentPermissions pure =<< maybe (pure Nothing) toPermissions permissions permissions <- maybe currentPermissions pure $ inputPermissionsToPermissions <$> permissions
let expires = Nothing let expires = Nothing
pure NewTokenData {..} pure NewTokenData {..}
@ -155,7 +162,7 @@ resolveQuery = Query
, user = \(Arg id) -> targetUser id >>= \user -> requirePermission (Profile user) ReadOnly >> , user = \(Arg id) -> targetUser id >>= \user -> requirePermission (Profile user) ReadOnly >>
(Just . dbUserToUser <$> (dbGetUser user >>= liftDBEither)) `catchError` const (pure Nothing) (Just . dbUserToUser <$> (dbGetUser user >>= liftDBEither)) `catchError` const (pure Nothing)
, applications = requirePermission Applications ReadOnly >> map dbUserToUser <$> dbGetApplications , applications = requirePermission Applications ReadOnly >> map dbUserToUser <$> dbGetApplications
, permissions = currentPermissions , permissions = fromPermissions <$> currentPermissions
} }
resolveMutation :: (MonadRequest m, MonadEmail m, MonadRandom m, MonadTime m, resolveMutation :: (MonadRequest m, MonadEmail m, MonadRandom m, MonadTime m,
@ -203,13 +210,14 @@ data UpdateArgs = UpdateArgs
, nickname :: Maybe Text , nickname :: Maybe Text
, homeplace :: Maybe Text , homeplace :: Maybe Text
, application :: Maybe Text , application :: Maybe Text
, permissions :: Maybe [InputScopePermission]
} deriving (Generic, GQLType, Eq, Show) } deriving (Generic, GQLType, Eq, Show)
data NewKeyArgs = NewKeyArgs { comment :: Maybe Text, keyData :: Base64, expires :: Maybe Time } data NewKeyArgs = NewKeyArgs { comment :: Maybe Text, keyData :: Base64, expires :: Maybe Time }
deriving (Generic, GQLType, Eq, Show) deriving (Generic, GQLType, Eq, Show)
data NewTokenArgs = NewTokenArgs data NewTokenArgs = NewTokenArgs
{ comment :: Maybe Text, name :: Maybe Text, permissions :: Maybe Text } { comment :: Maybe Text, name :: Maybe Text, permissions :: Maybe [InputScopePermission] }
deriving (Generic, GQLType) deriving (Generic, GQLType)
data User m = User data User m = User
@ -223,7 +231,7 @@ data User m = User
, homeplace :: m Text , homeplace :: m Text
, registered :: m Time , registered :: m Time
, accepted :: m (Maybe Time) , accepted :: m (Maybe Time)
, permissions :: m Text , permissions :: m [ScopePermission]
, isMember :: m Bool , isMember :: m Bool
, application :: m Text , application :: m Text
, tokens :: m [Token m] , tokens :: m [Token m]
@ -246,14 +254,15 @@ data Token m = Token
, comment :: m Text , comment :: m Text
, issued :: m Time , issued :: m Time
, expires :: m (Maybe Time) , expires :: m (Maybe Time)
, permissions :: m Text , permissions :: m [ScopePermission]
, owner :: m (User m)
} deriving (Generic, GQLType) } deriving (Generic, GQLType)
data Query m = Query data Query m = Query
{ users :: m [User m] { users :: m [User m]
, user :: Arg "id" (Maybe UserID) -> m (Maybe (User m)) , user :: Arg "id" (Maybe UserID) -> m (Maybe (User m))
, applications :: m [User m] , applications :: m [User m]
, permissions :: m Text , permissions :: m [ScopePermission]
} deriving (Generic, GQLType) } deriving (Generic, GQLType)
data Mutation m = Mutation data Mutation m = Mutation
@ -279,7 +288,7 @@ dbUserToUser DBUser {..} = User
, homeplace = dbUserHomeplace , homeplace = dbUserHomeplace
, registered = dbUserRegistered , registered = dbUserRegistered
, accepted = dbUserAccepted , accepted = dbUserAccepted
, permissions = dbUserPermissions , permissions = fromPermissions <$> dbUserPermissions
, isMember = isJust <$> dbUserAccepted , isMember = isJust <$> dbUserAccepted
, application = dbUserApplication , application = dbUserApplication
, tokens = dbUserId >>= flip requirePermission ReadOnly . Tokens >> map dbTokenToToken <$> dbUserTokens , tokens = dbUserId >>= flip requirePermission ReadOnly . Tokens >> map dbTokenToToken <$> dbUserTokens
@ -296,7 +305,7 @@ dbPGPKeyToPGPKey DBPGPKey {..} = PGPKey
, comment = dbPGPKeyComment , comment = dbPGPKeyComment
} }
dbTokenToToken :: Monad m => DBToken m -> Token m dbTokenToToken :: (MonadPermissions m, MonadError GQLError m) => DBToken m -> Token m
dbTokenToToken DBToken {..} = Token dbTokenToToken DBToken {..} = Token
{ id = dbTokenId { id = dbTokenId
, name = dbTokenName , name = dbTokenName
@ -304,5 +313,20 @@ dbTokenToToken DBToken {..} = Token
, comment = dbTokenComment , comment = dbTokenComment
, issued = dbTokenIssued , issued = dbTokenIssued
, expires = dbTokenExpires , expires = dbTokenExpires
, permissions = dbTokenPermissions , permissions = fromPermissions <$> dbTokenPermissions
, owner = dbUserToUser <$> dbTokenUser
} }
inputPermissionsToPermissions :: [InputScopePermission] -> Permissions
inputPermissionsToPermissions = Permissions .
Map.fromList . map (\InputScopePermission {..} -> (scope, permission))
fromPermissions :: Permissions -> [ScopePermission]
fromPermissions (Permissions perms) =
map (\(scope, permission) -> ScopePermission scope permission) $ Map.toList perms
data InputScopePermission = InputScopePermission {scope :: Scope, permission :: Permission}
deriving (Generic, GQLType, Eq, Show)
data ScopePermission = ScopePermission {scope :: Scope, permission :: Permission}
deriving (Generic, GQLType)

View File

@ -31,7 +31,10 @@ dbUpdateUser UpdateData {..} = do
, SetUserPhoneNumber <$> phoneNumber , SetUserPhoneNumber <$> phoneNumber
, SetUserApplication <$> application , SetUserApplication <$> application
] ]
userUpdates = maybeToList $ (SqlUserPasswordCrypt =.) <$> passwordHash userUpdates = catMaybes
[ (SqlUserPasswordCrypt =.) <$> passwordHash
, (SqlUserPermissions =.) <$> permissions
]
sqlUser <- runQuery $ do sqlUser <- runQuery $ do
Sql.updateUserData user userUpdates memberDataUpdates Sql.updateUserData user userUpdates memberDataUpdates
case email of case email of

View File

@ -6,6 +6,7 @@
module Datarekisteri.Backend.Sql.Queries where module Datarekisteri.Backend.Sql.Queries where
import Control.Monad (void)
import Control.Monad.Logger (LoggingT) import Control.Monad.Logger (LoggingT)
import Data.Aeson (fromJSON, toJSON, Result(..)) import Data.Aeson (fromJSON, toJSON, Result(..))
import Data.Maybe (listToMaybe) import Data.Maybe (listToMaybe)
@ -46,11 +47,11 @@ getKeys user = select $ do
getKey :: KeyID -> SqlM (Maybe SqlKey) getKey :: KeyID -> SqlM (Maybe SqlKey)
getKey = get . fromID getKey = get . fromID
getPermissions :: UserID -> SqlM (Maybe Text) getPermissions :: UserID -> SqlM (Maybe Permissions)
getPermissions user = fmap (fmap sqlUserPermissions) $ get $ fromID user getPermissions user = fmap (fmap sqlUserPermissions) $ get $ fromID user
setPermissions :: UserID -> Text -> SqlM () setPermissions :: UserID -> Permissions -> SqlM ()
setPermissions user txt = updateUserData user [SqlUserPermissions Persist.=. txt] [] >> return () setPermissions user permissions = void $ updateUserData user [SqlUserPermissions Persist.=. permissions] []
getPrimaryKey :: UserID -> SqlM (Maybe (Entity SqlKey)) getPrimaryKey :: UserID -> SqlM (Maybe (Entity SqlKey))
getPrimaryKey user = fmap listToMaybe $ select $ do getPrimaryKey user = fmap listToMaybe $ select $ do

View File

@ -28,7 +28,7 @@ mkPersist sqlSettings [persistUpperCase|
SqlUser sql=users SqlUser sql=users
registered Time registered Time
passwordCrypt PasswordHash passwordCrypt PasswordHash
permissions Text permissions Permissions
accepted (Maybe Time) accepted (Maybe Time)
memberData Value sqltype=jsonb memberData Value sqltype=jsonb
@ -68,7 +68,7 @@ SqlToken sql=tokens
comment Text comment Text
issued Time issued Time
expires (Maybe Time) expires (Maybe Time)
permissions Text permissions Permissions
UniqueNameUid name uid UniqueNameUid name uid
UniqueData data UniqueData data

View File

@ -36,7 +36,7 @@ forward :: Monad m => [a] -> m [Maybe a]
forward = pure . map Just forward = pure . map Just
requirePermission :: (MonadPermissions m, MonadError GQLError m) => Scope -> Permission -> m () requirePermission :: (MonadPermissions m, MonadError GQLError m) => Scope -> Permission -> m ()
requirePermission scope permission = unlessM (hasPermission scope permission) $ requirePermission scope permission = unlessM (hasPermission scope permission <$> currentPermissions) $
throwError $ "Insufficient permissions, " <> show permission <> " for " throwError $ "Insufficient permissions, " <> show permission <> " for "
<> show scope <> " required." <> show scope <> " required."
@ -64,7 +64,7 @@ data ApplicationData = ApplicationData
, registered :: Time , registered :: Time
, verificationSecret :: Text , verificationSecret :: Text
, passwordHash :: PasswordHash , passwordHash :: PasswordHash
, permissions :: Text , permissions :: Permissions
} deriving (Generic, Eq, Show) } deriving (Generic, Eq, Show)
data UpdateData = UpdateData data UpdateData = UpdateData
@ -78,6 +78,7 @@ data UpdateData = UpdateData
, user :: UserID , user :: UserID
, updateTime :: Time , updateTime :: Time
, verificationSecret :: Text , verificationSecret :: Text
, permissions :: Maybe Permissions
} deriving (Generic, Eq, Show) } deriving (Generic, Eq, Show)
data NewKeyData = NewKeyData data NewKeyData = NewKeyData
@ -99,7 +100,7 @@ data Page a m = Page { pageData :: m a, cursor :: m (Maybe Cursor) }
data NewTokenData = NewTokenData data NewTokenData = NewTokenData
{ comment :: Maybe Text { comment :: Maybe Text
, name :: Maybe Text , name :: Maybe Text
, permissions :: Text , permissions :: Permissions
, tokenData :: Text , tokenData :: Text
, issued :: Time , issued :: Time
, expires :: Maybe Time , expires :: Maybe Time
@ -122,7 +123,7 @@ data DBUser m = DBUser
, dbUserHomeplace :: m Text , dbUserHomeplace :: m Text
, dbUserRegistered :: m Time , dbUserRegistered :: m Time
, dbUserAccepted :: m (Maybe Time) , dbUserAccepted :: m (Maybe Time)
, dbUserPermissions :: m Text , dbUserPermissions :: m Permissions
, dbUserApplication :: m Text , dbUserApplication :: m Text
, dbUserPasswordHash :: m PasswordHash , dbUserPasswordHash :: m PasswordHash
, dbUserTokens :: m [DBToken m] , dbUserTokens :: m [DBToken m]
@ -146,7 +147,7 @@ data DBToken m = DBToken
, dbTokenComment :: m Text , dbTokenComment :: m Text
, dbTokenIssued :: m Time , dbTokenIssued :: m Time
, dbTokenExpires :: m (Maybe Time) , dbTokenExpires :: m (Maybe Time)
, dbTokenPermissions :: m Text , dbTokenPermissions :: m Permissions
} }
class Monad m => MonadTime m where class Monad m => MonadTime m where
@ -180,10 +181,8 @@ class Monad m => MonadRequest m where
currentUser :: m (Maybe UserID) currentUser :: m (Maybe UserID)
class Monad m => MonadPermissions m where class Monad m => MonadPermissions m where
hasPermission :: Scope -> Permission -> m Bool currentPermissions :: m Permissions
currentPermissions :: m Text defaultPermissions :: m Permissions
defaultPermissions :: m Text
toPermissions :: Text -> m (Maybe Text)
instance (MonadDB m, LiftOperation o) => MonadDB (Resolver o () m) where instance (MonadDB m, LiftOperation o) => MonadDB (Resolver o () m) where
dbUpdateUser = fmap (fmap liftUser) . lift . dbUpdateUser dbUpdateUser = fmap (fmap liftUser) . lift . dbUpdateUser
@ -204,10 +203,8 @@ instance (MonadRequest m, LiftOperation o) => MonadRequest (Resolver o () m) whe
currentUser = lift currentUser currentUser = lift currentUser
instance (MonadPermissions m, LiftOperation o) => MonadPermissions (Resolver o () m) where instance (MonadPermissions m, LiftOperation o) => MonadPermissions (Resolver o () m) where
hasPermission scope permission = lift $ hasPermission scope permission
defaultPermissions = lift defaultPermissions defaultPermissions = lift defaultPermissions
currentPermissions = lift currentPermissions currentPermissions = lift currentPermissions
toPermissions = lift . toPermissions
instance (MonadEmail m, LiftOperation o) => MonadEmail (Resolver o () m) where instance (MonadEmail m, LiftOperation o) => MonadEmail (Resolver o () m) where
sendEmail = lift . sendEmail sendEmail = lift . sendEmail

View File

@ -43,7 +43,7 @@ addUserMain AddUserOpts {..} = do
userID <- Sql.addUser $ SqlUser userID <- Sql.addUser $ SqlUser
{ sqlUserRegistered = time { sqlUserRegistered = time
, sqlUserPasswordCrypt = passwordHash , sqlUserPasswordCrypt = passwordHash
, sqlUserPermissions = show addUserPermissions , sqlUserPermissions = addUserPermissions
, sqlUserAccepted = Just time , sqlUserAccepted = Just time
, sqlUserMemberData = toJSON $ MemberData , sqlUserMemberData = toJSON $ MemberData
{ nickname = addUserNickname { nickname = addUserNickname
@ -82,7 +82,7 @@ updateUserMain UpdateUserOpts {..} = runQuery $ do
Just application -> Sql.updateUserData updateUserId [] [Sql.SetUserApplication application] Just application -> Sql.updateUserData updateUserId [] [Sql.SetUserApplication application]
case updateUserPermissions of case updateUserPermissions of
Nothing -> pure () Nothing -> pure ()
Just permissions -> Sql.setPermissions updateUserId $ show permissions Just permissions -> Sql.setPermissions updateUserId permissions
cliOptions :: Parser CLIOptions cliOptions :: Parser CLIOptions
cliOptions = CLIOptions cliOptions = CLIOptions
@ -114,8 +114,8 @@ updateUserCommand = fmap UpdateUser $ UpdateUserOpts
<*> optional (strOption (long "application" <> metavar "TEXT")) <*> optional (strOption (long "application" <> metavar "TEXT"))
<*> optional permissionsParser <*> optional permissionsParser
permissionsParser :: Parser (Map Scope Permission) permissionsParser :: Parser Permissions
permissionsParser = fromList <$> many permissionParser permissionsParser = Permissions . fromList <$> many permissionParser
permissionParser :: Parser (Scope, Permission) permissionParser :: Parser (Scope, Permission)
permissionParser = (,) permissionParser = (,)
@ -136,7 +136,7 @@ data CLISubCommand = AddUser AddUserOpts
data UpdateUserOpts = UpdateUserOpts data UpdateUserOpts = UpdateUserOpts
{ updateUserId :: UserID { updateUserId :: UserID
, updateUserApplication :: Maybe Text , updateUserApplication :: Maybe Text
, updateUserPermissions :: Maybe (Map Scope Permission) , updateUserPermissions :: Maybe Permissions
} }
data AddUserOpts = AddUserOpts data AddUserOpts = AddUserOpts
@ -146,7 +146,7 @@ data AddUserOpts = AddUserOpts
, addUserHomeplace :: Text , addUserHomeplace :: Text
, addUserPhoneNumber :: PhoneNumber , addUserPhoneNumber :: PhoneNumber
, addUserEmail :: Email , addUserEmail :: Email
, addUserPermissions :: Map Scope Permission , addUserPermissions :: Permissions
, addUserApplication :: Text , addUserApplication :: Text
} }

View File

@ -3,7 +3,7 @@
(url "https://git.savannah.gnu.org/git/guix.git") (url "https://git.savannah.gnu.org/git/guix.git")
(branch "master") (branch "master")
(commit (commit
"a96f1c1bc0fa186414359890025e8acacbb1de02") "faf8ba9b43323dc61988a368690b20008611fc87")
(introduction (introduction
(make-channel-introduction (make-channel-introduction
"9edb3f66fd807b096b48283debdcddccfea34bad" "9edb3f66fd807b096b48283debdcddccfea34bad"

View File

@ -20,6 +20,7 @@ library
morpheus-graphql-core, morpheus-graphql-core,
morpheus-graphql-server, morpheus-graphql-server,
persistent, persistent,
persistent-postgresql,
relude, relude,
text, text,
time, time,

View File

@ -12,6 +12,7 @@ module Datarekisteri.Core.Types
, Email , Email
, KeyID(..) , KeyID(..)
, Permission(..) , Permission(..)
, Permissions(..)
, PhoneNumber , PhoneNumber
, Scope(..) , Scope(..)
, Time(..) , Time(..)
@ -21,7 +22,7 @@ module Datarekisteri.Core.Types
, addTime , addTime
, base64Decode , base64Decode
, base64Encode , base64Encode
, readPermission , hasPermission
, renderDate , renderDate
, renderEmail , renderEmail
, renderPhoneNumber , renderPhoneNumber
@ -36,18 +37,22 @@ import Relude
import qualified "base64" Data.ByteString.Base64 as B64 import qualified "base64" Data.ByteString.Base64 as B64
import Data.Aeson (ToJSON(..), FromJSON(..)) import Data.Aeson (Value, ToJSON(..), FromJSON(..), Result(..), ToJSONKey(..), FromJSONKey(..), FromJSONKeyFunction(..), ToJSONKeyFunction(..), fromJSON, eitherDecodeStrict)
import Data.Aeson.Encoding (string)
import Data.Char (isSpace) import Data.Char (isSpace)
import Data.Morpheus.Server.Types (SCALAR) import qualified Data.Map as Map
import Data.Morpheus.Server.Types (SCALAR, TYPE)
import Data.Morpheus.Types (GQLType, DecodeScalar(..), KIND, EncodeScalar(..), ScalarValue(..)) import Data.Morpheus.Types (GQLType, DecodeScalar(..), KIND, EncodeScalar(..), ScalarValue(..))
import Data.Morpheus.Types.GQLScalar (scalarToJSON, scalarFromJSON) import Data.Morpheus.Types.GQLScalar (scalarToJSON, scalarFromJSON)
import Data.Time (UTCTime, NominalDiffTime, addUTCTime, Day) import Data.Time (UTCTime, NominalDiffTime, addUTCTime, Day)
import Data.Time.Format.ISO8601 (iso8601Show, iso8601ParseM) import Data.Time.Format.ISO8601 (iso8601Show, iso8601ParseM)
import Database.Persist.Class (PersistField(..)) import Database.Persist.Class (PersistField(..))
import Database.Persist.PersistValue (PersistValue(..)) import Database.Persist.PersistValue (PersistValue(..))
import Database.Persist.Postgresql.JSON () -- persistent instances for Aeson's Value
import Database.Persist.Sql (PersistFieldSql(..)) import Database.Persist.Sql (PersistFieldSql(..))
import Text.Email.Validate (EmailAddress, toByteString, validate, emailAddress) import Text.Email.Validate (EmailAddress, toByteString, validate, emailAddress)
import qualified Data.Map as Map
import qualified Data.Text as T import qualified Data.Text as T
base64Encode :: ByteString -> Base64 base64Encode :: ByteString -> Base64
@ -77,21 +82,63 @@ renderDate (Date x) = toText $ iso8601Show x
addTime :: NominalDiffTime -> Time -> Time addTime :: NominalDiffTime -> Time -> Time
addTime diff (Time time) = Time $ addUTCTime diff time addTime diff (Time time) = Time $ addUTCTime diff time
data Scope = OwnProfile data Scope = Profile UserID
| OwnTokens
| Profile UserID
| Tokens UserID | Tokens UserID
| Members | Members
| Applications | Applications
deriving (Show, Eq, Ord, Read) deriving (Show, Eq, Ord, Read, Generic)
instance DecodeScalar Scope where
decodeScalar (String s) = maybe (Left $ "invalid Scope: \"" <> s <> "\"") Right $ readMaybe (toString s)
decodeScalar _ = Left "Invalid type for Scope, should be string"
instance EncodeScalar Scope where
encodeScalar = String . show
instance GQLType Scope where type KIND Scope = SCALAR
instance ToJSON Scope where toJSON = scalarToJSON
instance FromJSON Scope where parseJSON = scalarFromJSON <=< parseJSON
instance ToJSONKey Scope where toJSONKey = ToJSONKeyText (fromString . show) (string . show)
instance FromJSONKey Scope
where fromJSONKey = FromJSONKeyTextParser $
either fail pure . (eitherDecodeStrict . encodeUtf8)
data Permission = None data Permission = None
| ReadOnly | ReadOnly
| ReadWrite | ReadWrite
deriving (Show, Eq, Ord, Read) deriving (Show, Eq, Ord, Read, Generic)
readPermission :: Text -> Maybe (Map Scope Permission) instance DecodeScalar Permission where
readPermission = rightToMaybe . readEither . toString decodeScalar (String s) = maybe (Left $ "invalid Permission: \"" <> s <> "\"") Right $ readMaybe (toString s)
decodeScalar _ = Left "Invalid type for Permission, should be string"
instance EncodeScalar Permission where
encodeScalar = String . show
instance GQLType Permission where type KIND Permission = SCALAR
instance ToJSON Permission where toJSON = scalarToJSON
instance FromJSON Permission where parseJSON = scalarFromJSON <=< parseJSON
hasPermission :: Scope -> Permission -> Permissions -> Bool
hasPermission scope permission (Permissions permissionMap) =
Map.findWithDefault None scope permissionMap >= permission
newtype Permissions = Permissions (Map Scope Permission)
deriving (Show, Eq, Read, Generic, ToJSON, FromJSON, Semigroup, Monoid)
instance GQLType Permissions where type KIND Permissions = TYPE
instance PersistField Permissions where
toPersistValue = toPersistValue . toJSON
fromPersistValue = resultToEither . fromJSON <=< fromPersistValue
where resultToEither (Success x) = Right x
resultToEither (Error err) = Left $ T.pack err
instance PersistFieldSql Permissions where sqlType _ = sqlType (Proxy :: Proxy Value)
instance Ord Permissions where
(Permissions a) <= (Permissions b) =
and $ Map.intersectionWith (<=) a $ b <> (const None <$> Map.difference a b)
newtype PhoneNumber = PhoneNumber Text deriving (Show, Generic) newtype PhoneNumber = PhoneNumber Text deriving (Show, Generic)

View File

@ -6,8 +6,12 @@ scalar Email
scalar KeyID scalar KeyID
scalar Permission
scalar PhoneNumber scalar PhoneNumber
scalar Scope
scalar Time scalar Time
scalar TokenID scalar TokenID
@ -19,6 +23,11 @@ enum Unit {
Unit2 Unit2
} }
input InputScopePermission {
scope: Scope!
permission: Permission!
}
type PGPKey { type PGPKey {
id: KeyID! id: KeyID!
pgpKeyData: Base64! pgpKeyData: Base64!
@ -27,6 +36,11 @@ type PGPKey {
comment: String! comment: String!
} }
type ScopePermission {
scope: Scope!
permission: Permission!
}
type Token { type Token {
id: TokenID! id: TokenID!
name: String name: String
@ -34,7 +48,8 @@ type Token {
comment: String! comment: String!
issued: Time! issued: Time!
expires: Time expires: Time
permissions: String! permissions: [ScopePermission!]!
owner: User!
} }
type User { type User {
@ -48,7 +63,7 @@ type User {
homeplace: String! homeplace: String!
registered: Time! registered: Time!
accepted: Time accepted: Time
permissions: String! permissions: [ScopePermission!]!
isMember: Boolean! isMember: Boolean!
application: String! application: String!
tokens: [Token!]! tokens: [Token!]!
@ -60,15 +75,15 @@ type Query {
users: [User!]! users: [User!]!
user(id: UserID): User user(id: UserID): User
applications: [User!]! applications: [User!]!
permissions: String! permissions: [ScopePermission!]!
} }
type Mutation { type Mutation {
apply(email: Email!, phoneNumber: PhoneNumber!, password: String!, name: String!, nickname: String, birthdate: Date!, homeplace: String!, application: String!): User! apply(email: Email!, phoneNumber: PhoneNumber!, password: String!, name: String!, nickname: String, birthdate: Date!, homeplace: String!, application: String!): User!
verifyEmail(secret: String!): Boolean! verifyEmail(secret: String!): Boolean!
resendVerificationEmail(user: UserID): Unit! resendVerificationEmail(user: UserID): Unit!
update(email: Email, phoneNumber: PhoneNumber, password: String, name: String, nickname: String, homeplace: String, application: String, user: UserID): User! update(email: Email, phoneNumber: PhoneNumber, password: String, name: String, nickname: String, homeplace: String, application: String, permissions: [InputScopePermission!], user: UserID): User!
newToken(comment: String, name: String, permissions: String): Token! newToken(comment: String, name: String, permissions: [InputScopePermission!]): Token!
newKey(comment: String, keyData: Base64!, expires: Time): PGPKey! newKey(comment: String, keyData: Base64!, expires: Time): PGPKey!
accept(user: UserID!): Unit! accept(user: UserID!): Unit!
reject(user: UserID!): Unit! reject(user: UserID!): Unit!

View File

@ -9,7 +9,8 @@
{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE NoImplicitPrelude #-}
module Datarekisteri.Frontend.Handlers module Datarekisteri.Frontend.Handlers
( module Datarekisteri.Frontend.Handlers.Profile ( module Datarekisteri.Frontend.Handlers.Permissions
, module Datarekisteri.Frontend.Handlers.Profile
, module Datarekisteri.Frontend.Handlers.Applications , module Datarekisteri.Frontend.Handlers.Applications
, module Datarekisteri.Frontend.Handlers.Apply , module Datarekisteri.Frontend.Handlers.Apply
, module Datarekisteri.Frontend.Handlers.Members , module Datarekisteri.Frontend.Handlers.Members
@ -23,6 +24,7 @@ import Yesod
import Yesod.Auth import Yesod.Auth
import Datarekisteri.Frontend.Handlers.Profile import Datarekisteri.Frontend.Handlers.Profile
import Datarekisteri.Frontend.Handlers.Permissions
import Datarekisteri.Frontend.Handlers.Apply import Datarekisteri.Frontend.Handlers.Apply
import Datarekisteri.Frontend.Handlers.Applications import Datarekisteri.Frontend.Handlers.Applications
import Datarekisteri.Frontend.Handlers.VerifyEmail import Datarekisteri.Frontend.Handlers.VerifyEmail
@ -30,4 +32,4 @@ import Datarekisteri.Frontend.Handlers.Members
import Datarekisteri.Frontend.Types import Datarekisteri.Frontend.Types
getHomeR :: Handler Html getHomeR :: Handler Html
getHomeR = ifM (isJust <$> maybeAuthId) (redirect OwnProfileR) (redirect $ AuthR LoginR) getHomeR = maybeAuthId >>= maybe (redirect $ AuthR LoginR) (redirect . ProfileR . userID)

View File

@ -115,12 +115,12 @@ applicationsW applications = do
.#{detailsClass}[open] { border-color: #339ca1; } .#{detailsClass}[open] { border-color: #339ca1; }
|] |]
getApplicationsR :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => Handler Html getApplicationsR :: Handler Html
getApplicationsR = do getApplicationsR = do
Applications applications <- apiRequest @Applications True () Applications applications <- apiRequest @Applications True ()
defaultLayout $ applicationsW applications defaultLayout $ applicationsW applications
postAcceptR :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => Handler Html postAcceptR :: Handler Html
postAcceptR = do postAcceptR = do
((result, _), _) <- runFormPost $ resolveApplicationForm Nothing ((result, _), _) <- runFormPost $ resolveApplicationForm Nothing
case result of case result of
@ -128,7 +128,7 @@ postAcceptR = do
_ -> pure () _ -> pure ()
redirect ApplicationsR redirect ApplicationsR
postRejectR :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => Handler Html postRejectR :: Handler Html
postRejectR = do postRejectR = do
((result, _), _) <- runFormPost $ resolveApplicationForm Nothing ((result, _), _) <- runFormPost $ resolveApplicationForm Nothing
case result of case result of

View File

@ -75,12 +75,12 @@ applyW (applyWidget, applyEnctype) = do
^{form ApplyR applyEnctype formContent} ^{form ApplyR applyEnctype formContent}
|] |]
getApplyR :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => Handler Html getApplyR :: Handler Html
getApplyR = do getApplyR = do
applyForm <- liftHandler $ generateFormPost applyForm applyForm <- liftHandler $ generateFormPost applyForm
defaultLayout $ applyW applyForm defaultLayout $ applyW applyForm
postApplyR :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => Handler Html postApplyR :: Handler Html
postApplyR = do postApplyR = do
((result, widget), enctype) <- runFormPost applyForm ((result, widget), enctype) <- runFormPost applyForm
case result of case result of

View File

@ -38,7 +38,7 @@ instance ToTableRow DataIdClient MembersPageUsers where
tableHeader _ = toWidget <$> ["Nimi" :: Text, "Kutsumanimi", "Kotipaikka"] tableHeader _ = toWidget <$> ["Nimi" :: Text, "Kutsumanimi", "Kotipaikka"]
toCells MembersPageUsers {..} = toWidget <$> [name, nickname, homeplace] toCells MembersPageUsers {..} = toWidget <$> [name, nickname, homeplace]
getMembersR :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => Handler Html getMembersR :: Handler Html
getMembersR = do getMembersR = do
MembersPage {..} <- apiRequest True () MembersPage {..} <- apiRequest True ()
defaultLayout $ do defaultLayout $ do

View File

@ -0,0 +1,62 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Datarekisteri.Frontend.Handlers.Permissions where
import Relude hiding (id)
import Data.Morpheus.Client (raw, declareLocalTypesInline)
import Yesod
import Yesod.Auth
import Datarekisteri.Core.Types
import Datarekisteri.Frontend.ApiRequests
import Datarekisteri.Frontend.Types
import Datarekisteri.Frontend.Widgets
declareLocalTypesInline "schema.gql" [raw|
query PermissionsPage($id: UserID!) {
user(id: $id) {
id
name
nickname
permissions {
scope
permission
}
}
}
|]
-- declareLocalTypesInline "schema.gql" [raw|
-- mutation UpdatePermissions($user: UserID, $permissions: [InputScopePermission!]!) {
-- update(user: $user, permissions: $permissions) {
-- id
-- }
-- }
-- |]
getPermissionsR :: UserID -> Handler Html
getPermissionsR userID = defaultLayout $ do
PermissionsPage {..} <- liftHandler $ apiRequest True (PermissionsPageArgs {id = userID})
PermissionsPageUser {..} <- maybe undefined pure user
let permissions' = (\PermissionsPageUserPermissions {..} -> (show scope, show permission) :: (Text, Text)) <$> permissions
setTitle "Käyttöoikeudet"
[whamlet|
<h1>
Käyttöoikeudet
^{keyValueTable permissions'}
|]
postPermissionsR :: UserID -> Handler Html
postPermissionsR = undefined

View File

@ -26,7 +26,7 @@ import Datarekisteri.Frontend.FormFields
import Datarekisteri.Frontend.Widgets import Datarekisteri.Frontend.Widgets
declareLocalTypesInline "schema.gql" [raw| declareLocalTypesInline "schema.gql" [raw|
query ProfilePage($id: UserID) { query ProfilePage($id: UserID!) {
user(id: $id) { user(id: $id) {
id id
name name
@ -39,7 +39,6 @@ query ProfilePage($id: UserID) {
isMember isMember
application application
} }
permissions
} }
|] |]
@ -120,8 +119,8 @@ profile user (profileWidget, profileEnctype) (passwordWidget, passwordEnctype) =
^{form route passwordEnctype passwordFormContent} ^{form route passwordEnctype passwordFormContent}
|] |]
getProfile :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => (Maybe UserID) -> Handler Html getProfileR :: UserID -> Handler Html
getProfile userID = do getProfileR userID = do
ProfilePage {..} <- apiRequest True (ProfilePageArgs {id = userID}) ProfilePage {..} <- apiRequest True (ProfilePageArgs {id = userID})
passwordForm <- liftHandler $ generateFormPost passwordForm passwordForm <- liftHandler $ generateFormPost passwordForm
profileForm <- liftHandler $ generateFormPost $ profileForm <- liftHandler $ generateFormPost $
@ -131,15 +130,9 @@ getProfile userID = do
fromJust = fromMaybe $ error "Tried to access the profile of an inexistent user" fromJust = fromMaybe $ error "Tried to access the profile of an inexistent user"
getOwnProfileR :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => Handler Html postProfileR :: UserID -> Handler Html
getOwnProfileR = getProfile Nothing
getProfileR :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => UserID -> Handler Html
getProfileR = getProfile . Just
postProfileR :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => UserID -> Handler Html
postProfileR userID = do postProfileR userID = do
ProfilePage {..} <- apiRequest True (ProfilePageArgs {id = Just userID}) ProfilePage {..} <- apiRequest True (ProfilePageArgs {id = userID})
((result, widget), enctype) <- runFormPost $ profileForm (fromJust user) ((result, widget), enctype) <- runFormPost $ profileForm (fromJust user)
case result of case result of
FormSuccess update -> apiRequest @UpdateProfile True update >> redirect (ProfileR userID) FormSuccess update -> apiRequest @UpdateProfile True update >> redirect (ProfileR userID)
@ -148,14 +141,14 @@ postProfileR userID = do
defaultLayout $ profile (fromJust user) (widget, enctype) passwordForm defaultLayout $ profile (fromJust user) (widget, enctype) passwordForm
--- XXX fromJust explodes if the user no longer exists --- XXX fromJust explodes if the user no longer exists
postUpdatePasswordR :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => UserID -> Handler Html postUpdatePasswordR :: UserID -> Handler Html
postUpdatePasswordR userID = do postUpdatePasswordR userID = do
((result, widget), enctype) <- runFormPost passwordForm ((result, widget), enctype) <- runFormPost passwordForm
case result of case result of
FormSuccess new -> FormSuccess new ->
apiRequest @UpdatePassword True (UpdatePasswordArgs {password = new, user = Just userID}) >> redirect (ProfileR userID) apiRequest @UpdatePassword True (UpdatePasswordArgs {password = new, user = Just userID}) >> redirect (ProfileR userID)
_ -> do _ -> do
ProfilePage {..} <- apiRequest True (ProfilePageArgs {id = Just userID}) ProfilePage {..} <- apiRequest True (ProfilePageArgs {id = userID})
profileForm <- liftHandler $ generateFormPost $ profileForm (fromJust user) profileForm <- liftHandler $ generateFormPost $ profileForm (fromJust user)
defaultLayout $ profile (fromJust user) profileForm (widget, enctype) defaultLayout $ profile (fromJust user) profileForm (widget, enctype)
--- XXX fromJust explodes if the user no longer exists --- XXX fromJust explodes if the user no longer exists

View File

@ -33,14 +33,17 @@ getVerifyEmailR = do
codeForm <- generateFormPost verifyForm codeForm <- generateFormPost verifyForm
defaultLayout $ verifyEmailW codeForm defaultLayout $ verifyEmailW codeForm
postVerifyEmailR :: (YesodAuth DataIdClient, AuthId DataIdClient ~ Text) => Handler Html postVerifyEmailR :: Handler Html
postVerifyEmailR = do postVerifyEmailR = do
((result, widget), enctype) <- runFormPost verifyForm ((result, widget), enctype) <- runFormPost verifyForm
case result of case result of
FormSuccess verify -> do FormSuccess verify -> do
success <- apiRequest @VerifyEmail False verify success <- apiRequest @VerifyEmail False verify
case success of case success of
VerifyEmail True -> setMessage "Sähköpostiosoite vahvistettu" >> redirect OwnProfileR VerifyEmail True -> do
setMessage "Sähköpostiosoite vahvistettu"
user <- userID <$> requireAuthId
redirect $ ProfileR user
VerifyEmail False -> setMessage "Virheellinen vahvistuskoodi" >> redirect VerifyEmailR VerifyEmail False -> setMessage "Virheellinen vahvistuskoodi" >> redirect VerifyEmailR
_ -> defaultLayout $ verifyEmailW (widget, enctype) _ -> defaultLayout $ verifyEmailW (widget, enctype)

View File

@ -1,5 +1,6 @@
{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
@ -29,10 +30,12 @@ import Yesod
import Yesod.Auth import Yesod.Auth
import Yesod.Static import Yesod.Static
import Datarekisteri.Core.Types (UserID(..), Scope(..), Permission(..), readPermission) import Datarekisteri.Core.Types (UserID(..), Scope(..), Permission(..), Permissions(..))
import Datarekisteri.Frontend.ApiRequests import Datarekisteri.Frontend.ApiRequests
import Datarekisteri.Frontend.Auth import Datarekisteri.Frontend.Auth
import qualified Datarekisteri.Core.Types as Core
data DataIdClient = DataIdClient data DataIdClient = DataIdClient
{ getStatic :: Static { getStatic :: Static
, getConfig :: Config , getConfig :: Config
@ -51,8 +54,8 @@ instance PathPiece UserID where
mkYesodData "DataIdClient" [parseRoutes| mkYesodData "DataIdClient" [parseRoutes|
/ HomeR GET / HomeR GET
/profile OwnProfileR GET /user/#UserID/profile ProfileR GET POST
/profile/#UserID ProfileR GET POST /user/#UserID/permissions PermissionsR GET POST
/update-password/#UserID UpdatePasswordR POST /update-password/#UserID UpdatePasswordR POST
/verify-email VerifyEmailR GET POST /verify-email VerifyEmailR GET POST
@ -70,9 +73,9 @@ mkYesodData "DataIdClient" [parseRoutes|
declareLocalTypesInline "schema.gql" [raw| declareLocalTypesInline "schema.gql" [raw|
query GetPermissions { query GetPermissions {
permissions permissions {
user { scope
id permission
} }
} }
|] |]
@ -81,35 +84,47 @@ declareLocalTypesInline "schema.gql" [raw|
mutation GetWebUIToken { mutation GetWebUIToken {
newToken(comment: "id.datat.fi webui") { newToken(comment: "id.datat.fi webui") {
tokenData tokenData
owner {
id
}
} }
} }
|] |]
data UserToken = UserToken
{ userID :: UserID
, token :: Text
} deriving (Read, Show)
instance PathPiece UserToken where
toPathPiece userToken = show userToken
fromPathPiece s = readMaybe (toString s)
instance YesodAuth DataIdClient where instance YesodAuth DataIdClient where
type AuthId DataIdClient = Text type AuthId DataIdClient = UserToken
maybeAuthId = lookupSession credsKey maybeAuthId = (>>= readMaybe . toString) <$> lookupSession credsKey
loginDest = const HomeR loginDest = const HomeR
logoutDest = const HomeR logoutDest = const HomeR
authPlugins = const $ [authExternalBasic getToken] authPlugins = const $ [authExternalBasic getToken]
where getToken auth = (>>= fmap (tokenData . newToken) . rightToMaybe) <$> where getToken :: Text -> HandlerFor DataIdClient (Maybe Text)
apiRequestAuth @GetWebUIToken [] (Just auth) () getToken auth = do
authenticate = pure . Authenticated . credsIdent response <- (>>= rightToMaybe) <$> apiRequestAuth @GetWebUIToken [] (Just auth) ()
pure $ do
token <- tokenData . newToken <$> response
userID <- (\GetWebUITokenNewTokenOwner {..} -> id) . owner . newToken <$> response
pure $ show $ UserToken userID token
authenticate = pure . maybe (ServerError "Couldn't read UserToken") Authenticated . readMaybe . toString . credsIdent
withAuthenticated :: (AuthId DataIdClient -> Handler AuthResult) -> Handler AuthResult withAuthenticated :: (AuthId DataIdClient -> Handler AuthResult) -> Handler AuthResult
withAuthenticated m = maybeAuthId >>= maybe (pure AuthenticationRequired) m withAuthenticated m = maybeAuthId >>= maybe (pure AuthenticationRequired) m
readPermissions :: [GetPermissionsPermissions] -> Permissions
readPermissions = Permissions . fromList . map (\GetPermissionsPermissions {..} -> (scope, permission))
hasPermission :: Scope -> Permission -> Handler Bool hasPermission :: Scope -> Permission -> Handler Bool
hasPermission scope permission = do hasPermission scope permission = do
GetPermissions {..} <- apiRequest @GetPermissions False () GetPermissions {..} <- apiRequest @GetPermissions False ()
let permissionMap = fromMaybe mempty $ readPermission permissions pure $ Core.hasPermission scope permission $ readPermissions permissions
findPermission scope = findWithDefault None scope permissionMap
userID = (\x -> id (x :: GetPermissionsUser)) <$> user
scopes = scope :| case scope of
Tokens uid | Just uid == userID -> [OwnTokens]
Profile uid | Just uid == userID -> [OwnProfile]
_ -> []
-- TODO add Members and Applications to Profile Scopes if profile owner is member/applicant
pure $ (>= permission) $ maximum1 $ findPermission <$> scopes
requirePermission :: Scope -> Permission -> Handler AuthResult requirePermission :: Scope -> Permission -> Handler AuthResult
requirePermission scope permission = ifM (hasPermission scope permission) requirePermission scope permission = ifM (hasPermission scope permission)
@ -118,9 +133,7 @@ requirePermission scope permission = ifM (hasPermission scope permission)
where renderPermission ReadWrite = "kirjoitusoikeuden" where renderPermission ReadWrite = "kirjoitusoikeuden"
renderPermission ReadOnly = "lukuoikeuden" renderPermission ReadOnly = "lukuoikeuden"
renderPermission None = "nollaoikeuden" renderPermission None = "nollaoikeuden"
renderScope OwnProfile = "omaan profiliin"
renderScope (Profile _) = "käyttäjän profiliin" renderScope (Profile _) = "käyttäjän profiliin"
renderScope OwnTokens = "omiin tokeneihin" -- TODO kunnon suomennos
renderScope (Tokens _) = "käyttäjän tokeneihin" -- TODO kunnon suomennos renderScope (Tokens _) = "käyttäjän tokeneihin" -- TODO kunnon suomennos
renderScope Members = "kaikkiin jäseniin" renderScope Members = "kaikkiin jäseniin"
renderScope Applications = "jäsenhakemuksiin" renderScope Applications = "jäsenhakemuksiin"
@ -139,7 +152,6 @@ instance Yesod DataIdClient where
isAuthorized HomeR _ = pure Authorized isAuthorized HomeR _ = pure Authorized
isAuthorized ApplyR _ = pure Authorized isAuthorized ApplyR _ = pure Authorized
isAuthorized VerifyEmailR _ = pure Authorized isAuthorized VerifyEmailR _ = pure Authorized
isAuthorized OwnProfileR isWrite = withAuthenticated $ const $ authorizedHelper OwnProfile isWrite
isAuthorized (ProfileR user) isWrite = withAuthenticated $ const $ authorizedHelper (Profile user) isWrite isAuthorized (ProfileR user) isWrite = withAuthenticated $ const $ authorizedHelper (Profile user) isWrite
isAuthorized (UpdatePasswordR user) isWrite = isAuthorized (UpdatePasswordR user) isWrite =
withAuthenticated $ const $ authorizedHelper (Profile user) isWrite withAuthenticated $ const $ authorizedHelper (Profile user) isWrite
@ -191,8 +203,10 @@ instance Yesod DataIdClient where
|] |]
navigationBar :: Widget navigationBar :: Widget
navigationBar = do navigationBar = do
maybeUser <- fmap userID <$> maybeAuthId
applicationsRoute <- handlerToWidget $ maybeAuthorized ApplicationsR False applicationsRoute <- handlerToWidget $ maybeAuthorized ApplicationsR False
profileRoute <- handlerToWidget $ maybeAuthorized OwnProfileR False profileRoute <- handlerToWidget $ maybe (pure Nothing)
(\user -> maybeAuthorized (ProfileR user) False) maybeUser
membersRoute <- handlerToWidget $ maybeAuthorized MembersR False membersRoute <- handlerToWidget $ maybeAuthorized MembersR False
currentRoute <- getCurrentRoute currentRoute <- getCurrentRoute
loggedIn <- isJust <$> maybeAuthId loggedIn <- isJust <$> maybeAuthId
@ -301,7 +315,7 @@ fgColor = "#181c22"
instance ApiRequest DataIdClient where instance ApiRequest DataIdClient where
getApiUrl = configServerUrl . getConfig <$> getYesod getApiUrl = configServerUrl . getConfig <$> getYesod
authIdToAuthorization = flip const authIdToAuthorization _ UserToken {..} = token
instance RenderMessage DataIdClient FormMessage where instance RenderMessage DataIdClient FormMessage where
renderMessage _ _ = defaultFormMessage renderMessage _ _ = defaultFormMessage