Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
1 change: 1 addition & 0 deletions changelog.d/4-docs/WPB-24006
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Updated docs for the team feature `validateSAMLemails`
12 changes: 7 additions & 5 deletions docs/src/developer/reference/config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,15 +288,17 @@ The lock status for individual teams can be changed via the internal API (`PUT /

The feature status for individual teams can be changed via the public API (if the feature is unlocked).

### Validate SAML Emails
### Require External Email Verification

The feature only affects email address changes originating from SCIM or SAML. Personal users and team users provisioned through the team management app will *always* be validated.
The external feature name `validateSAMLemails` is kept for backward compatibility, but it is misleading: the feature applies to email addresses originating from both SCIM and SAML, and it controls ownership verification rather than generic email validation.

`enabled` means "user has authority over email address": if a new user account with an email address is created, the user behind the account will receive a validation email. If they follow the validation procedure, they will be able to receive emails about their account, eg., if a new device is associated with the account. If the user does not validate their email address, they can still use it to login.
The feature only affects email address changes originating from SCIM or SAML. Personal users and team users provisioned through the team management app will *always* go through email verification.

`disabled` means "team admin has authority over email address, and by extension over all member accounts": if a user account with an email address is created, the address is considered valid immediately, without any emails being sent out, and without confirmation from the recipient.
`enabled` means "user has authority over email address": if a new user account with an email address is created, the user behind the account will receive a verification email. If they complete the verification flow, they will be able to receive emails about their account, eg., if a new device is associated with the account. If they do not verify their email address, they can still use it to log in.

Validate SAML emails is enabled by default. To disable, use the following syntax:
`disabled` means "team admin has authority over email address, and by extension over all member accounts": if a user account with an email address is created, the address is auto-activated immediately, without any emails being sent out and without confirmation from the recipient.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

They will however receive emails about their account to this email address in the future, eg., if a new device is associated with the account.

Correct? @battermann

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yes, I will update the docs to make this more clear.


This feature is enabled by default. To disable it, use the following syntax:

```yaml
# galley.yaml
Expand Down
2 changes: 1 addition & 1 deletion integration/integration.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ library
Test.FeatureFlags.MlsE2EId
Test.FeatureFlags.MlsMigration
Test.FeatureFlags.OutlookCalIntegration
Test.FeatureFlags.RequireExternalEmailVerification
Test.FeatureFlags.SearchVisibilityAvailable
Test.FeatureFlags.SearchVisibilityInbound
Test.FeatureFlags.SelfDeletingMessages
Expand All @@ -167,7 +168,6 @@ library
Test.FeatureFlags.StealthUsers
Test.FeatureFlags.User
Test.FeatureFlags.Util
Test.FeatureFlags.ValidateSAMLEmails
Test.Federation
Test.Federator
Test.LegalHold
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Test.FeatureFlags.ValidateSAMLEmails where
module Test.FeatureFlags.RequireExternalEmailVerification where

import SetupHelpers
import Test.FeatureFlags.Util
import Testlib.Prelude

testPatchValidateSAMLEmails :: (HasCallStack) => App ()
testPatchValidateSAMLEmails =
testPatchRequireExternalEmailVerification :: (HasCallStack) => App ()
testPatchRequireExternalEmailVerification =
checkPatch OwnDomain "validateSAMLemails"
$ object ["status" .= "disabled"]

testValidateSAMLEmailsInternal :: (HasCallStack) => App ()
testValidateSAMLEmailsInternal = do
testRequireExternalEmailVerification :: (HasCallStack) => App ()
testRequireExternalEmailVerification = do
(alice, tid, _) <- createTeam OwnDomain 0
withWebSocket alice $ \ws -> do
setFlag InternalAPI ws tid "validateSAMLemails" disabled
Expand Down
24 changes: 12 additions & 12 deletions integration/test/Test/Spar.hs
Original file line number Diff line number Diff line change
Expand Up @@ -887,12 +887,12 @@ testSsoLoginAndEmailVerification = do
user %. "email" `shouldMatch` email

-- | This test may be covered by `testScimUpdateEmailAddress` and maybe can be removed.
testSsoLoginNoSamlEmailValidation :: (HasCallStack) => TaggedBool "validateSAMLEmails" -> App ()
testSsoLoginNoSamlEmailValidation (TaggedBool validateSAMLEmails) = do
testSsoLoginNoSamlEmailValidation :: (HasCallStack) => TaggedBool "requireExternalEmailVerification" -> App ()
testSsoLoginNoSamlEmailValidation (TaggedBool requireExternalEmailVerification) = do
(owner, tid, _) <- createTeam OwnDomain 1
emailDomain <- randomDomain

let status = if validateSAMLEmails then "enabled" else "disabled"
let status = if requireExternalEmailVerification then "enabled" else "disabled"
assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" status

void $ setTeamFeatureStatus owner tid "sso" "enabled"
Expand All @@ -910,7 +910,7 @@ testSsoLoginNoSamlEmailValidation (TaggedBool validateSAMLEmails) = do
eid = CI.original $ uref ^. SAML.uidSubject . to SAML.unsafeShowNameID
eid `shouldMatch` email

when validateSAMLEmails $ do
when requireExternalEmailVerification $ do
getUsersId OwnDomain [uid] `bindResponse` \res -> do
res.status `shouldMatchInt` 200
user <- res.json & asList >>= assertOne
Expand All @@ -936,11 +936,11 @@ testSsoLoginNoSamlEmailValidation (TaggedBool validateSAMLEmails) = do
user %. "email" `shouldMatch` email

-- | create user with non-email externalId. then use put to add an email address.
testScimUpdateEmailAddress :: (HasCallStack) => TaggedBool "extIdIsEmail" -> TaggedBool "validateSAMLEmails" -> App ()
testScimUpdateEmailAddress (TaggedBool extIdIsEmail) (TaggedBool validateSAMLEmails) = do
testScimUpdateEmailAddress :: (HasCallStack) => TaggedBool "extIdIsEmail" -> TaggedBool "requireExternalEmailVerification" -> App ()
testScimUpdateEmailAddress (TaggedBool extIdIsEmail) (TaggedBool requireExternalEmailVerification) = do
(owner, tid, _) <- createTeam OwnDomain 1

let status = if validateSAMLEmails then "enabled" else "disabled"
let status = if requireExternalEmailVerification then "enabled" else "disabled"
assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" status

void $ setTeamFeatureStatus owner tid "sso" "enabled"
Expand Down Expand Up @@ -991,7 +991,7 @@ testScimUpdateEmailAddress (TaggedBool extIdIsEmail) (TaggedBool validateSAMLEma
res.status `shouldMatchInt` 200
res.json %. "emails" `shouldMatch` [object ["value" .= newEmail]]

when validateSAMLEmails $ do
when requireExternalEmailVerification $ do
getUsersId OwnDomain [uid] `bindResponse` \res -> do
res.status `shouldMatchInt` 200
user <- res.json & asList >>= assertOne
Expand Down Expand Up @@ -1164,11 +1164,11 @@ testScimUpdateEmailAddressAndExternalId = do
user %. "status" `shouldMatch` "active"
user %. "email" `shouldMatch` newEmail1

testScimLoginNoSamlEmailValidation :: (HasCallStack) => TaggedBool "validateSAMLEmails" -> App ()
testScimLoginNoSamlEmailValidation (TaggedBool validateSAMLEmails) = do
testScimLoginNoSamlEmailValidation :: (HasCallStack) => TaggedBool "requireExternalEmailVerification" -> App ()
testScimLoginNoSamlEmailValidation (TaggedBool requireExternalEmailVerification) = do
(owner, tid, _) <- createTeam OwnDomain 1

let status = if validateSAMLEmails then "enabled" else "disabled"
let status = if requireExternalEmailVerification then "enabled" else "disabled"
assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" status

void $ setTeamFeatureStatus owner tid "sso" "enabled"
Expand All @@ -1187,7 +1187,7 @@ testScimLoginNoSamlEmailValidation (TaggedBool validateSAMLEmails) = do
res.status `shouldMatchInt` 200
res.json %. "id" `shouldMatch` uid

when validateSAMLEmails $ do
when requireExternalEmailVerification $ do
getUsersId OwnDomain [uid] `bindResponse` \res -> do
res.status `shouldMatchInt` 200
user <- res.json & asList >>= assertOne
Expand Down
16 changes: 8 additions & 8 deletions integration/test/Test/Spar/GetByEmail.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ import Testlib.Prelude
-- | Test the /sso/get-by-email endpoint with multi-ingress setup
testGetSsoCodeByEmailWithMultiIngress ::
(HasCallStack) =>
TaggedBool "validateSAMLemails" ->
TaggedBool "requireExternalEmailVerification" ->
TaggedBool "idpScimToken" ->
App ()
testGetSsoCodeByEmailWithMultiIngress (TaggedBool validateSAMLemails) (TaggedBool isIdPScimToken) = do
testGetSsoCodeByEmailWithMultiIngress (TaggedBool requireExternalEmailVerification) (TaggedBool isIdPScimToken) = do
let ernieZHost = "nginz-https.ernie.example.com"
bertZHost = "nginz-https.bert.example.com"

Expand Down Expand Up @@ -65,7 +65,7 @@ testGetSsoCodeByEmailWithMultiIngress (TaggedBool validateSAMLemails) (TaggedBoo
assertSuccess =<< setTeamFeatureStatus domain tid "sso" "enabled"

-- The test should work for both: SCIM user with and without email confirmation
let status = if validateSAMLemails then "enabled" else "disabled"
let status = if requireExternalEmailVerification then "enabled" else "disabled"
assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" status

-- Create IdP for ernie domain
Expand Down Expand Up @@ -98,7 +98,7 @@ testGetSsoCodeByEmailWithMultiIngress (TaggedBool validateSAMLemails) (TaggedBoo
createScimUser domain scimToken scimUser >>= assertSuccess

if isIdPScimToken
then when validateSAMLemails $ do
then when requireExternalEmailVerification $ do
-- Activate the email so the user can be found by email
activateEmail domain userEmail
else
Expand All @@ -124,15 +124,15 @@ testGetSsoCodeByEmailWithMultiIngress (TaggedBool validateSAMLemails) (TaggedBoo
ssoCodeStr `shouldMatch` idpIdBert

-- | Test the /sso/get-by-email endpoint with regular (non-multi-ingress) setup
testGetSsoCodeByEmailRegular :: (HasCallStack) => (TaggedBool "validateSAMLemails") -> (TaggedBool "idpScimToken") -> App ()
testGetSsoCodeByEmailRegular (TaggedBool validateSAMLemails) (TaggedBool isIdPScimToken) =
testGetSsoCodeByEmailRegular :: (HasCallStack) => (TaggedBool "requireExternalEmailVerification") -> (TaggedBool "idpScimToken") -> App ()
testGetSsoCodeByEmailRegular (TaggedBool requireExternalEmailVerification) (TaggedBool isIdPScimToken) =
withModifiedBackend def {sparCfg = setField "enableIdPByEmailDiscovery" True}
$ \domain -> do
(owner, tid, _) <- createTeam domain 1
void $ setTeamFeatureStatus owner tid "sso" "enabled"

-- The test should work for both: SCIM user with and without email confirmation
let status = if validateSAMLemails then "enabled" else "disabled"
let status = if requireExternalEmailVerification then "enabled" else "disabled"
assertSuccess =<< setTeamFeatureStatus owner tid "validateSAMLemails" status

-- Create IdP without domain binding
Expand All @@ -156,7 +156,7 @@ testGetSsoCodeByEmailRegular (TaggedBool validateSAMLemails) (TaggedBool isIdPSc
createScimUser domain scimToken scimUser >>= assertSuccess

if isIdPScimToken
then when validateSAMLemails $ do
then when requireExternalEmailVerification $ do
-- Activate the email so the user can be found by email
activateEmail domain userEmail
else
Expand Down
2 changes: 2 additions & 0 deletions libs/wire-api/src/Wire/API/Routes/Features.hs
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ type family FeatureErrors cfg where
type family FeatureAPIDesc cfg where
FeatureAPIDesc EnforceFileDownloadLocationConfig =
"<p><b>Custom feature: only supported on some dedicated on-prem systems.</b></p>"
FeatureAPIDesc RequireExternalEmailVerificationConfig =
"<p>Controls whether externally managed email addresses (from SAML or SCIM) must be verified by the user, or are auto-activated.</p><p>The external feature name is kept as <code>validateSAMLemails</code> for backward compatibility. That name is misleading because the feature also applies to SCIM-managed users, and it controls email ownership verification rather than generic email validation.</p>"
FeatureAPIDesc _ = ""
6 changes: 3 additions & 3 deletions libs/wire-api/src/Wire/API/Routes/Public/Galley/Feature.hs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type FeatureAPI =
:<|> FeatureAPIGetPut SearchVisibilityAvailableConfig
:<|> SearchVisibilityGet
:<|> SearchVisibilitySet
:<|> FeatureAPIGet ValidateSAMLEmailsConfig
:<|> FeatureAPIGet RequireExternalEmailVerificationConfig
:<|> FeatureAPIGet DigitalSignaturesConfig
:<|> FeatureAPIGetPut AppLockConfig
:<|> FeatureAPIGetPut FileSharingConfig
Expand Down Expand Up @@ -108,7 +108,7 @@ type DeprecatedFeatureConfigs =
[ LegalholdConfig,
SSOConfig,
SearchVisibilityAvailableConfig,
ValidateSAMLEmailsConfig,
RequireExternalEmailVerificationConfig,
DigitalSignaturesConfig,
AppLockConfig,
FileSharingConfig,
Expand All @@ -129,7 +129,7 @@ type family AllDeprecatedFeatureConfigAPI cfgs where
type DeprecatedFeatureAPI =
FeatureStatusDeprecatedGet DeprecationNotice1 SearchVisibilityAvailableConfig V2
:<|> FeatureStatusDeprecatedPut DeprecationNotice1 SearchVisibilityAvailableConfig V2
:<|> FeatureStatusDeprecatedGet DeprecationNotice1 ValidateSAMLEmailsConfig V2
:<|> FeatureStatusDeprecatedGet DeprecationNotice1 RequireExternalEmailVerificationConfig V2
:<|> FeatureStatusDeprecatedGet DeprecationNotice2 DigitalSignaturesConfig V2

type FeatureAPIGet cfg =
Expand Down
40 changes: 22 additions & 18 deletions libs/wire-api/src/Wire/API/Team/Feature.hs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ module Wire.API.Team.Feature
SearchVisibilityAvailableConfig (..),
SelfDeletingMessagesConfigB (..),
SelfDeletingMessagesConfig,
ValidateSAMLEmailsConfig (..),
RequireExternalEmailVerificationConfig (..),
DigitalSignaturesConfig (..),
ConferenceCallingConfigB (..),
ConferenceCallingConfig,
Expand Down Expand Up @@ -256,7 +256,7 @@ data FeatureSingleton cfg where
FeatureSingletonLegalholdConfig :: FeatureSingleton LegalholdConfig
FeatureSingletonSSOConfig :: FeatureSingleton SSOConfig
FeatureSingletonSearchVisibilityAvailableConfig :: FeatureSingleton SearchVisibilityAvailableConfig
FeatureSingletonValidateSAMLEmailsConfig :: FeatureSingleton ValidateSAMLEmailsConfig
FeatureSingletonRequireExternalEmailVerificationConfig :: FeatureSingleton RequireExternalEmailVerificationConfig
FeatureSingletonDigitalSignaturesConfig :: FeatureSingleton DigitalSignaturesConfig
FeatureSingletonConferenceCallingConfig :: FeatureSingleton ConferenceCallingConfig
FeatureSingletonSndFactorPasswordChallengeConfig :: FeatureSingleton SndFactorPasswordChallengeConfig
Expand Down Expand Up @@ -753,29 +753,33 @@ instance ToSchema SearchVisibilityAvailableConfig where
type instance DeprecatedFeatureName V2 SearchVisibilityAvailableConfig = "search-visibility"

--------------------------------------------------------------------------------
-- ValidateSAMLEmails feature
-- RequireExternalEmailVerification feature

-- | This feature does not have a PUT endpoint. See Note [unsettable features].
data ValidateSAMLEmailsConfig = ValidateSAMLEmailsConfig
-- | Controls whether externally managed email addresses (from SAML or SCIM)
-- must be verified by the user, or are auto-activated.
-- The external feature name is kept for backward compatibility.
--
-- (This feature does not have a PUT endpoint. See Note [unsettable features].)
data RequireExternalEmailVerificationConfig = RequireExternalEmailVerificationConfig
deriving (Eq, Show, Generic, GSOP.Generic)
deriving (Arbitrary) via (GenericUniform ValidateSAMLEmailsConfig)
deriving (RenderableSymbol) via (RenderableTypeName ValidateSAMLEmailsConfig)
deriving (ParseDbFeature, Default) via (TrivialFeature ValidateSAMLEmailsConfig)
deriving (Arbitrary) via (GenericUniform RequireExternalEmailVerificationConfig)
deriving (RenderableSymbol) via (RenderableTypeName RequireExternalEmailVerificationConfig)
deriving (ParseDbFeature, Default) via (TrivialFeature RequireExternalEmailVerificationConfig)

instance ToSchema ValidateSAMLEmailsConfig where
schema = object "ValidateSAMLEmailsConfig" objectSchema
instance ToSchema RequireExternalEmailVerificationConfig where
schema = object "RequireExternalEmailVerificationConfig" objectSchema

instance Default (LockableFeature ValidateSAMLEmailsConfig) where
instance Default (LockableFeature RequireExternalEmailVerificationConfig) where
def = defUnlockedFeature

instance ToObjectSchema ValidateSAMLEmailsConfig where
objectSchema = pure ValidateSAMLEmailsConfig
instance ToObjectSchema RequireExternalEmailVerificationConfig where
objectSchema = pure RequireExternalEmailVerificationConfig

instance IsFeatureConfig ValidateSAMLEmailsConfig where
type FeatureSymbol ValidateSAMLEmailsConfig = "validateSAMLemails"
featureSingleton = FeatureSingletonValidateSAMLEmailsConfig
instance IsFeatureConfig RequireExternalEmailVerificationConfig where
type FeatureSymbol RequireExternalEmailVerificationConfig = "validateSAMLemails"
featureSingleton = FeatureSingletonRequireExternalEmailVerificationConfig

type instance DeprecatedFeatureName V2 ValidateSAMLEmailsConfig = "validate-saml-emails"
type instance DeprecatedFeatureName V2 RequireExternalEmailVerificationConfig = "validate-saml-emails"

--------------------------------------------------------------------------------
-- DigitalSignatures feature
Expand Down Expand Up @@ -2207,7 +2211,7 @@ type Features =
SSOConfig,
SearchVisibilityAvailableConfig,
SearchVisibilityInboundConfig,
ValidateSAMLEmailsConfig,
RequireExternalEmailVerificationConfig,
DigitalSignaturesConfig,
AppLockConfig,
FileSharingConfig,
Expand Down
12 changes: 6 additions & 6 deletions libs/wire-api/src/Wire/API/Team/FeatureFlags.hs
Original file line number Diff line number Diff line change
Expand Up @@ -182,19 +182,19 @@ newtype instance FeatureDefaults SearchVisibilityInboundConfig
deriving (FromJSON, ToJSON) via Defaults (Feature SearchVisibilityInboundConfig)
deriving (ParseFeatureDefaults) via OptionalField SearchVisibilityInboundConfig

newtype instance FeatureDefaults ValidateSAMLEmailsConfig
= ValidateSAMLEmailsDefaults (Feature ValidateSAMLEmailsConfig)
newtype instance FeatureDefaults RequireExternalEmailVerificationConfig
= RequireExternalEmailVerificationDefaults (Feature RequireExternalEmailVerificationConfig)
deriving stock (Eq, Show)
deriving newtype (Default, GetFeatureDefaults)
deriving (FromJSON, ToJSON) via Defaults (Feature ValidateSAMLEmailsConfig)
deriving (FromJSON, ToJSON) via Defaults (Feature RequireExternalEmailVerificationConfig)

instance ParseFeatureDefaults (FeatureDefaults ValidateSAMLEmailsConfig) where
instance ParseFeatureDefaults (FeatureDefaults RequireExternalEmailVerificationConfig) where
parseFeatureDefaults obj =
do
-- Accept the legacy typo in config input for backward compatibility,
-- but prefer the canonical feature key when both are present.
mCanonical :: Maybe (FeatureDefaults ValidateSAMLEmailsConfig) <- obj .:? featureKey @ValidateSAMLEmailsConfig
mLegacy :: Maybe (FeatureDefaults ValidateSAMLEmailsConfig) <- obj .:? "validateSAMLEmails"
mCanonical :: Maybe (FeatureDefaults RequireExternalEmailVerificationConfig) <- obj .:? featureKey @RequireExternalEmailVerificationConfig
mLegacy :: Maybe (FeatureDefaults RequireExternalEmailVerificationConfig) <- obj .:? "validateSAMLEmails"
pure $ fromMaybe def (mCanonical <|> mLegacy)

data instance FeatureDefaults DigitalSignaturesConfig = DigitalSignaturesDefaults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ testObject_Feature_team_10 = Feature FeatureStatusDisabled SSOConfig
testObject_Feature_team_11 :: Feature SearchVisibilityAvailableConfig
testObject_Feature_team_11 = Feature FeatureStatusEnabled SearchVisibilityAvailableConfig

testObject_Feature_team_12 :: Feature ValidateSAMLEmailsConfig
testObject_Feature_team_12 = Feature FeatureStatusDisabled ValidateSAMLEmailsConfig
testObject_Feature_team_12 :: Feature RequireExternalEmailVerificationConfig
testObject_Feature_team_12 = Feature FeatureStatusDisabled RequireExternalEmailVerificationConfig

testObject_Feature_team_13 :: Feature DigitalSignaturesConfig
testObject_Feature_team_13 = Feature FeatureStatusEnabled DigitalSignaturesConfig
Expand Down
Loading
Loading