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

[WPB-14310] adjust existing onboarding flow to new domain registration constraints. #4409

Merged
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
d1becb0
Modify POST /teams/{id}/invitations to check for domain configuration…
fisx Jan 10, 2025
15655ba
double-WIP. (up next: write a failing test *in wire-subsystems*)
fisx Jan 10, 2025
cad63d1
make code compile
battermann Jan 10, 2025
364bf2b
handle (non-)existingi user flow
battermann Jan 13, 2025
65eb32a
add guard to invitation handler
battermann Jan 13, 2025
68701c0
domain registration store in mem interpreter
battermann Jan 13, 2025
b6febdd
wip
fisx Jan 13, 2025
8cba788
make tests compile and pass
battermann Jan 13, 2025
4b327fb
convert to property test
battermann Jan 13, 2025
2506d77
fix test
battermann Jan 13, 2025
f285fe0
nit-pick.
fisx Jan 14, 2025
386fc7c
unit test module for TeamInvitationSubsystem.
fisx Jan 14, 2025
c272f2e
Unit test for this PR.
fisx Jan 14, 2025
da72881
Enumerate unimplemented actions in miniGalleyAPIAccess interpreter.
fisx Jan 14, 2025
c13f7f8
prop test: wip
battermann Jan 14, 2025
764fbdc
team invitation test passes
battermann Jan 14, 2025
2d5c472
fix flaky dotless email arbitrary instance
battermann Jan 14, 2025
2f71628
Add guard for /register to EnterpriseLoginSubsystem.
fisx Jan 14, 2025
e0b0af5
[experiment / wip] make guard functions return a result value...
fisx Jan 15, 2025
e11f35f
Revert "[experiment / wip] make guard functions return a result value…
fisx Jan 15, 2025
5fb6da5
Fix linting issue
supersven Jan 17, 2025
48795a5
WIP: Test registration backend redirect
supersven Jan 17, 2025
ccce7b2
Fix imports.
fisx Jan 17, 2025
bde5381
WIP: Save for Matthias
supersven Jan 17, 2025
970781e
Revert "WIP: Save for Matthias"
fisx Jan 17, 2025
5e3d1be
More structure for guard errors.
fisx Jan 17, 2025
977a438
Arbitrary: use instance for Domain in instance for EmailAddress.
fisx Jan 17, 2025
6a2c1de
Call register guard in brig. (test still missing)
fisx Jan 17, 2025
ae12d26
Changelog.
fisx Jan 17, 2025
0123544
Remove dangling hspec.focus.
fisx Jan 17, 2025
139731c
make sanitize-pr
fisx Jan 17, 2025
5455369
nit-pick
fisx Jan 17, 2025
ef4704d
Fix imports.
fisx Jan 20, 2025
23d3442
Better name for mock interpreter.
fisx Jan 20, 2025
1a2913c
whitespace.
fisx Jan 20, 2025
3b39ebb
Polish error codes / messages.
fisx Jan 20, 2025
8b4cb95
orthography
fisx Jan 21, 2025
da10835
refactor.
fisx Jan 21, 2025
d6d1a72
Merge remote-tracking branch 'refs/remotes/origin/WPB-14310-make-exis…
fisx Jan 21, 2025
b70855d
refactor tests.
fisx Jan 21, 2025
958994c
revert brainfart.
fisx Jan 21, 2025
ebc997e
Better mock interpreter errors.
fisx Jan 21, 2025
10e7aa3
Remove unhelpful and misleading hints; better names.
fisx Jan 21, 2025
ad7add4
Unit tests: throw any error from underlying effect, not some.
fisx Jan 21, 2025
c046d25
Add link to confluence in comment.
fisx Jan 21, 2025
1ce524b
Commit missing hs file.
fisx Jan 21, 2025
c0f8e96
pffffff.
fisx Jan 21, 2025
cbe42b5
pffffffffffffffffffffffffffffffffff.
fisx Jan 21, 2025
3b27fcb
fix broken link.
fisx Jan 21, 2025
0266ee6
Merge remote-tracking branch 'origin/develop' into WPB-14310-make-exi…
fisx Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Adjust existing onboarding flow to new domain registration constraints.

end-points:
fisx marked this conversation as resolved.
Show resolved Hide resolved

