Sunday, March 30, 2008

featureserver authentication

As the name implies, featureserver serves vector features to various formats from a number of datasources, including OGR -- which means pretty much any vector format. That's extremely powerful. Really. That means, for instance, that when you're working on a really cool project and all anyone wants to know is if they can see it in KML/Google Earth, it's no extra work. Just point them to the REST-ful url like "http://example.com/featureserver/all.kml", and continue working on the cool project. Likewise for all.gml, .atom, etc. And, if you have a project with spatial data, if you put it in a format that featureserver understands, it's displayable, and editable in openlayers.

The next thing people want in a web application is some sort of user restrictions. In featureserver, by default, anyone can do any of the CRUD operations on any feature. I've been playing with a soon-to-be-open-sourced PPGIS (apparently the trendy acronym for that is now VGI) project where people can go to report sudden oak death. I want anyone to be able to report and view cases and add notes about existing cases, but only admins to be able to edit and delete existing reported sudden oak death cases. The simplest way is to use basic authentication in apache, but then anyone who goes to the site has to enter a user/password, and I think that really limits the public participation bit. If there's a way to do only authenticate sometimes with apache authentication, let me know.

unrested development

Since it's possible to use featureserver as an API, you can make your own server and add authentication in python. You can do this with any framework that supports sessions, I've done it with the development version (0.3) of web.py, using the nice sessions support. That it supports intuitive syntax for GET, PUT, DELETE, POST makes it a good fit as well. In the code below, the authentication related urls are /login and /logout, but those are never needed unless updating or deleting an existing point. Anyone can create new features. All featureserver related requests are made as with the original. Here's the wsgi script:

#!/usr/bin/python
import web
from FeatureServer.Server import Server
from FeatureServer.DataSource.SQLite import SQLite

urls = ( '/logout', 'logout'
,'/login', 'login'
,'/(.*)', 'features')

app = web.application(urls, globals())
session = web.session.Session(app
, web.session.DiskStore('/tmp/sessions')
, initializer={'authorized': False})

datasource = SQLite('fsauth', file="/tmp/fsauth.sqlite")
featureserver = Server({'fsauth': datasource })

application = app.wsgifunc()

class login(object):
"""for a real app, save usernames, hashed pws in the db"""
def POST(self):
pw = web.input(password=None).password
user = web.input(user=None).user
if (user == 'abc' and pw == '123'):
session.authorized = True
return '[authorized]'
return '[NOT-authorized]'

class logout(object):
def GET(self): session.kill()

class features(object):
"""all the featureserver routing"""
path = "/" + datasource.name + "/" # fsauth
format = "geojson"
def GET(self, feature_id=''):
if "." in feature_id:
feature_id, self.format = feature_id.split(".")

# get web.py parsed url
path = self.path + feature_id
data = dict(web.input().items())
data['format'] = self.format

format, rsp = featureserver.dispatchRequest(data, path, "", request_method="GET")
web.header('Content-type', format)
return rsp

def PUT(self, feature_id=None):
return self.POST(feature_id, "PUT")

def DELETE(self, feature_id=None):
if "." in feature_id:
feature_id, self.format = feature_id.split(".")
# cant delete unless authorized.
if not session.authorized:
web.header('Content-type', "text/plain")
return "not logged in"
path = self.path + feature_id
data = dict(web.input().items())
data['format'] = self.format
format, rsp = featureserver.dispatchRequest(data, path, "", request_method="DELETE")
web.header('Content-type', format)
return rsp


def POST(self, feature_id=None, method="POST"):
if feature_id is None: return []
if "." in feature_id:
feature_id, self.format = feature_id.split(".")
# must be an admin to do something with an existing feature.
if not session.authorized:
if not feature_id in ('new', 'create'):
return 'not logged in'
e = web.ctx.environ
post_data = e['wsgi.input'].read(int(e['CONTENT_LENGTH']))
path = self.path + feature_id
format, rsp = featureserver.dispatchRequest({'format':self.format}, path, "", post_data=post_data, request_method=method)
web.header('Content-type', format)
return rsp

That'll be called from OpenLayers, but to demo, since everyone else is using curl which supports cookies:

FS="http://localhost/fsauth/"

echo "\n\nfirst borrow a feature. "
curl --url "http://featureserver.org/featureserver.cgi/scribble/35.geojson" > 35.geojson

echo "\nsee the features ... "
curl --url $FS

echo "\n\nadd a feature (no auth required.)"
curl -d @35.geojson --url "$FS/create.geojson"

echo "\n\nsee the features ... "
curl --url $FS

echo "\n\ntry to delete ... but cant"
curl -s -X DELETE $FS/1

echo "\n\nlogin ... \n"
curl -s --cookie-jar "cookies.txt" -d "password=123&user=abc" --url $FS/login > /dev/null

echo "\n\nthen delete ... "
curl -s -X DELETE -b "cookies.txt" $FS/1 > /dev/null

echo "\n\nsee the empty features ... \n"
curl --url $FS

echo "\n\nreturn the borrowed feature. thanks. :-) "
curl -s -X PUT -d @35.geojson --url "http://featureserver.org/featureserver.cgi/scribble/35.geojson"

the important point there being that the DELETE fails until the user is logged in. I'm pretty sure adding authentication makes it un-REST-ful. but ???. Anyway, I won't lose any sleep over it.