Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow SCIM without saml #1200

Merged
merged 35 commits into from
Sep 9, 2020
Merged

Allow SCIM without saml #1200

merged 35 commits into from
Sep 9, 2020

Conversation

fisx
Copy link
Contributor

@fisx fisx commented Sep 2, 2020

Fixes https://github.com/zinfra/backend-issues/issues/1365, https://github.com/zinfra/backend-issues/issues/1572

documentation

Before this PR, scim tokens could only be added to teams that already had exactly one SAML IdP. Now, we also allow SAML-less teams to have SCIM provisioning. This is an alternative to onboarding via team-settings and produces user accounts that are authenticated with email and password. (Phone may or may not work, but is not officially supported.)

The way this works is different from team-settings: we don't send invites, but we create active users immediately the moment the SCIM user post is processed. The new thing is that the created user has neither email nor phone nor a SAML identity, nor a password.

How does this work?

email: If no SAML IdP is present, SCIM user posts must contain an externalId that is an email address. This email address is not added to the newly created user, because it has not been validated. Instead, the flow for changing an email address is triggered in brig: an email is sent to the address containing a validation key, and once the user completes the flow, brig will add the email address to the user. We had to add very little code for this in this PR, it's all an old feature.

When SCIM user gets are processed, in order to reconstruct the externalId from the user spar is retrieving from brig, we introduce a new json object for the sso_id field that looks like this: {'scim_external_id': '[email protected]'}.

In order to find users that have email addresses pending validation, we introduce a new table in spar's cassandra called scim_external_ids, in analogy to user. We have tried to use brig's internal GET /i/user&email=..., but that also finds pending email addresses, and there are corner cases when changing email addresses and waiting for the new address to be validated and the old to be removed... that made this approach seem infeasible.

password: once the user has validated their email address, they need to trigger the "forgot password" flow -- also old code.

That's the gist of it!

@fisx fisx force-pushed the fisx/allow-scim-without-saml branch from 8c4172f to c3534c3 Compare September 2, 2020 21:46
@fisx fisx force-pushed the fisx/allow-scim-without-saml branch from c3534c3 to 8fd50df Compare September 3, 2020 12:05
@fisx fisx force-pushed the fisx/allow-scim-without-saml branch from 8fd50df to e4523d1 Compare September 3, 2020 15:25
@fisx fisx force-pushed the fisx/allow-scim-without-saml branch from f189c96 to f88559d Compare September 3, 2020 20:00
@fisx
Copy link
Contributor Author

fisx commented Sep 3, 2020

TODO:

  • /wire-server/libs/wire-api/src/Wire/API/User/Identity.hs:256:9: error: [-Wpartial-fields, -Werror=partial-fields] (and two more)
  • f88559d
  • any important test cases that I should add?

Copy link
Contributor Author

@fisx fisx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-read everything except Spar.Scim.User (which is admittedly one of the more important modules). Some comments below.

\ and sso_id, team_id must be either both present or both absent."
(_, _, _, Just _, Nothing) -> fail "sso_id, team_id must be either both present or both absent."
(_, _, _, Nothing, Just _) -> fail "sso_id, team_id must be either both present or both absent."
_ -> fail "team_code, team, invitation_code, sso_id, and the pair (sso_id, team_id) are mutually exclusive"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change just makes the error message more specific.

Text
-- An XML blob specifying the user's ID on the identity provider's side.
Text
| UserScimExternalId
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for the case when there is no idp, but externalId is an email address. We should probably store this as an Email, too, non-email externalIds without SAML IdP cannot work (how would the user authenticate?).

At least we could leave this comment in the code.

@@ -173,6 +175,10 @@ sitemap = do
.&. accept "application" "json"
.&. jsonRequest @UserSSOId

delete "/i/users/:uid/sso-id" (continue deleteSSOIdH) $
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're not using this after all, but I'm tempted to keep it.

@@ -1096,7 +1096,7 @@ instance ToJSON GetActivationCodeResp where
updateUserH :: UserId ::: ConnId ::: JsonRequest Public.UserUpdate -> Handler Response
updateUserH (uid ::: conn ::: req) = do
uu <- parseJsonBody req
lift $ API.updateUser uid conn uu
lift $ API.updateUser uid (Just conn) uu
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conn is the device that has triggered the event, and doesn't need to receive it. In the case of SCIM, this is Nothing.

@@ -238,7 +238,7 @@ updateEmail u e = retry x5 $ write userEmailUpdate (params Quorum (e, u))
updatePhone :: UserId -> Phone -> AppIO ()
updatePhone u p = retry x5 $ write userPhoneUpdate (params Quorum (p, u))

updateSSOId :: UserId -> UserSSOId -> AppIO Bool
updateSSOId :: UserId -> Maybe UserSSOId -> AppIO Bool
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this is how you delete cells from rows in cassandra? By just writing Nothing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If in doubt, this just for the internal end-point that we're not using any more. We may want to use it in the future, but that's not a strong reason to not remove it again, then this change here would not be necessary.

validateEmailIfExists :: UserId -> SAML.UserRef -> Spar ()
validateEmailIfExists uid (SAML.UserRef _ nameid) = case nameid ^. SAML.nameID of
UNameIDEmail email -> do
Intra.isEmailValidationEnabledUser uid >>= \case
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function has gone away, I forget why.

}
wrapMonadClient $ Data.insertScimToken token info
pure $ CreateScimTokenResponse token info

case idps of
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where all the changes in the PR begin.

pendingWith "or deactivated? we should decide what we want here."

describe "getBrigUser" $ do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these tests aren't really improving the coverage, but it seemed easier to debug stuff by focussing on this smaller test.

@@ -130,20 +196,20 @@ instance MonadSparToBrig m => MonadSparToBrig (ReaderT r m) where
createBrigUser ::
(HasCallStack, MonadSparToBrig m) =>
-- | SSO identity
SAML.UserRef ->
ValidExternalId ->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one of the more meaningful changes in the PR. Above are helpers inspired by this new type, below I mostly re-aligned functions, imrpoved error handling a little, and called better (more internal) end-points.

@fisx
Copy link
Contributor Author

fisx commented Sep 4, 2020

