-
Notifications
You must be signed in to change notification settings - Fork 334
Allow choosing a role when creating a team member; fix role change events. #573
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
Conversation
- add optional `Role` field to `InvitationRequest`. - add 'Permissions' field to `Invitation` (schema change in brig-cassandra). - default to `RoleMember` if no role / permissions are given. - move `instance Cql Permissions` to galley-types.
b38ef0e to
a414f6e
Compare
|
I get this when I run integration tests locally: Interestingly, I also get this error on develop. I have no idea how it can be related to this PR. Will investigate tomorrow. If CI passes here, consider this my private problem. |
|
ya, private problem... perhaps if you run the tests often enough on the same state, it somehow goes bad. not sure it's worth exploring, or how to explore it? if we can't think of anything i'll just restart docker-ephemeral, and expect this to go away. |
|
@jschaul You can probably skip reading the two commits from last night that their reverts. I left that in for my own sanity and because non-destructive updates are nice. |
|
(background: clients need a way to get the list of all team members so they know who they can talk to, and they want to be able to extract permissions from that end-point where accessible to avoid extra communication rounds, so my earlier attempt was too simple.) (oh, and i will also push a small swagger update in a minute.) |
| ] | ||
|
|
||
| #ifdef WITH_CQL | ||
| instance Cql.Cql Permissions where |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's your rationale for moving the CQL instances to the galley-types package? Since brig is referring to permissions directly now (so it's used by brig and galley), wouldn't types-common be a better place to move it to? (it also already has a dependency/feature flag on cql).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure which place makes most sense given the related types, but sure, i can move it. will do!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what module should i move this to, then? i can't think of any way to move it that i would prefer over the current code here, but i'm happy to implement a concrete suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Data.Misc ? Or Data.<some new module>.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I decided I'm opposed to this change for now. It becomes more and more obvious that brig and galley aren't really two different services and should be merged. Until we solve that issue, I want to keep the galley types in their proper place, and not spread them out over Data.Galley and Galley.Types.Team.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following the above logic of service types dependencies, can we put this (Permissions and Role) under Data.Role on types-common? Even if galley and brig do get merged, at least until then let's keep things consistent;
Note that the whole invitation should and could be part of galley - the reason we decided against it was because all the email logic/templating/etc. was part of brig
| description "The permissions this user has in the given team \ | ||
| \ (only visible with permission `GetMemberPermissions`)." | ||
| optional -- not optional in the type, but in the json instance. (in | ||
| -- servant, we could probably just add a helper type for this.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not very nice. For other cases where the API-exposed json doesn't match the database-stored type, we created two separate data types that reflect that: the outside TeamMember data type could have an optional permission field, whereas the inside TeamMember doesn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's not something i broke in this PR, though. it's always been not very nice, i just fixed the swagger. i it's ok with you, i'll leave a TODO.
services/brig/src/Brig/API/User.hs
Outdated
| throwE $ DuplicateUserKey uk | ||
| let minvmeta :: Maybe (UserId, UTCTimeMillis) | ||
| minvmeta = (, inCreatedAt inv) <$> inCreatedBy inv | ||
| let minvmeta :: (Maybe (UserId, UTCTimeMillis), Team.Permissions) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is userId and time optional, while permissions are not here? That's a little strange.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
userid is not always applicable (e.g. SCIM), so that has to be optional.
time and userid have just turned up together like that. that could probably be changed, but that would increase the metadata storage further, and i don't see any advantage.
permissions are decided during creation of the Invitation. it's likely i could keep the Maybe Role here and decide it in galley during team user creation, but why? (i guess this has to do with your idea about admins getting removed from the system and the posthumusly causing member creations, but i'm still not entirely convinced.)
services/brig/src/Brig/API/User.hs
Outdated
| addUserToTeamSSO account tid ident = do | ||
| let uid = userId (accountUser account) | ||
| added <- lift $ Intra.addTeamMember uid tid Nothing | ||
| added <- lift $ Intra.addTeamMember uid tid (Nothing, Team.rolePermissions Team.RoleMember) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're setting the default here (RoleMember) in brig. Wouldn't it be good to keep the defaults of team permissions to galley? Galley handles all the permission logic so far. I.e. you could pass (Nothing, Nothing) here, and galley could assign the current default of RoleMember.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, that answers my previous comment. i'm convinced. :-)
|
|
||
| data Invitation = Invitation | ||
| { inTeam :: !TeamId | ||
| , inPerms :: !Permissions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has to be a Maybe - existing invitations don't have this field in the database. We need to keep backwards compatibility with existing invitations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
even more convinced!
| inv <- decodeBody =<< postInvitation brig tid owner invite | ||
| registerInvite inv aliceEmail | ||
|
|
||
| do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why this nested do is necessary or useful?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scoping. the are local variables that are declared twice, and i don't want to think of two different names.
| testInvitationRoles brig galley = do | ||
| (owner, tid) <- createUserWithTeam brig galley | ||
|
|
||
| let registerInvite :: Invitation -> Email -> Http UserId |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to keep the style of the code around, could you move this to a top-level function instead of an inner let?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are no other top-level helpers in this module, no? So it would just make this small function harder to find, and otherwise at best solve an aesthetic problem. I would like ot keep this self-contained.
| const 403 === statusCode | ||
| const (Just "too-many-team-invitations") === fmap Error.label . decodeBody | ||
|
|
||
| -- | Admins can invite collaborators, but not owners. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we already have a test checking that members/collaborators cannot invite?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no, but that not specific to the changes in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add a few more tests in a separate PR, though.
| aliceEmail <- randomEmail | ||
| let invite = InvitationRequest aliceEmail (Name "Alice") Nothing (Just Team.RoleAdmin) | ||
| inv <- decodeBody =<< postInvitation brig tid owner invite | ||
| registerInvite inv aliceEmail |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(style-opinion/consistency) - I find the nested blocks, which are nested purely for style reasons, unnecessary and confusing, and would prefer:
aliceEmail <- randomEmail
let invite = InvitationRequest aliceEmail (Name "Alice") Nothing (Just Team.RoleAdmin)
inv <- decodeBody =<< postInvitation brig tid owner invite
alice <- registerInvite inv aliceEmailEmpty lines and/or comments -- invite alice as an admin would provide enough separation between the different parts of the test, without extra indentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't know how to answer this. what i do is introduce smaller, self-contained scopes that do not leak noise into the other scopes. so how can it possibly be anything but easier to read?
can you motivate your objection better than "it's hard to read"? (i don't want to spend a lot of time on this, but i had to defeind this at least once.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW I am mildly in favor of self-contained scopes, though for "leak noise" reasons more than for readability reasons.
I would find this particular one more readable, too, if alice in alice <- do had a type annotation.
| const 400 === statusCode | ||
| const (Just "invalid-invitation-code") === fmap Error.label . decodeBody | ||
| where | ||
| invite email = InvitationRequest email (Name "Bob") Nothing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you motivate the where to let style change, or revert it? (Most of brig/galley tends to use let for things of kind *, and where for things of kind * -> .... That's of course arbitrary, but it would be nice to not change code style too much within the same service)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
motivation is that it's a let in 30 other places in the same module, with almost exactly the same line. i changed it for consistency.
|
|
||
| isTeamOwner :: TeamMember -> Bool | ||
| isTeamOwner tm = fullPermissions == (tm^.permissions) | ||
| -- (this should be reimplemented in terms of 'rolePermissions'.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO/FUTUREWORK?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i changed my mind. i'll remove this comment, it's more distracting than helpful.
| [ "email" .= irEmail i | ||
| , "inviter_name" .= irName i | ||
| , "locale" .= irLocale i | ||
| ] <> maybe [] (\role -> ["role" .= role]) (irRole i) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have (#) for this. It's used like this: https://github.com/wireapp/wire-server/blob/develop/libs/brig-types/src/Brig/Types/Common.hs#L266-L271
However, locale is also a Maybe and we're happy to encode a null in this case, which clients might be relying upon. So in this particular case we can't use #. Ughh.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i've been wondering about this. i'll just add it as null then. thanks for the # hint anyway!
| hasCopyPermission :: TeamMember -> Perm -> Bool | ||
| hasCopyPermission tm p = p `Set.member` (tm^.permissions.copy) | ||
|
|
||
| -- Note [team roles] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This note might need some updating now.
| instance ToJSON TeamMember where | ||
| toJSON = teamMemberJson (const True) | ||
|
|
||
| teamMemberJson :: (TeamMember -> Bool) -> TeamMember -> Value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like a haddock saying what (TeamMember -> Bool) is for.
services/brig/src/Brig/Team/Util.hs
Outdated
| where | ||
| check :: Maybe TeamMember -> Bool | ||
| check (Just m) = hasPermission m AddTeamMember && | ||
| and (hasCopyPermission m <$> (Set.toList $ inviteePerms ^. self)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm.
-
Why?
-
Is it the same as checking that
self == copy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does 279146c answer this?
| aliceEmail <- randomEmail | ||
| let invite = InvitationRequest aliceEmail (Name "Alice") Nothing (Just Team.RoleAdmin) | ||
| inv <- decodeBody =<< postInvitation brig tid owner invite | ||
| registerInvite inv aliceEmail |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW I am mildly in favor of self-contained scopes, though for "leak noise" reasons more than for readability reasons.
I would find this particular one more readable, too, if alice in alice <- do had a type annotation.
this is essential for backwards-compatibility, thanks @jschaul!
| instance FromJSON Invitation where | ||
| parseJSON = withObject "invitation" $ \o -> | ||
| Invitation <$> o .: "team" | ||
| <*> o .: "role" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a .:? instead
| import Data.Aeson | ||
| import Data.Id | ||
| import Data.Json.Util | ||
| import Galley.Types.Teams (Role) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's please not make a service types package depend on another service types package; it's just a matter of time until galley-types depends on brig-types and then we're stuck with circular dependencies.
If we need to share types, let's add them to the types-common package (FWIW, I see that galley-types currently depends on gundeck-types and we should aim to get rid of that in the same fashion).
| ] | ||
|
|
||
| #ifdef WITH_CQL | ||
| instance Cql.Cql Permissions where |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Following the above logic of service types dependencies, can we put this (Permissions and Role) under Data.Role on types-common? Even if galley and brig do get merged, at least until then let's keep things consistent;
Note that the whole invitation should and could be part of galley - the reason we decided against it was because all the email logic/templating/etc. was part of brig
|
All tests pass on my machine as of 727d8ee. How about this: I will clean up the |
👍 |
| rolePermissions role = Permissions p p where p = rolePerms role | ||
|
|
||
| rolePerms :: Role -> Set Perm | ||
| rolePerms RoleOwner = rolePerms RoleAdmin <> Set.fromList |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we want to introduce roles now, we should make sure that role Owner = fullPermissions or something like this; I am afraid that otherwise we may very well add a new permission and forget to add it (types won't help us there)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since you already approved: yes, but different PR! :-)
(won't be a visible change outside of the wire services. also need to think about the options for a minute.)
|
|
||
| insertInvitation :: MonadClient m | ||
| => TeamId | ||
| -> Maybe Role |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw, just a small note here: we could certainly change the insertInvitation to accept Role and the inviter: UserId, since those will always exist after this deployment;
We still need those Maybes on the read path but wondering if we can already ensure they get written during the creation of an invitation (you could also change the FromJSON instance of InvitationRequest to default to member, we do that in some places). Given that invitations only live on our DB for 2 weeks in production, the read path could even be changed after that.
Not necessary to change it, just food for thought, not sure what the general opinion about this topic is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Role: agreed (even though it doesn't seem important to me). UserId: what about SCIM?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, please let's not have UserId. Ideally we should have UserId | ScimTokenId for auditabilibilibility purposes, though then we might also need to think in the future about deactivating SCIM tokens instead of deleting them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I'm at it, we might also want to store info about who created which SCIM token. Auditabilibilibility!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw i feel that adding application logic to aeson parsers is generally a bad idea (and @jschaul seems to agree with me, see swagger remark above). adding a default role is admittedly a corner case, but then i would at least use instance Default Role where def = RoleMember.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, forget about the UserId
tiago-loureiro
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other that what was commented already 👍
|
spar tests fail on ci, but not on my machine. any ideas how this could be possible? it's not aws vs. fake-aws, spar does not do any of those tests. |
For a summary, read this commit-by-commit, starting with the commit messages.