T-Systems-MMS/skoop-server

Rework of Communities API

Closed this issue · 10 comments

Proposal for Communities API Design

GET /communities

Returns a list of all communities in the system. Every authenticated user is allowed to access this endpoint. The response is a List<CommunityResponse> with the following properties:

  • Basic community properties (ID, name, description, links)
  • List of community managers
  • List of skills related to the community

The web app can use this API to retrieve all communities to present a list view.

POST /communities

Creates a new community. Only the following base data can be passed for creation:

  • Name
  • Description
  • Links
  • Skills

GET /communities/{communityId}

Returns the details for the community given by its ID. Every authenticated user is allowed to access this endpoint. The response is a single CommunityResponse object.

The web app can use this API to retrieve a single community to present its data on the details page.

PUT /communities/{communityId}

Updates the details of the community given by its ID. Only community managers of the given community are allowed to access this endpoint. Only the following base data can be updated via this API:

  • Name
  • Description
  • Links
  • Skills

DELETE /communities/{communityId}

Deletes the community given by its ID. Only community managers of the given community are allowed to access this endpoint. There is no response body, just status 204 on success.

POST /communities/{communityId}/users

Adds a user as a member to the community. User ID is given in the request body. Permission to access this endpoint is as follows:

  • If the user ID given in the request body matches the user ID of the authentication and the community given by its ID is an open community then allow access (user can add themselves to open communities).
  • If the community given by its ID is a closed community, the authenticated user is a manager of that community and the user given by her/his ID in the request body has issued a request to join this community then allow access (community managers can add members but only if they requested to join in advance).
  • Otherwise, deny access.

Returns a single CommunityUserResponse object representing the newly created membership with the following properties:

  • User (type UserSimpleResponse)
  • Role

PUT /communities/{communityId}/users/{userId}

Updates the membership state of the user given by her/his ID in the community. Currently used to elevate a user's role to community manager. Only community managers of the given community are allowed to access this endpoint. Returns a single CommunityUserResponse object.

DELETE /communities/{communityId}/users/{userId}

Removes a user given by her/his ID from the community. Access is allowed to all community managers of the given community (they can kick other users) and if the given user ID matches the authentication user ID. There is no response body, just status 204 on success.

GET /communities/{communityId}/users

Returns a list of all users associated with the community given by its ID. Only managers and members of the given community are allowed to access this endpoint. Returns a List<CommunityUserResponse>.

Accepts the additional query parameter role to allow filtering results by the membership role (e.g. GET /communities/{communityId}/users?role=member returns only the members, not the managers).

The web app can use this endpoint to lazily load the list of community members on the details page. If access is denied (403) the web app simply displays: "Members are only visible to community members."

POST /communities/{communityId}/user-registrations

Allows a user or community manager to create requests to join a community. User IDs are given in the request body. Access is only allowed if the user ID given in the request body matches the user ID of the authentication (users can register themselves) or if the authenticated user is a manager of the given community.

For the sake of simplicity the web app can use this API endpoint to join open and closed communities.

Internally, it creates user registrations which have already been approved by the party represented by the authenticated user. For example, if a user wants to join a closed community the created user registration is already approved by the user but still needs approval by a community manager. When a user joins an open community it adds her/him as a member immediately because registrations for open communities can be regarded as "auto-approved" by the community.

Returns a single CommunityUserRegistrationResponse with the following properties:

  • ApprovedByUser (boolean flag to indicate if the user has agreed to join)
  • ApprovedByCommunity (boolean flag to indicate if at least one community manager has agreed)
  • (maybe) List of community managers associated with the registration (to let the user know who may be able to approve the request)
  • (maybe additional data useful to the web app)

PUT /communities/{communityId}/user-registrations/{registrationId}

Allows a user or community manager to approve the user registration given by its ID. The request body contains the boolean properties ApprovedByUser and ApprovedByCommunity. Permission to access this endpoint is as follows:

  • If the given user registration belongs to the authenticated user then allow the property ApprovedByUser in the request to change the user approval.
  • If the authenticated user is a manager of the given community then allow the property ApprovedByCommunity in the request to change the community approval.
  • Otherwise, deny access.

As soon as both approval flags of a registration are set to true the user is added as a member to the community.

Returns a single CommunityUserRegistrationResponse with the current registration state.

GET /users/{userId}/communities

Returns a list of communities the user given by her/his ID is a member/manager of. Access is only allowed if the given user ID matches the authentication user ID (users can only retrieve their own memberships). Returns a List<CommunityResponse> objects.

The web app can use this endpoint for two purposes:

  1. Display a special page to the user allowing her/him to get an overview of her/his community memberships.
  2. Determine which actions to offer for each community card in the communities list view (e.g. display "Join" action only for those communities not returned by this endpoint).

GET /users/{userId}/community-recommendations

Returns a list of communities recommended to the user based on the skill matching criteria. Access is only allowed if the given user ID matches the authentication user ID (users can only retrieve their own recommendations). Returns a List<CommunityResponse> objects.

The web app can use this endpoint to display a separate bar of recommendation cards above the general community list view (like promoted apps in an app store).


In general, I think we should adapt the API design to the needs of the web app (our only consumer at the moment). This is why I left the managers and skills as embedded resources in the CommunityResponse. The web app needs those related resources together with the base properties in almost every view of a community. It's a different thing for the community members and since we have additional security requirements who should be allowed to view members it makes sense to provide a dedicated API endpoint for that.

The separate community recommendations API is introduced to keep the general list API /communities fast and user-independent. It also allows us more flexibility to decide when to fetch recommendations (e.g. maybe users want to switch off recommendations and then we can save server resources).

I have a question about POST /communities/{communityId}/users. It is said that Access is only allowed if given user ID matches the user ID of the authentication (users can only add themselves to communities, not other users). Then what endpoint should be used to let a community manager confirm user's membership in a closed community? As a community manager I need the button Accept I can click on and make the server add a user as a member to a closed community.
And should POST /communities/{communityId}/users to send a user's request to join a closed community?

@LebedkoDmitry Very good point. 👍 I've adjusted my proposal accordingly.

I think that POST /communities/{communityId}/users should really add the user as a member in the database if access is allowed, otherwise it does nothing. Therefore, there needs to be a more detailed authorization concept in my point of view, even depending on the type of community.

So, joining an open community can be achieved by POST /communities/{communityId}/users with the user ID in the request body matching the user ID of the authenticated principal.

For the purpose of joining a closed community I've added another API endpoint POST /communities/{communityId}/user-registrations. The user who wants to join sends her/his user ID to this API and receives an info about the registration state. If the given community is an open community, the users joins immediately and the state is APPROVED. If it's a closed community the state is PENDING.

The community managers of a closed community can approve registrations by calling POST /communities/{communityId}/users with the user ID of a registered user in the request body. This approves the registration and makes the user given by the user ID a real member of the community. Important: The managers can only add users who have registered in advance. It's impossible to add arbitrary members.

@LebedkoDmitry @svetivanova What do you think?

@georgwittberger I went through the sections POST /communities/{communityId}/users, POST /communities/{communityId}/user-registrations again and it looks good from my perspective.

A question to If the community given by its ID is a closed community, the authenticated user is a manager of that community and the user given by her/his ID in the request body has issued a request to join this community then allow access - this implies several business-checks on a service layer to determine if a user has an access.
Should all these checks be implemented in MySkillsSecurityExpressionRoot (of course forwarding actual business logic to SecurityService) to respond with 403 status code? Or is it better to implement them with javax.validation.ConstraintValidator to response with 400 status code?

And there is one more thing I forgot...
The quote by our PO:

When I create a community, I want to invite other user straight away and it makes no sense for a user having to click to the viewing page first. Maybe it makes more sense here as the same page opens when you want to edit the community, right? So then everyone who is “community manager” can edit content and community members. What do you think?

So there has to be an option to invite users to join a community when a community is created / edited.

Now there is the invitedUserIds field to support invitations during creation / editing of a community.
What kind of mechanism should we use to let a community manager invite users during the stage of creation / editing?