also, this just failed (non-deterministically?):


  test-integration/Test/Spar/Scim/UserSpec.hs:660:10: 
  1) Spar.Scim.User, GET /Users, doesn't list deleted users
       predicate failed on: User {userId = 7b85c17c-30c7-4499-a27e-7920c34b215a, userIdentity = Just (SSOIdentity (UserSSOId "<Issuer xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:samla=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns:samlm=\"urn:oasis:names:tc:SAML:2.0:metadata\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">https://issuer.net/_23776545-901f-48e8-a044-e2e40d21d77a</Issuer>" "<NameID xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" xmlns:samla=\"urn:oasis:names:tc:SAML:2.0:assertion\" xmlns:samlm=\"urn:oasis:names:tc:SAML:2.0:metadata\" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\" xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">[email protected]</NameID>") Nothing Nothing), userDisplayName = Name {fromName = "Scim User #3066607"}, userPict = Pict {fromPict = []}, userAssets = [], userAccentId = ColourId {fromColourId = 0}, userDeleted = False, userLocale = en, userService = Nothing, userHandle = Just (Handle {fromHandle = "scimuser_3066607"}), userExpire = Nothing, userTeam = Just b53ce0d8-9032-460c-b6ae-fb15a8a3436e, userManagedBy = ManagedByScim}

  To rerun use: --match "/Spar.Scim.User/GET /Users/doesn't list deleted users/"

Randomized with seed 469431066

Data.getSAMLUser
undefined -- could be @Data.lookupScimExternalId@, but we don't hit that path.
veid
Left _email -> undefined -- runSparCass . Data.lookupScimExternalId . fromEmail $ _email
Copy link
Contributor

@arianvp arianvp Sep 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this case undefined? Because it never happens? can we do an fromRight instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it never happens. it's perfectly reasonable to allow this case, and the comment two lines above says how it could be implemented, but i felt that it's better to not write dead code, but rather make it clear where we don't walk.

feel free to turn it into a fromRight if you think it's an improvement.

-- TODO: yes, we should just test for error labels consistently, i know...
const (Just "Setting user passwords is not supported for security reasons.") =~= responseBody

testCreateUserNoIdP :: TestSpar ()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description of this test says it checks tht the email validation link is sent; but i dont see this being tested here.
How are we making sure this codepath is indeed taken?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scrap that ;I can't read :)

aFewTimes (runSpar $ Intra.getBrigUser userid) isJust
>>= maybe (error "could not find user in brig") pure
brigUser `userShouldMatch` WrappedScimStoredUser scimStoredUser
liftIO $ userEmail brigUser `shouldBe` Nothing
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the email Nothing ? I would expect it to be the email with which the user is being registered through scim? Wasn't that the point of this PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nevermind scrap that; I see if comes later in the test

@arianvp
Copy link
Contributor

arianvp commented Sep 4, 2020

It would be nice if we could document the new behaviour in the scim documentation; and also update the PR description to explain what exactly this PR implements. Especially because we changed designs like 3 times.

From what I understand the gist is that we send email activation emails upon inviting the user through scim; and modified brig to make users not ephemeral.

@fisx fisx marked this pull request as ready for review September 4, 2020 18:34
@fisx
Copy link
Contributor Author

fisx commented Sep 4, 2020

It would be nice if we could document the new behaviour in the scim documentation; and also update the PR description to explain what exactly this PR implements. Especially because we changed designs like 3 times.

From what I understand the gist is that we send email activation emails upon inviting the user through scim; and modified brig to make users not ephemeral.

Yes. I added a few more ideas to the description of this PR. We should probably move them to the docs somewhere, but I don't know where exactly. Ah, wait, I know! I'll make another commit. :)


In order to find users that have email addresses pending validation, we introduce a new table in spar's cassandra called `scim_external_ids`, in analogy to `user`. We have tried to use brig's internal `GET /i/user&email=...`, but that also finds pending email addresses, and there are corner cases when changing email addresses and waiting for the new address to be validated and the old to be removed... that made this approach seem infeasible.

**password:** once the user has validated their email address, they need to trigger the "forgot password" flow -- also old code.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't sound great? Will the reset-password flow be triggered automatically in any way? Without it; the UX of this approach is not good and I foresee a lot of handholding needed

@arianvp
Copy link
Contributor

arianvp commented Sep 7, 2020

CI is currently failing because this PR uses Scim.util.deletUser which has a bug (it expects 200 whilst it should expect 204). Easy fix


**email:** If no SAML IdP is present, SCIM user posts must contain an externalId that is an email address. This email address is not added to the newly created user, because it has not been validated. Instead, the flow for changing an email address is triggered in brig: an email is sent to the address containing a validation key, and once the user completes the flow, brig will add the email address to the user. We had to add very little code for this in this PR, it's all an old feature.

When SCIM user gets are processed, in order to reconstruct the externalId from the user spar is retrieving from brig, we introduce a new json object for the `sso_id` field that looks like this: `{'scim_external_id': '[email protected]'}`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
When SCIM user gets are processed, in order to reconstruct the externalId from the user spar is retrieving from brig, we introduce a new json object for the `sso_id` field that looks like this: `{'scim_external_id': '[email protected]'}`.
When SCIM user GETs are processed, in order to reconstruct the externalId from the user spar is retrieving from brig, we introduce a new json object for the `sso_id` field that looks like this: `{'scim_external_id': '[email protected]'}`.

The comment was right.  I just think this helper function wasn't used
before
@arianvp arianvp self-requested a review September 9, 2020 11:24
Copy link
Contributor

@arianvp arianvp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved.

No nits on the code; but still a big question-mark on whether this is how we want to have this feature at all. I don't think it's very user-friendly and I do not think it's going to save us time in the long run. However, it will unblock us for now. Which is good. This should be discussed in a retro.

@arianvp arianvp merged commit ee47bbc into develop Sep 9, 2020
@arianvp arianvp deleted the fisx/allow-scim-without-saml branch September 9, 2020 11:57
@jschaul jschaul mentioned this pull request Oct 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants