Skip to content

Conversation

@fisx
Copy link
Contributor

@fisx fisx commented Jan 16, 2019

For a summary, read this commit-by-commit, starting with the commit messages.

fisx added 10 commits January 16, 2019 19:23
- 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.
@fisx fisx changed the title [WIP] allow choosing a role when creating a team member; fix role change events. Allow choosing a role when creating a team member; fix role change events. Jan 16, 2019
@fisx fisx changed the title Allow choosing a role when creating a team member; fix role change events. [WIP] Allow choosing a role when creating a team member; fix role change events. Jan 16, 2019
@fisx fisx force-pushed the fisx-hack-invitations branch from b38ef0e to a414f6e Compare January 16, 2019 21:43
@fisx
Copy link
Contributor Author

fisx commented Jan 16, 2019

I get this when I run integration tests locally:

Brig API Integration
  user
    account
      put /self - 200:                             FAIL (0.74s)
        test/integration/API/User/Account.hs:494:
        User not in results for query: "dogbert"

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.

@fisx fisx changed the title [WIP] Allow choosing a role when creating a team member; fix role change events. Allow choosing a role when creating a team member; fix role change events. Jan 16, 2019
@fisx
Copy link
Contributor Author

fisx commented Jan 17, 2019

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.

@fisx
Copy link
Contributor Author

fisx commented Jan 17, 2019

@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.

@fisx fisx requested a review from jschaul January 17, 2019 10:10
@fisx
Copy link
Contributor Author

fisx commented Jan 17, 2019

(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.)

@fisx fisx mentioned this pull request Jan 17, 2019
2 tasks
]

#ifdef WITH_CQL
instance Cql.Cql Permissions where
Copy link
Member

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).

Copy link
Contributor Author

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!

Copy link
Contributor Author

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.

Copy link
Member

@jschaul jschaul Jan 17, 2019

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>.

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 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.

Copy link
Contributor

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.)
Copy link
Member

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.

Copy link
Contributor Author

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.

throwE $ DuplicateUserKey uk
let minvmeta :: Maybe (UserId, UTCTimeMillis)
minvmeta = (, inCreatedAt inv) <$> inCreatedBy inv
let minvmeta :: (Maybe (UserId, UTCTimeMillis), Team.Permissions)
Copy link
Member

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.

Copy link
Contributor Author

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.)

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)
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author

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

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?

Copy link
Contributor Author

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.

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 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
Copy link
Member

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 aliceEmail

Empty lines and/or comments -- invite alice as an admin would provide enough separation between the different parts of the test, without extra indentation.

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 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.)

Copy link
Contributor

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
Copy link
Member

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)

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

TODO/FUTUREWORK?

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

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.

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'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]
Copy link
Contributor

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

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.

where
check :: Maybe TeamMember -> Bool
check (Just m) = hasPermission m AddTeamMember &&
and (hasCopyPermission m <$> (Set.toList $ inviteePerms ^. self))
Copy link
Contributor

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?

Copy link
Contributor Author

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

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.

fisx added 2 commits January 18, 2019 10:08
this is essential for backwards-compatibility, thanks @jschaul!
instance FromJSON Invitation where
parseJSON = withObject "invitation" $ \o ->
Invitation <$> o .: "team"
<*> o .: "role"
Copy link
Contributor

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

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

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

@fisx
Copy link
Contributor Author

fisx commented Jan 18, 2019

All tests pass on my machine as of 727d8ee.

How about this: I will clean up the {gundeck,galley,brig}-types entanglement, but i'll do it in another PR?

@tiago-loureiro
Copy link
Contributor

How about this: I will clean up the {gundeck,galley,brig}-types entanglement, but i'll do it in another PR?

👍

rolePermissions role = Permissions p p where p = rolePerms role

rolePerms :: Role -> Set Perm
rolePerms RoleOwner = rolePerms RoleAdmin <> Set.fromList
Copy link
Contributor

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)

Copy link
Contributor Author

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

@tiago-loureiro tiago-loureiro Jan 18, 2019

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

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!

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor

@tiago-loureiro tiago-loureiro left a 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 👍

@fisx
Copy link
Contributor Author

fisx commented Jan 18, 2019

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.

@fisx fisx merged commit 20a1b74 into develop Jan 18, 2019
@fisx fisx deleted the fisx-hack-invitations branch January 18, 2019 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants