Team Participation Usage
Author: StuartBishop and CelsoProvidelo
Status: ImplementedSpecification, FoafSpecification
Created: 2005-02-09
Contributors: SteveAlexander
Introduction
This Specification aims to describe correct TeamParticipation usage in accordance with the FOAF approach.
Rationale
Everywhere we use a Person's reference we might be referring to a person or a team. We need to make sure that, when we query the database to see if a the user has a permission, we take into account the fact that the user might indirectly have that permission by virtue of his membership in a team.
In general, whenever you are checking if a person has a permission, or is the owner of an object, you need to make sure that your code is team-aware. This means using TeamParticipation!
Essential Team Participation
As we know the Person table stores both people and teams within the Launchpad System, and they differ only by the extra team-related fields such as teamdescription and teamowner which are filled when the entry refers to a team.
The TeamMembership table includes all the information related to each first level member of this team, including the fields
- status
- datecreated
- dateexpires
These are the direct memberships of a team. There is one entry in TeamMembership for every person who has ever requested to join that team (as discussed in TeamMembership). But since teams can be members of teams, we need a separate data structure to map from a team to the complete set of people who are current members of a team.
The Team``Participation table
That data structure is the TeamParticipation table, which stores the precise set of people that are members of a particular team. For every person who is a member of a team, or a member of a team which is in turn a member of that team, there will be a participation entry that expresses that persons membership in the team. So TeamParticipation expresses both direct AND indirect membership in a team. Note that TeamParticipation is entirely automatically managed. Only the TeamMembership table is administered, adding and removing members. The results of those operations are exploded out and stored in TeamParticipation.
TeamParticipation stores 3 important relationships. Consider the following structure of nested teams and persons:
T2(P4, T3(P1)) Team 2 contains both Person 4 and Team 3 (which contains Person 1). P4 Person 4 exists, and is not in any Team.
The compiled TeamMembership information about direct membership. For example, if Team 3 has a member 1 and Team 2 that has a member 4, it stores:
| id | team | person | | 1 | 2 | 4 | -> compiled team 4 membership | 2 | 3 | 1 | -> compiled team 3 membership
exploded relationships among Teams, ex: If Team 3 is a Member of Team 2, it stores that Team 2 contains Person 1. This is because (Team 2 contains Team 3) and (Team 3 contains Person 1), so Team 2 transitively contains Person 1.
| id | team | person | | 3 | 2 | 1 | -> exploded team 3 member of team 4
- The information relating a Person (not a Team) to itself, is:
| id | team | person | | 4 | 1 | 1 | -> refers to person 1 | 5 | 4 | 4 | -> refers to person 4
This behaviour allows us to optimize permission checks to use a single quick database query.
Assumptions
We assume that the following Actions are aware about the existence of TeamParticipation and maintain the table correctly:
CreatePerson: when a Person is created, probably on PersonSet.createPerson(), we should add a entry in TeamParticipation referring this Person to itself. Each Person is a member of their own Team effectively, so there is a TeamParticipation entry for each person, with themselves as the member.
ApproveTeamMembership: when Approving a Membership entry (status PROPOSED to CURRENT) by have an available method approveMember(), that creates the correspondent entries in TeamParticipation. Note that memberships with status ADMIN also imply membership, so there is no change to teamparticipation in moving the status between CURRENT and ADMIN.
ExpireTeamMembership: when the membership entry expires (whatever method we use to perform it) we should call expireMember() method, it'll set its status to EXPIRED and remove the correspondent TeamParticipation entries, IF that person is not also indirectly a team member. Note that a person might be a member of a team both directly and indirectly. Removing their direct membership should not remove their TeamParticipation entry. However, it should probably trigger a warning to the administrator who is doing the removal, because they will want to know that the person will retain membership despite being removed as a direct member.
DisableTeamMembership: when disabling a member either by rejecting an PROPOSED member or disable a CURRENT member, we should call the method disableMember() that also removes the correspondent entries in TeamParticipation. Note that this should behave in the same way as ExpireTeamMembership in that it will respect the difference between direct and indirect team membership.
An immediate member of a team is one that is directly in that team. So, if Team B is a member of Team A, and Person C is a member of Team B, then Person C is in Team A, but is not an immediate member of team A.
There are no use cases in the application code for knowing if a person is an immediate member of a particular team.
Note that when checking if a person is an admin of a given team, we are concerned only whether they (or the team they are in) are the immediate admin of that given team. It does not matter whether they are the admin of another team, where that other team is a member of the given team.
Use Cases
The most probable use case is Ownership handling, like:
- Owner of Distribution
- Owner of Product
- Maintainership in general
- Security Officer
- Almost every other foreign key relationship to the Person table
In all of these cases the security machinery will need to be able to check if the currently authenticated user is a member of the given role (as per TeamsAndRoles).
Implementation
Suggested Security Implementation:
Define ICrowd
1 #!python
2 class ICrowd(Interface):
3
4 def __contains__(person_or_team):
5 """Return True if the given person_or_team is in the crowd."""
6
7 def __add__(crowd):
8 """Return a new ICrowd that is this crowd added to the given crowd.
9
10 The returned crowd contains the person or teams in
11 both this crowd and the given crowd.
12 """
Create an adapter from IPerson -> ICrowd. Note that this adapter also works for ITeam, which inherits from IPerson.
The implementation for the __contains__ method is simply a SELECT on the TeamParticipation table, unless a non-SQLCrowd has been added.
Implement non-SQLCrowd classes for absolutely everybody (celebs.absolutelyanyone), for any Person known to Launchpad (celebs.people), and nobody at all (celebs.nobody). See LaunchpadCelebrities.
Passing celebs.anonymous crowd into contains means you want to know whether the "unknown Person" is in the given crowd. The "unknown Person" is the person that represents the unauthenticated user.
A probable implementation is given below. This needs to be confirmed by doctests, which should live in lib/canonical/launchpad/database/person.py
1 class SQLCrowd:
2
3 implements(ICrowd)
4
5 def __init__(self, person_or_team):
6 if person is not None:
7 person = IPerson(person_or_team)
8 self.person_ids = sets.Set([person.id])
9 self.crowds = [] # list because crowds may be not hashable.
10
11 def __contains__(self, person_or_team):
12 # Look in the non-SQLCrowds before hitting the database.
13 for crowd in self.crowds:
14 if person_or_team in crowd:
15 return True
16 # If we have None passed in, the return False at this stage.
17 if not IPerson.providedBy(person_or_team):
18 raise TypeError, person_or_team
19 string_ids = [str(person_id) for person_id in self.person_ids]
20 sql = 'SELECT COUNT(*) FROM TeamParticipation WHERE person=%d and team in (%s)' % (
21 person_or_team.id, ', '.join(string_ids)
22 )
23 rv = execute(sql)
24 return rv[0][0] >= 1
25
26 def _copy(self):
27 new_crowd = SQLCrowd(None)
28 new_crowd.persons = sets.Set(self.persons)
29 new_crowd.crowds = list(self.crowds)
30 return new_crowd
31
32 def __add__(self, crowd):
33 new_crowd = self._copy()
34 if isinstance(crowd, SQLCrowd):
35 new_crowd.persons |= crowd.persons
36 new_crowd.crowds += crowd.crowds
37 else:
38 new_crowd.crowds.append(crowd)
39 return new_crowd
We may want an inefficient ICrowd that just loops through the teams and persons for use in non-SQL based tests. In its __contains__ method, it would compare persons by == and look for team membership using ITeam.isMember().
Usage example
Unresolved Issues
It should be possible to maintain the TeamParticipation table using database triggers - this is to be investigated by StuartBishop.
The ICrowd interface was implemented but has not been used.