Advanced security machinery in Zope2


Mon 11 July 2005 By nuxeo

In the new calendar project we have the need for changing the access to an
object depending on the the attributes of the object. For example, you
should not be able to view an event unless one of these conditions are
fulfilled:

  1. You are invited to the event
  2. You have the right to view the calendar of somebody who is invited and
    the event is a public event

  3. You are the manager of somebody invited to the event (even if the event
    is set as private).


Also, you can only edit the event of you are the event organizer, or the
event organizers manager.



There are two basic paths to walk down here. The simplest, and usually the
best, is to simply recalcuate the security when you change the event. I
chose not to go that path this time, mainly because there are no good hooks
to use in pure Zope2. In CPS we have an event system that would work well
for this, and the same is true for Zope3, but since the calendar needs to
work in pure Zope2 it's not possible. Creating hooks on the events
themselves would be possiblem but we would then also need hooks on the
calendars to be able to recalculate the security when the security settinsg
change on a calendar.



Also, there is a risk that recalculating the security takes a long time of
you have many events in the calendar.



So instead, I opted to create the security settings dynamically, when
accessed. This requires using a couple of advanced Zope 2 security mechanisms
that I think is worth a blog.

Acquisition wrapping


The first "trick" is to make it possible to have security settings on the
attendee object. Since the attendee objects are non-persistent objects (they
may originate from an external user source, for example) we can't just store
the permission settings on the objects as is normal in Zope2. Also, even if
we did store the settings in the calendar tool somehow, having a good user
interface for this is not trivial.



Therefore, we decided to use the attendees main calendar as a "proxy" for
the attendee settings. You give people a role on the calendar, and they will
have the same role on the attendee. The result is mainly that you can check
access settings directly on the attendee, instead of having to look up the
main calendar for the attendee and check there. It greatly simplifies use of
the calendar for developers.



The "trick" is simple, we need to make the attendee appear as if it is a
subobject of it's own main calendar:

  def getAttendee(self, attendee_id):

attendee = Attendee(attendee_id, attendee_id, 'INDIVIDUAL')

homecal = self.getMainCalendarForAttendeeId(attendee_id)

if homecal is None:

return attendee.of(self)

# This is to acquire the security settings from the calendar:

return attendee.of(homecal)


It is the of method that wraps the attendee so it looks like a
subobject. If the attendee seems to have no homecalendar, it gets the
security settings of the calendar tool instead.



We also do the same trickery for events, but for another reason. We in the
case of events want to to actually look like, through the user interfaces,
as the events are subobject of the calendar. This allows us to have proper
crumbtrails and the like. Attendee objects however, have no visible user
interface, so for attendeesd this is done only to simplify the security
checking on attendees.

An ac_local_roles method


Usually ac_local_roles is a dictionary of user ids and roles. But Zope 2
allows you to make it a method that returns this dictionary instead. So for
an event, I can return a dictionary containing all the people and resources
attending an event, and giving them the appropriate "EventAttendee"
role.



In fact, to return a complete dictionary, I also need to go through all the
attendees to see which users have the AttendeeManager role on their
calendars, and give them the EventAttendee role to those users as well (see
the mentioning above of being somebodys manager). But this can take a very
long time if you have many attendees, so I have simplified it. I'll only
check the current user, and only the current calendar. That is, even if you
have the right to view an event through Martijns calendar, you may not have
the right to view the same event through Erics calendar. You also will not
be able to check what the permissions another user has. Other users will
always look like they have no permissions at all on events. This simplifies
the code a lot and speeds up the security handling.

  def ac_local_roles(self):

# Find the current attendee id:

attendee_src = self.portal_calendar.attendee_source

current_attendee = attendee_src.getCurrentUserAttendee()

current_id = current_attendee.getAttendeeId()

attendees = [current_id]



# Check if the current user is an attendee manager for this calendar

current_user = getSecurityManager().getUser()

calendar = self.getCalendar()

roles = current_user.getRolesInContext(calendar)

if 'AttendeeManager' in roles:

# Current user is attendee manager for this calendars user, so

# We'll add this calendars user to the list of current attendees

attendees.append(calendar.getAttendees()[0].getAttendeeId())



# Check if the current organizer is one of the relevant users, in that

# case we should get the organizer role as well as local roles:

if self.getOrganizerId() in attendees:

roles.append('EventOrganizer')



# Check through the attendees to see if they are participating:

event_attendees = self.getAttendeeIds()

for id in attendees:

if id in event_attendees and 'EventParticipant' not in roles:

roles.append('EventParticipant')

return {current_user.getId(): list(roles)}

Dynamic permission proxying


This last technique is probably the least obvious, as it is really a
combination of two: Permission proxying and overriding getattr.



In Zope2, the roles that have a permission on a particular object is stored
in an attribute called "_[name_of_permission]_Permission". For example
_View_event_Permission in the case of the permission named "View event".
This should be a tuple of roles. In the case of events, we really want the
View event to have different sets of roles depending on if the event is
public or private. We can make that happen by overriding the events
getattr-

However returning dictionaries of roles in this case means that the mapping
between roles and permissions is fixed, which means that the normal security
settings in Zope no longer has any impact on the permission to role mapping
for this permission. Breaking standard functionality like that is not a good
idea, but luckily there is a method to get around this as well. The
permission attribute can be a string instead of a tuple of lists. If it is a
string, it is assumed to be the name of another permission, and that
permission is used instead. This way we can have three permissions; one for
viewing public events, one for viewing private events, and a third "proxy
permission" for viewing events that returns the correct permission to use
for this event:

  def getattr(self, name, default=_marker):

# View event is a "proxy permission". Nobody actually needs to

# have that permission, instead, the name of the permission to

# be used is returned.

if name == '_View_event_Permission':

if self.dict['access'] == 'PUBLIC':

return '_View_public_event_Permission'

else:

return '_View_private_event_Permission'

if default is _marker:

return getattr(self.dict, name)

return getattr(self.dict, name, default)


As you see, every time the permission "View event" is accessed, the security
machinery is told to look at "View private event" or "View public event",
depending on if the event is public or private.

(Post originally written by Lennart Regebro on the old Nuxeo blogs.)


Category: Product & Development