Sharing Objects in Mantissa
Prerequisites
You should have all the Divmod code installed and runnable.
You should have at least a passing familiarity with Zope Interface, including the Twisted components system. You can read about Zope Interface in the Zope Interface README, and in the Twisted documentation, Components: Interfaces and Adapters.
You should also have read the axiom tutorial, because the Mantissa sharing system is all about sharing axiom items, and you will need to know how to create and manipulate them.
You also need to have a basic familiarity with Nevow, and ideally Athena as well, to understand how to render web content.
Introduction
Since you have ready the axiom tutorial (you didn't skip the prerequisites section, did you?) you already know how to create items in a database and manipulate them directly. Items in a database are much more interesting, however, when they can be created and accessed by different people. Even more interesting when you can provide different levels of access to different people so that your data is safe even when it's shared.
Mantissa is a multi-protocol application server. This means that unlike many web frameworks, mantissa's access control is not specific to HTTP. Nowhere in sharing will you find a request, or headers, or even usernames and passwords. The Mantissa 'sharing' system is about abstractly declaring what operations are allowed by what people, and it should be usable by any protocol. So, much of this tutorial will be couched in very general terms.
Don't worry, though: in this web 2.0 world, we know that HTTP is a very interesting protocol. The sharing system was very definitely designed with the web in mind, and this tutorial will discuss how to use the sharing system to expose your items over HTTP.
The General Idea
You have an item in a database and you want to share it with someone.
The first thing you have to decide is what you want to share. This is any Item.
Here's a simple one.
# sharestuff.py from axiom.item import Item from axiom.attributes import text class Post(Item): contents = text() def read(self): return self.contents def write(self, contents): self.contents = contents
It's a simple item with two methods; one for reading and one for writing.
Next, we need to decide what operations we're going to allow on this object. For determining what is and is not allowed, the sharing system uses Interface objects, so we need to declare some interfaces that this class implements.
# sharestuff.py from zope.interface import implements, Interface from axiom.item import Item from axiom.attributes import text class IRead(Interface): def read(self): "Read this object." class IWrite(Interface): def write(self, contents): "Write to this object." class Post(Item): implements(IRead, IWrite) contents = text() def read(self): return self.contents def write(self, contents): self.contents = contents
Now, we need to create the actual post so we can share it. Let's open up an interactive interpreter and make one.
$ python
>>> from axiom.store import Store
>>> s = Store("sharestuff.axiom")
>>> from sharestuff import Post
>>> post = Post(store=s)
Let's say our post is an article entitled 'man bites dog'. So we'll give it some contents.
>>> post.write(u'A local man bit a dog today.')
In order to share this post, we need to decide three things: what it will be called, who will be able to access it, and what those people will be able to do with it once they've got it.
Abstractly, we'd like to allow everyone to read this article. We've already declared that this post provides 2 interfaces - IRead and IWrite, so we will share it as an IRead provider only.
>>> from sharestuff import IRead
Then we need to decide who to share it with. In the sharing system, both individual people and groups are referred to as Role objects. We'd like to allow everyone to read this object, so we need to get a Role object which refers to everyone. There are a few special roles; sharing.getEveryoneRole is a function which will return the role that represents everyone.
>>> from xmantissa.sharing import getEveryoneRole >>> everybody = getEveryoneRole(s)
Finally, we need to decide what they're going to call it when we share it. When someone views an item that was shared, they have to get to it somehow. That is usually through a web browser. For example, if bob at mantissa.example.com shared an item called 'frisbee', you would access that in your web browser by typing <http://mantissa.example.com/users/bob/frisbee>. We'll get to exactly how that actually gets hooked up to a web page in a moment, but the need for a name holds true for other protocols too. For example - although mantissa does not currently support this - you might be able to send an email to bob+frisbee@mantissa.example.com to address the same object.
So here's how we share the post with everybody as a thing that can be read, named u'man-bites-dog':
>>> everybody.shareItem(post, u'man-bites-dog', [IRead])
This small sample database is not actually connected to anything. In order for an item to actually be accessed, it needs to be shared by a user in a running Mantissa instance. We'll get to that in a moment, but for now, let's look at what a Mantissa protocol would do in order to access this item.
First Mantissa would decide who you're logged in as. If you're logged in anonymously, then that will be the 'everyone' role, which we've already retrieved, above. Then, as I've already described, it will determine from the request what the name of the shared thing you're looking for is. It will then call the getShare method on the appropriate Role object.
>>> publicPost = everybody.getShare(u'man-bites-dog')
The object that comes back from this call is a SharedProxy. This is an access-controlled version of the original Post item. Whatever code interacts with it should treat it as a provider of the interfaces it was shared with - in this case sharestuff.IRead. For example, we can read it:
>>> publicPost.read() u'A local man bit a dog today.'
Importantly, however, this post object is not an actual Post and therefore does not do things like provide the IWrite interface. If just anyone came along and tried to write to this object:
>>> publicPost.write(u'No dogs were bitten today.')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "Mantissa/xmantissa/sharing.py", line 449, in __getattribute__
raise AttributeError("%r has no attribute %r" % (self, name))
AttributeError: SharedProxy(Post(contents=u'A local man bit a dog today.', storeID=7)@0x87A66CC, [<InterfaceClass sharestuff.IRead>], u'man-bites-dog') has no attribute 'write'
The security model in Mantissa's sharing system is designed to make it difficult to make mistakes. In many other permission systems, you need to explicitly check permission before doing something, or ask which user is accessing stuff.
When using sharing, by the time your application code is running, the server has already decided what user is logged in, what permissions they are allowed to have and has given you a model object that can perform only those operations. If you make a mistake and forget to check permissions for a certain operation, attempting that operation will generate an error rather than do something that would break your security rules.
Checking Permissions
Of course, you don't always want to get an ugly exception if something isn't allowed. You can specifically ask if an action is allowed before you try it. You can use checks like hasattr and getattr, as you would with any Python object, or you can use zope.interface APIs to ask what interfaces are supposed to be available:
>>> IRead.providedBy(publicPost) True >>> IWrite.providedBy(publicPost) False
This can be useful, for example, to decide whether to display the UI element that will allow the user to attempt a particular action. If they manage to convince your system to invoke the code to perform the action, they'll still get an exception; but in the default case, they'll see a nice page that just won't let them attempt to do the disallowed thing.
Creating Users and Groups
Of course, not everyone is anonymous. Our hypothetical news site will have editors as well as the general public. Let's create some users and groups. Both users and groups are identified by Role objects.
In Mantissa, individual users are identified by Role objects with an @ in them, separating a username and a domain. Groups are identified by Role objects that do not contain an @.
So, let's create a group for our editors:
>>> from xmantissa.sharing import getPrimaryRole >>> editors = getPrimaryRole(s, u'Editors', True)
And then we can create a user for our friend Bob and make him a member of the Editors group.
>>> bob = getPrimaryRole(s, u'bob@xmantissa.example.com', True) >>> bob.becomeMemberOf(editors)
Different Permissions for Different People
Now, let's let our editors edit that story. To do this, we just need to share the item again, with the same name, but to a different Role and with a different list of Interfaces that are allowed for that Role.
>>> editors.shareItem(p, u'man-bites-dog', [IRead, IWrite]) Share(shareID=u'man-bites-dog', sharedInterfaceNames=u'sharestuff.IRead,sharestuff.IWrite', sharedItem=reference(7), sharedTo=reference(12), storeID=17)@0x90A9C0C
Now that we've allowed editors to edit the story, let's see what it looks like to Bob:
>>> IRead.providedBy(editorPost) True >>> IWrite.providedBy(editorPost) True
Indeed, bob is an editor, so he can both read and write. But, he's still accessing this post through the sharing system, so only features allowed by those interfaces can be accessed. For example, although the post is an item, and that item has a store attribute:
>>> editorPost.store
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/home/glyph/Projects/Divmod/trunk/Mantissa/xmantissa/sharing.py", line 449, in __getattribute__
raise AttributeError("%r has no attribute %r" % (self, name))
AttributeError: SharedProxy(Post(contents=u'A local man bit a dog today.', storeID=7)@0x90A3F4C, [<InterfaceClass sharestuff.IRead>, <InterfaceClass sharestuff.IRead>, <InterfaceClass sharestuff.IWrite>], u'man-bites-dog') has no attribute 'store'
Querying for Shared Stuff
Let's say that we want to get a list of all the posts in the system. Of course it's easy to do that with axiom:
>>> list(s.query(Post)) [Post(contents=u'A local man bit a dog today.', storeID=7)@0x90A3F4C]
But what if we need a list that depends on whether items or shared or not? For example, let's say our editors need time to prepare a draft for publication: they want to be able to create a Post, and share it with each other, but not with the public.
>>> draft = Post(store=s) >>> draft.write(u'Loreem ipsam dealer sick omit')
Clearly, this post needs some editing. Let's share it with the editors so they can see it.
>>> editors.shareItem(draft, u'lorem-ipsum', [IRead, IWrite]) Share(shareID=u'lorem-ipsum', sharedInterfaceNames=u'sharestuff.IRead,sharestuff.IWrite', sharedItem=reference(18), sharedTo=reference(12), storeID=20)@0x90A9DEC
Now, we want to do a query for available posts so that Bob can see this draft article but the general public just sees our man-bites-dog story. If we were to use the regular Axiom query API, we'd get back both posts.
>>> [x.read() for x in s.query(Post)] [u'A local man bit a dog today.', u'Loreem ipsam dealer sick omit']
So instead, we'll get access to a Role, and have it filter the query for us. Let's ask the 'everyone' role how this query looks when accessed by the general public:
>>> [x.read() for x in everybody.asAccessibleTo(s.query(Post))] [u'A local man bit a dog today.']
Now instead, let's ask Bob what he sees:
>>> [x.read() for x in bob.asAccessibleTo(s.query(Post))] [u'A local man bit a dog today.', u'Loreem ipsam dealer sick omit']
As you can see here, editors can see the posts which are shared with the editors, but everyone else can only see posts that were shared with 'everyone'.
You can read the documentation for asAccessibleTo for more information.
Putting a Shared Item on the Web
Everything we've done so far has just been demonstrating how the sharing APIs work internally; we haven't actually exposed anything to the world at large. Now we're going to fire up an actual Mantissa server and start putting some items on the web.
We'll write the code that we need to display a Post on the web. Before that, though, a bit of background.
When you look at a shared Mantissa object through a web browser, three things happen.
- Mantissa figures out who you are, based on who you logged in as. This determines what Role is used to look things up for you.
- Mantissa locates the item that you are looking for. If you've supplied a URL like http://localhost:8080/users/foo/bar, then it looks for an item shared with the ID 'bar', owned by a user named 'foo' (meaning, in the user 'foo's database). It uses the appropriate role object to do a getShare to retrieve a SharedProxy for that object, as we showed above - it creates an object that has only the methods on it which you have shared.
- Mantissa then adapts that object to an interface, INavigableFragment, which contains the view logic for your object.
Step 3 has a few interesting consequences. For one thing, you need to register an INavigableFragment adapter for an Interface, not for a concrete class. The SharedProxy item for a Post is not a Post, and therefore an adapter registered for a Post won't work. Another thing is that your adapter will receive that SharedProxy, not an Item. You need to take that into account and make sure to expose any methods that your INavigableFragment adapter needs to call on its model object via an Interface.
Now, on to the code. Let's add this to the end of sharestuff.py:
# sharestuff.py, part 2 from twisted.python.components import registerAdapter from nevow.athena import LiveElement from nevow.page import renderer from nevow.tags import head, body, html, directive from nevow.loaders import stan from xmantissa.ixmantissa import INavigableFragment class PostElement(LiveElement): docFactory = stan(html[ head(render=directive('liveElement')), body(render=directive('showPost'))]) def __init__(self, post): LiveElement.__init__(self) self.post = post @renderer def showPost(self, request, tag): return tag[self.post.read()] registerAdapter(PostElement, IRead, INavigableFragment)
Now let's create an axiom store.
$ axiomatic mantissa Use database 'mantissa.axiom'? (Y/n) Enter Divmod™ Mantissa™ password for 'admin@localhost': Confirm Divmod™ Mantissa™ password for 'admin@localhost':
Normally at this point we would want to create a Mantissa offering, install it, add it to a product, and sign up some users. For simplicity's sake though, we'll just manually create some objects for the administrative user.
$ axiomatic -d mantissa.axiom/files/account/localhost/admin.axiom browse [axiom, version 0.5.28+r16406]. Autocommit is off. >>> from sharestuff import Post, IRead, IWrite >>> from xmantissa.sharing import getEveryoneRole >>> everyone = getEveryoneRole(db) >>> post = Post(store=db) >>> post.write(u"Today, I shared an object with the web.") >>> everyone.shareItem(post, u'shared-object', [IRead]) Share(shareID=u'shared-object', sharedInterfaceNames=u'sharestuff.IRead', sharedItem=reference(61), sharedTo=reference(60), storeID=62)@0x8D7E12C
Much like our earlier example, that gives us a post which is shared with everyone. Let's start up the server so that we can see it.
$ axiomatic start -n Use database 'mantissa.axiom'? (Y/n) Y 2008-08-19 05:56:13-0400 [-] Log opened. 2008-08-19 05:56:13-0400 [-] twistd 8.1.0+r24580 (/usr/bin/python 2.5.1) starting up. 2008-08-19 05:56:13-0400 [-] reactor class: twisted.internet.selectreactor.SelectReactor. 2008-08-19 05:56:13-0400 [-] xmantissa.web.AxiomSite starting on 8080 2008-08-19 05:56:13-0400 [-] Starting factory <xmantissa.web.AxiomSite instance at 0x8cf388c> 2008-08-19 05:56:13-0400 [-] xmantissa.web.AxiomSite starting on 8443 2008-08-19 05:56:13-0400 [-] Starting factory <xmantissa.web.AxiomSite instance at 0x8cf334c>
Now, all you have to do is hit your web browser on http://localhost:8080/users/admin/shared-object, and you'll see the object that you shared.
