Skip to content

Commit d1df63f

Browse files
Leif Battermannfisx
andauthored
Separate Access Control For Guests And Services (#2035)
Co-authored-by: fisx <[email protected]>
1 parent 15af92d commit d1df63f

File tree

60 files changed

+517
-248
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+517
-248
lines changed

changelog.d/0-release-notes/pr-2035

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Backend now separates conversation access control for guests and services. The old access roles are still supported but it is encouraged to upgrade clients since mapping between the old access roles and the new access roles are is not isomorphic. For more details refer to the API changes changelog, the Swagger docs, or the technical specification.

changelog.d/1-api-changes/pr-2035

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Endpoints that recently have accepted `access_role` in their payload will now accept `access_role_v2` as well which will take precedence over `access_role`. See Swagger docs for how values are mapped. Endpoints that recently have returned `access_role` in their payload will now additionally return the `access_role_v2` field.

changelog.d/2-features/pr-2035

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Conversation access roles now distinguish between guests and services.

docs/reference/cassandra-schema.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ CREATE TABLE galley_test.conversation (
227227
conv uuid PRIMARY KEY,
228228
access set<int>,
229229
access_role int,
230+
access_roles_v2 set<int>,
230231
creator uuid,
231232
deleted boolean,
232233
message_timer bigint,

libs/galley-types/src/Galley/Types.hs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ module Galley.Types
2727
cnvType,
2828
cnvCreator,
2929
cnvAccess,
30-
cnvAccessRole,
30+
cnvAccessRoles,
3131
cnvName,
3232
cnvTeam,
3333
cnvMessageTimer,
@@ -55,7 +55,8 @@ module Galley.Types
5555
TypingData (..),
5656
OtrMessage (..),
5757
Access (..),
58-
AccessRole (..),
58+
AccessRoleV2 (..),
59+
AccessRoleLegacy (..),
5960
ConversationList (..),
6061
ConversationRename (..),
6162
ConversationAccessData (..),

libs/wire-api-federation/src/Wire/API/Federation/API/Galley.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import Servant.API
2929
import Wire.API.Arbitrary (Arbitrary, GenericUniform (..))
3030
import Wire.API.Conversation
3131
( Access,
32-
AccessRole,
32+
AccessRoleV2,
3333
ConvType,
3434
ConversationMetadata,
3535
ReceiptMode,
@@ -118,7 +118,7 @@ data NewRemoteConversation conv = NewRemoteConversation
118118
-- | The conversation type
119119
rcCnvType :: ConvType,
120120
rcCnvAccess :: [Access],
121-
rcCnvAccessRole :: AccessRole,
121+
rcCnvAccessRoles :: Set AccessRoleV2,
122122
-- | The conversation name,
123123
rcCnvName :: Maybe Text,
124124
-- | Members of the conversation apart from the creator

libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/NewRemoteConversation.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ testObject_NewRemoteConversation1 =
3737
rcCnvId = Id (fromJust (UUID.fromString "d13dbe58-d4e3-450f-9c0c-1e632f548740")),
3838
rcCnvType = RegularConv,
3939
rcCnvAccess = [InviteAccess, CodeAccess],
40-
rcCnvAccessRole = ActivatedAccessRole,
40+
rcCnvAccessRoles = Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole],
4141
rcCnvName = Just "gossip",
4242
rcNonCreatorMembers =
4343
Set.fromList
@@ -76,7 +76,7 @@ testObject_NewRemoteConversation2 =
7676
rcCnvId = Id (fromJust (UUID.fromString "d13dbe58-d4e3-450f-9c0c-1e632f548740")),
7777
rcCnvType = One2OneConv,
7878
rcCnvAccess = [],
79-
rcCnvAccessRole = ActivatedAccessRole,
79+
rcCnvAccessRoles = Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole],
8080
rcCnvName = Nothing,
8181
rcNonCreatorMembers = Set.fromList [],
8282
rcMessageTimer = Nothing,

libs/wire-api-federation/test/golden/testObject_NewRemoteConversation1.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
"id": "6801e49b-918c-4eef-baed-f18522152fca"
3030
}
3131
],
32-
"cnv_access_role": "activated",
32+
"cnv_access_roles": [
33+
"team_member",
34+
"non_team_member"
35+
],
3336
"cnv_type": 0,
3437
"receipt_mode": 42,
3538
"message_timer": 1000,

