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

New feature flags backend (part 2) #818

Merged
merged 15 commits into from
Aug 8, 2019
6 changes: 6 additions & 0 deletions deploy/services-demo/conf/nginz/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ http {
include common_response_with_zauth.conf;
proxy_pass http://brig;
}

# Cargohold Endpoints

rewrite ^/api-docs/assets /assets/api-docs?base_url=http://127.0.0.1:8080/ break;
Expand Down Expand Up @@ -266,6 +267,11 @@ http {
proxy_pass http://galley;
}

location ~* ^/teams/([^/]*)/features/([^/]*) {
include common_response_with_zauth.conf;
proxy_pass http://galley;
}

# Gundeck Endpoints

rewrite ^/api-docs/push /push/api-docs?base_url=http://127.0.0.1:8080/ break;
Expand Down
2 changes: 0 additions & 2 deletions services/galley/schema/src/V34.hs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import Imports
import Cassandra.Schema
import Text.RawString.QQ

-- TODO: After we've migrated legalhold to a separate feature table,
-- delete `legalhold_team_config`
migration :: Migration
migration = Migration 34 "Add team features table" $ do
schema' [r|
Expand Down
11 changes: 11 additions & 0 deletions services/galley/schema/src/V35.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module V35 (migration) where

import Imports
import Cassandra.Schema
import Text.RawString.QQ

migration :: Migration
fisx marked this conversation as resolved.
Show resolved Hide resolved
migration = Migration 34 "Delete deprecated legalhold_team_config" $ do
fisx marked this conversation as resolved.
Show resolved Hide resolved
schema' [r|
DROP TABLE legalhold_team_config;
|]
2 changes: 1 addition & 1 deletion services/galley/src/Galley/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ sitemap = do

get "/i/test/clients" (continue getClients)
zauthUserId
-- TODO: What is this endpoint? Is this used anywhere?
-- eg. https://github.com/wireapp/wire-server/blob/3bdca5fc8154e324773802a0deb46d884bd09143/services/brig/test/integration/API/User/Client.hs#L319

post "/i/clients/:client" (continue addClient) $
zauthUserId
Expand Down
13 changes: 13 additions & 0 deletions services/galley/src/Galley/API/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,16 @@ userLegalHoldNotPending = Error status412 "legalhold-not-pending" "legal hold ca

noLegalHoldDeviceAllocated :: Error
noLegalHoldDeviceAllocated = Error status404 "legalhold-no-device-allocated" "no legal hold device is registered for this user. POST /teams/:tid/legalhold/:uid/ to start the flow."

disableSsoNotImplemented :: Error
disableSsoNotImplemented = Error status403 "not-implemented"
"The SSO feature flag is disabled by default. It can only be enabled once for any team, never disabled.\n\
\\n\
\Rationale: there are two services in the backend that need to keep their status in sync: galley (teams),\n\
\and spar (SSO). Galley keeps track of team features. If galley creates an idp, the feature flag is\n\
\checked. For authentication, spar avoids this expensive check and assumes that the idp can only have\n\
\been created if the SSO is enabled. This assumption does not hold any more if the switch is turned off\n\
\again, so we do not support this.\n\
\\n\
\It is definitely feasible to change this. Tf you have a use case, please contact customer support, or\n\
fisx marked this conversation as resolved.
Show resolved Hide resolved
\open an issue on https://github.com/wireapp/wire-server."
5 changes: 2 additions & 3 deletions services/galley/src/Galley/API/Teams.hs
Original file line number Diff line number Diff line change
Expand Up @@ -521,9 +521,8 @@ setSSOStatusInternal :: TeamId ::: JsonRequest SSOTeamConfig ::: JSON -> Galley
setSSOStatusInternal (tid ::: req ::: _) = do
ssoTeamConfig <- fromJsonBody req
case ssoTeamConfigStatus ssoTeamConfig of
-- TODO: What to do when it's disabled, notify spar?
SSODisabled -> pure ()
SSOEnabled -> pure ()
SSODisabled -> throwM disableSsoNotImplemented
SSOEnabled -> pure () -- this one is easy to implement :)
SSOData.setSSOTeamConfig tid ssoTeamConfig
pure noContent

Expand Down
2 changes: 1 addition & 1 deletion services/galley/src/Galley/Data.hs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ import qualified System.Logger.Class as Log
newtype ResultSet a = ResultSet { page :: Page a }

