Skip to content

Commit b480a34

Browse files
supersvenfisx
andauthored
Disallow email change to blocked domain (#4624)
This changes the rules for blocked email domains: - Users cannot activate their accounts with a blocked email (this stays the same) - Users with blocked domains cannot be invited to teams - Users cannot change their email address to one of a blocked domain The pre-condition to this commit is that legitimate domains are not part of the blocked domains set anymore. In the past, legitimate domains were blocked to force users to be invited to teams. This should now be handled by the new enterprise login feature(s). --------- Co-authored-by: Matthias Fischmann <[email protected]>
1 parent 42cdfb6 commit b480a34

File tree

25 files changed

+561
-179
lines changed

25 files changed

+561
-179
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
The blocked domains feature
2+
(`optSettings.setCustomerExtensions.domainsBlockedForRegistration`) is now
3+
stricter: It is not only forbidden to register users with these domains in
4+
their email addresses, but also to change a user's email address to one of
5+
these domains.
6+
7+
This affects the endpoints:
8+
- `/register` (as before)
9+
- `/activate/send`
10+
- `/users/{uid}/email`
11+
- `/i/self/email` (internal endpoint)
12+
- `/access/self/email`
13+
- `/i/teams/{tid}/invitations` (internal endpoint)
14+
- `/teams/{tid}/invitations`

hls.json

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
{
2+
"cabalFormattingProvider": "cabal-gild",
3+
"checkParents": "CheckOnSave",
4+
"checkProject": true,
5+
"formattingProvider": "ormolu",
6+
"maxCompletions": 40,
7+
"plugin": {
8+
"alternateNumberFormat": {
9+
"globalOn": true
10+
},
11+
"cabal": {
12+
"codeActionsOn": true,
13+
"completionOn": true,
14+
"diagnosticsOn": true,
15+
"hoverOn": true,
16+
"symbolsOn": true
17+
},
18+
"cabal-fmt": {
19+
"config": {
20+
"path": "cabal-fmt"
21+
}
22+
},
23+
"cabal-gild": {
24+
"config": {
25+
"path": "cabal-gild"
26+
}
27+
},
28+
"cabalHaskellIntegration": {
29+
"globalOn": true
30+
},
31+
"callHierarchy": {
32+
"globalOn": true
33+
},
34+
"changeTypeSignature": {
35+
"globalOn": true
36+
},
37+
"class": {
38+
"codeActionsOn": true,
39+
"codeLensOn": true
40+
},
41+
"eval": {
42+
"config": {
43+
"diff": true,
44+
"exception": false
45+
},
46+
"globalOn": true
47+
},
48+
"explicit-fields": {
49+
"codeActionsOn": true,
50+
"inlayHintsOn": true
51+
},
52+
"explicit-fixity": {
53+
"globalOn": true
54+
},
55+
"fourmolu": {
56+
"config": {
57+
"external": false,
58+
"path": "fourmolu"
59+
}
60+
},
61+
"gadt": {
62+
"globalOn": true
63+
},
64+
"ghcide-code-actions-bindings": {
65+
"globalOn": true
66+
},
67+
"ghcide-code-actions-fill-holes": {
68+
"globalOn": true
69+
},
70+
"ghcide-code-actions-imports-exports": {
71+
"globalOn": true
72+
},
73+
"ghcide-code-actions-type-signatures": {
74+
"globalOn": true
75+
},
76+
"ghcide-completions": {
77+
"config": {
78+
"autoExtendOn": true,
79+
"snippetsOn": true
80+
},
81+
"globalOn": true
82+
},
83+
"ghcide-hover-and-symbols": {
84+
"hoverOn": true,
85+
"symbolsOn": true
86+
},
87+
"ghcide-type-lenses": {
88+
"config": {
89+
"mode": "always"
90+
},
91+
"globalOn": true
92+
},
93+
"hlint": {
94+
"codeActionsOn": true,
95+
"config": {
96+
"flags": []
97+
},
98+
"diagnosticsOn": true
99+
},
100+
"importLens": {
101+
"codeActionsOn": true,
102+
"codeLensOn": false,
103+
"inlayHintsOn": true
104+
},
105+
"moduleName": {
106+
"globalOn": true
107+
},
108+
"ormolu": {
109+
"config": {
110+
"external": false
111+
}
112+
},
113+
"overloaded-record-dot": {
114+
"globalOn": true
115+
},
116+
"pragmas-completion": {
117+
"globalOn": true
118+
},
119+
"pragmas-disable": {
120+
"globalOn": true
121+
},
122+
"pragmas-suggest": {
123+
"globalOn": true
124+
},
125+
"qualifyImportedNames": {
126+
"globalOn": true
127+
},
128+
"rename": {
129+
"config": {
130+
"crossModule": false
131+
},
132+
"globalOn": true
133+
},
134+
"retrie": {
135+
"globalOn": true
136+
},
137+
"semanticTokens": {
138+
"config": {
139+
"classMethodToken": "method",
140+
"classToken": "class",
141+
"dataConstructorToken": "enumMember",
142+
"functionToken": "function",
143+
"moduleToken": "namespace",
144+
"operatorToken": "operator",
145+
"patternSynonymToken": "macro",
146+
"recordFieldToken": "property",
147+
"typeConstructorToken": "enum",
148+
"typeFamilyToken": "interface",
149+
"typeSynonymToken": "type",
150+
"typeVariableToken": "typeParameter",
151+
"variableToken": "variable"
152+
},
153+
"globalOn": true
154+
},
155+
"splice": {
156+
"globalOn": true
157+
},
158+
"stan": {
159+
"globalOn": false
160+
}
161+
},
162+
"sessionLoading": "multipleComponents"
163+
}

integration/integration.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ library
112112
Test.AssetUpload
113113
Test.Auth
114114
Test.B2B
115+
Test.BlockedDomains
115116
Test.Bot
116117
Test.Brig
117118
Test.Cargohold.API

integration/test/API/Brig.hs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -422,12 +422,21 @@ putSelfLocale caller locale = do
422422
--
423423
-- NOTE: the full process of changing (and confirming) the email address is more complicated.
424424
-- see /services/brig/test/integration for details.
425-
putSelfEmail :: (HasCallStack, MakesValue caller) => caller -> String -> App Response
426-
putSelfEmail caller emailAddress = do
427-
callerid <- asString $ caller %. "id"
428-
req <- baseRequest caller Brig Versioned $ joinHttpPath ["users", callerid, "email"]
425+
putUserEmail :: (HasCallStack, MakesValue caller, MakesValue target) => caller -> target -> String -> App Response
426+
putUserEmail caller target emailAddress = do
427+
uid <- asString $ target %. "id"
428+
req <- baseRequest caller Brig Versioned $ joinHttpPath ["users", uid, "email"]
429429
submit "PUT" $ req & addJSONObject ["email" .= emailAddress]
430430

431+
putSelfEmail :: (HasCallStack, MakesValue user) => user -> String -> String -> String -> App Response
432+
putSelfEmail user cookie token emailAddress = do
433+
req <- baseRequest user Brig Versioned "/access/self/email"
434+
submit "PUT" $
435+
req
436+
& setCookie cookie
437+
& addHeader "Authorization" ("Bearer " <> token)
438+
& addJSONObject ["email" .= emailAddress]
439+
431440
-- | https://staging-nginz-https.zinfra.io/v6/api/swagger-ui/#/default/delete_self_email
432441
deleteSelfEmail :: (HasCallStack, MakesValue caller) => caller -> App Response
433442
deleteSelfEmail caller = do
@@ -882,6 +891,11 @@ activate domain key code = do
882891
req
883892
& addQueryParams [("key", key), ("code", code)]
884893

894+
activateSend :: (HasCallStack, MakesValue domain) => domain -> String -> Maybe String -> App Response
895+
activateSend domain email locale = do
896+
req <- rawBaseRequest domain Brig Versioned $ joinHttpPath ["activate", "send"]
897+
submit "POST" $ req & addJSONObject (["email" .= email] <> maybeToList (((.=) "locale") <$> locale))
898+
885899
acceptTeamInvitation :: (HasCallStack, MakesValue user) => user -> String -> Maybe String -> App Response
886900
acceptTeamInvitation user code mPw = do
887901
req <- baseRequest user Brig Versioned $ joinHttpPath ["teams", "invitations", "accept"]
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
module Test.BlockedDomains where
2+
3+
import API.Brig as Brig
4+
import API.Brig as BrigAddUser (AddUser (..))
5+
import API.Common
6+
import Data.Yaml (array)
7+
import SetupHelpers
8+
import Testlib.Prelude
9+
10+
testCannotSendActivationCodeToBlockedDomain :: (HasCallStack) => App ()
11+
testCannotSendActivationCodeToBlockedDomain = do
12+
let blockedDomain = "blocked.example.com"
13+
validDomain = "valid.example.com"
14+
withModifiedBackend
15+
def
16+
{ brigCfg =
17+
setField
18+
"optSettings.setCustomerExtensions.domainsBlockedForRegistration"
19+
(array [fromString blockedDomain])
20+
}
21+
$ \domain -> do
22+
username <- randomName
23+
let validEmail = username <> "@" <> validDomain
24+
addUser domain def {BrigAddUser.email = Just validEmail} `bindResponse` \resp -> do
25+
resp.status `shouldMatchInt` 201
26+
27+
let blockedEmail = username <> "@" <> blockedDomain
28+
bindResponse (activateSend domain blockedEmail Nothing) $ \resp -> do
29+
resp.status `shouldMatchInt` 451
30+
resp.json %. "label" `shouldMatch` "domain-blocked-for-registration"
31+
32+
let otherValidEmail = username <> "-1@" <> validDomain
33+
activateSend domain otherValidEmail Nothing >>= assertSuccess
34+
35+
testCannotChangeOwnEmailWithBlockedDomain :: (HasCallStack) => App ()
36+
testCannotChangeOwnEmailWithBlockedDomain = do
37+
let blockedDomain = "blocked.example.com"
38+
validDomain = "valid.example.com"
39+
withModifiedBackend
40+
def
41+
{ brigCfg =
42+
setField
43+
"optSettings.setCustomerExtensions.domainsBlockedForRegistration"
44+
(array [fromString blockedDomain])
45+
}
46+
$ \domain -> do
47+
validUser <- randomUser domain def
48+
validUserEmail <- validUser %. "email" & asString
49+
(cookie, token) <-
50+
login domain validUserEmail defPassword `bindResponse` \resp -> do
51+
resp.status `shouldMatchInt` 200
52+
token <- resp.json %. "access_token" & asString
53+
let cookie = fromJust $ getCookie "zuid" resp
54+
pure ("zuid=" <> cookie, token)
55+
56+
username2 <- randomName
57+
bindResponse (putSelfEmail validUser cookie token (username2 <> "@" <> blockedDomain)) $ \resp -> do
58+
resp.status `shouldMatchInt` 451
59+
resp.json %. "label" `shouldMatch` "domain-blocked-for-registration"
60+
61+
putSelfEmail validUser cookie token (username2 <> "@" <> validDomain) >>= assertSuccess
62+
63+
testCannotChangeTeamMemberEmailWithBlockedDomain :: (HasCallStack) => App ()
64+
testCannotChangeTeamMemberEmailWithBlockedDomain = do
65+
let blockedDomain = "blocked.example.com"
66+
validDomain = "valid.example.com"
67+
withModifiedBackend
68+
def
69+
{ brigCfg =
70+
setField
71+
"optSettings.setCustomerExtensions.domainsBlockedForRegistration"
72+
(array [fromString blockedDomain])
73+
}
74+
$ \domain -> do
75+
(owner, _team, [mem1]) <- createTeam domain 2
76+
77+
username <- randomName
78+
bindResponse (putUserEmail owner mem1 (username <> "@" <> blockedDomain)) $ \resp -> do
79+
resp.status `shouldMatchInt` 451
80+
resp.json %. "label" `shouldMatch` "domain-blocked-for-registration"
81+
82+
putUserEmail owner mem1 (username <> "@" <> validDomain) >>= assertSuccess
83+
84+
ownerUsername <- randomName
85+
bindResponse (putUserEmail owner owner (ownerUsername <> "@" <> blockedDomain)) $ \resp -> do
86+
resp.status `shouldMatchInt` 451
87+
resp.json %. "label" `shouldMatch` "domain-blocked-for-registration"
88+
89+
putUserEmail owner owner (ownerUsername <> "@" <> validDomain) >>= assertSuccess
90+
91+
testCannotCreateTeamInvitationWithBlockedDomain :: (HasCallStack) => App ()
92+
testCannotCreateTeamInvitationWithBlockedDomain = do
93+
let blockedDomain = "blocked.example.com"
94+
validDomain = "valid.example.com"
95+
withModifiedBackend
96+
def
97+
{ brigCfg =
98+
setField
99+
"optSettings.setCustomerExtensions.domainsBlockedForRegistration"
100+
(array [fromString blockedDomain])
101+
}
102+
$ \domain -> do
103+
(owner, _team, []) <- createTeam domain 1
104+
105+
username <- randomName
106+
bindResponse (postInvitation owner (PostInvitation (Just (username <> "@" <> blockedDomain)) Nothing))
107+
$ \resp -> do
108+
resp.status `shouldMatchInt` 451
109+
resp.json %. "label" `shouldMatch` "domain-blocked-for-registration"
110+
111+
void $ postInvitation owner (PostInvitation (Just (username <> "@" <> validDomain)) Nothing) >>= getJSON 201

integration/test/Test/User.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ testUpdateSelf (MkTagged mode) = do
169169
-- allowed unconditionally *for owner* (this is a bit off-topic: team members can't
170170
-- change their email addresses themselves under any conditions)
171171
someEmail <- (<> "@example.com") . UUID.toString <$> liftIO UUID.nextRandom
172-
bindResponse (putSelfEmail owner someEmail) $ \resp -> do
172+
bindResponse (putUserEmail owner owner someEmail) $ \resp -> do
173173
resp.status `shouldMatchInt` 200
174174
TestUpdateLocale -> do
175175
-- scim maps "User.preferredLanguage" to brig's locale field. allowed unconditionally.

libs/types-common/src/Data/Domain.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import Data.ByteString qualified as BS
3232
import Data.ByteString.Builder qualified as Builder
3333
import Data.ByteString.Char8 qualified as BS.Char8
3434
import Data.ByteString.Conversion
35+
import Data.Hashable
3536
import Data.OpenApi qualified as S
3637
import Data.Schema
3738
import Data.Text qualified as Text
@@ -66,6 +67,7 @@ newtype Domain = Domain {_domainText :: Text}
6667
deriving stock (Eq, Ord, Generic, Show)
6768
deriving newtype (S.ToParamSchema, Binary)
6869
deriving (FromJSON, ToJSON, S.ToSchema) via Schema Domain
70+
deriving (Hashable)
6971

7072
instance ToSchema Domain where
7173
schema =

libs/wire-api/src/Wire/API/Error/Brig.hs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,7 @@ type instance
293293
'StaticError
294294
451
295295
"domain-blocked-for-registration"
296-
"[Customer extension] the email domain example.com \
297-
\that you are attempting to register a user with has been \
298-
\blocked for creating wire users. Please contact your IT department."
296+
"[Customer extension] The email domain has been blocked for Wire users. Please contact your IT department."
299297

300298
type instance MapError 'PasswordResetInProgress = 'StaticError 409 "code-exists" "A password reset is already in progress."
301299

libs/wire-subsystems/src/Wire/TeamInvitationSubsystem/Error.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ data TeamInvitationSubsystemError
1515
| TeamInvitationEmailTaken
1616
| TeamInvitationInvalidEmail
1717
| TeamInvitationNotAllowedForEmail
18+
| TeamInvitationBlockedDomain
1819
deriving (Eq, Show)
1920

2021
instance Exception TeamInvitationSubsystemError
@@ -29,3 +30,4 @@ teamInvitationErrorToHttpError =
2930
TeamInvitationEmailTaken -> errorToWai @E.EmailExists
3031
TeamInvitationInvalidEmail -> errorToWai @E.InvalidEmail
3132
TeamInvitationNotAllowedForEmail -> Wai.mkError status403 "condition-failed" "Emails from this domain are not allowed to be invited to this team"
33+
TeamInvitationBlockedDomain -> errorToWai @E.CustomerExtensionBlockedDomain

0 commit comments

Comments
 (0)