diff options
Diffstat (limited to 'overlays')
| -rw-r--r-- | overlays/spm/frontend/src/app/spm/spm.component.ts | 4 | ||||
| -rw-r--r-- | overlays/spm/lib/Spm/Api.hs | 31 | ||||
| -rw-r--r-- | overlays/spm/package.yaml | 3 | ||||
| -rw-r--r-- | overlays/spm/server/Crypto/JOSE/Error/Instances.hs | 11 | ||||
| -rw-r--r-- | overlays/spm/server/Spm/Server.hs | 24 | ||||
| -rw-r--r-- | overlays/spm/spm.nix | 4 |
6 files changed, 60 insertions, 17 deletions
diff --git a/overlays/spm/frontend/src/app/spm/spm.component.ts b/overlays/spm/frontend/src/app/spm/spm.component.ts index f6892d7c..361a1ce4 100644 --- a/overlays/spm/frontend/src/app/spm/spm.component.ts +++ b/overlays/spm/frontend/src/app/spm/spm.component.ts | |||
| @@ -36,7 +36,7 @@ export interface ClaimedSpmMail { | |||
| 36 | } | 36 | } |
| 37 | 37 | ||
| 38 | export interface SpmJwtUnregisteredPayload { | 38 | export interface SpmJwtUnregisteredPayload { |
| 39 | local: string; | 39 | 'li.yggdrasil.local': string; |
| 40 | } | 40 | } |
| 41 | 41 | ||
| 42 | type SpmJwtPayload = JwtPayload & SpmJwtUnregisteredPayload; | 42 | type SpmJwtPayload = JwtPayload & SpmJwtUnregisteredPayload; |
| @@ -136,7 +136,7 @@ export class SpmComponent implements OnInit, OnDestroy { | |||
| 136 | const curr: Map<string, SpmMail> = this.spmMails$.getValue(); | 136 | const curr: Map<string, SpmMail> = this.spmMails$.getValue(); |
| 137 | curr.set(k, { | 137 | curr.set(k, { |
| 138 | state: 'loaded', | 138 | state: 'loaded', |
| 139 | local: payload.local, | 139 | local: payload['li.yggdrasil.local'], |
| 140 | domain: payload.aud, | 140 | domain: payload.aud, |
| 141 | jwt: encoded, | 141 | jwt: encoded, |
| 142 | expiration: payload.exp ? new Date(1000 * payload.exp) : undefined, | 142 | expiration: payload.exp ? new Date(1000 * payload.exp) : undefined, |
diff --git a/overlays/spm/lib/Spm/Api.hs b/overlays/spm/lib/Spm/Api.hs index 04dff2c9..8285cc55 100644 --- a/overlays/spm/lib/Spm/Api.hs +++ b/overlays/spm/lib/Spm/Api.hs | |||
| @@ -5,6 +5,7 @@ module Spm.Api | |||
| 5 | , SpmMailbox, SpmDomain | 5 | , SpmMailbox, SpmDomain |
| 6 | , SpmLocal(..), SpmExtension(..) | 6 | , SpmLocal(..), SpmExtension(..) |
| 7 | , SpmMapping(..), SpmMappingState(..), SpmMappingListingItem(..), SpmMappingListing(..) | 7 | , SpmMapping(..), SpmMappingState(..), SpmMappingListingItem(..), SpmMappingListing(..) |
| 8 | , SpmJWTClaims(..), _spmjwtLocal, _SpmJWTLocal | ||
| 8 | , _SpmMappingText, _SpmMappingStateReject | 9 | , _SpmMappingText, _SpmMappingStateReject |
| 9 | , spmMappingAncestors | 10 | , spmMappingAncestors |
| 10 | , SpmApi, spmApi | 11 | , SpmApi, spmApi |
| @@ -24,18 +25,21 @@ import Type.Reflection (Typeable) | |||
| 24 | 25 | ||
| 25 | import Control.Lens | 26 | import Control.Lens |
| 26 | 27 | ||
| 28 | import Control.Monad (guard) | ||
| 29 | |||
| 27 | import Data.CaseInsensitive (CI) | 30 | import Data.CaseInsensitive (CI) |
| 28 | import qualified Data.CaseInsensitive as CI | 31 | import qualified Data.CaseInsensitive as CI |
| 29 | import Data.CaseInsensitive.Instances () | 32 | import Data.CaseInsensitive.Instances () |
| 30 | 33 | ||
| 31 | import Crypto.JOSE.JWK (JWKSet) | 34 | import Crypto.JOSE.JWK (JWKSet) |
| 32 | import Crypto.JWT (SignedJWT) | 35 | import Crypto.JWT (SignedJWT, ClaimsSet, HasClaimsSet(..), emptyClaimsSet) |
| 33 | import Crypto.JWT.Instances () | 36 | import Crypto.JWT.Instances () |
| 34 | 37 | ||
| 35 | import Data.UUID (UUID) | 38 | import Data.UUID (UUID) |
| 36 | import Data.UUID.Instances () | 39 | import Data.UUID.Instances () |
| 37 | 40 | ||
| 38 | import qualified Data.Aeson as JSON | 41 | import qualified Data.Aeson as JSON |
| 42 | import qualified Data.Aeson.Lens as JSON | ||
| 39 | import Data.Aeson.TH (deriveJSON) | 43 | import Data.Aeson.TH (deriveJSON) |
| 40 | import Data.Aeson.Casing | 44 | import Data.Aeson.Casing |
| 41 | 45 | ||
| @@ -163,6 +167,31 @@ deriveJSON (aesonPrefix trainCase) ''SpmMappingListingItem | |||
| 163 | instance ToJSON SpmMappingListing where | 167 | instance ToJSON SpmMappingListing where |
| 164 | toJSON SpmMappingListing{..} = JSON.object [ "mappings" JSON..= unSpmMappingListing ] | 168 | toJSON SpmMappingListing{..} = JSON.object [ "mappings" JSON..= unSpmMappingListing ] |
| 165 | 169 | ||
| 170 | data SpmJWTClaims = SpmJWTClaims | ||
| 171 | { spmjwtStdClaims :: ClaimsSet | ||
| 172 | , spmjwtLocal :: SpmLocal | ||
| 173 | } deriving stock (Eq, Show, Generic, Typeable) | ||
| 174 | |||
| 175 | makeLensesFor [("spmjwtStdClaims", "_stdClaims"), ("spmjwtLocal", "_spmjwtLocal")] ''SpmJWTClaims | ||
| 176 | |||
| 177 | instance HasClaimsSet SpmJWTClaims where | ||
| 178 | claimsSet = _stdClaims | ||
| 179 | |||
| 180 | _SpmJWTLocal :: (Wrapped l, Unwrapped l ~ Unwrapped SpmLocal) => Prism' SpmJWTClaims l | ||
| 181 | _SpmJWTLocal = prism' toClaims fromClaims | ||
| 182 | where toClaims (view $ _Wrapped' . _Unwrapped' -> spmjwtLocal) = SpmJWTClaims{..} | ||
| 183 | where spmjwtStdClaims = emptyClaimsSet | ||
| 184 | fromClaims SpmJWTClaims{..} = view (_Wrapped' . _Unwrapped') spmjwtLocal <$ guard (spmjwtStdClaims == emptyClaimsSet) | ||
| 185 | |||
| 186 | instance JSON.ToJSON SpmJWTClaims where | ||
| 187 | toJSON SpmJWTClaims{..} = JSON.toJSON spmjwtStdClaims | ||
| 188 | & JSON._Object . at "li.yggdrasil.local" .~ Just (JSON.toJSON spmjwtLocal) | ||
| 189 | |||
| 190 | instance JSON.FromJSON SpmJWTClaims where | ||
| 191 | parseJSON = JSON.withObject "SpmJWTClaims" $ \o -> SpmJWTClaims | ||
| 192 | <$> JSON.parseJSON (JSON._Object # o) | ||
| 193 | <*> o JSON..: "li.yggdrasil.local" | ||
| 194 | |||
| 166 | 195 | ||
| 167 | type SpmApi = "whoami" :> Get '[PlainText, JSON] SpmMailbox | 196 | type SpmApi = "whoami" :> Get '[PlainText, JSON] SpmMailbox |
| 168 | :<|> "domain" :> Get '[PlainText, JSON] SpmDomain | 197 | :<|> "domain" :> Get '[PlainText, JSON] SpmDomain |
diff --git a/overlays/spm/package.yaml b/overlays/spm/package.yaml index e188dc77..6d4abe1c 100644 --- a/overlays/spm/package.yaml +++ b/overlays/spm/package.yaml | |||
| @@ -1,5 +1,5 @@ | |||
| 1 | name: spm | 1 | name: spm |
| 2 | version: 0.1.0 | 2 | version: 0.1.1 |
| 3 | 3 | ||
| 4 | default-extensions: | 4 | default-extensions: |
| 5 | - NoImplicitPrelude | 5 | - NoImplicitPrelude |
| @@ -36,6 +36,7 @@ library: | |||
| 36 | - lens | 36 | - lens |
| 37 | - case-insensitive | 37 | - case-insensitive |
| 38 | - aeson | 38 | - aeson |
| 39 | - lens-aeson | ||
| 39 | - jose | 40 | - jose |
| 40 | - uuid | 41 | - uuid |
| 41 | - containers | 42 | - containers |
diff --git a/overlays/spm/server/Crypto/JOSE/Error/Instances.hs b/overlays/spm/server/Crypto/JOSE/Error/Instances.hs new file mode 100644 index 00000000..9f3eea51 --- /dev/null +++ b/overlays/spm/server/Crypto/JOSE/Error/Instances.hs | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | {-# OPTIONS_GHC -Wno-orphans #-} | ||
| 2 | |||
| 3 | module Crypto.JOSE.Error.Instances () where | ||
| 4 | |||
| 5 | import Data.Monoid (Monoid) | ||
| 6 | import Control.Monad (Monad) | ||
| 7 | import Control.Applicative (Alternative) | ||
| 8 | |||
| 9 | import Crypto.JOSE.Error | ||
| 10 | |||
| 11 | deriving newtype instance (Monad m, Monoid e) => Alternative (JOSE e m) | ||
diff --git a/overlays/spm/server/Spm/Server.hs b/overlays/spm/server/Spm/Server.hs index df12e3d8..8e7f8786 100644 --- a/overlays/spm/server/Spm/Server.hs +++ b/overlays/spm/server/Spm/Server.hs | |||
| @@ -44,7 +44,7 @@ import Database.Persist | |||
| 44 | import Database.Persist.Postgresql | 44 | import Database.Persist.Postgresql |
| 45 | import UnliftIO.Pool | 45 | import UnliftIO.Pool |
| 46 | 46 | ||
| 47 | import Control.Monad.Trans.Reader (ReaderT, runReaderT) | 47 | import Control.Monad.Trans.Reader (ReaderT, runReaderT, mapReaderT) |
| 48 | 48 | ||
| 49 | import Control.Monad.Logger | 49 | import Control.Monad.Logger |
| 50 | 50 | ||
| @@ -58,12 +58,12 @@ import qualified Data.UUID as UUID | |||
| 58 | import qualified Data.UUID.V4 as UUID | 58 | import qualified Data.UUID.V4 as UUID |
| 59 | 59 | ||
| 60 | import qualified Data.Aeson as JSON | 60 | import qualified Data.Aeson as JSON |
| 61 | import Data.Aeson.Lens (_JSON) | ||
| 62 | 61 | ||
| 63 | import System.FilePath ((</>), isRelative) | 62 | import System.FilePath ((</>), isRelative) |
| 64 | 63 | ||
| 65 | import Crypto.JOSE.JWK hiding (Context) | 64 | import Crypto.JOSE.JWK hiding (Context) |
| 66 | import Crypto.JOSE.JWK.Instances () | 65 | import Crypto.JOSE.JWK.Instances () |
| 66 | import Crypto.JOSE.Error.Instances () | ||
| 67 | 67 | ||
| 68 | import Crypto.Random.Instances () | 68 | import Crypto.Random.Instances () |
| 69 | import qualified Crypto.Random as Crypto | 69 | import qualified Crypto.Random as Crypto |
| @@ -197,10 +197,13 @@ mkSpmApp = do | |||
| 197 | spmSql :: ReaderT SqlBackend Handler' a -> Handler' a | 197 | spmSql :: ReaderT SqlBackend Handler' a -> Handler' a |
| 198 | spmSql act = do | 198 | spmSql act = do |
| 199 | sqlPool <- view sctxSqlPool | 199 | sqlPool <- view sctxSqlPool |
| 200 | withResource sqlPool $ runReaderT act | 200 | mapReaderT (mapLoggingT $ either throwError pure <=< liftIO) . withResource sqlPool $ mapReaderT (mapLoggingT runHandler) . runReaderT act |
| 201 | 201 | ||
| 202 | spmJWT :: forall error a. Show error => ServerError -> ExceptT error IO a -> Handler' a | 202 | spmJWT :: forall error a. Show error => ServerError -> JOSE error IO a -> Handler' a |
| 203 | spmJWT errTemplate = either (\err -> throwError errTemplate{ errBody = LBS.fromStrict . Text.encodeUtf8 . Text.pack $ show err }) return <=< liftIO . runExceptT | 203 | spmJWT errTemplate = either (\err -> throwError errTemplate{ errBody = LBS.fromStrict . Text.encodeUtf8 . Text.pack $ show err }) return <=< liftIO . runJOSE |
| 204 | |||
| 205 | withJOSE :: forall m e e' a. Functor m => (e -> e') -> JOSE e m a -> JOSE e' m a | ||
| 206 | withJOSE f = JOSE . withExceptT f . unwrapJOSE | ||
| 204 | 207 | ||
| 205 | generateLocal :: MonadIO m => SpmStyle -> m MailLocal | 208 | generateLocal :: MonadIO m => SpmStyle -> m MailLocal |
| 206 | generateLocal SpmWords = fmap (review _Wrapped . CI.mk) . liftIO $ do | 209 | generateLocal SpmWords = fmap (review _Wrapped . CI.mk) . liftIO $ do |
| @@ -254,24 +257,23 @@ spmServer dom mbox = whoami | |||
| 254 | instanceId' <- view sctxInstanceId | 257 | instanceId' <- view sctxInstanceId |
| 255 | jwks <- view $ sctxJwkSet . _Wrapped | 258 | jwks <- view $ sctxJwkSet . _Wrapped |
| 256 | tokenId <- liftIO UUID.nextRandom | 259 | tokenId <- liftIO UUID.nextRandom |
| 257 | let claimsSet = emptyClaimsSet | 260 | let jwtClaims = (_SpmJWTLocal # local) |
| 258 | & claimIss ?~ (JWT.string # UUID.toText instanceId') | 261 | & claimIss ?~ (JWT.string # UUID.toText instanceId') |
| 259 | & claimAud ?~ Audience (pure $ dom ^. _Wrapped . to CI.original . re JWT.string) | 262 | & claimAud ?~ Audience (pure $ dom ^. _Wrapped . to CI.original . re JWT.string) |
| 260 | & claimNbf ?~ NumericDate t | 263 | & claimNbf ?~ NumericDate t |
| 261 | & claimIat ?~ NumericDate t | 264 | & claimIat ?~ NumericDate t |
| 262 | & claimExp ?~ NumericDate (600 `addUTCTime` t) | 265 | & claimExp ?~ NumericDate (600 `addUTCTime` t) |
| 263 | & claimJti ?~ UUID.toText tokenId | 266 | & claimJti ?~ UUID.toText tokenId |
| 264 | & unregisteredClaims . at "local" ?~ view (_Wrapped . re _JSON) local | ||
| 265 | spmJWT @JWT.Error err500 $ do | 267 | spmJWT @JWT.Error err500 $ do |
| 266 | (jwsAlg, selectedJwk) <- withExceptT (fromMaybe JWT.NoUsableKeys . getFirst) . asum $ map (\jwk' -> (, jwk') <$> withExceptT (First . Just) (bestJWSAlg jwk')) jwks | 268 | (jwsAlg, selectedJwk) <- withJOSE (fromMaybe JWT.NoUsableKeys . getFirst) . asum $ map (\jwk' -> (, jwk') <$> withJOSE (First . Just) (bestJWSAlg jwk')) jwks |
| 267 | signClaims selectedJwk (newJWSHeader ((), jwsAlg)) claimsSet | 269 | signJWT selectedJwk (newJWSHeader ((), jwsAlg)) jwtClaims |
| 268 | 270 | ||
| 269 | claim jwt = do | 271 | claim jwt = do |
| 270 | jwks <- view sctxJwkSet | 272 | jwks <- view sctxJwkSet |
| 271 | let validationSettings' = defaultJWTValidationSettings ((== Just dom) . fmap (review _Wrapped . CI.mk) . preview JWT.string) | 273 | let validationSettings' = defaultJWTValidationSettings ((== Just dom) . fmap (review _Wrapped . CI.mk) . preview JWT.string) |
| 272 | & jwtValidationSettingsAllowedSkew .~ 5 | 274 | & jwtValidationSettingsAllowedSkew .~ 5 |
| 273 | claims <- spmJWT @JWT.JWTError err403 $ verifyClaims validationSettings' jwks jwt | 275 | jwtClaims <- spmJWT @JWT.JWTError err403 $ verifyJWT validationSettings' jwks jwt |
| 274 | mailboxMappingLocal <- maybe (throwError err400{ errBody = "Claim ‘local’ missing" }) (return . Just) $ claims ^? unregisteredClaims . ix "local" . _JSON . to CI.mk . re _Wrapped | 276 | let mailboxMappingLocal = Just $ jwtClaims ^. _spmjwtLocal . _Wrapped . _Unwrapped |
| 275 | 277 | ||
| 276 | spmSql $ do | 278 | spmSql $ do |
| 277 | Entity mailboxMappingMailbox _ <- maybe (throwError err404) return <=< getBy $ UniqueMailbox mbox | 279 | Entity mailboxMappingMailbox _ <- maybe (throwError err404) return <=< getBy $ UniqueMailbox mbox |
diff --git a/overlays/spm/spm.nix b/overlays/spm/spm.nix index 7c9eb933..2d1f7eed 100644 --- a/overlays/spm/spm.nix +++ b/overlays/spm/spm.nix | |||
| @@ -10,13 +10,13 @@ | |||
| 10 | }: | 10 | }: |
| 11 | mkDerivation { | 11 | mkDerivation { |
| 12 | pname = "spm"; | 12 | pname = "spm"; |
| 13 | version = "0.1.0"; | 13 | version = "0.1.1"; |
| 14 | src = ./.; | 14 | src = ./.; |
| 15 | isLibrary = true; | 15 | isLibrary = true; |
| 16 | isExecutable = true; | 16 | isExecutable = true; |
| 17 | libraryHaskellDepends = [ | 17 | libraryHaskellDepends = [ |
| 18 | aeson aeson-casing base case-insensitive containers jose lens | 18 | aeson aeson-casing base case-insensitive containers jose lens |
| 19 | servant text uuid | 19 | lens-aeson servant text uuid |
| 20 | ]; | 20 | ]; |
| 21 | libraryToolDepends = [ hpack ]; | 21 | libraryToolDepends = [ hpack ]; |
| 22 | executableHaskellDepends = [ | 22 | executableHaskellDepends = [ |