@georgwittberger I went through the sections POST /communities/{communityId}/users, POST /communities/{communityId}/user-registrations again and it looks good from my perspective.

A question to If the community given by its ID is a closed community, the authenticated user is a manager of that community and the user given by her/his ID in the request body has issued a request to join this community then allow access - this implies several business-checks on a service layer to determine if a user has an access.
Should all these checks be implemented in MySkillsSecurityExpressionRoot (of course forwarding actual business logic to SecurityService) to respond with 403 status code? Or is it better to implement them with javax.validation.ConstraintValidator to response with 400 status code?

I think we should not bloat our SecurityExpressionRoot with such highly business-related functions. I originally introduced it to encapsulate the user permission checks which is a quite generic, cross-cutting concept. But checking the type of the community and whether a user is a manager in that community is so business-specific that I wouldn't like to have it inside the rather general MySkillsSecurityExpressionRoot. This security logic should go to the controller + service implementation of the communities feature.

So there has to be an option to invite users to join a community when a community is created / edited.

This is not entirely correct anymore. I discussed that on Friday last week with our PO and we agreed that adding members only makes sense in the "creation" dialog, not when editing a community.

For the invitation of users during creation of a new community we can stick with the current API approach, using an embedded user ID list in the request to POST /communities.

But I know what's on your mind, Dmitry. We cannot add those users as members immediately because this would compromise our security concept that users must give consent to join a community. Therefore, when inviting users during community creation we also need a kind of "registration" - no matter what the type of the community is. In this scenario the user is the one who has to approve the registration.

Maybe we can internally design the community registration like this: A CommunityUserRegistration object has the following properties:

  • User reference (the user who wants to join)
  • Community reference (the community to join)
  • ApprovedByUser (boolean flag to indicate if the user has consented to join)
  • ApprovedByCommunity (boolean flag to indicate if one of the community managers has agreed)

The different joining scenarios would be handled like this:

  • User joins an open community via community list/detail page: Web app sends an API request to POST /communities/{communityId}/user-registrations and the internal logic adds the user directly as a member without creating a registration entity - of course, checking if the user ID matches the one in the authentication. It's like a shortcut for open communities.
  • User requests to join a closed community via community list/details page: Web app sends an API request to POST /communities/{communityId}/user-registrations and the internal logic creates a user registration entity with ApprovedByUser set to true - again checking if the user ID in the request body matches the authentication user ID. Now only the community manager approval is missing. This could be done by a community manager via PUT /communities/{communityId}/user-registrations/{registrationId} with a flag approvedByCommunity = true in the request body. As soon as the manager approves the registration the user is added as a member to the community.
  • Manager invites users to join a community on details page: The Web app sends an API request to POST /communities/{communityId}/user-registrations to create registrations for the users to invite. In this case the service logic must detect that the user IDs given in the request body differ from the one in the authentication. This is the criterion to also create registrations for open communities instead of adding the users immediately. The service creates user registration entities with ApprovedByCommunity set to true. Now only the users must still approve their membership by sending their API request to PUT /communities/{communityId}/user-registrations/{registrationId} with approvedByUser = true in the request body.
  • Manager invites users directly during creation of a community: User IDs are embedded in the request to POST /communities. The internal service logic is the same as for invitations from the details page.

What do you think about that new API endpoint PUT /communities/{communityId}/user-registrations/{registrationId} to approve a pending user registration? Just an idea because I think it would be more consistent instead of approving via posting a user ID to /communities/{communityId}/users.

Thanks a lot, Georg, for such detailed and neat description of API! It looks fantastic!

I like the idea of using PUT /communities/{communityId}/user-registrations/{registrationId} as there is a domain object called registration and it looks evident if a registraion is updated with its own URI when reacting somehow to a user's request to join a community.

If registration is approved by sending POST /communities/{communityId}/users it will not comply with REST approach as the domain object registration will be updated with URI not mentioning the domain object at all.

@LebedkoDmitry Alright, I have adjusted the issue description accordingly to describe the new user registration API. Please have a look. 😃

@georgwittberger that looks good for me. Thank you for the update of the description.

Implemented in #50