Skip to content
Draft
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
522203e
Add Spar.Scim.Group module.
fisx Oct 13, 2025
00dab5f
Create scim user group in subsystem.
fisx Oct 13, 2025
ed52e30
createScimGroupImpl (wip)
fisx Oct 13, 2025
93fffce
Unit tests, mostly.
fisx Oct 15, 2025
b451fde
More unit test stuff.
fisx Oct 15, 2025
f64499f
Test works: create scim user group, get it back.
fisx Oct 15, 2025
424b937
Add failing unit test (no non-scim users in scim groups).
fisx Oct 15, 2025
66ad80a
Make unit test pass (wip).
fisx Oct 15, 2025
12468a9
Add pending test case for getScimGroup.
fisx Oct 15, 2025
96619bf
Tweak probability distribution over test cases.
fisx Oct 16, 2025
9d93c3a
Fix typo
fisx Oct 16, 2025
9758c8c
Better name for effect action.
fisx Oct 16, 2025
17e83f3
Connect scim subsystem to api in spar. (wip)
fisx Oct 16, 2025
7b16b78
rm unused library import in spar.cabal
fisx Oct 16, 2025
615dff6
update obsolete comment
fisx Oct 17, 2025
7b17acc
hook scim subsystem into spar. (wip)
fisx Oct 17, 2025
810dac2
Extend spar env to handle scim subsystem. (wip)
fisx Oct 17, 2025
0778c5c
drive-by refactoring.
fisx Oct 17, 2025
761759e
ScimSubsystem error handling in spar.
fisx Oct 17, 2025
a2ce9d2
Fixup
fisx Oct 17, 2025
95f2595
Add effects to spar: GalleyAPIAccess, TeamSubsystem
fisx Oct 20, 2025
07ae8f8
Removed TODO (this is not what we should focus on!)
fisx Oct 21, 2025
f8ddcf4
Fill in more effects in Spar.CanonicalInterpreter.
fisx Oct 21, 2025
ece8fd5
Add {user,auth}subsystem to spar.
fisx Oct 21, 2025
531533a
Entire code base: rework error handling [WIP]
fisx Oct 22, 2025
617936d
fixup
fisx Oct 22, 2025
28eb166
check point: spar compiles with undefineds
blackheaven Oct 22, 2025
c621779
fix: complete most of the interpreters
blackheaven Oct 22, 2025
8a365d0
Blindly fix some compiler errors.
fisx Oct 23, 2025
cb48f22
Minor refactor
eyeinsky Oct 22, 2025
79f0bf6
Get user group via SCIM
eyeinsky Oct 23, 2025
a15b603
effect and interpreter for update
battermann Oct 28, 2025
1dcac60
clean up
battermann Oct 28, 2025
90dc8b8
wire up the handler
battermann Oct 28, 2025
7809e2e
basic test
battermann Oct 28, 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
12 changes: 12 additions & 0 deletions integration/test/API/Spar.hs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ updateScimUser domain scimToken userId scimUser = do
& addJSON body . addHeader "Authorization" ("Bearer " <> scimToken)
& addHeader "Accept" "application/scim+json"

createScimUserGroup :: (HasCallStack, MakesValue domain, MakesValue scimUserGroup) => domain -> String -> scimUserGroup -> App Response
createScimUserGroup domain token scimUserGroup = do
req <- baseRequest domain Spar Versioned "/scim/v2/Groups"
body <- make scimUserGroup
submit "POST" $ req & addJSON body . addHeader "Authorization" ("Bearer " <> token)

updateScimUserGroup :: (HasCallStack, MakesValue domain, MakesValue scimUserGroup) => domain -> String -> String -> scimUserGroup -> App Response
updateScimUserGroup domain token groupId scimUserGroup = do
req <- baseRequest domain Spar Versioned $ joinHttpPath ["scim", "v2", "Groups", groupId]
body <- make scimUserGroup
submit "PUT" $ req & addJSON body . addHeader "Authorization" ("Bearer " <> token)

-- | https://staging-nginz-https.zinfra.io/v12/api/swagger-ui/#/default/idp-create
createIdp :: (HasCallStack, MakesValue user) => user -> SAML.IdPMetadata -> App Response
createIdp user metadata = do
Expand Down
70 changes: 69 additions & 1 deletion integration/test/Test/Spar.hs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{-# OPTIONS_GHC -Wno-incomplete-patterns -Wno-ambiguous-fields #-}
{-# OPTIONS_GHC -Wno-incomplete-patterns -Wno-ambiguous-fields #-}

module Test.Spar where

Expand Down Expand Up @@ -362,6 +362,74 @@ testSparCreateScimTokenWithName = do
assoc <- token %. "id"
token %. "name" `shouldMatch` Just assoc

----------------------------------------------------------------------
-- scim group stuff

testSparScimCreateUserGroup :: (HasCallStack) => App ()
testSparScimCreateUserGroup = do
(owner, _, _) <- createTeam OwnDomain 1
tok <- createScimTokenV6 owner def >>= \resp -> resp.json %. "token" >>= asString
let scimUserGroup =
object
[ "schemas" .= ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName" .= "ze groop",
"members"
.= [ object
[ "typ" .= "User",
"$ref" .= "https://...", -- TODO: we should probably validate these? or just ignore them?
"value" .= "ea2e4bf0-aa5e-11f0-96ad-e776a606779b"
]
]
]
resp <- createScimUserGroup OwnDomain tok scimUserGroup
assertSuccess resp

-- get group here via resp

testSparScimUpdateUserGroup :: (HasCallStack) => App ()
testSparScimUpdateUserGroup = do
(owner, _, u1 : u2 : u3 : _) <- createTeam OwnDomain 4
u1Id <- u1 %. "id" >>= asString
u2Id <- u2 %. "id" >>= asString
u3Id <- u3 %. "id" >>= asString
tok <- createScimToken owner def >>= getJSON 200 >>= (%. "token") >>= asString
let scimUserGroup =
object
[ "schemas" .= ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName" .= "My funky group",
"members"
.= [ object
[ "value" .= u1Id,
"type" .= "User",
"$ref" .= ("http://example.com:8088/scim/v2/Users/" <> u1Id)
],
object
[ "value" .= u2Id,
"type" .= "User",
"$ref" .= ("http://example.com:8088/scim/v2/Users/" <> u2Id)
]
]
]
gid <- createScimUserGroup OwnDomain tok scimUserGroup >>= getJSON 200 >>= (%. "id") >>= asString
let scimUserGroupUpdated =
object
[ "schemas" .= ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"displayName" .= "My even funkier group",
"members"
.= [ object
[ "value" .= u2Id,
"type" .= "User",
"$ref" .= ("http://example.com:8088/scim/v2/Users/" <> u2Id)
],
object
[ "value" .= u3Id,
"type" .= "User",
"$ref" .= ("http://example.com:8088/scim/v2/Users/" <> u3Id)
]
]
]
updateScimUserGroup OwnDomain tok gid scimUserGroupUpdated >>= assertSuccess

----------------------------------------------------------------------
-- saml stuff

Expand Down
8 changes: 6 additions & 2 deletions libs/hscim/hscim.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ description:
The README file will answer all the questions you might have

category: Web
homepage: https://github.com/wireapp/wire-server/libs/hscim/README.md
homepage:
https://github.com/wireapp/wire-server/blob/develop/libs/hscim/README.md

bug-reports: https://github.com/wireapp/wire-server/issues
author: Wire Swiss GmbH
maintainer: Wire Swiss GmbH <backend@wire.com>
Expand All @@ -21,7 +23,7 @@ extra-source-files:
source-repository head
type: git
location: https://github.com/wireapp/wire-server
subdir: hscim
subdir: libs/hscim

library
exposed-modules:
Expand Down Expand Up @@ -115,9 +117,11 @@ library
, template-haskell
, text
, time
, utf8-string
, uuid
, wai
, wai-extra
, wai-utilities

default-language: Haskell2010

Expand Down
7 changes: 7 additions & 0 deletions libs/hscim/src/Web/Scim/Schema/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import qualified Data.CaseInsensitive as CI
import Data.List (nub, (\\))
import Data.String.Conversions (cs)
import Data.Text (Text, pack, unpack)
import qualified Data.Text as Text
import qualified Network.URI as Network

data WithId id a = WithId
Expand All @@ -49,6 +50,12 @@ instance (FromJSON id, FromJSON a) => FromJSON (WithId id a) where
newtype URI = URI {unURI :: Network.URI}
deriving (Show, Eq)

uriToString :: URI -> String
uriToString = (\uri -> Network.uriToString Prelude.id uri "") . unURI

uriToText :: URI -> Text
uriToText = Text.pack . uriToString

instance FromJSON URI where
parseJSON = withText "URI" $ \uri -> case Network.parseURI (unpack uri) of
Nothing -> fail "Invalid URI"
Expand Down
18 changes: 17 additions & 1 deletion libs/hscim/src/Web/Scim/Schema/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,21 @@ module Web.Scim.Schema.Error
forbidden,
serverError,

-- * Servant interoperability
-- * Servant/Wai interoperability
scimToServerError,
scimToWaiError,
)
where

import Control.Exception
import Data.Aeson hiding (Error)
import Data.ByteString.UTF8 (fromString)
import Data.Text (Text, pack)
import qualified Data.Text.Lazy.Encoding as LText
import GHC.Generics (Generic)
import qualified Network.HTTP.Types.Header as HTTP
import qualified Network.HTTP.Types.Status as HTTP
import qualified Network.Wai.Utilities.Error as Wai
import Servant (ServerError (..))
import Web.Scim.Schema.Common
import Web.Scim.Schema.Schema
Expand Down Expand Up @@ -175,6 +181,16 @@ serverError details =
----------------------------------------------------------------------------
-- Servant

-- | Convert a SCIM 'Error' to a Servant one by encoding it with the
-- appropriate headers.
-- We would like to use Wire.Error.HttpError from wire-subsystems,
-- but hscim can't depend on that.
scimToWaiError :: ScimError -> (Wai.Error, [HTTP.Header])
scimToWaiError err = (Wai.mkError e "scim-error" (LText.decodeUtf8 $ encode err), hs)
where
e = HTTP.Status (unStatus (status err)) (fromString $ reasonPhrase (status err))
hs = [("Content-Type", "application/scim+json;charset=utf-8")]

-- | Convert a SCIM 'Error' to a Servant one by encoding it with the
-- appropriate headers.
scimToServerError :: ScimError -> ServerError
Expand Down
4 changes: 4 additions & 0 deletions libs/types-common/src/Data/Id.hs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ module Data.Id
ScimTokenId,
parseIdFromText,
idToText,
idToString,
idObjectSchema,
IdObject (..),

Expand Down Expand Up @@ -263,6 +264,9 @@ parseIdFromText = maybe (Left "UUID.fromText failed") (Right . Id) . UUID.fromTe
idToText :: Id a -> Text
idToText = UUID.toText . toUUID

idToString :: Id a -> String
idToString = UUID.toString . toUUID

instance Cql (Id a) where
ctype = retag (ctype :: Tagged UUID ColumnType)
toCql = toCql . toUUID
Expand Down
4 changes: 2 additions & 2 deletions libs/wire-api/src/Wire/API/Routes/Public/Brig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ type UserGroupAPI =
)
:<|> Named
"get-user-group"
( Summary "Fetch a group accessible from the logged-in user"
( Summary "Fetch a group accessible to the logged-in user"
:> From 'V10
:> ZLocalUser
:> CanThrow 'UserGroupNotFound
Expand All @@ -335,7 +335,7 @@ type UserGroupAPI =
)
:<|> Named
"get-user-groups"
( Summary "Fetch groups accessible from the logged-in user"
( Summary "Fetch groups accessible to the logged-in user"
:> From 'V10
:> ZLocalUser
:> "user-groups"
Expand Down
8 changes: 7 additions & 1 deletion libs/wire-api/src/Wire/API/Routes/Public/Spar.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import Servant.Server.Experimental.Auth
import URI.ByteString qualified as URI
import Web.Scim.Capabilities.MetaSchema as Scim.Meta
import Web.Scim.Class.Auth as Scim.Auth
import Web.Scim.Class.Group as Scim.Group
import Web.Scim.Class.User as Scim.User
import Wire.API.Deprecated (Deprecated)
import Wire.API.Error
Expand Down Expand Up @@ -260,7 +261,12 @@ data ScimSite tag route = ScimSite
route
:- Header "Authorization" (Scim.Auth.AuthData tag)
:> "Users"
:> ToServantApi (Scim.User.UserSite tag)
:> ToServantApi (Scim.User.UserSite tag),
groups ::
route
:- Header "Authorization" (Scim.Auth.AuthData tag)
:> "Groups"
:> ToServantApi (Scim.Group.GroupSite tag)
}
deriving (Generic)

Expand Down
2 changes: 1 addition & 1 deletion libs/wire-api/src/Wire/API/User/Scim.hs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ instance Scim.User.UserTypes SparTag where
supportedSchemas = userSchemas

instance Scim.Group.GroupTypes SparTag where
type GroupId SparTag = ()
type GroupId SparTag = UserGroupId

instance Scim.Auth.AuthTypes SparTag where
type AuthData SparTag = ScimToken
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 @@ -45,6 +45,7 @@
, hasql-th
, hasql-transaction
, hex
, hscim
, HsOpenSSL
, hspec
, hspec-discover
Expand Down Expand Up @@ -157,6 +158,7 @@ mkDerivation {
hasql-th
hasql-transaction
hex
hscim
HsOpenSSL
hspec
html-entities
Expand Down Expand Up @@ -257,6 +259,7 @@ mkDerivation {
hasql-th
hasql-transaction
hex
hscim
HsOpenSSL
hspec
html-entities
Expand Down
12 changes: 12 additions & 0 deletions libs/wire-subsystems/src/Wire/Error.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ import Imports
import Network.HTTP.Types
import Network.Wai.Utilities.Error qualified as Wai
import Network.Wai.Utilities.JSONResponse
import Servant (ServerError)

-- | Error thrown to the user
data HttpError where
StdError :: !Wai.Error -> HttpError
RichError :: (ToJSON a) => !Wai.Error -> !a -> [Header] -> HttpError

instance Eq HttpError where
StdError e == StdError e' = e == e'
-- RichErrors are always different because we don't know the types a, a' here
_ == _ = False

instance Show HttpError where
show (StdError werr) = "StdError (" <> show werr <> ")"
show e@(RichError _ _ headers) = "RichError (json = " <> Text.unpack (Text.decodeUtf8 $ BS.toStrict $ encode e) <> ", headers = " <> show headers <> ")"
Expand Down Expand Up @@ -52,3 +58,9 @@ postgresUsageErrorToHttpError err = case err of
StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err))
ConnectionUsageError _ -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err))
AcquisitionTimeoutUsageError -> StdError (Wai.mkError status500 "server-error" (LT.pack $ "postgres: " <> show err))

httpErrorToServerError :: HttpError -> ServerError
httpErrorToServerError = undefined

serverErrorToHttpError :: ServerError -> HttpError
serverErrorToHttpError = undefined
4 changes: 4 additions & 0 deletions libs/wire-subsystems/src/Wire/ParseException.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Network.HTTP.Types
import Network.Wai.Utilities
import Network.Wai.Utilities.JSONResponse
import Wire.API.Error
import Wire.Error

-- | Failed to parse a response from another service.
data ParseException = ParseException
Expand All @@ -23,3 +24,6 @@ instance Exception ParseException where

instance APIError ParseException where
toResponse _ = waiErrorToJSONResponse $ mkError status500 "internal-error" "Internal server error"

parseExceptionToHttpError :: ParseException -> HttpError
parseExceptionToHttpError (ParseException _ _) = StdError (mkError status500 "internal-error" mempty)
15 changes: 15 additions & 0 deletions libs/wire-subsystems/src/Wire/ScimSubsystem.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{-# LANGUAGE TemplateHaskell #-}

module Wire.ScimSubsystem where

import Data.Id
import Polysemy
import Web.Scim.Class.Group qualified as SCG
import Wire.API.User.Scim (SparTag)

data ScimSubsystem m a where
ScimCreateUserGroup :: TeamId -> SCG.Group -> ScimSubsystem m (SCG.StoredGroup SparTag)
ScimGetUserGroup :: TeamId -> UserGroupId -> ScimSubsystem m (SCG.StoredGroup SparTag)
ScimUpdateUserGroup :: TeamId -> UserGroupId -> SCG.Group -> ScimSubsystem m (SCG.StoredGroup SparTag)

makeSem ''ScimSubsystem
Loading