= Team Participation Usage = Author: StuartBishop and CelsoProvidelo<
> Status: ImplementedSpecification, FoafSpecification<
> Created: 2005-02-09<
> Contributors: SteveAlexander<
> == Introduction == This Specification aims to describe correct Team``Participation 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 Team``Participation! == 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 Team``Participation 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 Team``Participation expresses both direct AND indirect membership in a team. Note that Team``Participation 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 Team``Participation. Team``Participation 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 Team``Membership 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 Team``Participation and maintain the table correctly: * !CreatePerson: when a Person is created, probably on PersonSet.createPerson(), we should add a entry in Team``Participation 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 Team``Participation 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 Team``Participation 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 Team``Participation. 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''' {{{ #!python class ICrowd(Interface): def __contains__(person_or_team): """Return True if the given person_or_team is in the crowd.""" def __add__(crowd): """Return a new ICrowd that is this crowd added to the given crowd. The returned crowd contains the person or teams in both this crowd and the given crowd. """ }}} * 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 Team``Participation 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}}} {{{ #!python class SQLCrowd: implements(ICrowd) def __init__(self, person_or_team): if person is not None: person = IPerson(person_or_team) self.person_ids = sets.Set([person.id]) self.crowds = [] # list because crowds may be not hashable. def __contains__(self, person_or_team): # Look in the non-SQLCrowds before hitting the database. for crowd in self.crowds: if person_or_team in crowd: return True # If we have None passed in, the return False at this stage. if not IPerson.providedBy(person_or_team): raise TypeError, person_or_team string_ids = [str(person_id) for person_id in self.person_ids] sql = 'SELECT COUNT(*) FROM TeamParticipation WHERE person=%d and team in (%s)' % ( person_or_team.id, ', '.join(string_ids) ) rv = execute(sql) return rv[0][0] >= 1 def _copy(self): new_crowd = SQLCrowd(None) new_crowd.persons = sets.Set(self.persons) new_crowd.crowds = list(self.crowds) return new_crowd def __add__(self, crowd): new_crowd = self._copy() if isinstance(crowd, SQLCrowd): new_crowd.persons |= crowd.persons new_crowd.crowds += crowd.crowds else: new_crowd.crowds.append(crowd) 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 === {{{ #!python class EditByOwnerOrAssignee(AuthorizationBase): permission = 'launchpad.Edit' usedfor = IBugTask def getAllowedCrowd(self): # Note that this is using the new API from CrowdControl. return ICrowd(self.obj.owner) + ICrowd(self.obj.assignee) }}} == Unresolved Issues == * It should be possible to maintain the Team``Participation table using database triggers - this is to be investigated by StuartBishop. * The '''ICrowd''' interface was implemented but has not been used.