diff --git a/changelog.d/5-internal/idp-invariants b/changelog.d/5-internal/idp-invariants new file mode 100644 index 0000000000..e523ef2a2f --- /dev/null +++ b/changelog.d/5-internal/idp-invariants @@ -0,0 +1 @@ +Enforce some IdP invariants diff --git a/deploy/services-demo/create_test_team_scim.sh b/deploy/services-demo/create_test_team_scim.sh index b9ff961277..552b4e15c1 100755 --- a/deploy/services-demo/create_test_team_scim.sh +++ b/deploy/services-demo/create_test_team_scim.sh @@ -6,7 +6,7 @@ BRIG_HOST="http://localhost:8082" SPAR_HOST="http://localhost:8088" USAGE=" -This bash script craates +This bash script creates 1) team 2) team admin 3) scim token diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs index e656d62a92..859d2ad13d 100644 --- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs +++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs @@ -66,7 +66,10 @@ data WireIdPAPIVersion deriving stock (Eq, Show, Enum, Bounded, Generic) deriving (Arbitrary) via (GenericUniform WireIdPAPIVersion) --- | (Internal issue for making v2 the default: https://wearezeta.atlassian.net/browse/SQSERVICES-781) +-- | (Internal issue for making v2 the default: +-- https://wearezeta.atlassian.net/browse/SQSERVICES-781. BEWARE: We probably shouldn't ever +-- do this, but remove V1 entirely instead. which requires migrating away from the old table +-- on all on-prem installations. which takes time.) defWireIdPAPIVersion :: WireIdPAPIVersion defWireIdPAPIVersion = WireIdPAPIV1 diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 0dc733def6..32badbaa38 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -51,7 +51,6 @@ library Spar.Sem.IdPConfigStore Spar.Sem.IdPConfigStore.Cassandra Spar.Sem.IdPConfigStore.Mem - Spar.Sem.IdPConfigStore.Spec Spar.Sem.IdPRawMetadataStore Spar.Sem.IdPRawMetadataStore.Cassandra Spar.Sem.IdPRawMetadataStore.Mem @@ -716,7 +715,6 @@ test-suite spec Test.Spar.Roundtrip.ByteString Test.Spar.ScimSpec Test.Spar.Sem.DefaultSsoCodeSpec - Test.Spar.Sem.IdPConfigStoreSpec Test.Spar.Sem.IdPRawMetadataStoreSpec Test.Spar.Sem.NowSpec Test.Spar.Sem.ScimExternalIdStoreSpec diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 8cf4bd33ac..b8f5a34b23 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -44,7 +44,9 @@ module Spar.API ) where +import Brig.Types.Intra import Brig.Types.User (HavePendingInvitations (NoPendingInvitations)) +import Cassandra as Cas import Control.Lens import Control.Monad.Except import qualified Data.ByteString as SBS @@ -75,7 +77,7 @@ import Spar.Sem.DefaultSsoCode (DefaultSsoCode) import qualified Spar.Sem.DefaultSsoCode as DefaultSsoCode import Spar.Sem.GalleyAccess (GalleyAccess) import qualified Spar.Sem.GalleyAccess as GalleyAccess -import Spar.Sem.IdPConfigStore (GetIdPResult (..), IdPConfigStore, Replaced (..), Replacing (..)) +import Spar.Sem.IdPConfigStore (IdPConfigStore, Replaced (..), Replacing (..)) import qualified Spar.Sem.IdPConfigStore as IdPConfigStore import Spar.Sem.IdPRawMetadataStore (IdPRawMetadataStore) import qualified Spar.Sem.IdPRawMetadataStore as IdPRawMetadataStore @@ -95,7 +97,7 @@ import qualified Spar.Sem.VerdictFormatStore as VerdictFormatStore import System.Logger (Msg) import qualified URI.ByteString as URI import Wire.API.Routes.Public.Spar -import Wire.API.User (userIssuer) +import Wire.API.User import Wire.API.User.IdentityProvider import Wire.API.User.Saml import Wire.Sem.Logger (Logger) @@ -230,8 +232,8 @@ authreqPrecheck :: Sem r NoContent authreqPrecheck msucc merr idpid = validateAuthreqParams msucc merr - *> getIdPConfig idpid - *> pure NoContent + *> IdPConfigStore.getConfig idpid + $> NoContent authreq :: Members @@ -255,7 +257,7 @@ authreq :: authreq authreqttl msucc merr idpid = do vformat <- validateAuthreqParams msucc merr form@(SAML.FormRedirect _ ((^. SAML.rqID) -> reqid)) <- do - idp :: IdP <- IdPConfigStore.getConfig idpid >>= maybe (throwSparSem (SparIdPNotFound (cs $ show idpid))) pure + idp :: IdP <- IdPConfigStore.getConfig idpid let mbtid :: Maybe TeamId mbtid = case fromMaybe defWireIdPAPIVersion (idp ^. SAML.idpExtraInfo . wiApiVersion) of WireIdPAPIV1 -> Nothing @@ -342,7 +344,7 @@ idpGet :: SAML.IdPId -> Sem r IdP idpGet zusr idpid = withDebugLog "idpGet" (Just . show . (^. SAML.idpId)) $ do - idp <- getIdPConfig idpid + idp <- IdPConfigStore.getConfig idpid _ <- authorizeIdP zusr idp pure idp @@ -359,7 +361,7 @@ idpGetRaw :: SAML.IdPId -> Sem r RawIdPMetadata idpGetRaw zusr idpid = do - idp <- getIdPConfig idpid + idp <- IdPConfigStore.getConfig idpid _ <- authorizeIdP zusr idp IdPRawMetadataStore.get idpid >>= \case Just txt -> pure $ RawIdPMetadata txt @@ -408,25 +410,12 @@ idpDelete :: SAML.IdPId -> Maybe Bool -> Sem r NoContent -idpDelete zusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (const Nothing) $ do - idp <- getIdPConfig idpid - _ <- authorizeIdP zusr idp +idpDelete mbzusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (const Nothing) $ do + idp <- IdPConfigStore.getConfig idpid + (zusr, team) <- authorizeIdP mbzusr idp let issuer = idp ^. SAML.idpMetadata . SAML.edIssuer - team = idp ^. SAML.idpExtraInfo . wiTeam - -- if idp is not empty: fail or purge - let doPurge :: Sem r () - doPurge = do - some <- SAMLUserStore.getSomeByIssuer issuer - forM_ some $ \(uref, uid) -> do - BrigAccess.delete uid - SAMLUserStore.delete uid uref - unless (null some) doPurge - idpIsEmpty <- isNothing <$> SAMLUserStore.getAnyByIssuer issuer - whenM (maybe (pure False) (idpDoesAuthSelf idp) zusr) $ throwSparSem SparIdPCannotDeleteOwnIdp - unless idpIsEmpty $ - if purge - then doPurge - else throwSparSem SparIdPHasBoundUsers + whenM (idpDoesAuthSelf idp zusr) $ throwSparSem SparIdPCannotDeleteOwnIdp + SAMLUserStore.getAllByIssuerPaginated issuer >>= assertEmptyOrPurge team updateOldIssuers idp updateReplacingIdP idp -- Delete tokens associated with given IdP (we rely on the fact that @@ -441,23 +430,37 @@ idpDelete zusr idpid (fromMaybe False -> purge) = withDebugLog "idpDelete" (cons IdPRawMetadataStore.delete idpid pure NoContent where + assertEmptyOrPurge :: TeamId -> Cas.Page (SAML.UserRef, UserId) -> Sem r () + assertEmptyOrPurge team page = do + forM_ (Cas.result page) $ \(uref, uid) -> do + mAccount <- BrigAccess.getAccount NoPendingInvitations uid + let mUserTeam = userTeam . accountUser =<< mAccount + when (mUserTeam == Just team) $ do + if purge + then do + BrigAccess.delete uid + SAMLUserStore.delete uid uref + else do + throwSparSem SparIdPHasBoundUsers + when (Cas.hasMore page) $ + SAMLUserStore.nextPage page >>= assertEmptyOrPurge team + updateOldIssuers :: IdP -> Sem r () updateOldIssuers _ = pure () -- we *could* update @idp ^. SAML.idpExtraInfo . wiReplacedBy@ to not keep the idp about -- to be deleted in its old issuers list, but it's tricky to avoid race conditions, and -- there is little to be gained here: we only use old issuers to find users that have not -- been migrated yet, and if an old user points to a deleted idp, it just means that we - -- won't find any users to migrate. still, doesn't hurt much to look either. so we + -- won't find any users to migrate. still, doesn't hurt mucht to look either. so we -- leave old issuers dangling for now. updateReplacingIdP :: IdP -> Sem r () - updateReplacingIdP idp = forM_ (idp ^. SAML.idpExtraInfo . wiOldIssuers) $ \oldIssuer -> - getIdPIdByIssuer oldIssuer (idp ^. SAML.idpExtraInfo . wiTeam) >>= \case - GetIdPFound iid -> IdPConfigStore.clearReplacedBy $ Replaced iid - GetIdPNotFound -> pure () - GetIdPDanglingId _ -> pure () - GetIdPNonUnique _ -> pure () - GetIdPWrongTeam _ -> pure () + updateReplacingIdP idp = forM_ (idp ^. SAML.idpExtraInfo . wiOldIssuers) $ \oldIssuer -> do + iid <- + view SAML.idpId <$> case fromMaybe defWireIdPAPIVersion $ idp ^. SAML.idpExtraInfo . wiApiVersion of + WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1 oldIssuer + WireIdPAPIV2 -> IdPConfigStore.getIdPByIssuerV2 oldIssuer (idp ^. SAML.idpExtraInfo . wiTeam) + IdPConfigStore.clearReplacedBy $ Replaced iid idpDoesAuthSelf :: IdP -> UserId -> Sem r Bool idpDoesAuthSelf idp uid = do @@ -505,13 +508,13 @@ idpCreateXML :: Maybe SAML.IdPId -> Maybe WireIdPAPIVersion -> Sem r IdP -idpCreateXML zusr raw idpmeta mReplaces (fromMaybe defWireIdPAPIVersion -> apiversion) = withDebugLog "idpCreate" (Just . show . (^. SAML.idpId)) $ do +idpCreateXML zusr raw idpmeta mReplaces (fromMaybe defWireIdPAPIVersion -> apiversion) = withDebugLog "idpCreateXML" (Just . show . (^. SAML.idpId)) $ do teamid <- Brig.getZUsrCheckPerm zusr CreateUpdateDeleteIdp GalleyAccess.assertSSOEnabled teamid assertNoScimOrNoIdP teamid idp <- validateNewIdP apiversion idpmeta teamid mReplaces IdPRawMetadataStore.store (idp ^. SAML.idpId) raw - storeIdPConfig idp + IdPConfigStore.insertConfig idp forM_ mReplaces $ \replaces -> IdPConfigStore.setReplacedBy (Replaced replaces) (Replacing (idp ^. SAML.idpId)) pure idp @@ -577,35 +580,26 @@ validateNewIdP apiversion _idpMetadata teamId mReplaces = withDebugLog "validate oldIssuers :: [SAML.Issuer] <- case mReplaces of Nothing -> pure [] Just replaces -> do - idp <- IdPConfigStore.getConfig replaces >>= maybe (throwSparSem (SparIdPNotFound (cs $ show mReplaces))) pure + idp <- IdPConfigStore.getConfig replaces pure $ (idp ^. SAML.idpMetadata . SAML.edIssuer) : (idp ^. SAML.idpExtraInfo . wiOldIssuers) let requri = _idpMetadata ^. SAML.edRequestURI _idpExtraInfo = WireIdP teamId (Just apiversion) oldIssuers Nothing enforceHttps requri - idp <- getIdPConfigByIssuer (_idpMetadata ^. SAML.edIssuer) teamId + mbIdp <- case apiversion of + WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1Maybe (_idpMetadata ^. SAML.edIssuer) + WireIdPAPIV2 -> IdPConfigStore.getIdPByIssuerV2Maybe (_idpMetadata ^. SAML.edIssuer) teamId Logger.log Logger.Debug $ show (apiversion, _idpMetadata, teamId, mReplaces) - Logger.log Logger.Debug $ show (_idpId, oldIssuers, idp) - - let handleIdPClash :: Either id idp -> m () - -- (HINT: using type vars above instead of the actual types constitutes a proof that - -- we're not using any properties of the arguments in this function.) - handleIdPClash = case apiversion of - WireIdPAPIV1 -> const $ do - throwSparSem $ SparNewIdPAlreadyInUse "you can't create an IdP with api_version v1 if the issuer is already in use on the wire instance." - WireIdPAPIV2 -> \case - (Right _) -> do - -- idp' was found by lookup with teamid, so it's in the same team. - throwSparSem $ SparNewIdPAlreadyInUse "if the exisitng IdP is registered for a team, the new one can't have it." - (Left _) -> do - -- this idp *id* is from a different team, and we're in the 'WireIdPAPIV2' case, so this is fine. - pure () - - case idp of - GetIdPFound idp' {- same team -} -> handleIdPClash (Right idp') - GetIdPNotFound -> pure () - res@(GetIdPDanglingId _) -> throwSparSem . SparIdPNotFound . ("validateNewIdP: " <>) . cs . show $ res -- database inconsistency - GetIdPNonUnique ids' {- same team didn't yield anything, but there are at least two other teams with this issuer already -} -> handleIdPClash (Left ids') - GetIdPWrongTeam id' {- different team -} -> handleIdPClash (Left id') + Logger.log Logger.Debug $ show (_idpId, oldIssuers, mbIdp) + + let failWithIdPClash :: m () + failWithIdPClash = throwSparSem . SparNewIdPAlreadyInUse $ case apiversion of + WireIdPAPIV1 -> + "you can't create an IdP with api_version v1 if the issuer is already in use on the wire instance." + WireIdPAPIV2 -> + -- idp was found by lookup with teamid, so it's in the same team. + "you can't create an IdP with api_version v1 if the issuer is already in use in your team." + + unless (isNothing mbIdp) failWithIdPClash pure SAML.IdPConfig {..} @@ -645,17 +639,20 @@ idpUpdateXML :: SAML.IdPMetadata -> SAML.IdPId -> Sem r IdP -idpUpdateXML zusr raw idpmeta idpid = withDebugLog "idpUpdate" (Just . show . (^. SAML.idpId)) $ do +idpUpdateXML zusr raw idpmeta idpid = withDebugLog "idpUpdateXML" (Just . show . (^. SAML.idpId)) $ do (teamid, idp) <- validateIdPUpdate zusr idpmeta idpid GalleyAccess.assertSSOEnabled teamid IdPRawMetadataStore.store (idp ^. SAML.idpId) raw -- (if raw metadata is stored and then spar goes out, raw metadata won't match the -- structured idp config. since this will lead to a 5xx response, the client is expected to -- try again, which would clean up cassandra state.) - storeIdPConfig idp + IdPConfigStore.insertConfig idp -- if the IdP issuer is updated, the old issuer must be removed explicitly. -- if this step is ommitted (due to a crash) resending the update request should fix the inconsistent state. - forM_ (idp ^. SAML.idpExtraInfo . wiOldIssuers) IdPConfigStore.deleteIssuer + let mbteamid = case fromMaybe defWireIdPAPIVersion $ idp ^. SAML.idpExtraInfo . wiApiVersion of + WireIdPAPIV1 -> Nothing + WireIdPAPIV2 -> Just teamid + forM_ (idp ^. SAML.idpExtraInfo . wiOldIssuers) (flip IdPConfigStore.deleteIssuer mbteamid) pure idp -- | Check that: idp id is valid; calling user is admin in that idp's home team; team id in @@ -678,30 +675,32 @@ validateIdPUpdate :: SAML.IdPMetadata -> SAML.IdPId -> m (TeamId, IdP) -validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateNewIdP" (Just . show . (_2 %~ (^. SAML.idpId))) $ do - previousIdP <- - IdPConfigStore.getConfig _idpId >>= \case - Nothing -> throw errUnknownIdPId - Just idp -> pure idp - teamId <- authorizeIdP zusr previousIdP +validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateIdPUpdate" (Just . show . (_2 %~ (^. SAML.idpId))) $ do + previousIdP <- IdPConfigStore.getConfig _idpId + (_, teamId) <- authorizeIdP zusr previousIdP unless (previousIdP ^. SAML.idpExtraInfo . wiTeam == teamId) $ throw errUnknownIdP _idpExtraInfo <- do let previousIssuer = previousIdP ^. SAML.idpMetadata . SAML.edIssuer newIssuer = _idpMetadata ^. SAML.edIssuer if previousIssuer == newIssuer - then pure $ previousIdP ^. SAML.idpExtraInfo + then do + -- idempotency + pure $ previousIdP ^. SAML.idpExtraInfo else do - foundConfig <- getIdPConfigByIssuerAllowOld newIssuer (Just teamId) - notInUseByOthers <- case foundConfig of - GetIdPFound c -> pure $ c ^. SAML.idpId == _idpId - GetIdPNotFound -> pure True - res@(GetIdPDanglingId _) -> throwSparSem . SparIdPNotFound . ("validateIdPUpdate: " <>) . cs . show $ res -- impossible - res@(GetIdPNonUnique _) -> throwSparSem . SparIdPNotFound . ("validateIdPUpdate: " <>) . cs . show $ res -- impossible (because team id was used in lookup) - GetIdPWrongTeam _ -> pure False - if notInUseByOthers - then pure $ previousIdP ^. SAML.idpExtraInfo & wiOldIssuers %~ nub . (previousIssuer :) - else throwSparSem SparIdPIssuerInUse + idpIssuerInUse <- + ( case fromMaybe defWireIdPAPIVersion $ previousIdP ^. SAML.idpExtraInfo . wiApiVersion of + WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1Maybe newIssuer + WireIdPAPIV2 -> IdPConfigStore.getIdPByIssuerV2Maybe newIssuer teamId + ) + <&> ( \case + Just idpFound -> idpFound ^. SAML.idpId /= _idpId + Nothing -> False + ) + if idpIssuerInUse + then throwSparSem SparIdPIssuerInUse + else pure $ previousIdP ^. SAML.idpExtraInfo & wiOldIssuers %~ nub . (previousIssuer :) + let requri = _idpMetadata ^. SAML.edRequestURI enforceHttps requri pure (teamId, SAML.IdPConfig {..}) @@ -710,7 +709,6 @@ validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateNewIdP" (Just where enc = cs . toLazyByteString . URI.serializeURIRef uri = _idpMetadata ^. SAML.edIssuer . SAML.fromIssuer - errUnknownIdPId = SAML.UnknownIdP . cs . SAML.idPIdToST $ _idpId withDebugLog :: Member (Logger String) r => String -> (a -> Maybe String) -> Sem r a -> Sem r a withDebugLog msg showval action = do @@ -724,12 +722,12 @@ authorizeIdP :: (HasCallStack, Members '[GalleyAccess, BrigAccess, Error SparError] r) => Maybe UserId -> IdP -> - Sem r TeamId + Sem r (UserId, TeamId) authorizeIdP Nothing _ = throw (SAML.CustomError $ SparNoPermission (cs $ show CreateUpdateDeleteIdp)) authorizeIdP (Just zusr) idp = do let teamid = idp ^. SAML.idpExtraInfo . wiTeam GalleyAccess.assertHasPermission teamid CreateUpdateDeleteIdp zusr - pure teamid + pure (zusr, teamid) enforceHttps :: Member (Error SparError) r => URI.URI -> Sem r () enforceHttps uri = @@ -762,12 +760,9 @@ internalPutSsoSettings SsoSettings {defaultSsoCode = Nothing} = do DefaultSsoCode.delete pure NoContent internalPutSsoSettings SsoSettings {defaultSsoCode = Just code} = - IdPConfigStore.getConfig code >>= \case - Nothing -> - -- this will return a 404, which is not quite right, - -- but it's an internal endpoint and the message clearly says - -- "Could not find IdP". - throwSparSem $ SparIdPNotFound mempty - Just _ -> do - DefaultSsoCode.store code - pure NoContent + -- this can throw a 404, which is not quite right, + -- but it's an internal endpoint and the message clearly says + -- "Could not find IdP". + IdPConfigStore.getConfig code + *> DefaultSsoCode.store code + $> NoContent diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index 7ccecbe473..ce3e10ac04 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -26,17 +26,10 @@ module Spar.App GetUserResult (..), getUserIdByUref, getUserIdByScimExternalId, - insertUser, validateEmailIfExists, validateEmail, errorPage, - getIdPIdByIssuer, - getIdPConfigByIssuer, - getIdPConfigByIssuerAllowOld, deleteTeam, - getIdPConfig, - storeIdPConfig, - getIdPConfigByIssuerOptionalSPId, sparToServerErrorWithLogging, renderSparErrorWithLogging, ) @@ -62,8 +55,7 @@ import Polysemy import Polysemy.Error import SAML2.Util (renderURI) import SAML2.WebSSO - ( IdPId (..), - Issuer (..), + ( Issuer (..), UnqualifiedNameID (..), explainDeniedReason, idpExtraInfo, @@ -81,7 +73,7 @@ import Spar.Sem.BrigAccess (BrigAccess) import qualified Spar.Sem.BrigAccess as BrigAccess import Spar.Sem.GalleyAccess (GalleyAccess) import qualified Spar.Sem.GalleyAccess as GalleyAccess -import Spar.Sem.IdPConfigStore (GetIdPResult (..), IdPConfigStore) +import Spar.Sem.IdPConfigStore (IdPConfigStore) import qualified Spar.Sem.IdPConfigStore as IdPConfigStore import Spar.Sem.Reporter (Reporter) import qualified Spar.Sem.Reporter as Reporter @@ -118,31 +110,6 @@ data Env = Env sparCtxRequestId :: RequestId } -getIdPConfig :: - Members - '[ IdPConfigStore, - Error SparError - ] - r => - IdPId -> - Sem r IdP -getIdPConfig = maybe (throwSparSem (SparIdPNotFound mempty)) pure <=< IdPConfigStore.getConfig - -storeIdPConfig :: Member IdPConfigStore r => IdP -> Sem r () -storeIdPConfig = IdPConfigStore.storeConfig - -getIdPConfigByIssuerOptionalSPId :: Members '[IdPConfigStore, Error SparError] r => Issuer -> Maybe TeamId -> Sem r IdP -getIdPConfigByIssuerOptionalSPId issuer mbteam = do - getIdPConfigByIssuerAllowOld issuer mbteam >>= \case - GetIdPFound idp -> pure idp - GetIdPNotFound -> throwSparSem $ SparIdPNotFound mempty - res@(GetIdPDanglingId _) -> throwSparSem $ SparIdPNotFound (cs $ show res) - res@(GetIdPNonUnique _) -> throwSparSem $ SparIdPNotFound (cs $ show res) - res@(GetIdPWrongTeam _) -> throwSparSem $ SparIdPNotFound (cs $ show res) - -insertUser :: Member SAMLUserStore r => SAML.UserRef -> UserId -> Sem r () -insertUser = SAMLUserStore.insert - -- | Look up user locally in table @spar.user@ or @spar.scim_user@ (depending on the -- argument), then in brig, then return the 'UserId'. If either lookup fails, or user is not -- in a team, return 'Nothing'. @@ -232,7 +199,7 @@ createSamlUserWithId teamid buid suid = do uname <- either (throwSparSem . SparBadUserName . cs) pure $ Intra.mkUserName Nothing (UrefOnly suid) buid' <- BrigAccess.createSAML suid buid teamid uname ManagedByWire assert (buid == buid') $ pure () - insertUser suid buid + SAMLUserStore.insert suid buid -- | If the team has no scim token, call 'createSamlUser'. Otherwise, raise "invalid -- credentials". @@ -272,7 +239,9 @@ autoprovisionSamlUserWithId :: SAML.UserRef -> Sem r () autoprovisionSamlUserWithId mbteam buid suid = do - idp <- getIdPConfigByIssuerOptionalSPId (suid ^. uidTenant) mbteam + idp <- case mbteam of + Just team -> IdPConfigStore.getIdPByIssuerV2 (suid ^. uidTenant) team + Nothing -> IdPConfigStore.getIdPByIssuerV1 (suid ^. uidTenant) guardReplacedIdP idp guardScimTokens idp createSamlUserWithId (idp ^. idpExtraInfo . wiTeam) buid suid @@ -417,7 +386,9 @@ findUserIdWithOldIssuer :: SAML.UserRef -> Sem r (GetUserResult (SAML.UserRef, UserId)) findUserIdWithOldIssuer mbteam (SAML.UserRef issuer subject) = do - idp <- getIdPConfigByIssuerOptionalSPId issuer mbteam + idp <- case mbteam of + Just team -> IdPConfigStore.getIdPByIssuerV2 issuer team + Nothing -> IdPConfigStore.getIdPByIssuerV1 issuer let tryFind :: GetUserResult (SAML.UserRef, UserId) -> Issuer -> Sem r (GetUserResult (SAML.UserRef, UserId)) tryFind found@(GetUserFound _) _ = pure found tryFind _ oldIssuer = (uref,) <$$> getUserIdByUref mbteam uref @@ -623,65 +594,6 @@ errorPage err mpInputs = "" ] --- | Like 'getIdPIdByIssuer', but do not require a 'TeamId'. If none is provided, see if a --- single solution can be found without. -getIdPIdByIssuerAllowOld :: - HasCallStack => - Member IdPConfigStore r => - SAML.Issuer -> - Maybe TeamId -> - Sem r (GetIdPResult SAML.IdPId) -getIdPIdByIssuerAllowOld issuer mbteam = do - mbv2 <- maybe (pure Nothing) (IdPConfigStore.getIdByIssuerWithTeam issuer) mbteam - mbv1v2 <- maybe (IdPConfigStore.getIdByIssuerWithoutTeam issuer) (pure . GetIdPFound) mbv2 - case (mbv1v2, mbteam) of - (GetIdPFound idpid, Just team) -> do - IdPConfigStore.getConfig idpid >>= \case - Nothing -> do - pure $ GetIdPDanglingId idpid - Just idp -> - pure $ - if idp ^. SAML.idpExtraInfo . wiTeam /= team - then GetIdPWrongTeam idpid - else mbv1v2 - _ -> pure mbv1v2 - --- | See 'getIdPIdByIssuer'. -getIdPConfigByIssuer :: - (HasCallStack, Member IdPConfigStore r) => - SAML.Issuer -> - TeamId -> - Sem r (GetIdPResult IdP) -getIdPConfigByIssuer issuer = - getIdPIdByIssuer issuer >=> mapGetIdPResult - --- | See 'getIdPIdByIssuerAllowOld'. -getIdPConfigByIssuerAllowOld :: - (HasCallStack, Member IdPConfigStore r) => - SAML.Issuer -> - Maybe TeamId -> - Sem r (GetIdPResult IdP) -getIdPConfigByIssuerAllowOld issuer = do - getIdPIdByIssuerAllowOld issuer >=> mapGetIdPResult - --- | Lookup idp in table `issuer_idp_v2` (using both issuer entityID and teamid); if nothing --- is found there or if teamid is 'Nothing', lookup under issuer in `issuer_idp`. -getIdPIdByIssuer :: - (HasCallStack, Member IdPConfigStore r) => - SAML.Issuer -> - TeamId -> - Sem r (GetIdPResult SAML.IdPId) -getIdPIdByIssuer issuer = getIdPIdByIssuerAllowOld issuer . Just - --- | (There are probably category theoretical models for what we're doing here, but it's more --- straight-forward to just handle the one instance we need.) -mapGetIdPResult :: (HasCallStack, Member IdPConfigStore r) => GetIdPResult SAML.IdPId -> Sem r (GetIdPResult IdP) -mapGetIdPResult (GetIdPFound i) = IdPConfigStore.getConfig i <&> maybe (GetIdPDanglingId i) GetIdPFound -mapGetIdPResult GetIdPNotFound = pure GetIdPNotFound -mapGetIdPResult (GetIdPDanglingId i) = pure (GetIdPDanglingId i) -mapGetIdPResult (GetIdPNonUnique is) = pure (GetIdPNonUnique is) -mapGetIdPResult (GetIdPWrongTeam i) = pure (GetIdPWrongTeam i) - -- | Delete all tokens belonging to a team. deleteTeam :: (HasCallStack, Members '[ScimTokenStore, SAMLUserStore, IdPConfigStore] r) => diff --git a/services/spar/src/Spar/CanonicalInterpreter.hs b/services/spar/src/Spar/CanonicalInterpreter.hs index ab49bbce43..02475109a2 100644 --- a/services/spar/src/Spar/CanonicalInterpreter.hs +++ b/services/spar/src/Spar/CanonicalInterpreter.hs @@ -62,7 +62,7 @@ import Spar.Sem.ScimTokenStore (ScimTokenStore) import Spar.Sem.ScimTokenStore.Cassandra (scimTokenStoreToCassandra) import Spar.Sem.ScimUserTimesStore (ScimUserTimesStore) import Spar.Sem.ScimUserTimesStore.Cassandra (scimUserTimesStoreToCassandra) -import Spar.Sem.Utils (interpretClientToIO, ttlErrorToSparError) +import Spar.Sem.Utils (idpDbErrorToSparError, interpretClientToIO, ttlErrorToSparError) import Spar.Sem.VerdictFormatStore (VerdictFormatStore) import Spar.Sem.VerdictFormatStore.Cassandra (verdictFormatStoreToCassandra) import qualified System.Logger as TinyLog @@ -90,6 +90,7 @@ type CanonicalEffs = Embed Cas.Client, BrigAccess, GalleyAccess, + Error IdpDbError, Error TTLError, Error SparError, Reporter, @@ -117,6 +118,7 @@ runSparToIO ctx action = . reporterToTinyLogWai . runError @SparError . ttlErrorToSparError + . idpDbErrorToSparError . galleyAccessToHttp (sparCtxHttpManager ctx) (sparCtxHttpGalley ctx) . brigAccessToHttp (sparCtxHttpManager ctx) (sparCtxHttpBrig ctx) . interpretClientToIO (sparCtxCas ctx) diff --git a/services/spar/src/Spar/Error.hs b/services/spar/src/Spar/Error.hs index 7fb7db18f7..4dcf546d21 100644 --- a/services/spar/src/Spar/Error.hs +++ b/services/spar/src/Spar/Error.hs @@ -27,6 +27,7 @@ module Spar.Error ( SparError, SparCustomError (..), + IdpDbError (..), throwSpar, sparToServerErrorWithLogging, rethrow, @@ -95,6 +96,7 @@ data SparCustomError | SparIdPHasBoundUsers | SparIdPIssuerInUse | SparIdPCannotDeleteOwnIdp + | IdpDbError IdpDbError | SparProvisioningMoreThanOneIdP LT | SparProvisioningTokenLimitReached | -- | FUTUREWORK(fisx): This constructor is used in exactly one place (see @@ -107,6 +109,15 @@ data SparCustomError SparScimError Scim.ScimError deriving (Eq, Show) +data IdpDbError + = InsertIdPConfigCannotMixApiVersions + | AttemptToGetV1IssuerViaV2API + | AttemptToGetV2IssuerViaV1API + | IdpNonUnique + | IdpWrongTeam + | IdpNotFound -- like 'SparIdPNotFound', but a database consistency error. (should we consolidate something anyway?) + deriving (Eq, Show) + sparToServerErrorWithLogging :: MonadIO m => Log.Logger -> SparError -> m ServerError sparToServerErrorWithLogging logger err = do let errServant = sparToServerError err @@ -174,6 +185,12 @@ renderSparError (SAML.CustomError (SparNewIdPWantHttps msg)) = Right $ Wai.mkErr renderSparError (SAML.CustomError SparIdPHasBoundUsers) = Right $ Wai.mkError status412 "idp-has-bound-users" "an idp can only be deleted if it is empty" renderSparError (SAML.CustomError SparIdPIssuerInUse) = Right $ Wai.mkError status400 "idp-issuer-in-use" "The issuer of your IdP is already in use. Remove the entry in the team that uses it, or construct a new IdP issuer." renderSparError (SAML.CustomError SparIdPCannotDeleteOwnIdp) = Right $ Wai.mkError status409 "cannot-delete-own-idp" "You cannot delete the IdP used to login with your own account." +renderSparError (SAML.CustomError (IdpDbError InsertIdPConfigCannotMixApiVersions)) = Right $ Wai.mkError status409 "cannot-mix-idp-api-verions" "You cannot have two IdPs with the same issuerwhere one of them is using API V1 and one API V2." +renderSparError (SAML.CustomError (IdpDbError AttemptToGetV1IssuerViaV2API)) = Right $ Wai.mkError status409 "cannot-mix-idp-api-verions" "You cannot retrieve an API V1 IdP via API V2." +renderSparError (SAML.CustomError (IdpDbError AttemptToGetV2IssuerViaV1API)) = Right $ Wai.mkError status409 "cannot-mix-idp-api-verions" "You cannot retrieve an API V2 IdP via API V1." +renderSparError (SAML.CustomError (IdpDbError IdpNonUnique)) = Right $ Wai.mkError status409 "idp-non-unique" "We have found multiple IdPs with the same issuer. Please contact customer support." +renderSparError (SAML.CustomError (IdpDbError IdpWrongTeam)) = Right $ Wai.mkError status409 "idp-wrong-team" "The IdP is not part of this team." +renderSparError (SAML.CustomError (IdpDbError IdpNotFound)) = renderSparError (SAML.CustomError (SparIdPNotFound "")) -- Errors related to provisioning renderSparError (SAML.CustomError (SparProvisioningMoreThanOneIdP msg)) = Right $ Wai.mkError status400 "more-than-one-idp" ("Team can have at most one IdP configured: " <> msg) renderSparError (SAML.CustomError SparProvisioningTokenLimitReached) = Right $ Wai.mkError status403 "token-limit-reached" "The limit of provisioning tokens per team has been reached" diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index c2bf877936..2401223ed7 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -139,7 +139,7 @@ instance ) logScimUserIds $ do - mIdpConfig <- maybe (pure Nothing) (lift . IdPConfigStore.getConfig) stiIdP + mIdpConfig <- mapM (lift . IdPConfigStore.getConfig) stiIdP case filter' of Scim.FilterAttrCompare (Scim.AttrPath schema attrName _subAttr) Scim.OpEq (Scim.ValString val) | Scim.isUserSchema schema -> do @@ -163,7 +163,7 @@ instance ) logScimUserId $ do - mIdpConfig <- maybe (pure Nothing) (lift . IdPConfigStore.getConfig) stiIdP + mIdpConfig <- mapM (lift . IdPConfigStore.getConfig) stiIdP let notfound = Scim.notFound "User" (idToText uid) runMaybeT (getUserById mIdpConfig stiTeam uid) >>= maybe (throwError notfound) pure @@ -206,7 +206,7 @@ validateScimUser errloc tokinfo user = do tokenInfoToIdP :: Member IdPConfigStore r => ScimTokenInfo -> Scim.ScimHandler (Sem r) (Maybe IdP) tokenInfoToIdP ScimTokenInfo {stiIdP} = - maybe (pure Nothing) (lift . IdPConfigStore.getConfig) stiIdP + mapM (lift . IdPConfigStore.getConfig) stiIdP -- | Validate a handle (@userName@). validateHandle :: MonadError Scim.ScimError m => Text -> m Handle @@ -707,7 +707,7 @@ deleteScimUser tokeninfo@ScimTokenInfo {stiTeam, stiIdP} uid = throwError $ Scim.notFound "user" (idToText uid) - mIdpConfig <- maybe (pure Nothing) (lift . IdPConfigStore.getConfig) stiIdP + mIdpConfig <- mapM (lift . IdPConfigStore.getConfig) stiIdP case Brig.veidFromBrigUser brigUser ((^. SAML.idpMetadata . SAML.edIssuer) <$> mIdpConfig) of Left _ -> pure () diff --git a/services/spar/src/Spar/Sem/IdPConfigStore.hs b/services/spar/src/Spar/Sem/IdPConfigStore.hs index 630c71d01e..e980fb9bf0 100644 --- a/services/spar/src/Spar/Sem/IdPConfigStore.hs +++ b/services/spar/src/Spar/Sem/IdPConfigStore.hs @@ -21,11 +21,12 @@ module Spar.Sem.IdPConfigStore ( IdPConfigStore (..), Replacing (..), Replaced (..), - GetIdPResult (..), - storeConfig, + insertConfig, getConfig, - getIdByIssuerWithoutTeam, - getIdByIssuerWithTeam, + getIdPByIssuerV1, + getIdPByIssuerV1Maybe, + getIdPByIssuerV2, + getIdPByIssuerV2Maybe, getConfigsByTeam, deleteConfig, setReplacedBy, @@ -41,20 +42,6 @@ import Polysemy.Check (deriveGenericK) import qualified SAML2.WebSSO as SAML import qualified Wire.API.User.IdentityProvider as IP -data GetIdPResult a - = GetIdPFound a - | GetIdPNotFound - | -- | IdPId has been found, but no IdPConfig matching that Id. (Database - -- inconsistency or race condition.) - GetIdPDanglingId SAML.IdPId - | -- | You were looking for an idp by just providing issuer, not teamid, and `issuer_idp_v2` - -- has more than one entry (for different teams). - GetIdPNonUnique [SAML.IdPId] - | -- | An IdP was found, but it lives in another team than the one you were looking for. - -- This should be handled similarly to NotFound in most cases. - GetIdPWrongTeam SAML.IdPId - deriving (Eq, Show, Generic) - newtype Replaced = Replaced SAML.IdPId deriving (Eq, Ord, Show) @@ -62,16 +49,18 @@ newtype Replacing = Replacing SAML.IdPId deriving (Eq, Ord, Show) data IdPConfigStore m a where - StoreConfig :: IP.IdP -> IdPConfigStore m () - GetConfig :: SAML.IdPId -> IdPConfigStore m (Maybe IP.IdP) - GetIdByIssuerWithoutTeam :: SAML.Issuer -> IdPConfigStore m (GetIdPResult SAML.IdPId) - GetIdByIssuerWithTeam :: SAML.Issuer -> TeamId -> IdPConfigStore m (Maybe SAML.IdPId) + InsertConfig :: IP.IdP -> IdPConfigStore m () + GetConfig :: SAML.IdPId -> IdPConfigStore m IP.IdP + GetIdPByIssuerV1Maybe :: SAML.Issuer -> IdPConfigStore m (Maybe IP.IdP) + GetIdPByIssuerV1 :: SAML.Issuer -> IdPConfigStore m IP.IdP + GetIdPByIssuerV2Maybe :: SAML.Issuer -> TeamId -> IdPConfigStore m (Maybe IP.IdP) + GetIdPByIssuerV2 :: SAML.Issuer -> TeamId -> IdPConfigStore m IP.IdP GetConfigsByTeam :: TeamId -> IdPConfigStore m [IP.IdP] DeleteConfig :: IP.IdP -> IdPConfigStore m () -- affects _wiReplacedBy in GetConfig SetReplacedBy :: Replaced -> Replacing -> IdPConfigStore m () ClearReplacedBy :: Replaced -> IdPConfigStore m () - DeleteIssuer :: SAML.Issuer -> IdPConfigStore m () + DeleteIssuer :: SAML.Issuer -> Maybe TeamId -> IdPConfigStore m () deriving stock instance Show (IdPConfigStore m a) diff --git a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs index 65f4819f17..aafe43a00e 100644 --- a/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/IdPConfigStore/Cassandra.hs @@ -30,72 +30,88 @@ import qualified Data.List.NonEmpty as NL import Data.X509 (SignedCertificate) import Imports import Polysemy +import Polysemy.Error (Error, throw) import qualified SAML2.WebSSO as SAML import Spar.Data.Instances () -import Spar.Sem.IdPConfigStore (GetIdPResult (..), IdPConfigStore (..), Replaced (..), Replacing (..)) +import Spar.Error +import Spar.Sem.IdPConfigStore (IdPConfigStore (..), Replaced (..), Replacing (..)) import URI.ByteString import Wire.API.User.IdentityProvider idPToCassandra :: forall m r a. - (MonadClient m, Member (Embed m) r) => + (MonadClient m, Member (Embed m) r, Member (Error IdpDbError) r) => Sem (IdPConfigStore ': r) a -> Sem r a idPToCassandra = interpret $ - embed @m . \case - StoreConfig iw -> storeIdPConfig iw - GetConfig i -> getIdPConfig i - GetIdByIssuerWithoutTeam i -> getIdPIdByIssuerWithoutTeam i - GetIdByIssuerWithTeam i t -> getIdPIdByIssuerWithTeam i t - GetConfigsByTeam itlt -> getIdPConfigsByTeam itlt + \case + InsertConfig iw -> embed @m (runExceptT $ insertIdPConfig iw) >>= either throw pure + GetConfig i -> embed @m (runExceptT $ getIdPConfig i) >>= either throw pure + GetIdPByIssuerV1 i -> embed @m (runExceptT $ getIdPByIssuerV1 i) >>= either throw pure + GetIdPByIssuerV1Maybe i -> embed @m (runExceptT $ getIdPByIssuerV1May i) >>= either throw pure + GetIdPByIssuerV2 i t -> embed @m (runExceptT $ getIdPByIssuerV2 i t) >>= either throw pure + GetIdPByIssuerV2Maybe i t -> embed @m (runExceptT $ getIdPByIssuerV2May i t) >>= either throw pure + GetConfigsByTeam itlt -> embed @m (runExceptT $ getIdPConfigsByTeam itlt) >>= either throw pure DeleteConfig idp -> let idpid = idp ^. SAML.idpId issuer = idp ^. SAML.idpMetadata . SAML.edIssuer team = idp ^. SAML.idpExtraInfo . wiTeam - in deleteIdPConfig idpid issuer team - SetReplacedBy r r11 -> setReplacedBy r r11 - ClearReplacedBy r -> clearReplacedBy r - DeleteIssuer i -> deleteIssuer i + in embed @m $ deleteIdPConfig idpid issuer team + SetReplacedBy r r11 -> embed @m $ setReplacedBy r r11 + ClearReplacedBy r -> embed @m $ clearReplacedBy r + DeleteIssuer i t -> embed @m $ deleteIssuer i t type IdPConfigRow = (SAML.IdPId, SAML.Issuer, URI, SignedCertificate, [SignedCertificate], TeamId, Maybe WireIdPAPIVersion, [SAML.Issuer], Maybe SAML.IdPId) --- FUTUREWORK: should be called 'insertIdPConfig' for consistency. --- FUTUREWORK: enforce that wiReplacedby is Nothing, or throw an error. there is no --- legitimate reason to store an IdP that has already been replaced. and for updating an old --- one, call 'markReplacedIdP'. -storeIdPConfig :: - (HasCallStack, MonadClient m) => +insertIdPConfig :: + forall m. + (HasCallStack, MonadClient m, MonadError IdpDbError m) => IdP -> m () -storeIdPConfig idp = retry x5 . batch $ do - setType BatchLogged - setConsistency LocalQuorum - addPrepQuery - ins - ( idp ^. SAML.idpId, - idp ^. SAML.idpMetadata . SAML.edIssuer, - idp ^. SAML.idpMetadata . SAML.edRequestURI, - NL.head (idp ^. SAML.idpMetadata . SAML.edCertAuthnResponse), - NL.tail (idp ^. SAML.idpMetadata . SAML.edCertAuthnResponse), - -- (the 'List1' is split up into head and tail to make migration from one-element-only easier.) - idp ^. SAML.idpExtraInfo . wiTeam, - idp ^. SAML.idpExtraInfo . wiApiVersion, - idp ^. SAML.idpExtraInfo . wiOldIssuers, - idp ^. SAML.idpExtraInfo . wiReplacedBy - ) - addPrepQuery - byIssuer - ( idp ^. SAML.idpMetadata . SAML.edIssuer, - idp ^. SAML.idpExtraInfo . wiTeam, - idp ^. SAML.idpId - ) - addPrepQuery - byTeam - ( idp ^. SAML.idpId, - idp ^. SAML.idpExtraInfo . wiTeam - ) +insertIdPConfig idp = do + ensureDoNotMixApiVersions + retry x5 . batch $ do + setType BatchLogged + setConsistency LocalQuorum + addPrepQuery + ins + ( idp ^. SAML.idpId, + idp ^. SAML.idpMetadata . SAML.edIssuer, + idp ^. SAML.idpMetadata . SAML.edRequestURI, + NL.head (idp ^. SAML.idpMetadata . SAML.edCertAuthnResponse), + NL.tail (idp ^. SAML.idpMetadata . SAML.edCertAuthnResponse), + -- (the 'List1' is split up into head and tail to make migration from one-element-only easier.) + idp ^. SAML.idpExtraInfo . wiTeam, + idp ^. SAML.idpExtraInfo . wiApiVersion, + idp ^. SAML.idpExtraInfo . wiOldIssuers, + idp ^. SAML.idpExtraInfo . wiReplacedBy + ) + addPrepQuery + byIssuer + ( idp ^. SAML.idpMetadata . SAML.edIssuer, + idp ^. SAML.idpExtraInfo . wiTeam, + idp ^. SAML.idpId + ) + addPrepQuery + byTeam + ( idp ^. SAML.idpId, + idp ^. SAML.idpExtraInfo . wiTeam + ) where + ensureDoNotMixApiVersions :: m () + ensureDoNotMixApiVersions = do + let thisVersion = fromMaybe defWireIdPAPIVersion $ idp ^. SAML.idpExtraInfo . wiApiVersion + issuer = idp ^. SAML.idpMetadata . SAML.edIssuer + + failIfNot :: WireIdPAPIVersion -> IdP -> m () + failIfNot expectedVersion idp' = do + let actualVersion = fromMaybe defWireIdPAPIVersion $ idp' ^. SAML.idpExtraInfo . wiApiVersion + unless (actualVersion == expectedVersion) $ + throwError InsertIdPConfigCannotMixApiVersions + + getAllIdPsByIssuerUnsafe issuer >>= mapM_ (failIfNot thisVersion) + ins :: PrepQuery W IdPConfigRow () ins = "INSERT INTO idp (idp, issuer, request_uri, public_key, extra_public_keys, team, api_version, old_issuers, replaced_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" @@ -108,11 +124,12 @@ storeIdPConfig idp = retry x5 . batch $ do getIdPConfig :: forall m. - (HasCallStack, MonadClient m) => + (HasCallStack, MonadClient m, MonadError IdpDbError m) => SAML.IdPId -> - m (Maybe IdP) -getIdPConfig idpid = - traverse toIdp =<< retry x1 (query1 sel $ params LocalQuorum (Identity idpid)) + m IdP +getIdPConfig idpid = do + mbidp <- traverse toIdp =<< retry x1 (query1 sel $ params LocalQuorum (Identity idpid)) + maybe (throwError IdpNotFound) pure mbidp where toIdp :: IdPConfigRow -> m IdP toIdp @@ -135,47 +152,118 @@ getIdPConfig idpid = sel :: PrepQuery R (Identity SAML.IdPId) IdPConfigRow sel = "SELECT idp, issuer, request_uri, public_key, extra_public_keys, team, api_version, old_issuers, replaced_by FROM idp WHERE idp = ?" --- | Find 'IdPId' without team. Search both `issuer_idp_v2` and `issuer_idp`; in the former, --- make sure the result is unique (no two IdPs for two different teams). -getIdPIdByIssuerWithoutTeam :: - (HasCallStack, MonadClient m) => +-- | Get all idps with a given issuer, no matter what team or what idp version. +getAllIdPsByIssuerUnsafe :: + forall m. + (HasCallStack, MonadClient m, MonadError IdpDbError m) => SAML.Issuer -> - m (GetIdPResult SAML.IdPId) -getIdPIdByIssuerWithoutTeam issuer = do - (runIdentity <$$> retry x1 (query selv2 $ params LocalQuorum (Identity issuer))) >>= \case + m [IdP] +getAllIdPsByIssuerUnsafe issuer = do + v1Results <- runIdentity <$$> retry x1 (query selV1 $ params LocalQuorum (Identity issuer)) + v2Results <- runIdentity <$$> retry x1 (query selV2 $ params LocalQuorum (Identity issuer)) + mapM getIdPConfig $ v1Results <> v2Results + where + selV1 :: PrepQuery R (Identity SAML.Issuer) (Identity SAML.IdPId) + selV1 = "SELECT idp FROM issuer_idp WHERE issuer = ?" + + selV2 :: PrepQuery R (Identity SAML.Issuer) (Identity SAML.IdPId) + selV2 = "SELECT idp FROM issuer_idp_v2 WHERE issuer = ?" + +-- | Find 'IdPId' without team. +getIdPByIssuerV1 :: + forall m. + (HasCallStack, MonadClient m, MonadError IdpDbError m) => + SAML.Issuer -> + m IdP +getIdPByIssuerV1 issuer = getIdPByIssuerV1May issuer >>= maybe (throwError IdpNotFound) pure + +getIdPByIssuerV1May :: + forall m. + (HasCallStack, MonadClient m, MonadError IdpDbError m) => + SAML.Issuer -> + m (Maybe IdP) +getIdPByIssuerV1May issuer = do + let v1Results = runIdentity <$$> retry x1 (query selV1 $ params LocalQuorum (Identity issuer)) + let v2Results = runIdentity <$$> retry x1 (query selV2 $ params LocalQuorum (Identity issuer)) + v2Results >>= \case [] -> - (runIdentity <$$> retry x1 (query1 sel $ params LocalQuorum (Identity issuer))) >>= \case - Just idpid -> pure $ GetIdPFound idpid - Nothing -> pure GetIdPNotFound - [idpid] -> - pure $ GetIdPFound idpid - idpids@(_ : _ : _) -> - pure $ GetIdPNonUnique idpids + v1Results >>= \case + [] -> pure Nothing + [idpId] -> do + -- this table can only contain V1 idps. + Just <$> getIdPConfig idpId -- 'Nothing' is an internal database consistency issue (dangling IdPId) + _ : _ : _ -> throwError IdpNonUnique + [idpId] -> do + idp <- getIdPConfig idpId + doNotMixApiVersions WireIdPAPIV1 idp + pure $ Just idp -- 'Nothing' is an internal database consistency issue (dangling IdPId) + _ : _ : _ -> + throwError IdpNonUnique where - sel :: PrepQuery R (Identity SAML.Issuer) (Identity SAML.IdPId) - sel = "SELECT idp FROM issuer_idp WHERE issuer = ?" + selV1 :: PrepQuery R (Identity SAML.Issuer) (Identity SAML.IdPId) + selV1 = "SELECT idp FROM issuer_idp WHERE issuer = ?" - selv2 :: PrepQuery R (Identity SAML.Issuer) (Identity SAML.IdPId) - selv2 = "SELECT idp FROM issuer_idp_v2 WHERE issuer = ?" + selV2 :: PrepQuery R (Identity SAML.Issuer) (Identity SAML.IdPId) + selV2 = "SELECT idp FROM issuer_idp_v2 WHERE issuer = ?" -getIdPIdByIssuerWithTeam :: - (HasCallStack, MonadClient m) => +getIdPByIssuerV2 :: + forall m. + (HasCallStack, MonadClient m, MonadError IdpDbError m) => SAML.Issuer -> TeamId -> - m (Maybe SAML.IdPId) -getIdPIdByIssuerWithTeam issuer tid = do - runIdentity <$$> retry x1 (query1 sel $ params LocalQuorum (issuer, tid)) + m IdP +getIdPByIssuerV2 issuer tid = getIdPByIssuerV2May issuer tid >>= maybe (throwError IdpNotFound) pure + +getIdPByIssuerV2May :: + forall m. + (HasCallStack, MonadClient m, MonadError IdpDbError m) => + SAML.Issuer -> + TeamId -> + m (Maybe IdP) +getIdPByIssuerV2May issuer tid = do + let v1Results = runIdentity <$$> retry x1 (query selV1 $ params LocalQuorum (Identity issuer)) + let v2Results = runIdentity <$$> retry x1 (query selV2 $ params LocalQuorum (issuer, tid)) + v2Results >>= \case + [] -> + v1Results >>= \case + [] -> pure Nothing + [_idpId] -> do + -- this table can only contain V1 idps. + throwError AttemptToGetV1IssuerViaV2API + _ : _ : _ -> throwError IdpNonUnique + [idpId] -> do + idp <- getIdPConfig idpId + doNotMixApiVersions WireIdPAPIV2 idp + pure $ Just idp -- 'Nothing' is an internal database consistency issue (dangling IdPId) + _ : _ : _ -> + throwError IdpNonUnique where - sel :: PrepQuery R (SAML.Issuer, TeamId) (Identity SAML.IdPId) - sel = "SELECT idp FROM issuer_idp_v2 WHERE issuer = ? and team = ?" + selV1 :: PrepQuery R (Identity SAML.Issuer) (Identity SAML.IdPId) + selV1 = "SELECT idp FROM issuer_idp WHERE issuer = ?" + + selV2 :: PrepQuery R (SAML.Issuer, TeamId) (Identity SAML.IdPId) + selV2 = "SELECT idp FROM issuer_idp_v2 WHERE issuer = ? and team = ?" + +doNotMixApiVersions :: + forall m. + (HasCallStack, MonadError IdpDbError m) => + WireIdPAPIVersion -> + IdP -> + m () +doNotMixApiVersions expectVersion idp = do + let actualVersion = fromMaybe defWireIdPAPIVersion (idp ^. SAML.idpExtraInfo . wiApiVersion) + unless (actualVersion == expectVersion) $ do + throwError $ case expectVersion of + WireIdPAPIV1 -> AttemptToGetV1IssuerViaV2API + WireIdPAPIV2 -> AttemptToGetV2IssuerViaV1API getIdPConfigsByTeam :: - (HasCallStack, MonadClient m) => + (HasCallStack, MonadClient m, MonadError IdpDbError m) => TeamId -> m [IdP] getIdPConfigsByTeam team = do idpids <- runIdentity <$$> retry x1 (query sel $ params LocalQuorum (Identity team)) - catMaybes <$> mapM getIdPConfig idpids + mapM getIdPConfig idpids where sel :: PrepQuery R (Identity TeamId) (Identity SAML.IdPId) sel = "SELECT idp FROM team_idp WHERE team = ?" @@ -192,7 +280,7 @@ deleteIdPConfig idp issuer team = retry x5 . batch $ do addPrepQuery delDefaultIdp (Identity idp) addPrepQuery delIdp (Identity idp) addPrepQuery delIssuerIdp (Identity issuer) - addPrepQuery delIssuerIdpV2 (Identity issuer) + addPrepQuery delIssuerIdpV2 (issuer, team) addPrepQuery delTeamIdp (team, idp) where delDefaultIdp :: PrepQuery W (Identity SAML.IdPId) () @@ -204,8 +292,8 @@ deleteIdPConfig idp issuer team = retry x5 . batch $ do delIssuerIdp :: PrepQuery W (Identity SAML.Issuer) () delIssuerIdp = "DELETE FROM issuer_idp WHERE issuer = ?" - delIssuerIdpV2 :: PrepQuery W (Identity SAML.Issuer) () - delIssuerIdpV2 = "DELETE FROM issuer_idp_v2 WHERE issuer = ?" + delIssuerIdpV2 :: PrepQuery W (SAML.Issuer, TeamId) () + delIssuerIdpV2 = "DELETE FROM issuer_idp_v2 WHERE issuer = ? AND team = ?" delTeamIdp :: PrepQuery W (TeamId, SAML.IdPId) () delTeamIdp = "DELETE FROM team_idp WHERE team = ? and idp = ?" @@ -233,8 +321,22 @@ clearReplacedBy (Replaced old) = do ins :: PrepQuery W (Identity SAML.IdPId) () ins = "UPDATE idp SET replaced_by = null WHERE idp = ?" -deleteIssuer :: (HasCallStack, MonadClient m) => SAML.Issuer -> m () -deleteIssuer issuer = retry x5 $ write del (params LocalQuorum (Identity issuer)) +-- | If the IdP is 'WireIdPAPIV1', it must be deleted globally, if it is 'WireIdPAPIV2', it +-- must be deleted inside one team. 'V1' can be either in the old table without team index, +-- or in the new one, so we delete both. +deleteIssuer :: (HasCallStack, MonadClient m) => SAML.Issuer -> Maybe TeamId -> m () +deleteIssuer issuer = \case + Just tid -> do + retry x5 $ write delV2 (params LocalQuorum (issuer, tid)) + Nothing -> do + retry x5 $ write delV1 (params LocalQuorum (Identity issuer)) + retry x5 $ write delV1' (params LocalQuorum (Identity issuer)) where - del :: PrepQuery W (Identity SAML.Issuer) () - del = "DELETE FROM issuer_idp_v2 WHERE issuer = ?" + delV1 :: PrepQuery W (Identity SAML.Issuer) () + delV1 = "DELETE FROM issuer_idp WHERE issuer = ?" + + delV1' :: PrepQuery W (Identity SAML.Issuer) () + delV1' = "DELETE FROM issuer_idp_v2 WHERE issuer = ?" + + delV2 :: PrepQuery W (SAML.Issuer, TeamId) () + delV2 = "DELETE FROM issuer_idp_v2 WHERE issuer = ? AND team = ?" diff --git a/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs b/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs index a2f72884db..d0a5816b8d 100644 --- a/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs +++ b/services/spar/src/Spar/Sem/IdPConfigStore/Mem.hs @@ -26,8 +26,9 @@ import Imports import Polysemy import Polysemy.State import qualified SAML2.WebSSO.Types as SAML -import Spar.Sem.IdPConfigStore (GetIdPResult (..), IdPConfigStore (..), Replaced (..), Replacing (..)) +import Spar.Sem.IdPConfigStore (IdPConfigStore (..), Replaced (..), Replacing (..)) import qualified Wire.API.User.IdentityProvider as IP +import qualified Wire.API.User.IdentityProvider as SAML type TypedState = Map SAML.IdPId IP.IdP @@ -42,14 +43,18 @@ idPToMem = evState . evEff evEff :: Sem (IdPConfigStore ': r) a -> Sem (State TypedState ': r) a evEff = reinterpret @_ @(State TypedState) $ \case - StoreConfig iw -> - modify' (storeConfig iw) + InsertConfig iw -> + modify' (insertConfig iw) GetConfig i -> gets (getConfig i) - GetIdByIssuerWithoutTeam iss -> - gets (getIdByIssuerWithoutTeam iss) - GetIdByIssuerWithTeam iss team -> - gets (getIdByIssuerWithTeam iss team) + GetIdPByIssuerV1Maybe issuer -> + gets (getIdByIssuerWithoutTeamMaybe issuer) + GetIdPByIssuerV1 issuer -> + gets (getIdByIssuerWithoutTeam issuer) + GetIdPByIssuerV2Maybe issuer tid -> + gets (getIdByIssuerWithTeamMaybe issuer tid) + GetIdPByIssuerV2 issuer tid -> + gets (getIdByIssuerWithTeam issuer tid) GetConfigsByTeam team -> gets (getConfigsByTeam team) DeleteConfig idp -> @@ -58,10 +63,10 @@ idPToMem = evState . evEff modify' (updateReplacedBy (Just replacing) replaced <$>) ClearReplacedBy (Replaced replaced) -> modify' (updateReplacedBy Nothing replaced <$>) - DeleteIssuer issuer -> modify' (deleteIssuer issuer) + DeleteIssuer issuer _tid -> modify' (deleteIssuer issuer) -storeConfig :: IP.IdP -> TypedState -> TypedState -storeConfig iw = +insertConfig :: IP.IdP -> TypedState -> TypedState +insertConfig iw = M.insert (iw ^. SAML.idpId) iw . M.filter ( \iw' -> @@ -69,21 +74,27 @@ storeConfig iw = || (iw' ^. SAML.idpExtraInfo . IP.wiTeam /= iw ^. SAML.idpExtraInfo . IP.wiTeam) ) -getConfig :: SAML.IdPId -> TypedState -> Maybe IP.IdP -getConfig = M.lookup +getConfig :: SAML.IdPId -> TypedState -> IP.IdP +getConfig idpId mp = fromMaybe (error "idp not found") $ M.lookup idpId mp -getIdByIssuerWithoutTeam :: SAML.Issuer -> TypedState -> GetIdPResult SAML.IdPId -getIdByIssuerWithoutTeam iss mp = +getIdByIssuerWithoutTeam :: SAML.Issuer -> TypedState -> SAML.IdP +getIdByIssuerWithoutTeam issuer mp = fromMaybe (error "idp not found") $ getIdByIssuerWithoutTeamMaybe issuer mp + +getIdByIssuerWithoutTeamMaybe :: SAML.Issuer -> TypedState -> Maybe SAML.IdP +getIdByIssuerWithoutTeamMaybe iss mp = case filter (\idp -> idp ^. SAML.idpMetadata . SAML.edIssuer == iss) $ M.elems mp of - [] -> GetIdPNotFound - [a] -> GetIdPFound (a ^. SAML.idpId) - as@(_ : _ : _) -> GetIdPNonUnique ((^. SAML.idpId) <$> as) + [] -> Nothing + [a] -> Just a + _ : _ : _ -> error "impossible" + +getIdByIssuerWithTeam :: SAML.Issuer -> TeamId -> TypedState -> SAML.IdP +getIdByIssuerWithTeam issuer tid mp = fromMaybe (error "idp not found") $ getIdByIssuerWithTeamMaybe issuer tid mp -getIdByIssuerWithTeam :: SAML.Issuer -> TeamId -> TypedState -> Maybe SAML.IdPId -getIdByIssuerWithTeam iss team mp = +getIdByIssuerWithTeamMaybe :: SAML.Issuer -> TeamId -> TypedState -> Maybe SAML.IdP +getIdByIssuerWithTeamMaybe iss team mp = case filter fl $ M.elems mp of [] -> Nothing - [a] -> Just (a ^. SAML.idpId) + [a] -> Just a (_ : _ : _) -> -- (StoreConfig doesn't let this happen) error "GetIdByIssuerWithTeam: impossible" diff --git a/services/spar/src/Spar/Sem/IdPConfigStore/Spec.hs b/services/spar/src/Spar/Sem/IdPConfigStore/Spec.hs deleted file mode 100644 index c022971f0a..0000000000 --- a/services/spar/src/Spar/Sem/IdPConfigStore/Spec.hs +++ /dev/null @@ -1,332 +0,0 @@ -{-# LANGUAGE DeriveDataTypeable #-} -{-# LANGUAGE QuantifiedConstraints #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# OPTIONS_GHC -Wno-orphans #-} -{-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Spar.Sem.IdPConfigStore.Spec (propsForInterpreter) where - -import Control.Arrow -import Control.Lens -import Data.Data (Data) -import Imports -import Polysemy -import Polysemy.Check -import SAML2.WebSSO.Types -import qualified SAML2.WebSSO.Types as SAML -import Spar.Sem.IdPConfigStore -import Test.Hspec -import Test.Hspec.QuickCheck -import Test.QuickCheck -import qualified Wire.API.User.IdentityProvider as IP - -deriving instance Data IdPId - -deriving instance Data (GetIdPResult IdPId) - -propsForInterpreter :: - (Member IdPConfigStore r, PropConstraints r f) => - String -> - (forall x. f x -> x) -> - (forall x. Show x => Maybe (f x -> String)) -> - (forall x. Sem r x -> IO (f x)) -> - Spec -propsForInterpreter interpreter extract labeler lower = - describe interpreter $ do - prop "deleteConfig/deleteConfig" $ prop_deleteDelete Nothing lower - prop "deleteConfig/getConfig" $ prop_deleteGet labeler lower - prop "getConfig/storeConfig" $ prop_getStore (Just $ show . void . extract) lower - prop "getConfig/getConfig" $ prop_getGet (Just $ show . (void *** void) . extract) lower - prop "setReplacedBy/clearReplacedBy" $ prop_setClear labeler lower - prop "setReplacedBy/getReplacedBy" $ prop_setGet (Just $ show . fmap void . extract) lower - prop "setReplacedBy/setReplacedBy" $ prop_setSet (Just $ show . fmap void . extract) lower - prop "storeConfig/getConfig" $ prop_storeGet (Just $ show . void . extract) lower - xit "storeConfig/getIdByIssuerWithoutTeam" $ property $ prop_storeGetByIssuer (Just $ constructorLabel . extract) lower - prop "storeConfig/storeConfig (different keys)" $ prop_storeStoreInterleave Nothing lower - prop "storeConfig/storeConfig (same keys)" $ prop_storeStore Nothing lower - -getReplacedBy :: Member IdPConfigStore r => SAML.IdPId -> Sem r (Maybe (Maybe SAML.IdPId)) -getReplacedBy idpid = fmap (view $ SAML.idpExtraInfo . IP.wiReplacedBy) <$> getConfig idpid - --- | All the constraints we need to generalize properties in this module. --- A regular type synonym doesn't work due to dreaded impredicative --- polymorphism. -class - (Arbitrary Issuer, CoArbitrary Issuer, Arbitrary Replaced, Arbitrary Replaced, Arbitrary Replacing, Arbitrary IdPId, CoArbitrary IdPId, Arbitrary IP.IdP, CoArbitrary IP.IdP, CoArbitrary (GetIdPResult IdPId), Functor f, Member IdPConfigStore r, forall z. Show z => Show (f z), forall z. Eq z => Eq (f z)) => - PropConstraints r f - -instance - (Arbitrary Issuer, CoArbitrary Issuer, Arbitrary Replaced, Arbitrary Replaced, Arbitrary Replacing, Arbitrary IdPId, CoArbitrary IdPId, Arbitrary IP.IdP, CoArbitrary IP.IdP, CoArbitrary (GetIdPResult IdPId), Functor f, Member IdPConfigStore r, forall z. Show z => Show (f z), forall z. Eq z => Eq (f z)) => - PropConstraints r f - -prop_storeStore :: - PropConstraints r f => - Maybe (f () -> String) -> - (forall x. Sem r x -> IO (f x)) -> - Property -prop_storeStore = - prepropLaw @'[IdPConfigStore] $ do - s <- arbitrary - s' <- arbitrary - pure $ - Law - { lawLhs = do - storeConfig $ s & SAML.idpId .~ s' ^. SAML.idpId - storeConfig s', - lawRhs = do - storeConfig s', - lawPrelude = [], - lawPostlude = [getConfig $ s' ^. SAML.idpId] - } - -prop_storeStoreInterleave :: - PropConstraints r f => - Maybe (f () -> String) -> - (forall x. Sem r x -> IO (f x)) -> - Property -prop_storeStoreInterleave = - prepropLaw @'[IdPConfigStore] $ do - s <- arbitrary - s' <- arbitrary - !_ <- - when (s ^. SAML.idpId == s' ^. SAML.idpId) discard - pure $ - Law - { lawLhs = do - storeConfig s - storeConfig s', - lawRhs = do - storeConfig s' - storeConfig s, - lawPrelude = [], - lawPostlude = [getConfig $ s ^. SAML.idpId, getConfig $ s' ^. SAML.idpId] - } - -prop_storeGet :: - PropConstraints r f => - Maybe (f (Maybe IP.IdP) -> String) -> - (forall x. Sem r x -> IO (f x)) -> - Property -prop_storeGet = - prepropLaw @'[IdPConfigStore] $ - do - s <- arbitrary - pure $ - simpleLaw - ( do - storeConfig s - getConfig $ s ^. idpId - ) - ( do - storeConfig s - pure (Just s) - ) - -prop_deleteGet :: - PropConstraints r f => - Maybe (f (Maybe IP.IdP) -> String) -> - (forall x. Sem r x -> IO (f x)) -> - Property -prop_deleteGet = - prepropLaw @'[IdPConfigStore] $ do - s <- arbitrary - pure $ - Law - { lawLhs = do - deleteConfig s - getConfig $ s ^. SAML.idpId, - lawRhs = do - deleteConfig s - pure Nothing, - lawPrelude = - [ storeConfig s - ], - lawPostlude = [] :: [Sem r ()] - } - -prop_deleteDelete :: - PropConstraints r f => - Maybe (f () -> String) -> - (forall x. Sem r x -> IO (f x)) -> - Property -prop_deleteDelete = - prepropLaw @'[IdPConfigStore] $ do - s <- arbitrary - pure $ - simpleLaw - ( do - deleteConfig s - deleteConfig s - ) - ( do - deleteConfig s - ) - -prop_storeGetByIssuer :: - PropConstraints r f => - Maybe (f (GetIdPResult IdPId) -> String) -> - (forall x. Sem r x -> IO (f x)) -> - Property -prop_storeGetByIssuer = - prepropLaw @'[IdPConfigStore] $ - do - s <- arbitrary - pure $ - simpleLaw - ( do - storeConfig s - getIdByIssuerWithoutTeam $ s ^. idpMetadata . edIssuer - ) - ( do - storeConfig s - -- NOT TRUE! This can also return GetIdPNonUnique with nonzero probability! - pure $ GetIdPFound $ s ^. idpId - ) - -prop_setClear :: - PropConstraints r f => - Maybe (f (Maybe (Maybe IdPId)) -> String) -> - (forall x. Sem r x -> IO (f x)) -> - Property -prop_setClear = - prepropLaw @'[IdPConfigStore] $ - do - idp <- arbitrary - replaced_id <- arbitrary - let replaced = Replaced replaced_id - replacing <- arbitrary - pure $ - Law - { lawLhs = do - setReplacedBy replaced replacing - clearReplacedBy replaced - getReplacedBy replaced_id, - lawRhs = do - clearReplacedBy replaced - getReplacedBy replaced_id, - lawPrelude = - [ storeConfig $ idp & SAML.idpId .~ replaced_id - ], - lawPostlude = [] @(Sem _ ()) - } - -prop_getGet :: - forall r f. - PropConstraints r f => - Maybe (f (Maybe IP.IdP, Maybe IP.IdP) -> String) -> - (forall x. Sem r x -> IO (f x)) -> - Property -prop_getGet = - prepropLaw @'[IdPConfigStore] $ - do - idpid <- arbitrary - idp <- arbitrary - pure $ - Law - { lawLhs = do - liftA2 (,) (getConfig idpid) (getConfig idpid), - lawRhs = do - cfg <- getConfig idpid - pure (cfg, cfg), - lawPrelude = - [ storeConfig $ idp & SAML.idpId .~ idpid - ], - lawPostlude = [] :: [Sem r ()] - } - -prop_getStore :: - PropConstraints r f => - Maybe (f (Maybe IP.IdP) -> String) -> - (forall x. Sem r x -> IO (f x)) -> - Property -prop_getStore = - prepropLaw @'[IdPConfigStore] $ - do - idpid <- arbitrary - s <- arbitrary - let s' = s & SAML.idpId .~ idpid - pure $ - Law - { lawLhs = do - r <- getConfig idpid - maybe (pure ()) storeConfig r - pure r, - lawRhs = do - getConfig idpid, - lawPrelude = - [storeConfig s'], - lawPostlude = - [getConfig idpid] - } - -prop_setSet :: - PropConstraints r f => - Maybe (f (Maybe (Maybe IdPId)) -> String) -> - (forall x. Sem r x -> IO (f x)) -> - Property -prop_setSet = - prepropLaw @'[IdPConfigStore] $ - do - replaced_id <- arbitrary - s <- arbitrary - let s' = s & SAML.idpId .~ replaced_id - let replaced = Replaced replaced_id - replacing <- arbitrary - replacing' <- arbitrary - pure $ - Law - { lawLhs = do - setReplacedBy replaced replacing - setReplacedBy replaced replacing' - getReplacedBy replaced_id, - lawRhs = do - setReplacedBy replaced replacing' - getReplacedBy replaced_id, - lawPrelude = - [storeConfig s'], - lawPostlude = [] @(Sem _ ()) - } - -prop_setGet :: - PropConstraints r f => - Maybe (f (Maybe (Maybe IdPId)) -> String) -> - (forall x. Sem r x -> IO (f x)) -> - Property -prop_setGet = - prepropLaw @'[IdPConfigStore] $ - do - idp <- arbitrary - replaced_id <- arbitrary - let replaced = Replaced replaced_id - replacing_id <- arbitrary - let replacing = Replacing replacing_id - pure $ - Law - { lawLhs = do - setReplacedBy replaced replacing - getReplacedBy replaced_id, - lawRhs = do - setReplacedBy replaced replacing - (Just replacing_id <$) <$> getConfig replaced_id, - lawPrelude = - [ storeConfig $ idp & SAML.idpId .~ replaced_id - ], - lawPostlude = [] :: [Sem r ()] - } diff --git a/services/spar/src/Spar/Sem/SAML2/Library.hs b/services/spar/src/Spar/Sem/SAML2/Library.hs index fe7436ce48..d4bda54c33 100644 --- a/services/spar/src/Spar/Sem/SAML2/Library.hs +++ b/services/spar/src/Spar/Sem/SAML2/Library.hs @@ -33,13 +33,13 @@ import Polysemy.Input import Polysemy.Internal.Tactics import SAML2.WebSSO hiding (Error) import qualified SAML2.WebSSO as SAML hiding (Error) -import qualified Spar.App as App import Spar.Error (SparCustomError (..), SparError) import Spar.Sem.AReqIDStore (AReqIDStore) import qualified Spar.Sem.AReqIDStore as AReqIDStore import Spar.Sem.AssIDStore (AssIDStore) import qualified Spar.Sem.AssIDStore as AssIDStore import Spar.Sem.IdPConfigStore (IdPConfigStore) +import qualified Spar.Sem.IdPConfigStore as IdPConfigStore import Spar.Sem.SAML2 import Wire.API.User.IdentityProvider (WireIdP) import Wire.API.User.Saml @@ -92,9 +92,11 @@ instance Members '[Error SparError, IdPConfigStore, Final IO] r => SPStoreIdP Sp type IdPConfigExtra (SPImpl r) = WireIdP type IdPConfigSPId (SPImpl r) = TeamId - storeIdPConfig = SPImpl . App.storeIdPConfig - getIdPConfig = SPImpl . App.getIdPConfig - getIdPConfigByIssuerOptionalSPId a = SPImpl . App.getIdPConfigByIssuerOptionalSPId a + storeIdPConfig = SPImpl . IdPConfigStore.insertConfig + getIdPConfig = SPImpl . IdPConfigStore.getConfig + getIdPConfigByIssuerOptionalSPId issuer mbteam = SPImpl $ case mbteam of + Nothing -> IdPConfigStore.getIdPByIssuerV1 issuer + Just team -> IdPConfigStore.getIdPByIssuerV2 issuer team instance Member (Error SparError) r => MonadError SparError (SPImpl r) where throwError = SPImpl . throw diff --git a/services/spar/src/Spar/Sem/SAMLUserStore.hs b/services/spar/src/Spar/Sem/SAMLUserStore.hs index a910e766f7..c23f047cff 100644 --- a/services/spar/src/Spar/Sem/SAMLUserStore.hs +++ b/services/spar/src/Spar/Sem/SAMLUserStore.hs @@ -21,13 +21,14 @@ module Spar.Sem.SAMLUserStore ( SAMLUserStore (..), insert, get, - getAnyByIssuer, - getSomeByIssuer, deleteByIssuer, delete, + getAllByIssuerPaginated, + nextPage, ) where +import Cassandra (Page) import Data.Id import Imports import Polysemy @@ -36,10 +37,10 @@ import qualified SAML2.WebSSO as SAML data SAMLUserStore m a where Insert :: SAML.UserRef -> UserId -> SAMLUserStore m () Get :: SAML.UserRef -> SAMLUserStore m (Maybe UserId) - GetAnyByIssuer :: SAML.Issuer -> SAMLUserStore m (Maybe UserId) - GetSomeByIssuer :: SAML.Issuer -> SAMLUserStore m [(SAML.UserRef, UserId)] DeleteByIssuer :: SAML.Issuer -> SAMLUserStore m () Delete :: UserId -> SAML.UserRef -> SAMLUserStore m () + GetAllByIssuerPaginated :: SAML.Issuer -> SAMLUserStore m (Page (SAML.UserRef, UserId)) + NextPage :: Page a -> SAMLUserStore m (Page a) -- TODO(sandy): Inline this definition --- no TH makeSem ''SAMLUserStore diff --git a/services/spar/src/Spar/Sem/SAMLUserStore/Cassandra.hs b/services/spar/src/Spar/Sem/SAMLUserStore/Cassandra.hs index ea46bf0ff7..9630c97e44 100644 --- a/services/spar/src/Spar/Sem/SAMLUserStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/SAMLUserStore/Cassandra.hs @@ -19,6 +19,7 @@ module Spar.Sem.SAMLUserStore.Cassandra ( samlUserStoreToCassandra, + getAllByIssuerPaginated, ) where @@ -43,10 +44,26 @@ samlUserStoreToCassandra = embed . \case Insert ur uid -> insertSAMLUser ur uid Get ur -> getSAMLUser ur - GetAnyByIssuer is -> getSAMLAnyUserByIssuer is - GetSomeByIssuer is -> getSAMLSomeUsersByIssuer is DeleteByIssuer is -> deleteSAMLUsersByIssuer is Delete uid ur -> deleteSAMLUser uid ur + GetAllByIssuerPaginated is -> getAllSAMLUsersByIssuerPaginated is + NextPage page -> nextPage' page + +nextPage' :: (HasCallStack, MonadClient m) => Cas.Page a -> m (Cas.Page a) +nextPage' = Cas.liftClient . Cas.nextPage + +-- | Replaces `getSAML{Some,Any}UsersByIssuer`. +-- Since we currently do not have a team id stored together with the SAML user in user_v2 +-- we must get all and filter manually by asking brig for the team id when deleting an IdP. +-- FUTUREWORK: to migrate to a new table that contains the team id. +getAllSAMLUsersByIssuerPaginated :: (HasCallStack, MonadClient m) => SAML.Issuer -> m (Cas.Page (SAML.UserRef, UserId)) +getAllSAMLUsersByIssuerPaginated issuer = do + (_1 %~ SAML.UserRef issuer) <$$> retry x1 (paginate getAllByIssuer (paramsP LocalQuorum (Identity issuer) (size + 1))) + where + size = 200 + + getAllByIssuer :: PrepQuery R (Identity SAML.Issuer) (SAML.NameID, UserId) + getAllByIssuer = "SELECT sso_id, uid FROM user_v2 WHERE issuer = ?" -- | Add new user. If user with this 'SAML.UserId' exists, overwrite it. insertSAMLUser :: (HasCallStack, MonadClient m) => SAML.UserRef -> UserId -> m () @@ -55,25 +72,6 @@ insertSAMLUser (SAML.UserRef tenant subject) uid = retry x5 . write ins $ params ins :: PrepQuery W (SAML.Issuer, Data.NormalizedUNameID, SAML.NameID, UserId) () ins = "INSERT INTO user_v2 (issuer, normalized_uname_id, sso_id, uid) VALUES (?, ?, ?, ?)" --- | Sometimes we only need to know if it's none or more, so this function returns the first one. -getSAMLAnyUserByIssuer :: (HasCallStack, MonadClient m) => SAML.Issuer -> m (Maybe UserId) -getSAMLAnyUserByIssuer issuer = - runIdentity - <$$> (retry x1 . query1 sel $ params LocalQuorum (Identity issuer)) - where - sel :: PrepQuery R (Identity SAML.Issuer) (Identity UserId) - sel = "SELECT uid FROM user_v2 WHERE issuer = ? LIMIT 1" - --- | Sometimes (eg., for IdP deletion), we can start anywhere with deleting all users in an --- IdP, and if we don't get all users we just try again when we're done with these. -getSAMLSomeUsersByIssuer :: (HasCallStack, MonadClient m) => SAML.Issuer -> m [(SAML.UserRef, UserId)] -getSAMLSomeUsersByIssuer issuer = - (_1 %~ SAML.UserRef issuer) - <$$> (retry x1 . query sel $ params LocalQuorum (Identity issuer)) - where - sel :: PrepQuery R (Identity SAML.Issuer) (SAML.NameID, UserId) - sel = "SELECT sso_id, uid FROM user_v2 WHERE issuer = ? LIMIT 2000" - -- | Lookup a brig 'UserId' by IdP issuer and NameID. -- -- NB: It is not allowed for two distinct wire users from two different teams to have the same diff --git a/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs b/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs index f5ea68272e..8e66f0e732 100644 --- a/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs +++ b/services/spar/src/Spar/Sem/SAMLUserStore/Mem.hs @@ -23,7 +23,6 @@ module Spar.Sem.SAMLUserStore.Mem where import Control.Lens (view) -import Data.Coerce (coerce) import Data.Id import qualified Data.Map as M import Imports @@ -45,10 +44,12 @@ samlUserStoreToMem = (runState @(Map UserRefOrd UserId) mempty .) $ reinterpret $ \case Insert ur uid -> modify $ M.insert (UserRefOrd ur) uid Get ur -> gets $ M.lookup $ UserRefOrd ur - GetAnyByIssuer is -> gets $ fmap snd . find (eqIssuer is . fst) . M.toList - GetSomeByIssuer is -> gets $ coerce . filter (eqIssuer is . fst) . M.toList DeleteByIssuer is -> modify $ M.filterWithKey (\ref _ -> not $ eqIssuer is ref) Delete _uid ur -> modify $ M.delete $ UserRefOrd ur + -- 'GetAllByIssuerPaginated' and 'NextPage' are workarounds, please also see docs at + -- 'Spar.Sem.SAMLUserStore.Cassandra.getAllSAMLUsersByIssuerPaginated' + GetAllByIssuerPaginated _is -> error "not implemented as this has a dependency to Cassandra" + NextPage _ -> error "not implemented as this has a dependency to Cassandra" where eqIssuer :: SAML.Issuer -> UserRefOrd -> Bool eqIssuer is = (== is) . view uidTenant . unUserRefOrd diff --git a/services/spar/src/Spar/Sem/Utils.hs b/services/spar/src/Spar/Sem/Utils.hs index c7aba34aa8..d6cb57d840 100644 --- a/services/spar/src/Spar/Sem/Utils.hs +++ b/services/spar/src/Spar/Sem/Utils.hs @@ -17,7 +17,14 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Spar.Sem.Utils (viaRunHttp, RunHttpEnv (..), interpretClientToIO, ttlErrorToSparError) where +module Spar.Sem.Utils + ( viaRunHttp, + RunHttpEnv (..), + interpretClientToIO, + ttlErrorToSparError, + idpDbErrorToSparError, + ) +where import Bilge import Cassandra as Cas @@ -55,6 +62,9 @@ interpretClientToIO ctx = interpret $ \case ttlErrorToSparError :: Member (Error SparError) r => Sem (Error TTLError ': r) a -> Sem r a ttlErrorToSparError = mapError (SAML.CustomError . SparCassandraTTLError) +idpDbErrorToSparError :: Member (Error SparError) r => Sem (Error IdpDbError ': r) a -> Sem r a +idpDbErrorToSparError = mapError (SAML.CustomError . IdpDbError) + data RunHttpEnv r = RunHttpEnv { rheManager :: Bilge.Manager, rheRequest :: Bilge.Request diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 047205dd5f..592bf38f18 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -25,7 +25,7 @@ where import Bilge import Brig.Types.Intra (AccountStatus (Deleted)) -import Cassandra hiding (Value) +import Cassandra as Cas hiding (Value) import Control.Lens hiding ((.=)) import Control.Monad.Catch (MonadThrow) import Control.Monad.Random.Class (getRandomR) @@ -442,7 +442,7 @@ specFinalizeLogin = do statusCode sparresp `shouldBe` 404 -- body should contain the error label in the title, the verbatim haskell error, and the request: (cs . fromJust . responseBody $ sparresp) `shouldContain` "wire:sso:error:not-found" - (cs . fromJust . responseBody $ sparresp) `shouldContainInBase64` "CustomError (SparIdPNotFound" + (cs . fromJust . responseBody $ sparresp) `shouldContainInBase64` "(CustomError (IdpDbError IdpNotFound)" (cs . fromJust . responseBody $ sparresp) `shouldContainInBase64` "Input {iName = \"SAMLResponse\"" check mkareq mkaresp submitaresp checkresp @@ -702,6 +702,7 @@ specCRUDIdentityProvider = do ssoOwner <- mkSsoOwner firstOwner tid idp privcreds callIdpDeletePurge' (env ^. teSpar) (Just ssoOwner) (idp ^. idpId) `shouldRespondWith` checkErrHspec 409 "cannot-delete-own-idp" + describe "PUT /identity-providers/:idp" $ do testGetPutDelete callIdpUpdate' context "known IdP, client is team owner" $ do @@ -736,12 +737,15 @@ specCRUDIdentityProvider = do callIdpUpdate' (env ^. teSpar) (Just owner) idpid (IdPMetadataValue "bloo" undefined) `shouldRespondWith` checkErrHspec 400 "invalid-metadata" describe "issuer changed to one that already exists in *another* team" $ do - it "rejects" $ do + it "rejects if V1, succeeds if V2" $ do env <- ask (owner1, _, (^. idpId) -> idpid1) <- registerTestIdP (_, _, _, (IdPMetadataValue _ idpmeta2, _)) <- registerTestIdPWithMeta callIdpUpdate' (env ^. teSpar) (Just owner1) idpid1 (IdPMetadataValue (cs $ SAML.encode idpmeta2) undefined) - `shouldRespondWith` checkErrHspec 400 "idp-issuer-in-use" + `shouldRespondWith` ( case env ^. teWireIdPAPIVersion of + WireIdPAPIV1 -> checkErrHspec 400 "idp-issuer-in-use" + WireIdPAPIV2 -> (== 200) . statusCode + ) describe "issuer changed to one that already exists in *the same* team" $ do it "rejects" $ do env <- ask @@ -749,6 +753,9 @@ specCRUDIdentityProvider = do (SampleIdP idpmeta2 _ _ _) <- makeSampleIdPMetadata _ <- call $ callIdpCreate (env ^. teWireIdPAPIVersion) (env ^. teSpar) (Just owner1) idpmeta2 let idpmeta1' = idpmeta1 & edIssuer .~ (idpmeta2 ^. edIssuer) + + -- An IdP is unambiguously identified by teamid plus issuer, so a team cannot have + -- multiple IdPs with the same issuer, regardless of the API version. callIdpUpdate' (env ^. teSpar) (Just owner1) idpid1 (IdPMetadataValue (cs $ SAML.encode idpmeta1') undefined) `shouldRespondWith` checkErrHspec 400 "idp-issuer-in-use" describe "issuer changed to one that already existed in the same team in the past (but has been updated away)" $ do @@ -763,11 +770,11 @@ specCRUDIdentityProvider = do pure $ idpmeta1 & edIssuer .~ (idpmeta3 ^. edIssuer) do - midp <- runSpar $ IdPEffect.getConfig idpid1 + idp <- runSpar $ IdPEffect.getConfig idpid1 liftIO $ do - (midp ^? _Just . idpMetadata . edIssuer) `shouldBe` Just (idpmeta1 ^. edIssuer) - (midp ^? _Just . idpExtraInfo . wiOldIssuers) `shouldBe` Just [] - (midp ^? _Just . idpExtraInfo . wiReplacedBy) `shouldBe` Just Nothing + (idp ^. idpMetadata . edIssuer) `shouldBe` (idpmeta1 ^. edIssuer) + (idp ^. idpExtraInfo . wiOldIssuers) `shouldBe` [] + (idp ^. idpExtraInfo . wiReplacedBy) `shouldBe` Nothing let -- change idp metadata (only issuer, to be precise), and look at new issuer and -- old issuers. @@ -776,11 +783,11 @@ specCRUDIdentityProvider = do resp <- call $ callIdpUpdate' (env ^. teSpar) (Just owner1) idpid1 (IdPMetadataValue (cs $ SAML.encode new) undefined) liftIO $ statusCode resp `shouldBe` 200 - midp <- runSpar $ IdPEffect.getConfig idpid1 + idp <- runSpar $ IdPEffect.getConfig idpid1 liftIO $ do - (midp ^? _Just . idpMetadata . edIssuer) `shouldBe` Just (new ^. edIssuer) - sort <$> (midp ^? _Just . idpExtraInfo . wiOldIssuers) `shouldBe` Just (sort $ olds <&> (^. edIssuer)) - (midp ^? _Just . idpExtraInfo . wiReplacedBy) `shouldBe` Just Nothing + (idp ^. idpMetadata . edIssuer) `shouldBe` (new ^. edIssuer) + sort (idp ^. idpExtraInfo . wiOldIssuers) `shouldBe` sort (olds <&> (^. edIssuer)) + (idp ^. idpExtraInfo . wiReplacedBy) `shouldBe` Nothing -- update the name a few times, ending up with the original one. change idpmeta1' [idpmeta1] diff --git a/services/spar/test-integration/Test/Spar/DataSpec.hs b/services/spar/test-integration/Test/Spar/DataSpec.hs index ecd5839492..a50585ac15 100644 --- a/services/spar/test-integration/Test/Spar/DataSpec.hs +++ b/services/spar/test-integration/Test/Spar/DataSpec.hs @@ -31,10 +31,10 @@ import Imports import Polysemy import SAML2.WebSSO as SAML import Spar.App as App +import Spar.Error (IdpDbError (IdpNotFound), SparCustomError (IdpDbError)) import Spar.Intra.BrigApp (veidFromUserSSOId) import qualified Spar.Sem.AReqIDStore as AReqIDStore import qualified Spar.Sem.AssIDStore as AssIDStore -import Spar.Sem.IdPConfigStore (GetIdPResult (..), Replaced (..), Replacing (..)) import qualified Spar.Sem.IdPConfigStore as IdPEffect import qualified Spar.Sem.SAMLUserStore as SAMLUserStore import qualified Spar.Sem.ScimTokenStore as ScimTokenStore @@ -145,60 +145,44 @@ spec = do describe "Team" $ do testDeleteTeam describe "IdPConfig" $ do - it "storeIdPConfig, getIdPConfig are \"inverses\"" $ do + it "insertIdPConfig, getIdPConfig are \"inverses\"" $ do idp <- makeTestIdP - () <- runSpar $ IdPEffect.storeConfig idp + () <- runSpar $ IdPEffect.insertConfig idp midp <- runSpar $ IdPEffect.getConfig (idp ^. idpId) - liftIO $ midp `shouldBe` Just idp - it "getIdPConfigByIssuer works" $ do - idp <- makeTestIdP - () <- runSpar $ IdPEffect.storeConfig idp - midp <- runSpar $ App.getIdPConfigByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . wiTeam) - liftIO $ midp `shouldBe` GetIdPFound idp - it "getIdPIdByIssuer works" $ do + liftIO $ midp `shouldBe` idp + it "getIdPByIssuer works" $ do idp <- makeTestIdP - () <- runSpar $ IdPEffect.storeConfig idp - midp <- runSpar $ App.getIdPIdByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . wiTeam) - liftIO $ midp `shouldBe` GetIdPFound (idp ^. idpId) + () <- runSpar $ IdPEffect.insertConfig idp + midp <- getIdPByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . wiTeam) + liftIO $ midp `shouldBe` Just idp it "getIdPConfigsByTeam works" $ do skipIdPAPIVersions [WireIdPAPIV1] teamid <- nextWireId idp <- makeTestIdP <&> idpExtraInfo .~ WireIdP teamid Nothing [] Nothing - () <- runSpar $ IdPEffect.storeConfig idp + () <- runSpar $ IdPEffect.insertConfig idp idps <- runSpar $ IdPEffect.getConfigsByTeam teamid liftIO $ idps `shouldBe` [idp] it "deleteIdPConfig works" $ do teamid <- nextWireId idpApiVersion <- asks (^. teWireIdPAPIVersion) idp <- makeTestIdP <&> idpExtraInfo .~ WireIdP teamid (Just idpApiVersion) [] Nothing - () <- runSpar $ IdPEffect.storeConfig idp + () <- runSpar $ IdPEffect.insertConfig idp do midp <- runSpar $ IdPEffect.getConfig (idp ^. idpId) - liftIO $ midp `shouldBe` Just idp + liftIO $ midp `shouldBe` idp () <- runSpar $ IdPEffect.deleteConfig idp do - midp <- runSpar $ IdPEffect.getConfig (idp ^. idpId) - liftIO $ midp `shouldBe` Nothing + idpOrError <- runSparE $ IdPEffect.getConfig (idp ^. idpId) + liftIO $ idpOrError `shouldBe` Left (SAML.CustomError $ IdpDbError IdpNotFound) do - midp <- runSpar $ App.getIdPConfigByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . wiTeam) - liftIO $ midp `shouldBe` GetIdPNotFound + midp <- getIdPByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . wiTeam) + liftIO $ midp `shouldBe` Nothing do - midp <- runSpar $ App.getIdPIdByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . wiTeam) - liftIO $ midp `shouldBe` GetIdPNotFound + midp <- getIdPByIssuer (idp ^. idpMetadata . edIssuer) (idp ^. SAML.idpExtraInfo . wiTeam) + liftIO $ midp `shouldBe` Nothing do idps <- runSpar $ IdPEffect.getConfigsByTeam teamid liftIO $ idps `shouldBe` [] - describe "{set,clear}ReplacedBy" $ do - it "handle non-existent idps gradefully" $ do - pendingWith "this requires a cql{,-io} upgrade. https://gitlab.com/twittner/cql-io/-/issues/7" - idp1 <- makeTestIdP - idp2 <- makeTestIdP - runSpar $ IdPEffect.setReplacedBy (Replaced (idp1 ^. idpId)) (Replacing (idp2 ^. idpId)) - idp1' <- runSpar $ IdPEffect.getConfig (idp1 ^. idpId) - liftIO $ idp1' `shouldBe` Nothing - runSpar $ IdPEffect.clearReplacedBy (Replaced (idp1 ^. idpId)) - idp2' <- runSpar $ IdPEffect.getConfig (idp1 ^. idpId) - liftIO $ idp2' `shouldBe` Nothing -- TODO(sandy): This function should be more polymorphic over it's polysemy -- constraints than using 'RealInterpretation' in full anger. @@ -284,13 +268,13 @@ testDeleteTeam = it "cleans up all the right tables after deletion" $ do liftIO $ mbUser2 `shouldBe` Nothing -- The config from 'idp': do - mbIdp <- runSpar $ IdPEffect.getConfig (idp ^. SAML.idpId) - liftIO $ mbIdp `shouldBe` Nothing + idpOrError <- runSparE $ IdPEffect.getConfig (idp ^. SAML.idpId) + liftIO $ idpOrError `shouldBe` Left (SAML.CustomError $ IdpDbError IdpNotFound) -- The config from 'issuer_idp': do let issuer = idp ^. SAML.idpMetadata . SAML.edIssuer - mbIdp <- runSpar $ App.getIdPIdByIssuer issuer (idp ^. SAML.idpExtraInfo . wiTeam) - liftIO $ mbIdp `shouldBe` GetIdPNotFound + mbIdp <- getIdPByIssuer issuer (idp ^. SAML.idpExtraInfo . wiTeam) + liftIO $ mbIdp `shouldBe` Nothing -- The config from 'team_idp': do idps <- runSpar $ IdPEffect.getConfigsByTeam tid diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 9c493d55b7..51ee1ac069 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -116,6 +116,7 @@ module Util.Core ssoToUidSpar, runSimpleSP, runSpar, + runSparE, type CanonicalEffs, getSsoidViaSelf, getSsoidViaSelf', @@ -129,6 +130,7 @@ module Util.Core updateTeamMemberRole, checkChangeRoleOfTeamMember, eventually, + getIdPByIssuer, ) where @@ -172,11 +174,14 @@ import qualified SAML2.WebSSO.API.Example as SAML import SAML2.WebSSO.Test.Lenses (userRefL) import SAML2.WebSSO.Test.MockResponse import SAML2.WebSSO.Test.Util (SampleIdP (..), makeSampleIdPMetadata) +import qualified Spar.App as IdpConfigStire import qualified Spar.App as Spar import Spar.CanonicalInterpreter +import Spar.Error (SparError) import qualified Spar.Intra.BrigApp as Intra import qualified Spar.Options import Spar.Run +import qualified Spar.Sem.IdPConfigStore as IdPConfigStore import qualified Spar.Sem.SAMLUserStore as SAMLUserStore import qualified Spar.Sem.ScimExternalIdStore as ScimExternalIdStore import qualified System.Logger.Extended as Log @@ -1190,10 +1195,16 @@ runSpar :: Sem CanonicalEffs a -> m a runSpar action = do + result <- runSparE action + liftIO $ either (throwIO . ErrorCall . show) pure result + +runSparE :: + (MonadReader TestEnv m, MonadIO m) => + Sem CanonicalEffs a -> + m (Either SparError a) +runSparE action = do ctx <- (^. teSparEnv) <$> ask - liftIO $ do - result <- runSparToIO ctx action - either (throwIO . ErrorCall . show) pure result + liftIO $ runSparToIO ctx action getSsoidViaSelf :: HasCallStack => UserId -> TestSpar UserSSOId getSsoidViaSelf uid = maybe (error "not found") pure =<< getSsoidViaSelf' uid @@ -1286,3 +1297,10 @@ checkChangeRoleOfTeamMember tid adminId targetId = forM_ [minBound ..] $ \role - eventually :: HasCallStack => TestSpar a -> TestSpar a eventually = recovering (limitRetries 3 <> exponentialBackoff 100000) [] . const + +getIdPByIssuer :: HasCallStack => Issuer -> TeamId -> TestSpar (Maybe IdP) +getIdPByIssuer issuer tid = do + idpApiVersion <- view teWireIdPAPIVersion + runSpar $ case idpApiVersion of + WireIdPAPIV1 -> IdPConfigStore.getIdPByIssuerV1Maybe issuer + WireIdPAPIV2 -> IdPConfigStore.getIdPByIssuerV2Maybe issuer tid diff --git a/services/spar/test/Arbitrary.hs b/services/spar/test/Arbitrary.hs index 6737abeb2d..1bbe8a086c 100644 --- a/services/spar/test/Arbitrary.hs +++ b/services/spar/test/Arbitrary.hs @@ -92,8 +92,6 @@ instance Arbitrary E.Replacing where instance Arbitrary E.Replaced where arbitrary = E.Replaced <$> arbitrary -instance CoArbitrary a => CoArbitrary (E.GetIdPResult a) - -- TODO(sandy): IdPIds are unlikely to collide. Does the size parameter -- affect them? instance CoArbitrary IdPId diff --git a/services/spar/test/Test/Spar/Sem/IdPConfigStoreSpec.hs b/services/spar/test/Test/Spar/Sem/IdPConfigStoreSpec.hs deleted file mode 100644 index af387bd5fe..0000000000 --- a/services/spar/test/Test/Spar/Sem/IdPConfigStoreSpec.hs +++ /dev/null @@ -1,34 +0,0 @@ -{-# LANGUAGE QuantifiedConstraints #-} -{-# OPTIONS_GHC -Wno-orphans #-} -{-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Test.Spar.Sem.IdPConfigStoreSpec where - -import Arbitrary () -import Imports -import Polysemy -import Spar.Sem.IdPConfigStore.Mem -import Spar.Sem.IdPConfigStore.Spec -import Test.Hspec -import Test.Hspec.QuickCheck - -spec :: Spec -spec = modifyMaxSuccess (const 1000) $ do - propsForInterpreter "idPToMem" snd (Just $ show . snd) $ pure . run . idPToMem