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

Allow SCIM without saml #1200

Merged
merged 35 commits into from
Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ead3e5d
New brig internal end-points.
fisx Aug 31, 2020
0ae0455
Support for email/password-authenticated scim users.
fisx Aug 27, 2020
1bf9d9c
Cleanup
fisx Sep 2, 2020
c936365
Cleanup
fisx Sep 2, 2020
2f5ec95
Fix: spar's notion of brig's api.
fisx Sep 2, 2020
1f64197
Fix: allow UserSSOId in brig to carry scim external ids.
fisx Sep 2, 2020
09a2755
Fix: UserSSOId parsing in spar.
fisx Sep 3, 2020
7131b1a
Cleanup
fisx Sep 3, 2020
8d767be
Fix: brig internal api in spar.
fisx Sep 3, 2020
f38d535
Fix: store RichInfo in brig if it changes.
fisx Sep 3, 2020
1bfb2f9
Fix: do not support setting passwords.
fisx Sep 3, 2020
4907be8
Fix: re-align error message in test.
fisx Sep 3, 2020
011795f
Fix: remove redundant test.
fisx Sep 3, 2020
3fcb32b
Fix: test.
fisx Sep 3, 2020
82c920e
Fix: test.
fisx Sep 3, 2020
0025e9b
Fix: update sso_id in brig correctly.
fisx Sep 3, 2020
7dd4434
Fix: do not pull users with email pending validation.
fisx Sep 3, 2020
fb9c9be
Refactor: functions for handler-, email-based scim user lookup.
fisx Sep 3, 2020
ea229b8
Give externalIds that are emails their own lookup table in spar.
fisx Sep 3, 2020
91fca93
Fix: misc
fisx Sep 3, 2020
0e2e480
Fix: misc
fisx Sep 3, 2020
fe92678
Comment.
fisx Sep 3, 2020
f88559d
...
fisx Sep 3, 2020
dc00e06
Add missing test.
fisx Sep 4, 2020
77056b0
Fix compiler warning.
fisx Sep 4, 2020
c9897aa
Fix: check if email address is available (even without idp).
fisx Sep 4, 2020
1ba1b96
nit-picks.
fisx Sep 4, 2020
37131ee
Fix: scim-delete if there is no saml idp.
fisx Sep 4, 2020
11fe7d2
Refactor: reduce integration test setup time.
fisx Sep 4, 2020
0f3edd4
Fix: do not use email address as saml subject if no uref is found.
fisx Sep 4, 2020
0ff6166
Fix: test.
fisx Sep 4, 2020
1a2907f
Fix: test.
fisx Sep 4, 2020
a5b87d8
Fix: test.
fisx Sep 4, 2020
344080a
Add at least a few lines of docs.
fisx Sep 4, 2020
9d90058
Assert that deleteUser returns 204
arianvp Sep 9, 2020
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
19 changes: 19 additions & 0 deletions docs/reference/spar-braindump.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ documentation answering your questions, look here!
- if you want to work on our saml/scim implementation and do not have access to [https://github.com/zinfra/backend-issues/issues?q=is%3Aissue+is%3Aopen+label%3Aspar] and [https://github.com/wireapp/design-specs/tree/master/Single%20Sign%20On], please get in touch with us.


## design considerations

### SCIM without SAML.

Before https://github.com/wireapp/wire-server/pull/1200, 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]'}`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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]'}`.


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.
Copy link
Contributor

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



## operations

### enabling / disabling the sso feature for a team
Expand Down
2 changes: 1 addition & 1 deletion libs/brig-types/src/Brig/Types/Intra.hs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ instance ToJSON AccountStatus where
toJSON Deleted = String "deleted"
toJSON Ephemeral = String "ephemeral"

data AccountStatusResp = AccountStatusResp AccountStatus
data AccountStatusResp = AccountStatusResp {fromAccountStatusResp :: AccountStatus}

instance ToJSON AccountStatusResp where
toJSON (AccountStatusResp s) = object ["status" .= s]
Expand Down
10 changes: 5 additions & 5 deletions libs/wire-api/src/Wire/API/User.hs
Original file line number Diff line number Diff line change
Expand Up @@ -694,10 +694,9 @@ parseNewUserOrigin pass uid ssoid o = do
(Nothing, Nothing, Just a, Nothing, Nothing) -> return . Just . NewUserOriginTeamUser $ NewTeamCreator a
(Nothing, Nothing, Nothing, Just _, Just t) -> return . Just . NewUserOriginTeamUser $ NewTeamMemberSSO t
(Nothing, Nothing, Nothing, Nothing, Nothing) -> return Nothing
(_, _, _, _, _) ->
fail $
"team_code, team, invitation_code, sso_id are mutually exclusive\
\ 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"
Copy link
Contributor Author

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.

case (result, pass, uid) of
(_, _, Just SSOIdentity {}) -> pure result
(Just (NewUserOriginTeamUser _), Nothing, _) -> fail "all team users must set a password on creation"
Expand Down Expand Up @@ -729,7 +728,8 @@ data NewTeamUser
= -- | requires email address
NewTeamMember InvitationCode
| NewTeamCreator BindingNewTeamUser
| NewTeamMemberSSO TeamId
| -- | sso: users with saml credentials and/or created via scim
NewTeamMemberSSO TeamId
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform NewTeamUser)

