-
Notifications
You must be signed in to change notification settings - Fork 325
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
Allow SCIM without saml #1200
Conversation
- use internal end-points - return 'UserAccount' (contains status) instead of 'User'. - more transparent error handling.
8c4172f
to
c3534c3
Compare
c3534c3
to
8fd50df
Compare
8fd50df
to
e4523d1
Compare
f189c96
to
f88559d
Compare
TODO:
|
Also remove two deprecated combinators from Spar.Intra.Brig.
There was a problem hiding this 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" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) $ |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 -> |
There was a problem hiding this comment.
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.
also, this just failed (non-deterministically?):
|
Data.getSAMLUser | ||
undefined -- could be @Data.lookupScimExternalId@, but we don't hit that path. | ||
veid | ||
Left _email -> undefined -- runSparCass . Data.lookupScimExternalId . fromEmail $ _email |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 () |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
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. |
There was a problem hiding this comment.
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
CI is currently failing because this PR uses |
1cf250c
to
344080a
Compare
|
||
**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]'}`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this 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.
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 touser
. We have tried to use brig's internalGET /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!