libs/wire-api-federation/test/golden/testObject_NewRemoteConversation2.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
"time": "1864-04-12T12:22:43.673Z",
44
"cnv_access": [],
55
"non_creator_members": [],
6-
"cnv_access_role": "activated",
6+
"cnv_access_roles": [
7+
"team_member",
8+
"non_team_member"
9+
],
710
"cnv_type": 2,
811
"receipt_mode": null,
912
"message_timer": null,

libs/wire-api/src/Wire/API/Conversation.hs

Lines changed: 130 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,14 @@ module Wire.API.Conversation
2424
( -- * Conversation
2525
ConversationMetadata (..),
2626
Conversation (..),
27-
mkConversation,
2827
cnvType,
2928
cnvCreator,
3029
cnvAccess,
31-
cnvAccessRole,
3230
cnvName,
3331
cnvTeam,
3432
cnvMessageTimer,
3533
cnvReceiptMode,
34+
cnvAccessRoles,
3635
ConversationCoverView (..),
3736
ConversationList (..),
3837
ListConversations (..),
@@ -46,9 +45,14 @@ module Wire.API.Conversation
4645

4746
-- * Conversation properties
4847
Access (..),
49-
AccessRole (..),
48+
AccessRoleV2 (..),
49+
AccessRoleLegacy (..),
5050
ConvType (..),
5151
ReceiptMode (..),
52+
fromAccessRoleLegacy,
53+
toAccessRoleLegacy,
54+
defRole,
55+
maybeRole,
5256

5357
-- * create
5458
NewConv (..),
@@ -90,7 +94,6 @@ import Control.Applicative
9094
import Control.Lens (at, (?~))
9195
import Data.Aeson (FromJSON (..), ToJSON (..))
9296
import qualified Data.Aeson as A
93-
import qualified Data.Aeson.Types as A
9497
import Data.Id
9598
import Data.List.NonEmpty (NonEmpty)
9699
import Data.List1
@@ -118,7 +121,7 @@ data ConversationMetadata = ConversationMetadata
118121
-- FUTUREWORK: Make this a qualified user ID.
119122
cnvmCreator :: UserId,
120123
cnvmAccess :: [Access],
121-
cnvmAccessRole :: AccessRole,
124+
cnvmAccessRoles :: Set AccessRoleV2,
122125
cnvmName :: Maybe Text,
123126
-- FUTUREWORK: Think if it makes sense to make the team ID qualified due to
124127
-- federation.
@@ -130,13 +133,32 @@ data ConversationMetadata = ConversationMetadata
130133
deriving (Arbitrary) via (GenericUniform ConversationMetadata)
131134
deriving (FromJSON, ToJSON) via Schema ConversationMetadata
132135

133-
conversationMetadataObjectSchema ::
134-
SchemaP
135-
SwaggerDoc
136-
A.Object
137-
[A.Pair]
138-
ConversationMetadata
139-
ConversationMetadata
136+
accessRolesSchema :: ObjectSchema SwaggerDoc (Set AccessRoleV2)
137+
accessRolesSchema = toOutput .= accessRolesSchemaTuple `withParser` validate
138+
where
139+
toOutput accessRoles = (Just $ toAccessRoleLegacy accessRoles, Just accessRoles)
140+
validate =
141+
\case
142+
(_, Just v2) -> pure v2
143+
(Just legacy, Nothing) -> pure $ fromAccessRoleLegacy legacy
144+
(Nothing, Nothing) -> fail "access_role|access_role_v2"
145+
146+
accessRolesSchemaOpt :: ObjectSchema SwaggerDoc (Maybe (Set AccessRoleV2))
147+
accessRolesSchemaOpt = toOutput .= accessRolesSchemaTuple `withParser` validate
148+
where
149+
toOutput accessRoles = (toAccessRoleLegacy <$> accessRoles, accessRoles)
150+
validate =
151+
\case
152+
(_, Just v2) -> pure $ Just v2
153+
(Just legacy, Nothing) -> pure $ Just (fromAccessRoleLegacy legacy)
154+
(Nothing, Nothing) -> pure Nothing
155+
156+
accessRolesSchemaTuple :: ObjectSchema SwaggerDoc (Maybe AccessRoleLegacy, Maybe (Set AccessRoleV2))
157+
accessRolesSchemaTuple =
158+
(,) <$> fst .= optField "access_role" (maybeWithDefault A.Null schema)
159+
<*> snd .= optField "access_role_v2" (maybeWithDefault A.Null $ set schema)
160+
161+
conversationMetadataObjectSchema :: ObjectSchema SwaggerDoc ConversationMetadata
140162
conversationMetadataObjectSchema =
141163
ConversationMetadata
142164
<$> cnvmType .= field "type" schema
@@ -146,18 +168,17 @@ conversationMetadataObjectSchema =
146168
(description ?~ "The creator's user ID")
147169
schema
148170
<*> cnvmAccess .= field "access" (array schema)
149-
<*> cnvmAccessRole .= field "access_role" schema
171+
<*> cnvmAccessRoles .= accessRolesSchema
150172
<*> cnvmName .= optField "name" (maybeWithDefault A.Null schema)
151173
<* const ("0.0" :: Text) .= optional (field "last_event" schema)
152174
<* const ("1970-01-01T00:00:00.000Z" :: Text)
153175
.= optional (field "last_event_time" schema)
154176
<*> cnvmTeam .= optField "team" (maybeWithDefault A.Null schema)
155177
<*> cnvmMessageTimer
156-
.= ( optFieldWithDocModifier
157-
"message_timer"
158-
(description ?~ "Per-conversation message timer (can be null)")
159-
(maybeWithDefault A.Null schema)
160-
)
178+
.= optFieldWithDocModifier
179+
"message_timer"
180+
(description ?~ "Per-conversation message timer (can be null)")
181+
(maybeWithDefault A.Null schema)
161182
<*> cnvmReceiptMode .= optField "receipt_mode" (maybeWithDefault A.Null schema)
162183

163184
instance ToSchema ConversationMetadata where
@@ -178,21 +199,6 @@ data Conversation = Conversation
178199
deriving (Arbitrary) via (GenericUniform Conversation)
179200
deriving (FromJSON, ToJSON, S.ToSchema) via Schema Conversation
180201

181-
mkConversation ::
182-
Qualified ConvId ->
183-
ConvType ->
184-
UserId ->
185-
[Access] ->
186-
AccessRole ->
187-
Maybe Text ->
188-
ConvMembers ->
189-
Maybe TeamId ->
190-
Maybe Milliseconds ->
191-
Maybe ReceiptMode ->
192-
Conversation
193-
mkConversation qid ty uid acc role name mems tid ms rm =
194-
Conversation qid (ConversationMetadata ty uid acc role name tid ms rm) mems
195-
196202
cnvType :: Conversation -> ConvType
197203
cnvType = cnvmType . cnvMetadata
198204

@@ -202,8 +208,8 @@ cnvCreator = cnvmCreator . cnvMetadata
202208
cnvAccess :: Conversation -> [Access]
203209
cnvAccess = cnvmAccess . cnvMetadata
204210

205-
cnvAccessRole :: Conversation -> AccessRole
206-
cnvAccessRole = cnvmAccessRole . cnvMetadata
211+
cnvAccessRoles :: Conversation -> Set AccessRoleV2
212+
cnvAccessRoles = cnvmAccessRoles . cnvMetadata
207213

208214
cnvName :: Conversation -> Maybe Text
209215
cnvName = cnvmName . cnvMetadata
@@ -421,31 +427,108 @@ typeAccess = Doc.string . Doc.enum $ cs . A.encode <$> [(minBound :: Access) ..]
421427
-- | AccessRoles define who can join conversations. The roles are
422428
-- "supersets", i.e. Activated includes Team and NonActivated includes
423429
-- Activated.
424-
data AccessRole
430+
data AccessRoleLegacy
425431
= -- | Nobody can be invited to this conversation
426432
-- (e.g. it's a 1:1 conversation)
427433
PrivateAccessRole
428434
| -- | Team-only conversation
429435
TeamAccessRole
430436
| -- | Conversation for users who have activated
431-
-- email or phone
437+
-- email, phone or SSO and bots
432438
ActivatedAccessRole
433439
| -- | No checks
434440
NonActivatedAccessRole
441+
deriving stock (Eq, Ord, Show, Generic, Enum, Bounded)
442+
deriving (Arbitrary) via (GenericUniform AccessRoleLegacy)
443+
deriving (ToJSON, FromJSON, S.ToSchema) via Schema AccessRoleLegacy
444+
445+
fromAccessRoleLegacy :: AccessRoleLegacy -> Set AccessRoleV2
446+
fromAccessRoleLegacy = \case
447+
PrivateAccessRole -> privateAccessRole
448+
TeamAccessRole -> teamAccessRole
449+
ActivatedAccessRole -> activatedAccessRole
450+
NonActivatedAccessRole -> nonActivatedAccessRole
451+
452+
privateAccessRole :: Set AccessRoleV2
453+
privateAccessRole = Set.fromList []
454+
455+
teamAccessRole :: Set AccessRoleV2
456+
teamAccessRole = Set.fromList [TeamMemberAccessRole]
457+
458+
activatedAccessRole :: Set AccessRoleV2
459+
activatedAccessRole = Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole, ServiceAccessRole]
460+
461+
nonActivatedAccessRole :: Set AccessRoleV2
462+
nonActivatedAccessRole = Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole, GuestAccessRole, ServiceAccessRole]
463+
464+
defRole :: Set AccessRoleV2
465+
defRole = activatedAccessRole
466+
467+
maybeRole :: ConvType -> Maybe (Set AccessRoleV2) -> Set AccessRoleV2
468+
maybeRole SelfConv _ = privateAccessRole
469+
maybeRole ConnectConv _ = privateAccessRole
470+
maybeRole One2OneConv _ = privateAccessRole
471+
maybeRole RegularConv Nothing = defRole
472+
maybeRole RegularConv (Just r) = r
473+
474+
data AccessRoleV2
475+
= TeamMemberAccessRole
476+
| NonTeamMemberAccessRole
477+
| GuestAccessRole
478+
| ServiceAccessRole
435479
deriving stock (Eq, Ord, Show, Generic)
436-
deriving (Arbitrary) via (GenericUniform AccessRole)
437-
deriving (ToJSON, FromJSON, S.ToSchema) via Schema AccessRole
480+
deriving (Arbitrary) via (GenericUniform AccessRoleV2)
481+
deriving (ToJSON, FromJSON, S.ToSchema) via Schema AccessRoleV2
438482

439-
instance ToSchema AccessRole where
483+
toAccessRoleLegacy :: Set AccessRoleV2 -> AccessRoleLegacy
484+
toAccessRoleLegacy accessRoles = do
485+
fromMaybe NonActivatedAccessRole $ find (allMember accessRoles . fromAccessRoleLegacy) [minBound ..]
486+
where
487+
allMember :: Ord a => Set a -> Set a -> Bool
488+
allMember rhs lhs = all (`Set.member` lhs) rhs
489+
490+
instance ToSchema AccessRoleV2 where
440491
schema =
441-
(S.schema . description ?~ "Which users can join conversations") $
442-
enum @Text "AccessRole" $
492+
(S.schema . description ?~ desc) $
493+
enum @Text "AccessRoleV2" $
494+
mconcat
495+
[ element "team_member" TeamMemberAccessRole,
496+
element "non_team_member" NonTeamMemberAccessRole,
497+
element "guest" GuestAccessRole,
498+
element "service" ServiceAccessRole
499+
]
500+
where
501+
desc =
502+
"Which users/services can join conversations.\
503+
\This replaces the deprecated field `access_role`\
504+
\and allows for a more fine grained configuration of access roles\
505+
\in particular a separation of guest and services access."
506+
507+
instance ToSchema AccessRoleLegacy where
508+
schema =
509+
(S.schema . description ?~ desc) $
510+
enum @Text "AccessRoleLegacy" $
443511
mconcat
444512
[ element "private" PrivateAccessRole,
445513
element "team" TeamAccessRole,
446514
element "activated" ActivatedAccessRole,
447515
element "non_activated" NonActivatedAccessRole
448516
]
517+
where
518+
desc =
519+
"Which users can join conversations (deprecated, use `access_role_v2` instead).\
520+
\Maps to `access_role_v2` as follows:\
521+
\`private` => `[]` - nobody can be invited to this conversation (e.g. it's a 1:1 conversation)\
522+
\`team` => `[team_member]` - team-only conversation\
523+
\`activated` => `[team_member, non_team_member, service]` - conversation for users who have activated email, phone or SSO and services\
524+
\`non_activated` => `[team_member, non_team_member, service, guest]` - all allowed, no checks\
525+
\\
526+
\Maps from `access_role_v2` as follows:\
527+
\`[]` => `private` - nobody can be invited to this conversation (e.g. it's a 1:1 conversation)\
528+
\`[team_member]` => `team` - team-only conversation\
529+
\`[team_member, non_team_member, service]` => `activated` - conversation for users who have activated email, phone or SSO and services\
530+
\`[team_member, non_team_member, service, guest]` => `non_activated` - all allowed, no checks.\
531+
\All other configurations of `access_role_v2` are mapped to the smallest superset containing all given access roles."
449532

450533
data ConvType
451534
= RegularConv
@@ -586,7 +669,7 @@ data NewConv = NewConv
586669
newConvQualifiedUsers :: [Qualified UserId],
587670
newConvName :: Maybe Text,
588671
newConvAccess :: Set Access,
589-
newConvAccessRole :: Maybe AccessRole,
672+
newConvAccessRoles :: Maybe (Set AccessRoleV2),
590673
newConvTeam :: Maybe ConvTeamInfo,
591674
newConvMessageTimer :: Maybe Milliseconds,
592675
newConvReceiptMode :: Maybe ReceiptMode,
@@ -619,7 +702,7 @@ newConvSchema =
619702
<*> newConvName .= maybe_ (optField "name" schema)
620703
<*> (Set.toList . newConvAccess)
621704
.= (fromMaybe mempty <$> optField "access" (Set.fromList <$> array schema))
622-
<*> newConvAccessRole .= maybe_ (optField "access_role" schema)
705+
<*> newConvAccessRoles .= accessRolesSchemaOpt
623706
<*> newConvTeam
624707
.= maybe_
625708
( optFieldWithDocModifier
@@ -766,7 +849,7 @@ modelConversationUpdateName = Doc.defineModel "ConversationUpdateName" $ do
766849

767850
data ConversationAccessData = ConversationAccessData
768851
{ cupAccess :: Set Access,
769-
cupAccessRole :: AccessRole
852+
cupAccessRoles :: Set AccessRoleV2
770853
}
771854
deriving stock (Eq, Show, Generic)
772855
deriving (Arbitrary) via (GenericUniform ConversationAccessData)
@@ -777,14 +860,14 @@ instance ToSchema ConversationAccessData where
777860
object "ConversationAccessData" $
778861
ConversationAccessData
779862
<$> cupAccess .= field "access" (set schema)
780-
<*> cupAccessRole .= field "access_role" schema
863+
<*> cupAccessRoles .= accessRolesSchema
781864

782865
modelConversationAccessData :: Doc.Model
783866
modelConversationAccessData = Doc.defineModel "ConversationAccessData" $ do
784867
Doc.description "Contains conversation properties to update"
785868
Doc.property "access" (Doc.unique $ Doc.array typeAccess) $
786869
Doc.description "List of conversation access modes."
787-
Doc.property "access_role" (Doc.bytes') $
870+
Doc.property "access_role" Doc.bytes' $
788871
Doc.description "Conversation access role: private|team|activated|non_activated"
789872

790873
data ConversationReceiptModeUpdate = ConversationReceiptModeUpdate

0 commit comments

Comments
 (0)