Expand Down
34 changes: 22 additions & 12 deletions libs/wire-api/src/Wire/API/User/Identity.hs
Original file line number Diff line number Diff line change
Expand Up @@ -247,24 +247,34 @@ isValidPhone = either (const False) (const True) . parseOnly e164
-- Morally this is the same thing as 'SAML.UserRef', but we forget the
-- structure -- i.e. we just store XML-encoded SAML blobs. If the structure
-- of those blobs changes, Brig won't have to deal with it, only Spar will.
data UserSSOId = UserSSOId
{ -- | An XML blob pointing to the identity provider that can confirm
-- user's identity.
userSSOIdTenant :: Text,
-- | An XML blob specifying the user's ID on the identity provider's side.
userSSOIdSubject :: Text
}
--
-- FUTUREWORK: rename the data type to @UserSparId@ (not the two constructors, those are ok).
data UserSSOId
= UserSSOId
-- An XML blob pointing to the identity provider that can confirm
-- user's identity.
Text
-- An XML blob specifying the user's ID on the identity provider's side.
Text
| UserScimExternalId
Copy link
Contributor Author

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.

Text
deriving stock (Eq, Show, Generic)
deriving (Arbitrary) via (GenericUniform UserSSOId)

instance ToJSON UserSSOId where
toJSON (UserSSOId tenant subject) = object ["tenant" .= tenant, "subject" .= subject]
toJSON = \case
UserSSOId tenant subject -> object ["tenant" .= tenant, "subject" .= subject]
UserScimExternalId eid -> object ["scim_external_id" .= eid]

instance FromJSON UserSSOId where
parseJSON = withObject "UserSSOId" $ \obj ->
UserSSOId
<$> obj .: "tenant"
<*> obj .: "subject"
parseJSON = withObject "UserSSOId" $ \obj -> do
mtenant <- obj .:? "tenant"
msubject <- obj .:? "subject"
meid <- obj .:? "scim_external_id"
case (mtenant, msubject, meid) of
(Just tenant, Just subject, Nothing) -> pure $ UserSSOId tenant subject
(Nothing, Nothing, Just eid) -> pure $ UserScimExternalId eid
_ -> fail "either need tenant and subject, or scim_external_id, but not both"

-- | If the budget for SMS and voice calls for a phone number
-- has been exhausted within a certain time frame, this timeout
Expand Down
69 changes: 68 additions & 1 deletion services/brig/src/Brig/API/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import Brig.API.Handler
import qualified Brig.API.IdMapping as IdMapping
import Brig.API.Types
import qualified Brig.API.User as API
import Brig.API.Util (validateHandle)
import Brig.App
import qualified Brig.Data.User as Data
import Brig.Options hiding (internalEvents, sesQueue)
Expand Down Expand Up @@ -58,6 +59,7 @@ import Network.Wai.Routing
import Network.Wai.Utilities as Utilities
import Network.Wai.Utilities.Response (json)
import Network.Wai.Utilities.ZAuth (zauthConnId, zauthUserId)
import Wire.API.User
import Wire.API.User.RichInfo

---------------------------------------------------------------------------
Expand Down Expand Up @@ -173,6 +175,10 @@ sitemap = do
.&. accept "application" "json"
.&. jsonRequest @UserSSOId

delete "/i/users/:uid/sso-id" (continue deleteSSOIdH) $
Copy link
Contributor Author

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.

capture "uid"
.&. accept "application" "json"

put "/i/users/:uid/managed-by" (continue updateManagedByH) $
capture "uid"
.&. accept "application" "json"
Expand All @@ -183,6 +189,22 @@ sitemap = do
.&. accept "application" "json"
.&. jsonRequest @RichInfoUpdate

put "/i/users/:uid/handle" (continue updateHandleH) $
capture "uid"
.&. accept "application" "json"
.&. jsonRequest @HandleUpdate

put "/i/users/:uid/name" (continue updateUserNameH) $
capture "uid"
.&. accept "application" "json"
.&. jsonRequest @NameUpdate

get "/i/users/:uid/rich-info" (continue getRichInfoH) $
capture "uid"

head "/i/users/handles/:handle" (continue checkHandleInternalH) $
capture "handle"

post "/i/clients" (continue internalListClientsH) $
accept "application" "json"
.&. jsonRequest @UserSet
Expand Down Expand Up @@ -433,7 +455,14 @@ addPhonePrefixH (_ ::: req) = do
updateSSOIdH :: UserId ::: JSON ::: JsonRequest UserSSOId -> Handler Response
updateSSOIdH (uid ::: _ ::: req) = do
ssoid :: UserSSOId <- parseJsonBody req
success <- lift $ Data.updateSSOId uid ssoid
success <- lift $ Data.updateSSOId uid (Just ssoid)
if success
then return empty
else return . setStatus status404 $ plain "User does not exist or has no team."

deleteSSOIdH :: UserId ::: JSON -> Handler Response
deleteSSOIdH (uid ::: _) = do
success <- lift $ Data.updateSSOId uid Nothing
if success
then return empty
else return . setStatus status404 $ plain "User does not exist or has no team."
Expand All @@ -457,6 +486,44 @@ updateRichInfo uid rup = do
-- Intra.onUserEvent uid (Just conn) (richInfoUpdate uid ri)
lift $ Data.updateRichInfo uid (RichInfoAssocList richInfo)

getRichInfoH :: UserId -> Handler Response
getRichInfoH uid = json <$> getRichInfo uid

getRichInfo :: UserId -> Handler RichInfo
getRichInfo uid = RichInfo . fromMaybe emptyRichInfoAssocList <$> lift (API.lookupRichInfo uid)

updateHandleH :: UserId ::: JSON ::: JsonRequest HandleUpdate -> Handler Response
updateHandleH (uid ::: _ ::: body) = empty <$ (updateHandle uid =<< parseJsonBody body)

updateHandle :: UserId -> HandleUpdate -> Handler ()
updateHandle uid (HandleUpdate handleUpd) = do
handle <- validateHandle handleUpd
API.changeHandle uid Nothing handle !>> changeHandleError

updateUserNameH :: UserId ::: JSON ::: JsonRequest NameUpdate -> Handler Response
updateUserNameH (uid ::: _ ::: body) = empty <$ (updateUserName uid =<< parseJsonBody body)

updateUserName :: UserId -> NameUpdate -> Handler ()
updateUserName uid (NameUpdate nameUpd) = do
name <- either (const $ throwStd invalidUser) pure $ mkName nameUpd
let uu =
UserUpdate
{ uupName = Just name,
uupPict = Nothing,
uupAssets = Nothing,
uupAccentId = Nothing
}
lift (Data.lookupUser uid) >>= \case
Just _ -> lift $ API.updateUser uid Nothing uu
Nothing -> throwStd invalidUser

checkHandleInternalH :: Text -> Handler Response
checkHandleInternalH =
API.checkHandle >=> \case
API.CheckHandleInvalid -> throwE (StdError invalidHandle)
API.CheckHandleFound -> pure $ setStatus status200 empty
API.CheckHandleNotFound -> pure $ setStatus status404 empty

getContactListH :: JSON ::: UserId -> Handler Response
getContactListH (_ ::: uid) = do
contacts <- lift $ API.lookupContactList uid
Expand Down
4 changes: 2 additions & 2 deletions services/brig/src/Brig/API/Public.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

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.

return empty

changePhoneH :: UserId ::: ConnId ::: JsonRequest Public.PhoneUpdate -> Handler Response
Expand Down Expand Up @@ -1177,7 +1177,7 @@ changeHandleH (u ::: conn ::: req) = do
changeHandle :: UserId -> ConnId -> Public.HandleUpdate -> Handler ()
changeHandle u conn (Public.HandleUpdate h) = do
handle <- API.validateHandle h
API.changeHandle u conn handle !>> changeHandleError
API.changeHandle u (Just conn) handle !>> changeHandleError