- POST /teams/{id}/invitations
- POST /register
24 changes: 22 additions & 2 deletions libs/wire-api/src/Wire/API/EnterpriseLogin.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import Data.Text.Ascii (Ascii, AsciiText (toText))
import Data.Text.Ascii qualified as Ascii
import Imports
import SAML2.WebSSO qualified as SAML
import SAML2.WebSSO.Test.Arbitrary ()
import Test.QuickCheck (suchThat)
import Wire.Arbitrary

data DomainRedirect
= None
Expand All @@ -24,7 +27,8 @@ data DomainRedirect
| Backend HttpsUrl
| NoRegistration
| PreAuthorized
deriving stock (Eq, Show)
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via GenericUniform DomainRedirect

makePrisms ''DomainRedirect

Expand Down Expand Up @@ -97,7 +101,8 @@ data TeamInvite
= Allowed
| NotAllowed
| Team TeamId
deriving stock (Eq, Show)
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via GenericUniform TeamInvite

makePrisms ''TeamInvite

Expand Down Expand Up @@ -162,6 +167,21 @@ data DomainRegistrationUpdate = DomainRegistrationUpdate
deriving stock (Eq, Show)
deriving (ToJSON, FromJSON, OpenApi.ToSchema) via Schema DomainRegistrationUpdate

instance Arbitrary DomainRegistrationUpdate where
arbitrary = do
( DomainRegistrationUpdate
<$> arbitrary
<*> arbitrary
)
`suchThat` validate
where
validate :: DomainRegistrationUpdate -> Bool
validate dr =
case dr.domainRedirect of
Locked -> dr.teamInvite == Allowed
Backend _ -> dr.teamInvite == NotAllowed
_ -> True

instance ToSchema DomainRegistrationUpdate where
schema =
object "DomainRegistrationUpdate" $
Expand Down
12 changes: 4 additions & 8 deletions libs/wire-api/src/Wire/API/User/EmailAddress.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ where
import Cassandra.CQL qualified as C
import Data.ByteString.Conversion hiding (toByteString)
import Data.Data (Proxy (..))
import Data.Domain
import Data.OpenApi hiding (Schema, ToSchema)
import Data.Schema
import Data.Text hiding (null)
import Data.Text qualified as T
import Data.Text.Encoding
import Data.Text.Encoding.Error
import Deriving.Aeson
Expand Down Expand Up @@ -93,8 +95,8 @@ emailAddressText = emailAddress . encodeUtf8
arbitraryValidMail :: Gen EmailAddress
arbitraryValidMail = do
loc <- arbitrary `suchThat` isValidLoc
dom <- arbitrary `suchThat` isValidDom
pure . fromJust $ emailAddress (fromString $ loc <> "@" <> dom)
Domain dom <- arbitrary
pure . fromJust $ emailAddress (fromString $ loc <> "@" <> T.unpack dom)
where
notAt :: String -> Bool
notAt = notElem '@'
Expand All @@ -107,12 +109,6 @@ arbitraryValidMail = do
&& notAt x
&& isValid (fromString (x <> "@mail.com"))

isValidDom :: String -> Bool
isValidDom x =
notNull x
&& notAt x
&& isValid (fromString ("me@" <> x))

-- | FUTUREWORK(fisx): if saml2-web-sso exported the 'NameID' constructor, we could make this
-- function total without all that praying and hoping.
emailToSAMLNameID :: EmailAddress -> Either String SAML.NameID
Expand Down
3 changes: 3 additions & 0 deletions libs/wire-subsystems/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
, currency-codes
, data-default
, data-timeout
, email-validate
, errors
, exceptions
, extended
Expand Down Expand Up @@ -117,6 +118,7 @@ mkDerivation {
currency-codes
data-default
data-timeout
email-validate
errors
exceptions
extended
Expand Down Expand Up @@ -206,6 +208,7 @@ mkDerivation {
tinylog
transformers
types-common
uuid
wire-api
wire-api-federation
];
Expand Down
19 changes: 15 additions & 4 deletions libs/wire-subsystems/src/Wire/DomainRegistrationStore.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

module Wire.DomainRegistrationStore where

import Data.Domain
import Amazonka.Request (delete)
import Data.Domain as Domain
import Data.Id
import Data.Misc
import Data.Text as T
import Database.CQL.Protocol (Record (..), TupleType, recordInstance)
import Imports
import Polysemy
Expand All @@ -25,8 +27,17 @@ data StoredDomainRegistration = StoredDomainRegistration
recordInstance ''StoredDomainRegistration

data DomainRegistrationStore m a where
Upsert :: StoredDomainRegistration -> DomainRegistrationStore m ()
Lookup :: Domain -> DomainRegistrationStore m (Maybe StoredDomainRegistration)
Delete :: Domain -> DomainRegistrationStore m ()
UpsertInternal :: StoredDomainRegistration -> DomainRegistrationStore m ()
LookupInternal :: Domain -> DomainRegistrationStore m (Maybe StoredDomainRegistration)
DeleteInternal :: Domain -> DomainRegistrationStore m ()
fisx marked this conversation as resolved.
Show resolved Hide resolved

makeSem ''DomainRegistrationStore

upsert :: (Member DomainRegistrationStore r) => StoredDomainRegistration -> Sem r ()
upsert storedDomainReg = upsertInternal $ storedDomainReg {Wire.DomainRegistrationStore.domain = storedDomainReg.domain & (Domain . T.toLower . domainText)}
fisx marked this conversation as resolved.
Show resolved Hide resolved

lookup :: (Member DomainRegistrationStore r) => Domain -> Sem r (Maybe StoredDomainRegistration)
lookup domain = lookupInternal $ Domain . T.toLower . domainText $ domain
fisx marked this conversation as resolved.
Show resolved Hide resolved

delete :: (Member DomainRegistrationStore r) => Domain -> Sem r ()
delete domain = deleteInternal $ Domain . T.toLower . domainText $ domain
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ interpretDomainRegistrationStoreToCassandra ::
interpretDomainRegistrationStoreToCassandra casClient =
interpret $
embed @IO . runClient casClient . \case
Upsert dr -> upsert dr
Lookup domain -> lookup domain
Delete domain -> delete domain
UpsertInternal dr -> upsert dr
LookupInternal domain -> lookup domain
DeleteInternal domain -> delete domain

upsert :: (MonadClient m) => StoredDomainRegistration -> m ()
upsert dr = retry x5 $ write cqlUpsert (params LocalQuorum (asTuple dr))
Expand Down
10 changes: 10 additions & 0 deletions libs/wire-subsystems/src/Wire/EnterpriseLoginSubsystem.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
module Wire.EnterpriseLoginSubsystem where

import Data.Domain
import Data.Id
import Imports
import Polysemy
import Text.Email.Parser
import Wire.API.EnterpriseLogin
import Wire.Arbitrary

data InvitationFlow = ExistingUser | NewUser
deriving (Show, Eq, Generic)
deriving (Arbitrary) via GenericUniform InvitationFlow

data EnterpriseLoginSubsystem m a where
LockDomain :: Domain -> EnterpriseLoginSubsystem m ()
Expand All @@ -14,5 +22,7 @@ data EnterpriseLoginSubsystem m a where
UpdateDomainRegistration :: Domain -> DomainRegistrationUpdate -> EnterpriseLoginSubsystem m ()
DeleteDomain :: Domain -> EnterpriseLoginSubsystem m ()
GetDomainRegistration :: Domain -> EnterpriseLoginSubsystem m DomainRegistration
GuardEmailDomainRegistrationTeamInvitation :: InvitationFlow -> TeamId -> EmailAddress -> EnterpriseLoginSubsystem m ()
GuardEmailDomainRegistrationRegister :: EmailAddress -> EnterpriseLoginSubsystem m ()

makeSem ''EnterpriseLoginSubsystem
26 changes: 26 additions & 0 deletions libs/wire-subsystems/src/Wire/EnterpriseLoginSubsystem/Error.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Wire.EnterpriseLoginSubsystem.Error where

import Data.Text.Lazy qualified as LT
import Imports
import Network.HTTP.Types
import Network.Wai.Utilities qualified as Wai
Expand All @@ -12,10 +13,20 @@ data EnterpriseLoginSubsystemError
| EnterpriseLoginSubsystemUnlockError
| EnterpriseLoginSubsystemUnAuthorizeError
| EnterpriseLoginSubsystemPreAuthorizeError
| EnterpriseLoginSubsystemGuardFailed GuardFailure
deriving (Show, Eq)

instance Exception EnterpriseLoginSubsystemError

data GuardFailure
= DomRedirSetToSSO
| DomRedirSetToBackend
| DomRedirSetToNoRegistration
| TeamInviteSetToNotAllowed
| TeamInviteRestrictedToOtherTeam
| InvalidDomain String
deriving (Show, Eq)

enterpriseLoginSubsystemErrorToHttpError :: EnterpriseLoginSubsystemError -> HttpError
enterpriseLoginSubsystemErrorToHttpError =
StdError . \case
Expand All @@ -25,3 +36,18 @@ enterpriseLoginSubsystemErrorToHttpError =
EnterpriseLoginSubsystemUnlockError -> Wai.mkError status409 "unlock-error" "Domain can only be unlocked from a locked state"
EnterpriseLoginSubsystemUnAuthorizeError -> Wai.mkError status409 "unauthorize-error" "Domain redirect can not bet set to unauthorized when locked or SSO"
EnterpriseLoginSubsystemPreAuthorizeError -> Wai.mkError status409 "preauthorize-error" "Domain redirect must be 'none' to be pre-authorized"
EnterpriseLoginSubsystemGuardFailed err ->
-- TODO: maybe we can show these wai errors to client devs and see if they have change
-- requests? also, are we leaking too much info?
let e409 msg = Wai.mkError status409 "enterprise-login-guard-failed" ("condition failed: " <> msg)
e423 msg = Wai.mkError status423 "enterprise-login-guard-failed" ("could not parse domain: " <> LT.pack msg)
in case err of
DomRedirSetToSSO -> e409 "`domain_redirect` is set to `sso:{code}`"
DomRedirSetToBackend -> e409 "`domain_redirect` is set to `backend`" -- TODO: https://wearezeta.atlassian.net/wiki/spaces/ENGINEERIN/pages/1570832467/Email+domain+registration+and+configuration says this should be 403 with another label?
DomRedirSetToNoRegistration -> e409 "`domain_redirect` is set to `no-registration`"
TeamInviteSetToNotAllowed -> e409 "`teamInvite` is set to `not-allowed`"
TeamInviteRestrictedToOtherTeam -> e409 "`teamInvite` is restricted to another team."
InvalidDomain msg -> e423 msg
where
status423 :: Status
status423 = mkStatus 423 "Locked"
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ where
import Data.Aeson qualified as Aeson
import Data.Aeson.Encode.Pretty qualified as Aeson
import Data.ByteString.Conversion (toByteString')
import Data.Domain (Domain, domainText)
import Data.Domain (Domain, domainText, mkDomain)
import Data.Id
import Data.Misc (HttpsUrl (..))
import Data.Text.Encoding as T
import Data.Text.Internal.Builder (fromLazyText, fromText, toLazyText)
import Data.Text.Lazy.Builder (Builder)
import Data.Text.Lazy.Encoding (decodeUtf8, encodeUtf8)
import Data.Text.Lazy.Encoding as LT
import Imports hiding (lookup)
import Network.Mail.Mime (Address (Address), Mail (mailHeaders, mailParts, mailTo), emptyMail, plainPart)
import Polysemy
Expand All @@ -26,12 +27,13 @@ import Polysemy.TinyLog (TinyLog)
import Polysemy.TinyLog qualified as Log
import SAML2.WebSSO qualified as SAML
import System.Logger.Message qualified as Log
import Text.Email.Parser qualified as Email
import Wire.API.EnterpriseLogin
import Wire.API.User.EmailAddress (EmailAddress, fromEmail)
import Wire.DomainRegistrationStore
import Wire.EmailSending (EmailSending, sendMail)
import Wire.EnterpriseLoginSubsystem
import Wire.EnterpriseLoginSubsystem.Error
import Wire.EnterpriseLoginSubsystem.Error as Error

data EnterpriseLoginSubsystemConfig = EnterpriseLoginSubsystemConfig
{ auditEmailSender :: EmailAddress,
Expand All @@ -56,6 +58,8 @@ runEnterpriseLoginSubsystem = interpret $
UpdateDomainRegistration domain update -> updateDomainRegistrationImpl domain update
DeleteDomain domain -> deleteDomainImpl domain
GetDomainRegistration domain -> getDomainRegistrationImpl domain
GuardEmailDomainRegistrationTeamInvitation flow tid email -> guardEmailDomainRegistrationTeamInvitationImpl flow tid email
GuardEmailDomainRegistrationRegister email -> guardEmailDomainRegistrationRegisterImpl email

deleteDomainImpl ::
( Member DomainRegistrationStore r,
Expand Down Expand Up @@ -328,15 +332,90 @@ sendAuditMail url subject mBefore mAfter = do
toLazyText $
url
<> " called;\nOld value:\n"
<> fromLazyText (decodeUtf8 (maybe "null" Aeson.encodePretty mBefore))
<> fromLazyText (LT.decodeUtf8 (maybe "null" Aeson.encodePretty mBefore))
<> "\nNew value:\n"
<> fromLazyText (decodeUtf8 (maybe "null" Aeson.encodePretty mAfter))
<> fromLazyText (LT.decodeUtf8 (maybe "null" Aeson.encodePretty mAfter))
Log.info $
Log.msg (Log.val "Domain registration audit log")
. Log.field "url" (encodeUtf8 $ toLazyText url)
. Log.field "url" (LT.encodeUtf8 $ toLazyText url)
. Log.field "old_value" (maybe "null" Aeson.encode mBefore)
. Log.field "new_value" (maybe "null" Aeson.encode mAfter)
mConfig <- input
for_ mConfig $ \config -> do
let mail = mkAuditMail (config.auditEmailSender) (config.auditEmailRecipient) subject auditLog
sendMail mail

-- More info on the behavioral implications of domain registration records:
-- https://wearezeta.atlassian.net/wiki/spaces/ENGINEERIN/pages/1570832467/Email+domain+registration+and+configuration#Configuration-values
emailToDomainRegistration ::
forall r.
( Member DomainRegistrationStore r,
Member (Error EnterpriseLoginSubsystemError) r,
Member TinyLog r
) =>
EmailAddress ->
Sem r (Maybe DomainRegistration)
emailToDomainRegistration email = case mkDomain $ T.decodeUtf8 $ Email.domainPart email of
Right dom -> tryGetDomainRegistrationImpl dom
Left msg ->
-- The EmailAddress parser and servant *should* make this impossible, but they use
-- different parsers, one of us is ours and may change any time, so who knows?
throw . EnterpriseLoginSubsystemGuardFailed $ InvalidDomain msg

guardEmailDomainRegistrationTeamInvitationImpl ::
forall r.
( Member DomainRegistrationStore r,
Member (Error EnterpriseLoginSubsystemError) r,
Member TinyLog r
) =>
InvitationFlow ->
TeamId ->
EmailAddress ->
Sem r ()
guardEmailDomainRegistrationTeamInvitationImpl invitationFlow tid email = do
mReg <- emailToDomainRegistration email
for_ mReg $ \reg -> do
-- fail if domain-redirect is set to no-registration, or
case reg.domainRedirect of
None -> ok
Locked -> ok
SSO _ -> ok
Backend _ -> ok
NoRegistration -> case invitationFlow of
ExistingUser -> nope DomRedirSetToNoRegistration
NewUser -> ok
fisx marked this conversation as resolved.
Show resolved Hide resolved
PreAuthorized -> ok
-- team-invitation is set to not-allowed or team:{team id} for any team ID that is not
-- the team of the inviter
case reg.teamInvite of
Allowed -> ok
NotAllowed -> nope TeamInviteSetToNotAllowed
Team allowedTid ->
if allowedTid == tid
then ok
else nope TeamInviteRestrictedToOtherTeam
where
ok = pure ()
nope = throw . EnterpriseLoginSubsystemGuardFailed

guardEmailDomainRegistrationRegisterImpl ::
forall r.
( Member DomainRegistrationStore r,
Member (Error EnterpriseLoginSubsystemError) r,
Member TinyLog r
) =>
EmailAddress ->
Sem r ()
guardEmailDomainRegistrationRegisterImpl email = do
mReg <- emailToDomainRegistration email
for_ mReg $ \reg -> do
case reg.domainRedirect of
None -> ok
Locked -> ok
SSO _ -> nope DomRedirSetToSSO
Backend _ -> nope DomRedirSetToBackend
NoRegistration -> nope DomRedirSetToNoRegistration
PreAuthorized -> ok
where
ok = pure ()
nope = throw . EnterpriseLoginSubsystemGuardFailed
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ data TeamInvitationSubsystemError
| TooManyTeamInvitations
| TeamInvitationBlacklistedEmail
| TeamInvitationEmailTaken
deriving (Show)
deriving (Eq, Show)

instance Exception TeamInvitationSubsystemError

teamInvitationErrorToHttpError :: TeamInvitationSubsystemError -> HttpError
teamInvitationErrorToHttpError =
Expand Down
Loading
Loading