diff --git a/.gitignore b/.gitignore index c8af0964ef..8e9a7a3b4c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,7 +33,6 @@ TAGS .stack-docker-profile .metadata *.tix -*.pem .DS_Store services/nginz/src services/.env @@ -99,4 +98,4 @@ i.yaml b.yaml telepresence.log -/.ghci \ No newline at end of file +/.ghci diff --git a/changelog.d/3-bug-fixes/pr-1828 b/changelog.d/3-bug-fixes/pr-1828 new file mode 100644 index 0000000000..f711d6561b --- /dev/null +++ b/changelog.d/3-bug-fixes/pr-1828 @@ -0,0 +1 @@ +SAML columns (Issuer, NameID) in CSV files with team members. \ No newline at end of file diff --git a/libs/wire-api/package.yaml b/libs/wire-api/package.yaml index 18108e1cd3..9d813db174 100644 --- a/libs/wire-api/package.yaml +++ b/libs/wire-api/package.yaml @@ -16,6 +16,8 @@ dependencies: - servant-swagger-ui - case-insensitive - hscim +- saml2-web-sso +- filepath library: source-dirs: src dependencies: @@ -60,7 +62,6 @@ library: - QuickCheck >=2.14 - quickcheck-instances >=0.3.16 - resourcet - - saml2-web-sso - servant - servant-client - servant-client-core diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 07ab1e05df..7a98a355eb 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -120,7 +120,6 @@ import Data.Schema import qualified Data.Swagger as S import qualified Data.Swagger.Build.Api as Doc import Data.Text.Ascii -import qualified Data.Text.Lazy as TL import Data.UUID (UUID, nil) import qualified Data.UUID as UUID import Deriving.Swagger @@ -412,12 +411,12 @@ userSCIMExternalId :: User -> Maybe Text userSCIMExternalId usr = userSSOId >=> ssoIdExtId $ usr where ssoIdExtId :: UserSSOId -> Maybe Text - ssoIdExtId (UserSSOId _ nameIdXML) = case userManagedBy usr of + ssoIdExtId (UserSSOId (SAML.UserRef _ nameIdXML)) = case userManagedBy usr of ManagedByWire -> Nothing ManagedByScim -> - -- FUTUREWORK: keep the CI value, store the original in the database, but always use - -- the CI value for processing. - CI.original . SAML.unsafeShowNameID <$> either (const Nothing) pure (SAML.decodeElem (TL.fromStrict nameIdXML)) + -- FUTUREWORK: this is only ignoring case in the email format, and emails should be + -- handled case-insensitively. https://wearezeta.atlassian.net/browse/SQSERVICES-909 + Just . CI.original . SAML.unsafeShowNameID $ nameIdXML ssoIdExtId (UserScimExternalId extId) = pure extId connectedProfile :: User -> UserLegalHoldStatus -> UserProfile diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index f70575cd1a..55584f1eba 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -40,28 +40,43 @@ module Wire.API.User.Identity -- * UserSSOId UserSSOId (..), - - -- * Swagger + emailFromSAML, + emailToSAML, + emailToSAMLNameID, + emailFromSAMLNameID, + mkSampleUref, + mkSimpleSampleUref, ) where import Control.Applicative (optional) -import Control.Lens ((.~), (?~)) +import Control.Lens ((.~), (?~), (^.)) import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as A +import qualified Data.Aeson.Types as A import Data.Attoparsec.Text -import Data.Bifunctor (first) +import Data.Bifunctor (first, second) import Data.ByteString.Conversion +import qualified Data.CaseInsensitive as CI import Data.Proxy (Proxy (..)) import Data.Schema +import Data.String.Conversions (cs) import qualified Data.Swagger as S import qualified Data.Text as Text import Data.Text.Encoding (decodeUtf8', encodeUtf8) import Data.Time.Clock import Imports +import SAML2.WebSSO.Test.Arbitrary () +import qualified SAML2.WebSSO.Types as SAML +import qualified SAML2.WebSSO.Types.Email as SAMLEmail +import qualified SAML2.WebSSO.XML as SAML +import System.FilePath (()) import qualified Test.QuickCheck as QC import qualified Text.Email.Validate as Email.V +import qualified URI.ByteString as URI +import URI.ByteString.QQ (uri) import Wire.API.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) +import Wire.API.User.Profile (fromName, mkName) -------------------------------------------------------------------------------- -- UserIdentity @@ -267,30 +282,27 @@ isValidPhone = either (const False) (const True) . parseOnly e164 -- | User's external identity. -- --- Morally this is the same thing as 'SAML.UserRef', but we forget the --- structure -- i.e. we just store XML-encoded SAML blobs. If the structure --- of those blobs changes, Brig won't have to deal with it, only Spar will. +-- NB: this type is serialized to the full xml encoding of the `SAML.UserRef` components, but +-- deserialiation is more lenient: it also allows for the `Issuer` to be a plain URL (without +-- xml around it), and the `NameID` to be an email address (=> format "email") or an arbitrary +-- text (=> format "unspecified"). This is for backwards compatibility and general +-- robustness. -- --- FUTUREWORK: rename the data type to @UserSparId@ (not the two constructors, those are ok). +-- FUTUREWORK: we should probably drop this entirely and store saml and scim data in separate +-- database columns. data UserSSOId - = UserSSOId - -- An XML blob pointing to the identity provider that can confirm - -- user's identity. - Text - -- An XML blob specifying the user's ID on the identity provider's side. - Text - | UserScimExternalId - Text + = UserSSOId SAML.UserRef + | UserScimExternalId Text deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserSSOId) --- FUTUREWORK: This schema should ideally be a choice of either tenant+subject, or scim_external_id +-- | FUTUREWORK: This schema should ideally be a choice of either tenant+subject, or scim_external_id -- but this is currently not possible to derive in swagger2 -- Maybe this becomes possible with swagger 3? instance S.ToSchema UserSSOId where declareNamedSchema _ = do - tenantSchema <- S.declareSchemaRef (Proxy @Text) - subjectSchema <- S.declareSchemaRef (Proxy @Text) + tenantSchema <- S.declareSchemaRef (Proxy @Text) -- FUTUREWORK: 'Issuer' + subjectSchema <- S.declareSchemaRef (Proxy @Text) -- FUTUREWORK: 'NameID' scimSchema <- S.declareSchemaRef (Proxy @Text) return $ S.NamedSchema (Just "UserSSOId") $ @@ -304,16 +316,16 @@ instance S.ToSchema UserSSOId where instance ToJSON UserSSOId where toJSON = \case - UserSSOId tenant subject -> A.object ["tenant" A..= tenant, "subject" A..= subject] + UserSSOId (SAML.UserRef tenant subject) -> A.object ["tenant" A..= SAML.encodeElem tenant, "subject" A..= SAML.encodeElem subject] UserScimExternalId eid -> A.object ["scim_external_id" A..= eid] instance FromJSON UserSSOId where parseJSON = A.withObject "UserSSOId" $ \obj -> do - mtenant <- obj A..:? "tenant" - msubject <- obj A..:? "subject" + mtenant <- lenientlyParseSAMLIssuer =<< (obj A..:? "tenant") + msubject <- lenientlyParseSAMLNameID =<< (obj A..:? "subject") meid <- obj A..:? "scim_external_id" case (mtenant, msubject, meid) of - (Just tenant, Just subject, Nothing) -> pure $ UserSSOId tenant subject + (Just tenant, Just subject, Nothing) -> pure $ UserSSOId (SAML.UserRef tenant subject) (Nothing, Nothing, Just eid) -> pure $ UserScimExternalId eid _ -> fail "either need tenant and subject, or scim_external_id, but not both" @@ -331,3 +343,78 @@ instance FromJSON PhoneBudgetTimeout where instance ToJSON PhoneBudgetTimeout where toJSON (PhoneBudgetTimeout t) = A.object ["expires_in" A..= t] + +lenientlyParseSAMLIssuer :: Maybe LText -> A.Parser (Maybe SAML.Issuer) +lenientlyParseSAMLIssuer mbtxt = forM mbtxt $ \txt -> do + let asxml :: Either String SAML.Issuer + asxml = SAML.decodeElem txt + + asurl :: Either String SAML.Issuer + asurl = + first show + . second SAML.Issuer + $ URI.parseURI URI.laxURIParserOptions (cs txt) + + err :: String + err = "lenientlyParseSAMLIssuer: " <> show (asxml, asurl, mbtxt) + + either (const $ fail err) pure $ asxml <|> asurl + +lenientlyParseSAMLNameID :: Maybe LText -> A.Parser (Maybe SAML.NameID) +lenientlyParseSAMLNameID Nothing = pure Nothing +lenientlyParseSAMLNameID (Just txt) = do + let asxml :: Either String SAML.NameID + asxml = SAML.decodeElem txt + + asemail :: Either String SAML.NameID + asemail = + maybe + (Left "not an email") + (fmap emailToSAMLNameID . validateEmail) + (parseEmail (cs txt)) + + astxt :: Either String SAML.NameID + astxt = do + nm <- mkName (cs txt) + SAML.mkNameID (SAML.mkUNameIDUnspecified (fromName nm)) Nothing Nothing Nothing + + err :: String + err = "lenientlyParseSAMLNameID: " <> show (asxml, asemail, astxt, txt) + + either + (const $ fail err) + (pure . Just) + (asxml <|> asemail <|> astxt) + +emailFromSAML :: HasCallStack => SAMLEmail.Email -> Email +emailFromSAML = fromJust . parseEmail . SAMLEmail.render + +emailToSAML :: HasCallStack => Email -> SAMLEmail.Email +emailToSAML = CI.original . fromRight (error "emailToSAML") . SAMLEmail.validate . toByteString + +-- | FUTUREWORK(fisx): if saml2-web-sso exported the 'NameID' constructor, we could make this +-- function total without all that praying and hoping. +emailToSAMLNameID :: HasCallStack => Email -> SAML.NameID +emailToSAMLNameID = fromRight (error "impossible") . SAML.emailNameID . fromEmail + +emailFromSAMLNameID :: HasCallStack => SAML.NameID -> Maybe Email +emailFromSAMLNameID nid = case nid ^. SAML.nameID of + SAML.UNameIDEmail email -> Just . emailFromSAML . CI.original $ email + _ -> Nothing + +-- | For testing. Create a sample 'SAML.UserRef' value with random seeds to make 'Issuer' and +-- 'NameID' unique. FUTUREWORK: move to saml2-web-sso. +mkSampleUref :: Text -> Text -> SAML.UserRef +mkSampleUref iseed nseed = SAML.UserRef issuer nameid + where + issuer :: SAML.Issuer + issuer = SAML.Issuer ([uri|http://example.com/|] & URI.pathL .~ cs ("/" cs iseed)) + + nameid :: SAML.NameID + nameid = fromRight (error "impossible") $ do + unqualified <- SAML.mkUNameIDEmail $ "me" <> nseed <> "@example.com" + SAML.mkNameID unqualified Nothing Nothing Nothing + +-- | @mkSampleUref "" ""@ +mkSimpleSampleUref :: SAML.UserRef +mkSimpleSampleUref = mkSampleUref "" "" diff --git a/libs/wire-api/test/golden/fromJSON/testObject_NewUserPublic_user_1-1.json b/libs/wire-api/test/golden/fromJSON/testObject_NewUserPublic_user_1-1.json deleted file mode 100644 index 1f1c81c450..0000000000 --- a/libs/wire-api/test/golden/fromJSON/testObject_NewUserPublic_user_1-1.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "accent_id": 39125, - "assets": [ - { - "key": "", - "size": "complete", - "type": "image" - }, - { - "key": "(󼊊\u001bp󳢼u]'􅄻", - "type": "image" - }, - { - "key": "􁿐f", - "size": "preview", - "type": "image" - } - ], - "email_code": "cfTQLlhl6H6sYloQXsghILggxWoGhM2WGbxjzm0=", - "label": ">>Mp१𤘇9:󺰽􋼒\u0010D1j󾮢􂊠;􄆇󳸪f#]", - "locale": "so", - "managed_by": "wire", - "name": "\\sY4]u󼛸\u0010󺲻\u001c\u0003 \u001f\u0017􄚐dw;}􆃪@𭂿\r8", - "password": "dX󹊒赲󶻎ht𘙏󴰏\u0007>\u0018\u000bO95\u0015\n(𩝙󻞌嶝f]_𪀮\u00002FQbNS=6g󿷼P𢲾􃨫󰧽􅤹M\u001e7\u0016~\u0017m󽎭\u0006\u0001\u000bkgmBp\u0017w悬𩓯f󹼮%Q\u0004𢔶kP|G𥬅\u0017B-\nJWH(8)4$󱠶<7𭨖\u001cI\u0008A\u0010\r?󹀊\u0008\u00085\u0006󶟨d \u00166􍉶G\u0018\u0008\t=qG􃁰 D\u0002vV\tYpg󸋮吝q\n \u0017L􁼛-􏕋\u0013󺃝F7Q􊔜]揃i?\r\u0010\u001b{=􎕻_?e􇢹%\u000eR󱆼\u001b+\u000ef\u0017q:g\\Rk馍𪝞[l\u0015􉜀VK\njwp\u00043TJྏEj\u0002R7d83ON\u0017q獿\u0019𮣜N8\n\u000f󻦼u:GꓻFZ\u001c<\u0015揤7􉖬tH󿳸;hbS{ꮯ\u001csMs󲷒9B4􀷾35c(~CUc󸇪\\V_XD3me@example.com", + "tenant": "http://example.com/" } } diff --git a/libs/wire-api/test/golden/testObject_ActivationResponse_user_19.json b/libs/wire-api/test/golden/testObject_ActivationResponse_user_19.json index f4ad262a30..caf5540093 100644 --- a/libs/wire-api/test/golden/testObject_ActivationResponse_user_19.json +++ b/libs/wire-api/test/golden/testObject_ActivationResponse_user_19.json @@ -2,7 +2,7 @@ "email": "R@K", "first": false, "sso_id": { - "subject": "", - "tenant": "" + "subject": "me@example.com", + "tenant": "http://example.com/" } } diff --git a/libs/wire-api/test/golden/testObject_NewUser_user_6.json b/libs/wire-api/test/golden/testObject_NewUser_user_6.json index 9302c14469..158591955d 100644 --- a/libs/wire-api/test/golden/testObject_NewUser_user_6.json +++ b/libs/wire-api/test/golden/testObject_NewUser_user_6.json @@ -2,8 +2,8 @@ "assets": [], "name": "test name", "sso_id": { - "subject": "thing", - "tenant": "some" + "subject": "me@example.com", + "tenant": "http://example.com/" }, "team_id": "00007b0e-0000-3489-0000-075c00005be7" } diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_16.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_16.json index 56073c95ac..156ade504d 100644 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_16.json +++ b/libs/wire-api/test/golden/testObject_UserIdentity_user_16.json @@ -2,7 +2,7 @@ "email": "%x\u0013􀔑\u0004.@G빯t.6", "phone": "+298116118047", "sso_id": { - "subject": "\u0013\u001c", - "tenant": "a\u001c" + "subject": "me@example.com", + "tenant": "http://example.com" } } diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_5.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_5.json index 68bd2291d2..902e47fbe8 100644 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_5.json +++ b/libs/wire-api/test/golden/testObject_UserIdentity_user_5.json @@ -2,7 +2,7 @@ "email": null, "phone": "+49198172826", "sso_id": { - "subject": "󴤰", - "tenant": ">􋲗􎚆󾪂" + "subject": "me@example.com", + "tenant": "http://example.com" } } diff --git a/libs/wire-api/test/golden/testObject_UserIdentity_user_8.json b/libs/wire-api/test/golden/testObject_UserIdentity_user_8.json index 5e01fb0c2b..f9a46004b6 100644 --- a/libs/wire-api/test/golden/testObject_UserIdentity_user_8.json +++ b/libs/wire-api/test/golden/testObject_UserIdentity_user_8.json @@ -2,7 +2,7 @@ "email": null, "phone": "+149548802116267", "sso_id": { - "subject": "", - "tenant": "" + "subject": "me@example.com", + "tenant": "http://example.com" } } diff --git a/libs/wire-api/test/golden/testObject_UserSSOId_user_2.json b/libs/wire-api/test/golden/testObject_UserSSOId_user_2.json index 6de1296422..431a302354 100644 --- a/libs/wire-api/test/golden/testObject_UserSSOId_user_2.json +++ b/libs/wire-api/test/golden/testObject_UserSSOId_user_2.json @@ -1,3 +1,4 @@ { - "scim_external_id": "퀶\u001a\u0002\u000bf\u0008-󿰣qA􄚨\u0005 >jJ" + "subject": "me@example.com", + "tenant": "http://example.com/" } diff --git a/libs/wire-api/test/unit/Test/Wire/API/Golden/FromJSON.hs b/libs/wire-api/test/unit/Test/Wire/API/Golden/FromJSON.hs index f0bdbc0320..5c0485a279 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Golden/FromJSON.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Golden/FromJSON.hs @@ -94,25 +94,13 @@ tests = testFromJSONFailureWithMsg @NewUser (Just "all team users must set a password on creation") "testObject_NewUser_user_5-2.json", - testCase "testObject_NewUser_user_6-2.json" $ - testFromJSONFailureWithMsg @NewUser - (Just "sso_id, team_id must be either both present or both absent.") - "testObject_NewUser_user_6-2.json", testCase "testObject_NewUser_user_6-3.json" $ testFromJSONFailureWithMsg @NewUser (Just "sso_id, team_id must be either both present or both absent.") - "testObject_NewUser_user_6-3.json", - testCase "testObject_NewUser_user_6-4.json" $ - testFromJSONFailureWithMsg @NewUser - (Just "team_code, team, invitation_code, sso_id, and the pair (sso_id, team_id) are mutually exclusive") - "testObject_NewUser_user_6-4.json" + "testObject_NewUser_user_6-3.json" ], testGroup "NewUserPublic: failure" $ - [ testCase "testObject_NewUserPublic_user_1-1.json" $ - testFromJSONFailureWithMsg @NewUserPublic - (Just "SSO-managed users are not allowed here.") - "testObject_NewUserPublic_user_1-1.json", - testCase "testObject_NewUserPublic_user_1-2.json" $ + [ testCase "testObject_NewUserPublic_user_1-2.json" $ testFromJSONFailureWithMsg @NewUserPublic (Just "it is not allowed to provide a UUID for the users here.") "testObject_NewUserPublic_user_1-2.json", diff --git a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated.hs index 5c2c181d66..7eacbb74d2 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated.hs @@ -1059,9 +1059,9 @@ tests = testGroup "Golden: Phone_user" $ testObjects [(Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_1, "testObject_Phone_user_1.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_2, "testObject_Phone_user_2.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_3, "testObject_Phone_user_3.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_4, "testObject_Phone_user_4.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_5, "testObject_Phone_user_5.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_6, "testObject_Phone_user_6.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_7, "testObject_Phone_user_7.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_8, "testObject_Phone_user_8.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_9, "testObject_Phone_user_9.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_10, "testObject_Phone_user_10.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_11, "testObject_Phone_user_11.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_12, "testObject_Phone_user_12.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_13, "testObject_Phone_user_13.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_14, "testObject_Phone_user_14.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_15, "testObject_Phone_user_15.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_16, "testObject_Phone_user_16.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_17, "testObject_Phone_user_17.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_18, "testObject_Phone_user_18.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_19, "testObject_Phone_user_19.json"), (Test.Wire.API.Golden.Generated.Phone_user.testObject_Phone_user_20, "testObject_Phone_user_20.json")], testGroup "Golden: UserSSOId_user" $ - testObjects [(Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_1, "testObject_UserSSOId_user_1.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_2, "testObject_UserSSOId_user_2.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_3, "testObject_UserSSOId_user_3.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_4, "testObject_UserSSOId_user_4.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_5, "testObject_UserSSOId_user_5.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_6, "testObject_UserSSOId_user_6.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_7, "testObject_UserSSOId_user_7.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_8, "testObject_UserSSOId_user_8.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_9, "testObject_UserSSOId_user_9.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_10, "testObject_UserSSOId_user_10.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_11, "testObject_UserSSOId_user_11.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_12, "testObject_UserSSOId_user_12.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_13, "testObject_UserSSOId_user_13.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_14, "testObject_UserSSOId_user_14.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_15, "testObject_UserSSOId_user_15.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_16, "testObject_UserSSOId_user_16.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_17, "testObject_UserSSOId_user_17.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_18, "testObject_UserSSOId_user_18.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_19, "testObject_UserSSOId_user_19.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_20, "testObject_UserSSOId_user_20.json")], + testObjects [(Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_2, "testObject_UserSSOId_user_2.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_9, "testObject_UserSSOId_user_9.json"), (Test.Wire.API.Golden.Generated.UserSSOId_user.testObject_UserSSOId_user_13, "testObject_UserSSOId_user_13.json")], testGroup "Golden: UserIdentity_user" $ - testObjects [(Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_1, "testObject_UserIdentity_user_1.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_2, "testObject_UserIdentity_user_2.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_3, "testObject_UserIdentity_user_3.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_4, "testObject_UserIdentity_user_4.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_5, "testObject_UserIdentity_user_5.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_6, "testObject_UserIdentity_user_6.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_7, "testObject_UserIdentity_user_7.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_8, "testObject_UserIdentity_user_8.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_9, "testObject_UserIdentity_user_9.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_10, "testObject_UserIdentity_user_10.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_11, "testObject_UserIdentity_user_11.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_12, "testObject_UserIdentity_user_12.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_13, "testObject_UserIdentity_user_13.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_14, "testObject_UserIdentity_user_14.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_15, "testObject_UserIdentity_user_15.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_16, "testObject_UserIdentity_user_16.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_17, "testObject_UserIdentity_user_17.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_18, "testObject_UserIdentity_user_18.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_19, "testObject_UserIdentity_user_19.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_20, "testObject_UserIdentity_user_20.json")], + testObjects [(Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_1, "testObject_UserIdentity_user_1.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_2, "testObject_UserIdentity_user_2.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_3, "testObject_UserIdentity_user_3.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_4, "testObject_UserIdentity_user_4.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_6, "testObject_UserIdentity_user_6.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_7, "testObject_UserIdentity_user_7.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_9, "testObject_UserIdentity_user_9.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_10, "testObject_UserIdentity_user_10.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_11, "testObject_UserIdentity_user_11.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_12, "testObject_UserIdentity_user_12.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_13, "testObject_UserIdentity_user_13.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_14, "testObject_UserIdentity_user_14.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_15, "testObject_UserIdentity_user_15.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_17, "testObject_UserIdentity_user_17.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_18, "testObject_UserIdentity_user_18.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_19, "testObject_UserIdentity_user_19.json"), (Test.Wire.API.Golden.Generated.UserIdentity_user.testObject_UserIdentity_user_20, "testObject_UserIdentity_user_20.json")], testGroup "Golden: NewPasswordReset_user" $ testObjects [(Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_1, "testObject_NewPasswordReset_user_1.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_2, "testObject_NewPasswordReset_user_2.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_3, "testObject_NewPasswordReset_user_3.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_4, "testObject_NewPasswordReset_user_4.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_5, "testObject_NewPasswordReset_user_5.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_6, "testObject_NewPasswordReset_user_6.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_7, "testObject_NewPasswordReset_user_7.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_8, "testObject_NewPasswordReset_user_8.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_9, "testObject_NewPasswordReset_user_9.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_10, "testObject_NewPasswordReset_user_10.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_11, "testObject_NewPasswordReset_user_11.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_12, "testObject_NewPasswordReset_user_12.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_13, "testObject_NewPasswordReset_user_13.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_14, "testObject_NewPasswordReset_user_14.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_15, "testObject_NewPasswordReset_user_15.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_16, "testObject_NewPasswordReset_user_16.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_17, "testObject_NewPasswordReset_user_17.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_18, "testObject_NewPasswordReset_user_18.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_19, "testObject_NewPasswordReset_user_19.json"), (Test.Wire.API.Golden.Generated.NewPasswordReset_user.testObject_NewPasswordReset_user_20, "testObject_NewPasswordReset_user_20.json")], testGroup "Golden: PasswordResetKey_user" $ diff --git a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs index 0b0bfc8cbb..0b0cdf459e 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/ActivationResponse_user.hs @@ -31,13 +31,14 @@ import Wire.API.User UserSSOId (UserSSOId, UserScimExternalId), ) import Wire.API.User.Activation (ActivationResponse (..)) +import Wire.API.User.Identity (mkSimpleSampleUref) testObject_ActivationResponse_user_1 :: ActivationResponse testObject_ActivationResponse_user_1 = ActivationResponse { activatedIdentity = SSOIdentity - (UserSSOId "" "\RS") + (UserSSOId mkSimpleSampleUref) (Just (Email {emailLocal = "\165918\rZ\a\ESC", emailDomain = "p\131777\62344"})) Nothing, activatedFirst = False @@ -169,7 +170,7 @@ testObject_ActivationResponse_user_18 = testObject_ActivationResponse_user_19 :: ActivationResponse testObject_ActivationResponse_user_19 = ActivationResponse - { activatedIdentity = SSOIdentity (UserSSOId "" "") (Just (Email {emailLocal = "R", emailDomain = "K"})) Nothing, + { activatedIdentity = SSOIdentity (UserSSOId mkSimpleSampleUref) (Just (Email {emailLocal = "R", emailDomain = "K"})) Nothing, activatedFirst = False } diff --git a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/NewUser_user.hs b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/NewUser_user.hs index d197d6bad0..68039b8ac3 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/NewUser_user.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/NewUser_user.hs @@ -61,7 +61,7 @@ import Wire.API.User ) import Wire.API.User.Activation (ActivationCode (ActivationCode, fromActivationCode)) import Wire.API.User.Auth (CookieLabel (CookieLabel, cookieLabelText)) -import Wire.API.User.Identity (Phone (..), UserSSOId (UserSSOId)) +import Wire.API.User.Identity (Phone (..), UserSSOId (UserSSOId), mkSimpleSampleUref) testObject_NewUser_user_1 :: NewUser testObject_NewUser_user_1 = @@ -140,7 +140,7 @@ testObject_NewUser_user_6 = (Name {fromName = "test name"}) ) { newUserOrigin = Just (NewUserOriginTeamUser (NewTeamMemberSSO tid)), - newUserIdentity = Just (SSOIdentity (UserSSOId "some" "thing") Nothing Nothing) + newUserIdentity = Just (SSOIdentity (UserSSOId mkSimpleSampleUref) Nothing Nothing) } where tid = Id (fromJust (UUID.fromString "00007b0e-0000-3489-0000-075c00005be7")) diff --git a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/UserIdentity_user.hs b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/UserIdentity_user.hs index 0345304768..19d70db470 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/UserIdentity_user.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/UserIdentity_user.hs @@ -25,6 +25,7 @@ import Wire.API.User UserIdentity (..), UserSSOId (UserSSOId, UserScimExternalId), ) +import Wire.API.User.Identity (mkSimpleSampleUref) testObject_UserIdentity_user_1 :: UserIdentity testObject_UserIdentity_user_1 = @@ -56,7 +57,7 @@ testObject_UserIdentity_user_4 = testObject_UserIdentity_user_5 :: UserIdentity testObject_UserIdentity_user_5 = - SSOIdentity (UserSSOId ">\1096855\1107590\1043074" "\1001776") Nothing (Just (Phone {fromPhone = "+49198172826"})) + SSOIdentity (UserSSOId mkSimpleSampleUref) Nothing (Just (Phone {fromPhone = "+49198172826"})) testObject_UserIdentity_user_6 :: UserIdentity testObject_UserIdentity_user_6 = PhoneIdentity (Phone {fromPhone = "+03038459796465"}) @@ -65,7 +66,7 @@ testObject_UserIdentity_user_7 :: UserIdentity testObject_UserIdentity_user_7 = PhoneIdentity (Phone {fromPhone = "+805676294"}) testObject_UserIdentity_user_8 :: UserIdentity -testObject_UserIdentity_user_8 = SSOIdentity (UserSSOId "" "") Nothing (Just (Phone {fromPhone = "+149548802116267"})) +testObject_UserIdentity_user_8 = SSOIdentity (UserSSOId mkSimpleSampleUref) Nothing (Just (Phone {fromPhone = "+149548802116267"})) testObject_UserIdentity_user_9 :: UserIdentity testObject_UserIdentity_user_9 = @@ -114,7 +115,7 @@ testObject_UserIdentity_user_15 = PhoneIdentity (Phone {fromPhone = "+0923809422 testObject_UserIdentity_user_16 :: UserIdentity testObject_UserIdentity_user_16 = SSOIdentity - (UserSSOId "a\FS" "\DC3\FS") + (UserSSOId mkSimpleSampleUref) (Just (Email {emailLocal = "%x\DC3\1049873\EOT.", emailDomain = "G\48751t.6"})) (Just (Phone {fromPhone = "+298116118047"})) diff --git a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/UserSSOId_user.hs b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/UserSSOId_user.hs index 5f01be95f0..51d29bcd37 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/UserSSOId_user.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Golden/Generated/UserSSOId_user.hs @@ -19,65 +19,13 @@ module Test.Wire.API.Golden.Generated.UserSSOId_user where import Wire.API.User (UserSSOId (..)) - -testObject_UserSSOId_user_1 :: UserSSOId -testObject_UserSSOId_user_1 = UserSSOId "#ph\1052492" "\121009\1055837S\ACK\\\ETB\\" +import Wire.API.User.Identity (mkSimpleSampleUref) testObject_UserSSOId_user_2 :: UserSSOId -testObject_UserSSOId_user_2 = UserScimExternalId "\53302\SUB\STX\vf\b\58777-\1047587qA\1066664\ENQ >jJ" - -testObject_UserSSOId_user_3 :: UserSSOId -testObject_UserSSOId_user_3 = UserSSOId "i\DEL\\\EOT\r\99405\NAK\992986\51508Vi" "\164492\&4X\EM" - -testObject_UserSSOId_user_4 :: UserSSOId -testObject_UserSSOId_user_4 = UserSSOId "0\1078858hK\150460Rc;/[Q9s{" "\1089121\&0\ESC\183599=1.2 , generics-sop , ghc-prim @@ -445,6 +446,7 @@ test-suite wire-api-tests , containers >=0.5 , currency-codes , directory + , filepath , hscim , imports , iso3166-country-codes @@ -454,6 +456,7 @@ test-suite wire-api-tests , pem , pretty , proto-lens + , saml2-web-sso , servant-swagger-ui , string-conversions , swagger2 diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index c2741ef906..f2021ee936 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -259,7 +259,7 @@ createUser new = do Nothing -> pure Nothing joinedTeamSSO <- case (newUserIdentity new', tid) of - (Just ident@(SSOIdentity (UserSSOId _ _) _ _), Just tid') -> Just <$> addUserToTeamSSO account tid' ident + (Just ident@(SSOIdentity (UserSSOId _) _ _), Just tid') -> Just <$> addUserToTeamSSO account tid' ident _ -> pure Nothing pure (activatedTeam <|> joinedTeamInvite <|> joinedTeamSSO) diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 89a63abf80..ccb2e40414 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -79,7 +79,6 @@ import Imports hiding (log, searchable) import Network.HTTP.Client hiding (path) import Network.HTTP.Types (hContentType, statusCode) import qualified SAML2.WebSSO.Types as SAML -import qualified SAML2.WebSSO.XML as SAML import qualified System.Logger as Log import System.Logger.Class ( Logger, @@ -737,8 +736,6 @@ reindexRowToIndexUser ] idpUrl :: UserSSOId -> Maybe Text - idpUrl (UserSSOId tenant _subject) = - case SAML.decodeElem $ cs tenant of - Left _ -> Nothing - Right (SAML.Issuer uri) -> Just $ (cs . toLazyByteString . serializeURIRef) uri + idpUrl (UserSSOId (SAML.UserRef (SAML.Issuer uri) _subject)) = + Just $ (cs . toLazyByteString . serializeURIRef) uri idpUrl (UserScimExternalId _) = Nothing diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index aa338e2253..d8e695b783 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -56,6 +56,7 @@ import UnliftIO.Async (mapConcurrently_, pooledForConcurrentlyN_, replicateConcu import Util import Util.AWS as Util import Web.Cookie (parseSetCookie, setCookieName) +import Wire.API.User.Identity (mkSimpleSampleUref) newtype TeamSizeLimit = TeamSizeLimit Word32 @@ -757,7 +758,7 @@ testConnectionSameTeam brig = do testCreateUserInternalSSO :: Brig -> Galley -> Http () testCreateUserInternalSSO brig galley = do teamid <- snd <$> createUserWithTeam brig - let ssoid = UserSSOId "nil" "nil" + let ssoid = UserSSOId mkSimpleSampleUref -- creating users requires both sso_id and team_id postUser' True False "dummy" True False (Just ssoid) Nothing brig !!! const 400 === statusCode @@ -788,7 +789,7 @@ testCreateUserInternalSSO brig galley = do testDeleteUserSSO :: Brig -> Galley -> Http () testDeleteUserSSO brig galley = do (creator, tid) <- createUserWithTeam brig - let ssoid = UserSSOId "nil" "nil" + let ssoid = UserSSOId mkSimpleSampleUref mkuser :: Bool -> Http (Maybe User) mkuser withemail = responseJsonMaybe diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index a7a4de6b24..a4b9646b15 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -73,6 +73,7 @@ import Util as Util import Util.AWS as Util import Web.Cookie (parseSetCookie) import Wire.API.User (ListUsersQuery (..)) +import Wire.API.User.Identity (mkSampleUref, mkSimpleSampleUref) tests :: ConnectionLimit -> Opt.Timeout -> Opt.Opts -> Manager -> Brig -> Cannon -> CargoHold -> Galley -> AWS.Env -> TestTree tests _ at opts p b c ch g aws = @@ -389,7 +390,7 @@ testCreateUserBlacklist _ brig aws = testCreateUserExternalSSO :: Brig -> Http () testCreateUserExternalSSO brig = do teamid <- Id <$> liftIO UUID.nextRandom - let ssoid = UserSSOId "nil" "nil" + let ssoid = UserSSOId mkSimpleSampleUref p withsso withteam = RequestBodyLBS . encode . object $ ["name" .= ("foo" :: Text)] @@ -1203,7 +1204,7 @@ testUpdateSSOId brig galley = do put ( brig . paths ["i", "users", toByteString' noSuchUserId, "sso-id"] - . Bilge.json (UserSSOId "1" "1") + . Bilge.json (UserSSOId (mkSampleUref "1" "1")) ) !!! const 404 === statusCode let go :: HasCallStack => User -> UserSSOId -> Http () @@ -1230,8 +1231,8 @@ testUpdateSSOId brig galley = do when (not hasEmail) $ do error "not implemented" selfUser <$> (responseJsonError =<< get (brig . path "/self" . zUser (userId member))) - let ssoids1 = [UserSSOId "1" "1", UserSSOId "1" "2"] - ssoids2 = [UserSSOId "2" "1", UserSSOId "2" "2"] + let ssoids1 = [UserSSOId (mkSampleUref "1" "1"), UserSSOId (mkSampleUref "1" "2")] + ssoids2 = [UserSSOId (mkSampleUref "2" "1"), UserSSOId (mkSampleUref "2" "2")] users <- sequence [ mkMember True False, @@ -1325,7 +1326,7 @@ testRestrictedUserCreation opts brig = do -- NOTE: SSO users are anyway not allowed on the `/register` endpoint teamid <- Id <$> liftIO UUID.nextRandom - let ssoid = UserSSOId "nil" "nil" + let ssoid = UserSSOId mkSimpleSampleUref let Object ssoUser = object [ "name" .= Name "Alice", diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 57fabd1915..0c6dbd6d86 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -4,7 +4,7 @@ cabal-version: 1.12 -- -- see: https://github.com/sol/hpack -- --- hash: c121411458d6b0f7118ae1589134cb37711d29cad840e07d0f135663c59cc53a +-- hash: 1daf2eec8d6d9666168a442a3c2856c2d453361e3bbffcc4a17c55d8bbf914f4 name: galley version: 0.83.0 @@ -182,6 +182,7 @@ executable galley , imports , raw-strings-qq >=1.0 , safe >=0.3 + , saml2-web-sso >=0.18 , servant-client , ssl-util , tagged @@ -264,6 +265,7 @@ executable galley-integration , raw-strings-qq >=1.0 , retry , safe >=0.3 + , saml2-web-sso >=0.18 , schema-profunctor , servant , servant-client @@ -325,6 +327,7 @@ executable galley-migrate-data , optparse-applicative , raw-strings-qq >=1.0 , safe >=0.3 + , saml2-web-sso >=0.18 , servant-client , ssl-util , tagged @@ -391,6 +394,7 @@ executable galley-schema , optparse-applicative , raw-strings-qq >=1.0 , safe >=0.3 + , saml2-web-sso >=0.18 , servant-client , ssl-util , tagged @@ -431,6 +435,7 @@ test-suite galley-types-tests , lens , raw-strings-qq >=1.0 , safe >=0.3 + , saml2-web-sso >=0.18 , servant-client , servant-swagger , ssl-util diff --git a/services/galley/package.yaml b/services/galley/package.yaml index 2ae2bfe98a..ead7824b38 100644 --- a/services/galley/package.yaml +++ b/services/galley/package.yaml @@ -21,6 +21,7 @@ dependencies: - wire-api-federation - tagged - servant-client +- saml2-web-sso >=0.18 library: source-dirs: src @@ -69,7 +70,6 @@ library: - resourcet >=1.1 - retry >=0.5 - safe-exceptions >=0.1 - - saml2-web-sso >=0.18 - servant - servant-server - servant-swagger diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 330aa44c94..d0221f3206 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -71,12 +71,11 @@ import qualified Data.LegalHold as LH import qualified Data.List.Extra as List import Data.List1 (list1) import qualified Data.Map.Strict as M -import Data.Misc (HttpsUrl) +import Data.Misc (HttpsUrl, mkHttpsUrl) import Data.Qualified import Data.Range as Range import Data.Set (fromList) import qualified Data.Set as Set -import Data.String.Conversions (cs) import Data.Time.Clock (UTCTime (..), getCurrentTime) import qualified Data.UUID as UUID import qualified Data.UUID.Util as UUID @@ -429,7 +428,7 @@ getTeamMembersCSVH (zusr ::: tid ::: _) = do defaultEncodeOptions :: EncodeOptions defaultEncodeOptions = EncodeOptions - { encDelimiter = 44, -- comma + { encDelimiter = fromIntegral (ord ','), encUseCrLf = True, -- to be compatible with Mac and Windows encIncludeHeader = False, -- (so we can flush when the header is on the wire) encQuoting = QuoteAll @@ -476,7 +475,7 @@ getTeamMembersCSVH (zusr ::: tid ::: _) = do userToIdPIssuer :: U.User -> Maybe HttpsUrl userToIdPIssuer usr = case (U.userIdentity >=> U.ssoIdentity) usr of - Just (U.UserSSOId issuer _) -> fromByteString' $ cs issuer + Just (U.UserSSOId (SAML.UserRef issuer _)) -> either (const Nothing) Just . mkHttpsUrl $ issuer ^. SAML.fromIssuer Just _ -> Nothing Nothing -> Nothing @@ -489,7 +488,7 @@ getTeamMembersCSVH (zusr ::: tid ::: _) = do samlNamedId :: User -> Maybe Text samlNamedId = userSSOId >=> \case - (UserSSOId _idp nameId) -> CI.original . SAML.unsafeShowNameID <$> either (const Nothing) pure (SAML.decodeElem (cs nameId)) + (UserSSOId (SAML.UserRef _idp nameId)) -> Just . CI.original . SAML.unsafeShowNameID $ nameId (UserScimExternalId _) -> Nothing bulkGetTeamMembersH :: UserId ::: TeamId ::: Range 1 Public.HardTruncationLimit Int32 ::: JsonRequest Public.UserIdList ::: JSON -> Galley Response diff --git a/services/galley/test/integration/API/Teams.hs b/services/galley/test/integration/API/Teams.hs index 6b289d6561..ebc335b9fc 100644 --- a/services/galley/test/integration/API/Teams.hs +++ b/services/galley/test/integration/API/Teams.hs @@ -42,11 +42,10 @@ import Data.Id import qualified Data.LegalHold as LH import Data.List1 import qualified Data.List1 as List1 -import Data.Misc (HttpsUrl, PlainTextPassword (..)) +import Data.Misc (HttpsUrl, PlainTextPassword (..), mkHttpsUrl) import Data.Qualified import Data.Range import qualified Data.Set as Set -import Data.String.Conversions (cs) import qualified Data.Text as T import qualified Data.UUID as UUID import qualified Data.UUID.Util as UUID @@ -66,6 +65,7 @@ import qualified Network.Wai.Utilities.Error as Error import qualified Network.Wai.Utilities.Error as Wai import qualified Proto.TeamEvents as E import qualified Proto.TeamEvents_Fields as E +import qualified SAML2.WebSSO.Types as SAML import Test.Tasty import Test.Tasty.Cannon (TimeoutUnit (..), (#)) import qualified Test.Tasty.Cannon as WS @@ -277,7 +277,7 @@ testListTeamMembersCsv numMembers = do where userToIdPIssuer :: HasCallStack => U.User -> Maybe HttpsUrl userToIdPIssuer usr = case (U.userIdentity >=> U.ssoIdentity) usr of - Just (U.UserSSOId issuer _) -> maybe (error "shouldn't happen") Just . fromByteString' . cs $ issuer + Just (U.UserSSOId (SAML.UserRef (SAML.Issuer issuer) _)) -> either (const $ error "shouldn't happen") Just $ mkHttpsUrl issuer Just _ -> Nothing Nothing -> Nothing diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index aa6549d3eb..91f7f1d1a9 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -121,6 +121,7 @@ import qualified Wire.API.Message.Proto as Proto import Wire.API.Routes.MultiTablePaging import Wire.API.User.Client (ClientCapability (..), UserClientsFull (UserClientsFull)) import qualified Wire.API.User.Client as Client +import Wire.API.User.Identity (mkSimpleSampleUref) ------------------------------------------------------------------------------- -- API Operations @@ -396,7 +397,7 @@ addUserToTeamWithRole' role inviter tid = do addUserToTeamWithSSO :: HasCallStack => Bool -> TeamId -> TestM TeamMember addUserToTeamWithSSO hasEmail tid = do - let ssoid = UserSSOId "nil" "nil" + let ssoid = UserSSOId mkSimpleSampleUref user <- responseJsonError =<< postSSOUser "SSO User" hasEmail ssoid tid let uid = Brig.Types.userId user getTeamMember uid tid uid diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index fd11882478..cc0be602d7 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -51,7 +51,6 @@ import Data.ByteString.Conversion import Data.Handle (Handle (fromHandle)) import Data.Id (Id (Id), TeamId, UserId) import Data.Misc (PlainTextPassword) -import Data.String.Conversions import Imports import Network.HTTP.Types.Method import qualified Network.Wai.Utilities.Error as Wai @@ -65,11 +64,9 @@ import Wire.API.User.Scim (ValidExternalId (..), runValidExternalId) ---------------------------------------------------------------------- +-- | FUTUREWORK: this is redundantly defined in "Spar.Intra.BrigApp". veidToUserSSOId :: ValidExternalId -> UserSSOId -veidToUserSSOId = runValidExternalId urefToUserSSOId (UserScimExternalId . fromEmail) - -urefToUserSSOId :: SAML.UserRef -> UserSSOId -urefToUserSSOId (SAML.UserRef t s) = UserSSOId (cs $ SAML.encodeElem t) (cs $ SAML.encodeElem s) +veidToUserSSOId = runValidExternalId UserSSOId (UserScimExternalId . fromEmail) -- | Similar to 'Network.Wire.Client.API.Auth.tokenResponse', but easier: we just need to set the -- cookie in the response, and the redirect will make the client negotiate a fresh auth token. @@ -102,7 +99,7 @@ createBrigUserSAML uref (Id buid) teamid uname managedBy = do newUser = (emptyNewUser uname) { newUserUUID = Just buid, - newUserIdentity = Just (SSOIdentity (urefToUserSSOId uref) Nothing Nothing), + newUserIdentity = Just (SSOIdentity (UserSSOId uref) Nothing Nothing), newUserOrigin = Just (NewUserOriginTeamUser . NewTeamMemberSSO $ teamid), newUserManagedBy = Just managedBy } diff --git a/services/spar/src/Spar/Intra/BrigApp.hs b/services/spar/src/Spar/Intra/BrigApp.hs index 6c4d2d34f0..68777a7db0 100644 --- a/services/spar/src/Spar/Intra/BrigApp.hs +++ b/services/spar/src/Spar/Intra/BrigApp.hs @@ -27,10 +27,6 @@ module Spar.Intra.BrigApp veidFromUserSSOId, mkUserName, renderValidExternalId, - emailFromSAML, - emailToSAML, - emailToSAMLNameID, - emailFromSAMLNameID, HavePendingInvitations (..), getBrigUser, getBrigUserTeam, @@ -38,6 +34,12 @@ module Spar.Intra.BrigApp authorizeScimTokenManagement, parseResponse, giveDefaultHandle, + + -- * re-exports, mostly for historical reasons and lazyness + emailFromSAML, + emailToSAML, + emailToSAMLNameID, + emailFromSAMLNameID, ) where @@ -55,7 +57,6 @@ import Imports import Polysemy import Polysemy.Error import qualified SAML2.WebSSO as SAML -import qualified SAML2.WebSSO.Types.Email as SAMLEmail import Spar.Error import Spar.Sem.BrigAccess (BrigAccess) import qualified Spar.Sem.BrigAccess as BrigAccess @@ -66,23 +67,16 @@ import Wire.API.User.Scim (ValidExternalId (..), runValidExternalId) ---------------------------------------------------------------------- +-- | FUTUREWORK: this is redundantly defined in "Spar.Intra.Brig" veidToUserSSOId :: ValidExternalId -> UserSSOId -veidToUserSSOId = runValidExternalId urefToUserSSOId (UserScimExternalId . fromEmail) - -urefToUserSSOId :: SAML.UserRef -> UserSSOId -urefToUserSSOId (SAML.UserRef t s) = UserSSOId (cs $ SAML.encodeElem t) (cs $ SAML.encodeElem s) +veidToUserSSOId = runValidExternalId UserSSOId (UserScimExternalId . fromEmail) veidFromUserSSOId :: MonadError String m => UserSSOId -> m ValidExternalId veidFromUserSSOId = \case - UserSSOId tenant subject -> - case (SAML.decodeElem $ cs tenant, SAML.decodeElem $ cs subject) of - (Right t, Right s) -> do - let uref = SAML.UserRef t s - case urefToEmail uref of - Nothing -> pure $ UrefOnly uref - Just email -> pure $ EmailAndUref email uref - (Left msg, _) -> throwError msg - (_, Left msg) -> throwError msg + UserSSOId uref -> + case urefToEmail uref of + Nothing -> pure $ UrefOnly uref + Just email -> pure $ EmailAndUref email uref UserScimExternalId email -> maybe (throwError "externalId not an email and no issuer") @@ -125,22 +119,6 @@ mkUserName Nothing = renderValidExternalId :: ValidExternalId -> Maybe Text renderValidExternalId = runValidExternalId urefToExternalId (Just . fromEmail) -emailFromSAML :: HasCallStack => SAMLEmail.Email -> Email -emailFromSAML = fromJust . parseEmail . SAMLEmail.render - -emailToSAML :: HasCallStack => Email -> SAMLEmail.Email -emailToSAML = CI.original . fromRight (error "emailToSAML") . SAMLEmail.validate . toByteString - --- | FUTUREWORK(fisx): if saml2-web-sso exported the 'NameID' constructor, we could make this --- function total without all that praying and hoping. -emailToSAMLNameID :: HasCallStack => Email -> SAML.NameID -emailToSAMLNameID = fromRight (error "impossible") . SAML.emailNameID . fromEmail - -emailFromSAMLNameID :: HasCallStack => SAML.NameID -> Maybe Email -emailFromSAMLNameID nid = case nid ^. SAML.nameID of - SAML.UNameIDEmail email -> Just . emailFromSAML . CI.original $ email - _ -> Nothing - ---------------------------------------------------------------------- getBrigUser :: (HasCallStack, Member BrigAccess r) => HavePendingInvitations -> UserId -> Sem r (Maybe User) diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index 549a23c89d..6b1499dcc5 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -176,7 +176,7 @@ instance ScimTokenInfo -> Scim.User ST.SparTag -> Scim.ScimHandler (Sem r) (Scim.StoredUser ST.SparTag) - postUser tokinfo user = createValidScimUser tokinfo =<< validateScimUser tokinfo user + postUser tokinfo user = createValidScimUser tokinfo =<< validateScimUser "post" tokinfo user putUser :: ScimTokenInfo -> @@ -184,7 +184,7 @@ instance Scim.User ST.SparTag -> Scim.ScimHandler (Sem r) (Scim.StoredUser ST.SparTag) putUser tokinfo uid newScimUser = - updateValidScimUser tokinfo uid =<< validateScimUser tokinfo newScimUser + updateValidScimUser tokinfo uid =<< validateScimUser "put" tokinfo newScimUser deleteUser :: ScimTokenInfo -> UserId -> Scim.ScimHandler (Sem r) () deleteUser tokeninfo uid = @@ -204,14 +204,15 @@ validateScimUser :: forall m r. (m ~ Scim.ScimHandler (Sem r)) => Members '[Input Opts, IdPEffect.IdP] r => + Text -> -- | Used to decide what IdP to assign the user to ScimTokenInfo -> Scim.User ST.SparTag -> m ST.ValidScimUser -validateScimUser tokinfo user = do +validateScimUser errloc tokinfo user = do mIdpConfig <- tokenInfoToIdP tokinfo richInfoLimit <- lift $ inputs richInfoLimit - validateScimUser' mIdpConfig richInfoLimit user + validateScimUser' errloc mIdpConfig richInfoLimit user tokenInfoToIdP :: Member IdPEffect.IdP r => ScimTokenInfo -> Scim.ScimHandler (Sem r) (Maybe IdP) tokenInfoToIdP ScimTokenInfo {stiIdP} = do @@ -254,24 +255,26 @@ validateHandle txt = case parseHandle txt of validateScimUser' :: forall m. (MonadError Scim.ScimError m) => + -- | Error location (call site, for debugging) + Text -> -- | IdP that the resulting user will be assigned to Maybe IdP -> -- | Rich info limit Int -> Scim.User ST.SparTag -> m ST.ValidScimUser -validateScimUser' midp richInfoLimit user = do +validateScimUser' errloc midp richInfoLimit user = do unless (isNothing $ Scim.password user) $ throwError $ Scim.badRequest Scim.InvalidValue - (Just "Setting user passwords is not supported for security reasons.") + (Just $ "Setting user passwords is not supported for security reasons. (" <> errloc <> ")") veid <- mkValidExternalId midp (Scim.externalId user) handl <- validateHandle . Text.toLower . Scim.userName $ user -- FUTUREWORK: 'Scim.userName' should be case insensitive; then the toLower here would -- be a little less brittle. uname <- do - let err = throwError . Scim.badRequest Scim.InvalidValue . Just . cs + let err msg = throwError . Scim.badRequest Scim.InvalidValue . Just $ cs msg <> " (" <> errloc <> ")" either err pure $ Brig.mkUserName (Scim.displayName user) veid richInfo <- validateRichInfo (Scim.extra user ^. ST.sueRichInfo) let active = Scim.active user @@ -291,6 +294,9 @@ validateScimUser' midp richInfoLimit user = do <> show richInfoLimit <> " characters, but got " <> show sze + <> " (" + <> cs errloc + <> ")" ) ) { Scim.status = Scim.Status 413 @@ -311,7 +317,7 @@ mkValidExternalId _ Nothing = do throwError $ Scim.badRequest Scim.InvalidValue - (Just "externalId is required for SAML users") + (Just "externalId is required") mkValidExternalId Nothing (Just extid) = do let err = Scim.badRequest @@ -519,7 +525,7 @@ updateValidScimUser tokinfo@ScimTokenInfo {stiTeam} uid newValidScimUser = oldScimStoredUser :: Scim.StoredUser ST.SparTag <- Scim.getUser tokinfo uid oldValidScimUser :: ST.ValidScimUser <- - validateScimUser tokinfo . Scim.value . Scim.thing $ oldScimStoredUser + validateScimUser "recover-old-value" tokinfo . Scim.value . Scim.thing $ oldScimStoredUser -- assertions about new valid scim user that cannot be checked in 'validateScimUser' because -- they differ from the ones in 'createValidScimUser'. diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 24a339cc80..a73b15ed14 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -43,7 +43,6 @@ import qualified Data.Aeson as Aeson import Data.Aeson.Lens (key, _String) import Data.Aeson.QQ (aesonQQ) import Data.Aeson.Types (fromJSON, toJSON) -import qualified Data.Bifunctor as Bifunctor import Data.ByteString.Conversion import qualified Data.CaseInsensitive as CI import qualified Data.Csv as Csv @@ -68,7 +67,6 @@ import qualified Spar.Sem.SAMLUserStore as SAMLUserStore import qualified Spar.Sem.ScimExternalIdStore as ScimExternalIdStore import qualified Spar.Sem.ScimUserTimesStore as ScimUserTimesStore import qualified Text.XML.DSig as SAML -import qualified URI.ByteString as URI import Util import Util.Invitation (getInvitation, getInvitationCode, headInvitation404, registerInvitation) import qualified Web.Scim.Class.User as Scim.UserC @@ -234,8 +232,9 @@ testCsvData :: UserId -> Maybe Text {- externalId -} -> Maybe UserSSOId -> + Bool -> TestSpar () -testCsvData tid owner uid mbeid mbsaml = do +testCsvData tid owner uid mbeid mbsaml hasissuer = do usersInCsv <- do g <- view teGalley resp <- @@ -254,17 +253,15 @@ testCsvData tid owner uid mbeid mbsaml = do let haveIssuer :: Maybe HttpsUrl haveIssuer = case mbsaml of - Just (UserSSOId issuer _) -> - either (const Nothing) Just - . (mkHttpsUrl <=< Bifunctor.first show . (URI.parseURI URI.laxURIParserOptions)) - $ cs issuer + Just (UserSSOId (SAML.UserRef (SAML.Issuer issuer) _)) -> either (const Nothing) Just $ mkHttpsUrl issuer Just (UserScimExternalId _) -> Nothing Nothing -> Nothing + ('h', haveIssuer) `shouldSatisfy` bool isNothing isJust hasissuer . snd ('i', CsvExport.tExportIdpIssuer export) `shouldBe` ('i', haveIssuer) let haveSubject :: Text haveSubject = case mbsaml of - Just (UserSSOId _ subject) -> either (error . show) (CI.original . SAML.unsafeShowNameID) $ SAML.decodeElem (cs subject) + Just (UserSSOId (SAML.UserRef _ subject)) -> CI.original $ SAML.unsafeShowNameID subject Just (UserScimExternalId _) -> "" Nothing -> "" ('n', CsvExport.tExportSAMLNamedId export) `shouldBe` ('n', haveSubject) @@ -362,7 +359,7 @@ testCreateUserNoIdP = do -- csv download should work let eid = Scim.User.externalId scimUser sml = Nothing - in testCsvData tid owner userid eid sml + in testCsvData tid owner userid eid sml False -- members table contains an entry -- (this really shouldn't be tested here, but by the type system!) @@ -438,7 +435,7 @@ testCreateUserWithSamlIdP = do eid = Scim.User.externalId user sml :: HasCallStack => UserSSOId sml = fromJust $ userIdentity >=> ssoIdentity $ brigUser - in testCsvData tid owner uid eid (Just sml) + in testCsvData tid owner uid eid (Just sml) True -- members table contains an entry -- (this really shouldn't be tested here, but by the type system!) @@ -1341,9 +1338,7 @@ testBrigSideIsUpdated = do user' <- randomScimUser let userid = scimUserId storedUser _ <- updateUser tok userid user' - validScimUser <- - either (error . show) pure $ - validateScimUser' (Just idp) 999999 user' + validScimUser <- either (error . show) pure $ validateScimUser' "testBrigSideIsUpdated" (Just idp) 999999 user' brigUser <- maybe (error "no brig user") pure =<< runSpar (Intra.getBrigUser Intra.WithPendingInvitations userid) brigUser `userShouldMatch` validScimUser diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index bc20e47856..f51f878974 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -190,7 +190,7 @@ import qualified Text.XML as XML import qualified Text.XML.Cursor as XML import Text.XML.DSig (SignPrivCreds) import qualified Text.XML.DSig as SAML -import URI.ByteString +import URI.ByteString as URI import Util.Options import Util.Types import qualified Web.Cookie as Web @@ -201,6 +201,7 @@ import qualified Wire.API.Team.Feature as Public import qualified Wire.API.Team.Invitation as TeamInvitation import Wire.API.User (HandleUpdate (HandleUpdate), UserUpdate) import qualified Wire.API.User as User +import Wire.API.User.Identity (mkSampleUref) import Wire.API.User.IdentityProvider import Wire.API.User.Saml import Wire.API.User.Scim (runValidExternalId) @@ -463,7 +464,8 @@ createTeamMember :: m UserId createTeamMember brigreq galleyreq teamid perms = do let randomtxt = liftIO $ UUID.toText <$> UUID.nextRandom - randomssoid = Brig.UserSSOId <$> randomtxt <*> randomtxt + randomssoid = liftIO $ Brig.UserSSOId <$> (mkSampleUref <$> rnd <*> rnd) + rnd = cs . show <$> randomRIO (0 :: Integer, 10000000) name <- randomtxt ssoid <- randomssoid resp :: ResponseLBS <- diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index 71a698d0e1..689fe08076 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -622,4 +622,6 @@ userShouldMatch u1 u2 = liftIO $ do -- what we expect a user that comes back from spar to look like in terms of what it looked -- like when we sent it there. whatSparReturnsFor :: HasCallStack => IdP -> Int -> Scim.User.User SparTag -> Either String (Scim.User.User SparTag) -whatSparReturnsFor idp richInfoSizeLimit = either (Left . show) (Right . synthesizeScimUser) . validateScimUser' (Just idp) richInfoSizeLimit +whatSparReturnsFor idp richInfoSizeLimit = + either (Left . show) (Right . synthesizeScimUser) + . validateScimUser' "whatSparReturnsFor" (Just idp) richInfoSizeLimit diff --git a/services/spar/test/Test/Spar/Intra/BrigSpec.hs b/services/spar/test/Test/Spar/Intra/BrigSpec.hs index d37e98582f..d8f6e8edf5 100644 --- a/services/spar/test/Test/Spar/Intra/BrigSpec.hs +++ b/services/spar/test/Test/Spar/Intra/BrigSpec.hs @@ -50,10 +50,9 @@ spec = do ( either (error . show) id $ mkNameID (mkUNameIDTransient "V") (Just "kati") (Just "rolli") (Just "jaan") ) - want = - UserSSOId - "http://wire.com/" - "V" + want = UserSSOId (SAML.UserRef iss nam) + iss :: SAML.Issuer = fromRight undefined $ SAML.decodeElem "http://wire.com/" + nam :: SAML.NameID = fromRight undefined $ SAML.decodeElem "V" veidToUserSSOId have `shouldBe` want veidFromUserSSOId want `shouldBe` Right have it "another example" $ do @@ -64,10 +63,10 @@ spec = do ( either (error . show) id $ mkNameID (mkUNameIDPersistent "PWkS") (Just "hendrik") Nothing (Just "marye") ) - want = - UserSSOId - "http://wire.com/" - "PWkS" + want = UserSSOId (SAML.UserRef iss nam) + iss :: SAML.Issuer = fromRight undefined $ SAML.decodeElem "http://wire.com/" + nam :: SAML.NameID = fromRight undefined $ SAML.decodeElem "PWkS" + veidToUserSSOId have `shouldBe` want veidFromUserSSOId want `shouldBe` Right have