schemaVersion :: Int32
schemaVersion = 32
schemaVersion = 34

-- | Insert a conversation code
insertCode :: MonadClient m => Code -> m ()
Expand Down
3 changes: 2 additions & 1 deletion services/galley/src/Galley/Data/LegalHold.hs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ getLegalHoldTeamConfig :: MonadClient m => TeamId -> m (Maybe LegalHoldTeamConfi
getLegalHoldTeamConfig tid = fmap toLegalHoldTeamConfig <$> do
retry x1 $ query1 selectLegalHoldTeamConfig (params Quorum (Identity tid))
where
toLegalHoldTeamConfig (Identity status) = LegalHoldTeamConfig status
toLegalHoldTeamConfig (Identity Nothing) = LegalHoldTeamConfig LegalHoldDisabled
toLegalHoldTeamConfig (Identity (Just status)) = LegalHoldTeamConfig status

-- | Determines whether a given team is allowed to enable/disable legalhold
setLegalHoldTeamConfig :: MonadClient m => TeamId -> LegalHoldTeamConfig -> m ()
Expand Down
14 changes: 4 additions & 10 deletions services/galley/src/Galley/Data/Queries.hs
Original file line number Diff line number Diff line change
Expand Up @@ -244,17 +244,11 @@ insertBot = "insert into member (conv, user, service, provider, status) values (

-- LegalHold ----------------------------------------------------------------

selectLegalHoldTeamConfig :: PrepQuery R (Identity TeamId) (Identity LegalHoldStatus)
selectLegalHoldTeamConfig = if True then _old else _new -- TODO: get rid of _old
where
_new = "select legalhold_status from team_features where team_id = ?"
_old = "select status from legalhold_team_config where team_id = ?"
selectLegalHoldTeamConfig :: PrepQuery R (Identity TeamId) (Identity (Maybe LegalHoldStatus))
selectLegalHoldTeamConfig = "select legalhold_status from team_features where team_id = ?"

updateLegalHoldTeamConfig :: PrepQuery W (LegalHoldStatus, TeamId) ()
updateLegalHoldTeamConfig = if True then _old else _new -- TODO: get rid of _old
where
_new = "update team_features set legalhold_status = ? where team_id = ?"
_old = "update legalhold_team_config set status = ? where team_id = ?"
updateLegalHoldTeamConfig = "update team_features set legalhold_status = ? where team_id = ?"

insertLegalHoldSettings :: PrepQuery W (HttpsUrl, Fingerprint Rsa, ServiceToken, ServiceKey, TeamId) ()
insertLegalHoldSettings =
Expand Down Expand Up @@ -304,7 +298,7 @@ updateUserLegalHoldStatus = [r|
where team = ? and user = ?
|]

selectSSOTeamConfig :: PrepQuery R (Identity TeamId) (Identity SSOStatus)
selectSSOTeamConfig :: PrepQuery R (Identity TeamId) (Identity (Maybe SSOStatus))
selectSSOTeamConfig =
"select sso_status from team_features where team_id = ?"

Expand Down
3 changes: 2 additions & 1 deletion services/galley/src/Galley/Data/SSO.hs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ getSSOTeamConfig :: MonadClient m => TeamId -> m (Maybe SSOTeamConfig)
getSSOTeamConfig tid = fmap toLegalHoldTeamConfig <$> do
fisx marked this conversation as resolved.
Show resolved Hide resolved
retry x1 $ query1 selectSSOTeamConfig (params Quorum (Identity tid))
where
toLegalHoldTeamConfig (Identity status) = SSOTeamConfig status
toLegalHoldTeamConfig (Identity Nothing) = SSOTeamConfig SSODisabled
toLegalHoldTeamConfig (Identity (Just status)) = SSOTeamConfig status

-- | Determines whether a given team is allowed to enable/disable sso
setSSOTeamConfig :: MonadClient m => TeamId -> SSOTeamConfig -> m ()
Expand Down
16 changes: 14 additions & 2 deletions services/galley/test/integration/API/Teams.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module API.Teams (tests) where

import Imports

import API.SQS
import API.Util
import Bilge.Assert
Expand All @@ -19,6 +20,7 @@ import Galley.Types.Teams
import Galley.Types.Teams.Intra
import Galley.Types.Teams.SSO
import Gundeck.Types.Notification
import Network.HTTP.Types.Status (status403)
import TestHelpers (test)
import TestSetup (TestSetup, TestM, tsCannon, tsGalley)
import Test.Tasty
Expand All @@ -34,6 +36,7 @@ import qualified Data.Text as T
import qualified Data.UUID as UUID
import qualified Galley.Types as Conv
import qualified Network.Wai.Utilities.Error as Error
import qualified Network.Wai.Utilities.Error as Wai
import qualified Test.Tasty.Cannon as WS

tests :: IO TestSetup -> TestTree
Expand Down Expand Up @@ -162,13 +165,22 @@ testEnableSSOPerTeam = do
SSOTeamConfig status <- jsonBody <$> (getSSOEnabledInternal tid <!! testResponse 200 Nothing)
liftIO $ assertEqual msg enabledness status

let putSSOEnabledInternalCheckNotImplemented :: HasCallStack => TestM ()
putSSOEnabledInternalCheckNotImplemented = do
g <- view tsGalley
Wai.Error status label _ <- jsonBody <$> put (g
. paths ["i", "teams", toByteString' tid, "features", "sso"]
. json (SSOTeamConfig SSODisabled))
liftIO $ do
assertEqual "bad status" status403 status
assertEqual "bad label" "not-implemented" label

check "Teams should start with SSO disabled" SSODisabled

putSSOEnabledInternal tid SSOEnabled
check "Calling 'putEnabled True' should enable SSO" SSOEnabled

putSSOEnabledInternal tid SSODisabled
check "Calling 'putEnabled False' should disable SSO" SSODisabled
putSSOEnabledInternalCheckNotImplemented


testCreateOne2OneFailNonBindingTeamMembers :: TestM ()
Expand Down
2 changes: 1 addition & 1 deletion services/spar/src/Spar/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ renderSparError (SAML.CustomError SparNotFound) = Rig
renderSparError (SAML.CustomError SparMissingZUsr) = Right $ Wai.Error status400 "client-error" "[header] 'Z-User' required"
renderSparError (SAML.CustomError SparNotInTeam) = Right $ Wai.Error status403 "no-team-member" "Requesting user is not a team member or not a member of this team."
renderSparError (SAML.CustomError SparNotTeamOwner) = Right $ Wai.Error status403 "insufficient-permissions" "You need to be a team owner."
renderSparError (SAML.CustomError SparSSODisabled) = Right $ Wai.Error status403 "sso-disabled" "Please ask customer support to enable this feature for you."
renderSparError (SAML.CustomError SparSSODisabled) = Right $ Wai.Error status403 "sso-disabled" "Please ask customer support to enable this feature for your team."
renderSparError (SAML.CustomError SparInitLoginWithAuth) = Right $ Wai.Error status403 "login-with-auth" "This end-point is only for login, not binding."
renderSparError (SAML.CustomError SparInitBindWithoutAuth) = Right $ Wai.Error status403 "bind-without-auth" "This end-point is only for binding, not login."
renderSparError (SAML.CustomError SparBindUserDisappearedFromBrig) = Right $ Wai.Error status404 "bind-user-disappeared" "Your user appears to have been deleted?"
Expand Down
4 changes: 1 addition & 3 deletions services/spar/test-integration/Test/Spar/APISpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import Util.Types
import qualified Data.ByteString.Builder as LB
import qualified Data.ZAuth.Token as ZAuth
import qualified Galley.Types.Teams as Galley
import qualified Galley.Types.Teams.SSO as Galley
import qualified Spar.Intra.Brig as Intra
import qualified Util.Scim as ScimT
import qualified Web.Cookie as Cky
Expand Down Expand Up @@ -608,8 +607,7 @@ specCRUDIdentityProvider = do
context "sso disabled for team" $ do
it "responds with 403 forbidden" $ do
env <- ask
(uid, tid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley)
call $ putSSOEnabledInternal (env ^. teGalley) tid Galley.SSODisabled
(uid, _tid) <- call $ createUserWithTeamDisableSSO (env ^. teBrig) (env ^. teGalley)
(callIdpCreate' (env ^. teSpar) (Just uid) =<< makeTestIdPMetadata)
`shouldRespondWith` checkErr (== 403) "sso-disabled"

Expand Down
8 changes: 7 additions & 1 deletion services/spar/test-integration/Util/Core.hs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module Util.Core
-- * Other
, defPassword
, createUserWithTeam
, createUserWithTeamDisableSSO
, getSSOEnabledInternal
, putSSOEnabledInternal
, createTeamMember
Expand Down Expand Up @@ -227,6 +228,12 @@ aFewTimes action good = do

createUserWithTeam :: (HasCallStack, MonadHttp m, MonadIO m) => BrigReq -> GalleyReq -> m (UserId, TeamId)
createUserWithTeam brg gly = do
(uid, tid) <- createUserWithTeamDisableSSO brg gly
putSSOEnabledInternal gly tid Galley.SSOEnabled
pure (uid, tid)

createUserWithTeamDisableSSO :: (HasCallStack, MonadHttp m, MonadIO m) => BrigReq -> GalleyReq -> m (UserId, TeamId)
createUserWithTeamDisableSSO brg gly = do
e <- randomEmail
n <- UUID.toString <$> liftIO UUID.nextRandom
let p = RequestBodyLBS . Aeson.encode $ object
Expand All @@ -243,7 +250,6 @@ createUserWithTeam brg gly = do
selfTeam <- Brig.userTeam . Brig.selfUser <$> getSelfProfile brg uid
() <- Control.Exception.assert {- "Team ID in self profile and team table do not match" -} (selfTeam == Just tid)
$ pure ()
putSSOEnabledInternal gly tid Galley.SSOEnabled
return (uid, tid)

getSSOEnabledInternal :: (HasCallStack, MonadHttp m, MonadIO m) => GalleyReq -> TeamId -> m ResponseLBS
Expand Down
13 changes: 7 additions & 6 deletions stack.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,35 @@ packages:
- libs/cargohold-types
- libs/cassandra-util
- libs/extended
- libs/imports
- libs/galley-types
- libs/gundeck-types
- libs/ssl-util
- libs/imports
- libs/metrics-collectd
- libs/metrics-core
- libs/metrics-wai
- libs/ropes
- libs/sodium-crypto-sign
- libs/ssl-util
- libs/tasty-cannon
- libs/types-common
- libs/types-common-aws
- libs/types-common-journal
- libs/wai-utilities
- libs/zauth
- services/brig
- services/spar
- services/cannon
- services/cargohold
- services/galley
- services/gundeck
- services/proxy
- services/spar
- tools/api-simulations
- tools/bonanza
- tools/db/auto-whitelist
- tools/db/migrate-sso-feature-flag
- tools/db/service-backfill
- tools/makedeb
- tools/api-simulations
- tools/stern
- tools/db/service-backfill
- tools/db/auto-whitelist

extra-deps:
- servant-swagger-1.1.6
Expand Down
27 changes: 27 additions & 0 deletions tools/db/migrate-sso-feature-flag/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
LANG := en_US.UTF-8

SHELL := /usr/bin/env bash

default: all

all: clean install

.PHONY: init
init:
mkdir -p dist

.PHONY: clean
clean:
stack clean

.PHONY: compile
compile:
stack build

.PHONY: install
install: init
stack install --pedantic --local-bin-path=dist

.PHONY: fast
fast: init
stack install --fast --local-bin-path=dist
3 changes: 3 additions & 0 deletions tools/db/migrate-sso-feature-flag/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Service backfill

A tool for filling tables `service_user` and `service_team` from existing data.
fisx marked this conversation as resolved.
Show resolved Hide resolved
42 changes: 42 additions & 0 deletions tools/db/migrate-sso-feature-flag/package.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
defaults:
local: ../../../package-defaults.yaml
name: migrate-sso-feature-flag
version: '1.0.0'
synopsis: Backfill sso feature flag into teams that already have an IdP.
category: Network
author: Wire Swiss GmbH
maintainer: Wire Swiss GmbH <backend@wire.com>
copyright: (c) 2018 Wire Swiss GmbH
license: UnspecifiedLicense
ghc-options:
- -funbox-strict-fields
- -threaded
- -with-rtsopts=-N
- -with-rtsopts=-T
- -rtsopts
dependencies:
- base
- attoparsec
- brig-types
- bytestring
- bytestring
- bytestring-conversion
- cassandra-util
- conduit
- extended
- galley
- galley-types
- imports
- lens
- mtl
- optparse-applicative
- text
- time
- tinylog
- types-common
- unliftio
- uuid
executables:
migrate-sso-feature-flag:
main: Main.hs
source-dirs: src
Loading