beginPasswordResetH :: JSON ::: JsonRequest Public.NewPasswordReset -> Handler Response
beginPasswordResetH (_ ::: req) = do
Expand Down
12 changes: 6 additions & 6 deletions services/brig/src/Brig/API/User.hs
Original file line number Diff line number Diff line change
Expand Up @@ -335,10 +335,10 @@ checkRestrictedUserCreation new = do
-- FUTUREWORK: this and other functions should refuse to modify a ManagedByScim user. See
-- {#SparBrainDump} https://github.com/zinfra/backend-issues/issues/1632

updateUser :: UserId -> ConnId -> UserUpdate -> AppIO ()
updateUser uid conn uu = do
updateUser :: UserId -> Maybe ConnId -> UserUpdate -> AppIO ()
updateUser uid mconn uu = do
Data.updateUser uid uu
Intra.onUserEvent uid (Just conn) (profileUpdated uid uu)
Intra.onUserEvent uid mconn (profileUpdated uid uu)

-------------------------------------------------------------------------------
-- Update Locale
Expand All @@ -359,8 +359,8 @@ changeManagedBy uid conn (ManagedByUpdate mb) = do
--------------------------------------------------------------------------------
-- Change Handle

changeHandle :: UserId -> ConnId -> Handle -> ExceptT ChangeHandleError AppIO ()
changeHandle uid conn hdl = do
changeHandle :: UserId -> Maybe ConnId -> Handle -> ExceptT ChangeHandleError AppIO ()
changeHandle uid mconn hdl = do
when (isBlacklistedHandle hdl) $
throwE ChangeHandleInvalid
usr <- lift $ Data.lookupUser uid
Expand All @@ -374,7 +374,7 @@ changeHandle uid conn hdl = do
claimed <- lift $ claimHandle (userId u) (userHandle u) hdl
unless claimed $
throwE ChangeHandleExists
lift $ Intra.onUserEvent uid (Just conn) (handleUpdated uid hdl)
lift $ Intra.onUserEvent uid mconn (handleUpdated uid hdl)

--------------------------------------------------------------------------------
-- Check Handle
Expand Down
4 changes: 2 additions & 2 deletions services/brig/src/Brig/Data/User.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

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?

Copy link
Contributor Author

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.

updateSSOId u ssoid = do
mteamid <- lookupUserTeam u
case mteamid of
Expand Down Expand Up @@ -549,7 +549,7 @@ userEmailUpdate = "UPDATE user SET email = ? WHERE id = ?"
userPhoneUpdate :: PrepQuery W (Phone, UserId) ()
userPhoneUpdate = "UPDATE user SET phone = ? WHERE id = ?"

userSSOIdUpdate :: PrepQuery W (UserSSOId, UserId) ()
userSSOIdUpdate :: PrepQuery W (Maybe UserSSOId, UserId) ()
userSSOIdUpdate = "UPDATE user SET sso_id = ? WHERE id = ?"

userManagedByUpdate :: PrepQuery W (ManagedBy, UserId) ()
Expand Down
2 changes: 1 addition & 1 deletion services/brig/src/Brig/Team/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ deleteInvitationH (_ ::: uid ::: tid ::: iid) = do
deleteInvitation :: UserId -> TeamId -> InvitationId -> Handler ()
deleteInvitation uid tid iid = do
ensurePermissions uid tid [Team.AddTeamMember]
lift $ DB.deleteInvitation tid iid
DB.deleteInvitation tid iid

listInvitationsH :: JSON ::: UserId ::: TeamId ::: Maybe InvitationId ::: Range 1 500 Int32 -> Handler Response
listInvitationsH (_ ::: uid ::: tid ::: start ::: size) = do
Expand Down
1 change: 1 addition & 0 deletions services/spar/package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ executables:
- silently
- spar
- stm
- tasty-hunit
- tinylog
- wai
- wai-extra
Expand Down
4 changes: 3 additions & 1 deletion services/spar/schema/src/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import qualified System.Logger.Extended as Log
import Util.Options
import qualified V0
import qualified V1
import qualified V10
import qualified V2
import qualified V3
import qualified V4
Expand Down Expand Up @@ -51,7 +52,8 @@ main = do
V6.migration,
V7.migration,
V8.migration,
V9.migration
V9.migration,
V10.migration
-- When adding migrations here, don't forget to update
-- 'schemaVersion' in Spar.Data

Expand Down
37 changes: 37 additions & 0 deletions services/spar/schema/src/V10.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2020 Wire Swiss GmbH <[email protected]>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- 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 V10
( migration,
)
where

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

migration :: Migration
migration = Migration 10 "Add table for mapping scim external ids to brig user ids" $ do
void $
schema'
[r|
CREATE TABLE if not exists scim_external_ids
( external text
, user uuid
, primary key (external)
) with compaction = {'class': 'LeveledCompactionStrategy'};
|]
5 changes: 4 additions & 1 deletion services/spar/spar.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ cabal-version: 1.12
--
-- see: https://github.com/sol/hpack
--
-- hash: c4215bccf7e235dad19c5eb68954faf664f92ff0b3d7aeacdfb6ac21b448503b
-- hash: 88a250ccd05fb0ad0b42a8b133068a52e559f6b5e00efb0df3098445c92f2a45

name: spar
version: 0.1
Expand Down Expand Up @@ -207,6 +207,7 @@ executable spar-integration
Test.Spar.Scim.UserSpec
Util
Util.Core
Util.Email
Util.Scim
Util.Types
Paths_spar
Expand Down Expand Up @@ -277,6 +278,7 @@ executable spar-integration
, stm
, string-conversions
, swagger2
, tasty-hunit
, text
, text-latin1
, time
Expand Down Expand Up @@ -306,6 +308,7 @@ executable spar-schema
other-modules:
V0
V1
V10
V2
V3
V4
Expand Down
Loading