The client module allows you to easily code an application that
talks to a Fedora Service. It handles the details of decoding the
data sent from the Service into a python data structure and raises an
Exception if an error is encountered.
The BaseClient class is the basis of all your interactions
with the server. It is flexible enough to be used as is for talking with a
service but is really meant to be subclassed and have methods written for it
that do the things you specifically need to interact with the Fedora
Service you care about. Authors of a Fedora Service are
encouraged to provide their own subclasses of BaseClient that make it
easier for other people to use a particular Service out of the box.
If you don't want to subclass, you can use BaseClient as a
utility class to talk to any Fedora Service. There's three steps to
this. First you import the BaseClient and Exceptions from the
fedora.client module. Then you create a new BaseClient with
the URL that points to the root of the Fedora Service you're
interacting with. Finally, you retrieve data from a method on the server.
Here's some code that illustrates the process:
from fedora.client import BaseClient, AppError, ServerError
client = BaseClient('https://admin.fedoraproject.org/pkgdb')
try:
collectionData = client.send_request('/collections', auth=False)
except ServerError as e:
print('%s' % e)
except AppError as e:
print('%s: %s' % (e.name, e.message))
for collection in collectionData['collections']:
print('%s %s' % (collection['name'], collection['version'])
In our example we only provide BaseClient() with the URL
fragment it uses as the base of all requests. There are several more
optional parameters that can be helpful.
If you need to make an authenticated request you can specify the
username and password to use when you construct your BaseClient using
the username and password keyword arguments. If you do not use
these, authenticated requests will try to connect via a cookie that was
saved from previous runs of BaseClient. If that fails as well,
BaseClient will throw an Exception which you can catch in
order to prompt for a new username and password:
from fedora.client import BaseClient, AuthError
import getpass
MAX_RETRIES = 5
client = BaseClient('https://admin.fedoraproject.org/pkgdb',
username='foo', password='bar')
# Note this is simplistic. It only prompts once for another password.
# Your application may want to loop through this several times.
while (count < MAX_RETRIES):
try:
collectionData = client.send_request('/collections', auth=True)
except AuthError as e:
client.password = getpass.getpass('Retype password for %s: ' % username)
else:
# data retrieved or we had an error unrelated to username/password
break
count = count + 1
WARNING:
Note that although you can set the username and
password as shown above you do have to be careful in cases where your
application is multithreaded or simply processes requests for more than one
user with the same BaseClient. In those cases, you can accidentally
overwrite the username and password between two requests. To
avoid this, make sure you instantiate a separate BaseClient for every
thread of control or for every request you handle or use ProxyClient
instead.
The useragent parameter is useful for identifying in log
files that your script is calling the server rather than another. The
default value is Fedora BaseClient/VERSION where VERSION is the
version of the BaseClient module. If you want to override this just
give another string to this:
client = BaseClient('https://admin.fedoraproject.org/pkgdb',
useragent='Package Database Client/1.0')
The debug parameter turns on a little extra output when
running the program. Set it to true if you're having trouble and want to
figure out what is happening inside of the BaseClient code.
send_request() is what does the heavy lifting of making a
request of the server, receiving the reply, and turning that into a python
dictionary. The usage is pretty straightforward.
The first argument to send_request() is method. It
contains the name of the method on the server. It also has any of the
positional parameters that the method expects (extra path information
interpreted by the server for those building non-TurboGears
applications).
The auth keyword argument is a boolean. If True, the
session cookie for the user is sent to the server. If this fails, the
username and password are sent. If that fails, an
Exception is raised that you can handle in your code.
req_params contains a dictionary of additional keyword
arguments for the server method. These would be the names and values
returned via a form if it was a CGI. Note that parameters passed as extra
path information should be added to the method argument instead.
An example:
import BaseClient
client = BaseClient('https://admin.fedoraproject.org/pkgdb/')
client.send_request('/package/name/python-fedora', auth=False,
req_params={'collectionVersion': '9', 'collectionName': 'Fedora'})
In this particular example, knowing how the server works,
/packages/name/ defines the method that the server is going to
invoke. python-fedora is a positional parameter for the name of the
package we're looking up. auth=False means that we'll try to look at
this method without having to authenticate. The req_params sends two
additional keyword arguments: collectionName which specifies whether
to filter on a single distro or include Fedora, Fedora EPEL, Fedora OLPC,
and Red Hat Linux in the output and collectionVersion which specifies
which version of the distribution to output for.
The URL constructed by BaseClient to the server could be
expressed as[#]_:
https://admin.fedoraproject.org/pkgdb/package/name/python-fedora/?collectionName=Fedora&collectionVersion=9
In previous releases of python-fedora, there would be one further
query parameter: tg_format=json. That parameter instructed the server
to return the information as JSON data instead of HTML. Although this is
usually still supported in the server, BaseClient has deprecated this
method. Servers should be configured to use an Accept header to get
this information instead. See the JSON output section of the
Fedora Service documentation for more information about the server
side.
Building a client using subclassing builds on the information
you've already seen inside of BaseClient. You might want to use this
if you want to provide a module for third parties to access a particular
Fedora Service. A subclass can provide a set of standard methods for
calling the server instead of forcing the user to remember the URLs used to
access the server directly.
Here's an example that turns the previous calls into the basis of
a python API to the Fedora Package Database:
import getpass
import sys
from fedora.client import BaseClient, AuthError
class MyClient(BaseClient):
def __init__(self, baseURL='https://admin.fedoraproject.org/pkgdb',
username=None, password=None,
useragent='Package Database Client/1.0', debug=None):
super(BaseClient, self).__init__(baseURL, username, password,
useragent, debug)
def collection_list(self):
'''Return a list of collections.'''
return client.send_request('/collection')
def package_owners(self, package, collectionName=None,
collectionVersion=None):
'''Return a mapping of release to owner for this package.'''
pkgData = client.send_request('/packages/name/%s' % (package),
{'collectionName': collectionName,
'collectionVersion': collectionVersion})
ownerMap = {}
for listing in pkgData['packageListings']:
ownerMap['-'.join(listing['collection']['name'],
listing['collection']['version'])] = \
listing['owneruser']
return ownerMap
A few things to note:
- 1.
- In our constructor we list a default baseURL and useragent.
This is usually a good idea as we know the URL of the Fedora
Service we're connecting to and we want to know that people are using
our specific API.
- 2.
- Sometimes we'll want methods that are thin shells around the server
methods like collection_list(). Other times we'll want to do more
post processing to get specific results as package_owners() does.
Both types of methods are valid if they fit the needs of your API. If you
find yourself writing more of the latter, though, you may want to consider
getting a new method implemented in the server that can return results
more appropriate to your needs as it could save processing on the server
and bandwidth downloading the data to get information that more closely
matches what you need.
See pydoc fedora.client.fas2 for a module that implements a
standard client API for the Fedora Account System
BaseClient will throw a variety of errors that can be
caught to tell you what kind of error was generated.
- FedoraServiceError
- The base of all exceptions raised by BaseClient. If your code needs
to catch any of the listed errors then you can catch that to do so.
- ServerError
- Raised if there's a problem communicating with the service. For instance,
if we receive an HTML response instead of JSON.
- AuthError
- If something happens during authentication, like an invalid usernsme or
password, AuthError will be raised. You can catch this to prompt
the user for a new usernsme.
- AppError
- If there is a server side error when processing a request, the
Fedora Service can alert the client of this by setting certain
flags in the response. BaseClient will see these flags and raise an
AppError. The name of the error will be stored in AppError's name
field. The error's message will be stored in message.
Here's an example of the exceptions in action:
from fedora.client import ServerError, AuthError, AppError, BaseClient
import getpass
MAXRETRIES = 5
client = BaseClient('https://admin.fedoraproject.org/pkgdb')
for retry in range(0, MAXRETRIES):
try:
collectionData = client.send_request('/collections', auth=True)
except AuthError as e:
from six.moves import input
client.username = input('Username: ').strip()
client.password = getpass.getpass('Password: ')
continue
except ServerError as e:
print('Error talking to the server: %s' % e)
break
except AppError as e:
print('The server issued the following exception: %s: %s' % (
e.name, e.message))
for collection in collectionData['collections']:
print('%s %s' % (collection[0]['name'], collection[0]['version']))
Applications that use OpenId to authenticate are not able to use
the standard BaseClient because the pattern of authenticating is very
different. We've written a separate client object called
OpenIdBaseClient to do this.
In the loosest sense, a Fedora Service is a web application that
sends data that BaseClient is able to understand. This document
defines things that a web application must currently do for
BaseClient to understand it.
All current Fedora Services are written in TurboGears.
Examples in this document will be for that framework. However, other
frameworks can be used to write a Service if they can correctly create the
data that BaseClient needs.
A Fedora Service differs from other web applications in that
certain URLs for the web application are an API layer that the Service can
send and receive JSON data from for BaseClient to interpret.
This imposes certain constraints on what data can be sent and what data can
be received from those URLs. The fedora.tg module contains functions
that help you write code that communicated well with BaseClient.
The TurboGears framework separates an application into
model, view, and controller layers. The model is
typically a database and holds the raw data that the application needs. The
view formats the data before output. The controller makes decisions about
what data to retrieve from the model and which view to pass it onto. The
code that you'll most often need to use from fedora.tg operates on
the controller layer but there's also code that works on the model
and view behind the scenes.
The controller is the code that processes an http request.
It validates and processes requests and parameters sent by the user, gets
data from the model to satisfy the request, and then passes it onto a view
layer to be returned to the user. fedora.tg.utils contains several
helpful functions for working with controllers.
In TurboGears and most web frameworks, a URL is a kind of
API for accessing the data your web app provides. This data can be made
available in multiple formats. TurboGears allows you to use one URL
to serve multiple formats by specifying a query parameter or a special
header.
A URL in TurboGears can serve double duty by returning
multiple formats depending on how it is called. In most cases, the URL will
return HTML or XHTML by default. By adding the query parameter,
tg_format=json you can switch from returning the default format to
returning JSON data. You need to add an
@expose(allow_json=True) decorator [1] to your controller method to
tell TurboGears that this controller should return JSON
data:
@expose(allow_json=True)
@expose(template='my.templates.amplifypage')
def amplify(self, data):
allow_json=True is a shortcut for this:
@expose("json", content_type="text/javascript",
as_format="json", accept_format="text/javascript")
That means that the controller method will use the json
template (uses TurboJson to marshal the returned data to JSON) to
return data of type text/javascript when either of these conditions
is met: a query param of tg_format=json or an Accept:
text/javascript header is sent.
BaseClient in python-fedora 0.1.x and 0.2.x use the query
parameter method of selecting JSON output. BaseClient 0.2.99.6
and 0.3.x use both the header method and the query parameter since the
argument was made that the header method is friendlier to other web
frameworks. 0.4 intends to use the header alone. If you use
allow_json=True this change shouldn't matter. If you specify the
@expose("json") decorator only accept_format is set.
So it is advisable to include both as_format and accept_format
in your decorator. Note that this is a limitation of TurboGears <=
1.0.4.4. If your application is only going to run on TurboGears >
1.0.4.4 you should be able to use a plain @expose("json")
[2].
- [1]
- http://docs.turbogears.org/1.0/ExposeDecorator#same-method-different-template
- [2]
- http://trac.turbogears.org/ticket/1459#comment:4
When designing your URLs you might wonder why you'd want to return
JSON and HTML from a single controller method instead of having two
separate controller methods. For instance, separating the URLs into their
own namespaces might seem logical: /app/json/get_user/USERNAME as
opposed to /app/user/USERNAME. Doing things with two URLs as opposed
to one has both benefits and drawbacks.
- Usually less code as there's only one controller method
- When a user sees a page that they want to get data from, they can get it
as JSON instead of screen scraping.
- Forces the application designer to think more about the API that is being
provided to the users instead of just the needs of the web page they are
creating.
- Makes it easier to see what data an application will need to implement an
alternate interface since you can simply look at the template code to see
what variables are being used on a particular page.
- Avoids special casing for error handlers (See below)
- Separates URLs that you intend users to grab JSON data from URLs
where you only want to display HTML.
- Allows the URLs that support JSON to concentrate on trimming the
size of the data sent while URLs that only return HTML can return whole
objects.
- Organization can be better if you don't have to include all of the pages
that may only be useful for user interface elements.
Personal use has found that allowing JSON requests on one
controller method works well for cases where you want the user to get data
and for traditional form based user interaction. AJAX requests have been
better served via dedicated methods.
The toplevel of the return values should be a dict. This is the
natural return value for TurboGears applications.
All data should be encoded in JSON before being returned.
This is normally taken care of automatically by TurboGears and
simplejson. If you are returning non-builtin objects you may have to define
an __json__() method.
simplejson (and probably other JSON libraries) will take
care of encoding Unicode strings to JSON so be sure that you are
passing Unicode strings around rather than encoded byte strings.
In python, error conditions are handled by raising an exception.
However, an exception object will not propagate automatically through a
return from the server. Instead we set several special variables in the
returned data to inform BaseClient of any errors.
At present, when BaseClient receives an error it raises an
exception of its own with the exception information from the server inside.
Raising the same exception as the server is being investigated but may pose
security risks so hasn't yet been implemented.
All URLs which return JSON data should set the exc
variable when the method fails unexpectedly (a database call failed, a place
where you would normally raise an exception, or where you'd redirect to an
error page if a user was viewing the HTML version of the web app).
exc should be set to the name of an exception and tg_flash set
to the message that would normally be given to the exception's constructor.
If the return is a success (expected values are being returned from the
method or a value was updated successfully) exc may either be unset
or set to None.
When viewing the HTML web app, tg_flash can be set with a
message to display to the user either on the next page load or via an AJAX
handler. When used in conjunction with JSON,
exc=EXCEPTIONNAME, and BaseClient, tg_flash should be
set to an error message that the client can use to identify what went wrong
or display to the user. It's equivalent to the message you would normally
give when raising an exception.
Errors in authentication are a special case. Instead of returning
an error with exc='AuthError' set, the server should return with
response.status = 403. BaseClient will see the 403 and
raise an AuthError.
This is the signal for the client to ask the user for new
credentials (usually a new username and password).
NOTE:
Upstream TurboGears has switched to sending a 401
for authentication problems. However, their use of 401 is against the http
specification (It doesn't set the 'WWW-Authentication' header) and it causes
problems for konqueror and webkit based browsers so we probably will not be
switching.
So far we've run across three features of TurboGears that
provide value to a web application but don't work when returning JSON
data. We provide a function that can code around this.
fedora.tg.utils.request_format() will return the format that the page
is being returned as. Code can use this to check whether JSON output
is expected and do something different based on it:
output = {'tg_flash': 'An Error Occurred'}
if fedora.tg.utils.request_format() == 'json':
output['exc'] = 'ServerError'
else:
output['tg_template'] = 'my.templates.error'
return output
In this example, we return an error through our
"exception" mechanism if we are returning JSON and return
an error page by resetting the template if not.
Redirects do not play well with JSON [3] because
TurboGears is unable to turn the function returned from the redirect
into a dictionary that can be turned into JSON.
Redirects are commonly used to express errors. This is actually
better expressed using tg_template because that method leaves the URL
intact. That allows the end user to look for spelling mistakes in their URL.
If you need to use a redirect, the same recipe as above will allow you to
split your code paths.
- [3]
- Last checked in TurboGears 1.0.4
Setting what template is returned to a user by setting tg_template
in the return dict (for instance, to display an error page without changing
the URL) is a perfectly valid way to use TurboGears. Unfortunately,
since JSON is simply another template in TurboGears you have
to be sure not to interfere with the generation of JSON data. You
need to check whether JSON was requested using
fedora.tg.utils.request_format() and only return a different template
if that's not the case. The recipe above shows how to do this.
Validators are slightly different than the issues we've
encountered so far. Validators are used to check and convert parameters sent
to a controller method so that only good data is dealt with in the
controller method itself. The problem is that when a validator detects a
parameter that is invalid, it performs a special internal redirect to a
method that is its error_handler. We can't intercept this redirect
because it happens in the decorators before our method is invoked. So we
have to deal with the aftermath of the redirect in the error_handler
method:
class NotNumberValidator(turbogears.validators.FancyValidator):
messages = {'Number': 'Numbers are not allowed'}
def to_python(self, value, state=None):
try:
number = turbogears.validators.Number(value.strip())
except:
return value
raise validators.Invalid(self.message('Number', state), value,
state)
class AmplifyForm(turbogears.widgets.Form):
template = my.templates.amplifyform
submit_text = 'Enter word to amplify'
fields = [
turbogears.widgets.TextField(name='data',
validator=NotNumberValidator())
]
amplify_form = AmplifyForm()
class mycontroller(RootController):
@expose(template='my.templates.errorpage', allow_json=True)
def no_numbers(self, data):
errors = fedora.tg.utils.jsonify_validation_errors()
if errors:
return errors
# Construct a dict to return the data error message as HTML via
# the errorpage template
pass
@validate(form=amplify_form)
@error_handler('no_numbers')
@expose(template='my.templates.amplifypage', allow_json=True)
def amplify(self, data):
return dict(data=data.upper())
When a user hits amplify()'s URL, the validator checks
whether data is a number. If it is, it redirects to the
error_handler, no_numbers(). no_numbers() will normally return
HTML which is fine if we're simply hitting amplify() from a web
browser. If we're hitting it from a BaseClient app, however, we need
it to return JSON data instead. To do that we use
jsonify_validation_errors() which checks whether there was a
validation error and whether we need to return JSON data. If both of
those are true, it returns a dictionary with the validation errors. This
dictionary is appropriate for returning from the controller method in
response to a JSON request.
WARNING:
When defining @error_handler() order of decorators can be
important. The short story is to always make @validate() and @error_handler()
the first decorators of your method. The longer version is that this is known
to cause errors with the json request not being honored or skipping identity
checks when the method is its own error handler.
Certain controller methods are necessary in order for
BaseClient to properly talk to your service. TurboGears can
quickstart an application template for you that sets most of these variables
correctly:
$ tg-admin quickstart -i -s -p my my
# edit my/my/controllers.py
You need to have a login() method in your application's
root. This method allows BaseClient to authenticate against your
Service:
@expose(template="my.templates.login")
+ @expose(allow_json=True)
def login(self, forward_url=None, previous_url=None, \*args, \**kw):
if not identity.current.anonymous \
and identity.was_login_attempted() \
and not identity.get_identity_errors():
+ # User is logged in
+ if 'json' == fedora.tg.utils.request_format():
+ return dict(user=identity.current.user)
+ if not forward_url:
+ forward_url = turbogears.url('/')
raise redirect(forward_url)
If you are implementing a server in a non-TurboGears framework,
note that one of the ways to reach the login() method is through
special parameters parsed by the TurboGears framework.
BaseClient uses these parameters instead of invoking the
login() method directly as it saves a round trip when authenticating
to the server. It will be necessary for you to implement handling of these
parameters (passed via POST) on your application as well.
The parameters are: user_name, password, and
login. When these three parameters are sent to the server, the server
authenticates the user and records their information before deciding what
information to return to them from the URL.
The logout() method is similar to login(). It also
needs to be modified to allow people to connect to it via JSON:
- @expose()
+ @expose(allow_json=True)
def logout(self):
identity.current.logout()
+ if 'json' in fedora.tg.utils.request_format():
+ return dict()
raise redirect("/")
For an overview of CSRF and how to protect TurboGears 1
based services, look at this document.
fedora.tg.json contains several functions that help to
convert SQLAlchemy objects into JSON. For the most part, these
do their work behind the scenes. The SABase object, however, is one
that you might need to take an active role in using.
When you return an SQLAlchemy object in a controller to a
template, the template is able to access any of the relations mapped to it.
So, instead of having to construct a list of people records from a table and
the the list of groups that each of them are in you can pass in the list of
people and let your template reference the relation properties to get the
groups. This is extremely convenient for templates but has a negative effect
when returning JSON. Namely, the default methods for marshaling
SQLAlchemy objects to JSON only return the attributes of the
object, not the relations that are linked to it. So you can easily run into
a situation where someone querying the JSON data for a page will not
have all the information that a template has access to.
SABase fixes this by allowing you to specify relations that your
SQLAlchemy backed objects should marshal as JSON data.
Further information on SABase can be found in the API
documentation:
SABase is a base class that you can use when defining objects in
your project's model. So the first step is defining the classes in your
model to inherit from SABase:
from fedora.tg.json import SABase
from sqlalchemy import Table, Column, Integer, String, MetaData, ForeignKey
from turbogears.database import metadata, mapper
class Person(SABase):
pass
PersonTable = Table('person', metadata
Column('name', String, primary_key=True),
)
class Address(SABase):
pass
AddressTable = Table (
Column('id', Integer, primary_key=True),
Column('street', string),
Column('person_id', Integer, ForeignKey('person.name')
)
mapper(PersonTable, Person)
mapper(AddressTable, Address, properties = {
person: relation(Person, backref = 'addresses'),
})
The next step is to tell SABase which properties should be copied
(this allows you to omit large trees of objects when you only need the data
from a few of them):
@expose('my.templates.about_me')
@expose(allow_json=True)
def my_info(self):
person = Person.query.filter_by(name='Myself').one()
person.jsonProps = {'Person': ['addresses']}
return dict(myself=person}
Now, when someone requests JSON data from my_info, they
should get back a record for person that includes a property addresses.
Addresses will be a list of address records associated with the person.
SABase adds a special __json__() method to the class. By
default, this method returns a dict with all of the attributes that are
backed by fields in the database.
Adding entries to jsonProps adds the values for those properties
to the returned dict as well. If you need to override the __json__()
method in your class you probably want to call SABase's __json__()
unless you know that neither you nor any future subclasses will need it.
Sometimes you need to return an object that isn't a basic python
type (list, tuple, dict, number. string, etc). When that occurs,
simplejson won't know how to marshal the data into JSON until
you write own method to transform the values. If this method is named
__json__(), TurboGears will automatically perform the conversion when
you return the object.
Example:
class MyObject(object):
def _init__(self, number):
self.someNumber = number
self.cached = None
def _calc_data(self):
if not self.cached:
self.cached = self.someNumber * 2
return self.cached
twiceData = property(_calc_data)
def __json__(self):
return {'someNumber': self.someNumber, 'twiceData': self.twiceData}
In this class, you have a variable and a property. If you were to
return it from a controller method without defining the __json__() method,
TurboGears would give you an error that it was unable to adapt the
object to JSON. The JSON method transforms the object into a
dict with sensibly named values for the variable and property so that
simplejson is able to marshal the data to JSON. Note that you will
often have to choose between space (more data takes more bandwidth to
deliver to the end user) and completeness (you need to return enough data so
the client isn't looking for another method that can complete its needs)
when returning data.
The Fedora Account System has a JSON interface that
we make use of to authenticate users in our web apps. Currently, there are
two modes of operation. Some web apps have single sign-on capability
with Fedora Account System. These are the TurboGears
applications that use the jsonfasprovider. Other apps do not have
single sign-on but they do connect to Fedora Account System to
verify the username and password so changing the password in Fedora
Account System changes it everywhere.
An identity provider with CSRF protection.
This will install as a TurboGears identity plugin. To use it, set
the following in your APPNAME/config/app.cfg file:
identity.provider='jsonfas2'
visit.manager='jsonfas2'
SEE ALSO:
These methods are deprecated because they do not provide
the CSRF protection of TurboGears Identity Provider 2. Please
use that identity provider instead.
The flask_openid provider is an alternative to the flask_fas auth
plugin. It leverages our FAS-OpenID server to do authn and authz (group
memberships). Note that not every feature is available with a generic OpenID
provider -- the plugin depends on the OpenID provider having certain
extensions in order to provide more than basic OpenID auth.
- Any compliant OpenID server should allow you to use the basic authn
features of OpenID OpenID authentication core:
http://openid.net/specs/openid-authentication-2_0.html
- Retrieving simple information about the user such as username, human name,
email is done with sreg:
http://openid.net/specs/openid-simple-registration-extension-1_0.html
which is an extension supported by many providers.
- Advanced security features such as requiring a user to re-login to the
OpenID provider or specifying that the user login with a hardware token
requires the PAPE extension:
http://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html
- To get groups information, the provider must implement the
https://dev.launchpad.net/OpenIDTeams extension.
- We have extended the teams extension so you can request a team name of
_FAS_ALL_GROUPS_ to retrieve all the groups that a user belongs to.
Without this addition to the teams extension you will need to manually
configure which groups you are interested in knowing about. See the
documentation for how to do so.
- Retrieving information about whether a user has signed a CLA (For Fedora,
this is the Fedora Project Contributor Agreement).
http://fedoraproject.org/specs/open_id/cla
If the provider you use does not support one of these extensions,
the plugin should still work but naturally, it will return empty values for
the information that the extension would have provided.
The Fedora Account System has a OpenID provider that
applications can use to authenticate users in web apps. For our Flask
applications we have an identity provider that uses this OpenID service to
authenticate users. It is almost completely compatible with Flask Auth
Plugin except that it does not use the username/password provided by the
client application (it is silently ignored). It can be configured to use any
OpenID authentication service that implements the OpenID Teams Extension,
Simple Registration Extension and CLA Extension.
The FAS OpenID auth plugin has several config values that can be
used to control how the auth plugin functions. You can set these in your
application's config file.
- FAS_OPENID_ENDPOINT
- Set this to the OpenID endpoint url you are authenticating against.
Default is "http://id.fedoraproject.org/"
- FAS_CHECK_CERT
- When set, this will check the SSL Certificate for the FAS server to make
sure that it is who it claims to be. This is useful to set to False when
testing against a local FAS server but should always be set to True in
production. Default: True
The following is a sample, minimal flask application that uses
fas_flask for authentication:
#!/usr/bin/python -tt
# Flask-FAS-OpenID - A Flask extension for authorizing users with OpenID
# Primary maintainer: Patrick Uiterwijk <puiterwijk@fedoraproject.org>
#
# Copyright (c) 2012-2013, Red Hat, Inc., Patrick Uiterwijk
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of the Red Hat, Inc. nor the names of its contributors may
# be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# This is a sample application.
import flask
from flask_fas_openid import fas_login_required, cla_plus_one_required, FAS
# Set up Flask application
app = flask.Flask(__name__)
# Set up FAS extension
fas = FAS(app)
# Application configuration
# SECRET_KEY is necessary for the Flask session system. It needs to be secret to
# make the sessions secret but if you have multiple servers behind
# a load balancer, the key needs to be the same on each.
app.config['SECRET_KEY'] = 'change me!'
# Other configuration options for Flask-FAS-OpenID:
# FAS_OPENID_ENDPOINT: the OpenID endpoint URL
# (default http://id.fedoraproject.org/)
# FAS_CHECK_CERT: check the SSL certificate of FAS (default True)
# You should use these options' defaults for production applications!
app.config['FAS_OPENID_ENDPOINT'] = 'http://id.fedoraproject.org/'
app.config['FAS_CHECK_CERT'] = True
# Inline templates keep this test application all in one file. Don't do this in
# a real application. Please.
TEMPLATE_START = """
<h1>Flask-FAS-OpenID test app</h1>
{% if g.fas_user %}
<p>Hello, {{ g.fas_user.username }} —
<a href="{{ url_for("logout") }}">Log out</a>
{% else %}
<p>You are not logged in —
<a href="{{ url_for("auth_login", next=request.url) + '' }}">Log in</a>
{% endif %}
— <a href="{{ url_for("index") }}">Main page</a></p>
"""
@app.route('/')
def index():
data = TEMPLATE_START
data += '<p><a href="%s">Check if you are cla+1</a></p>' % \
flask.url_for('claplusone')
data += '<p><a href="%s">See a secret message (requires login)</a></p>' % \
flask.url_for('secret')
return flask.render_template_string(data)
@app.route('/login', methods=['GET', 'POST'])
def auth_login():
# Your application should probably do some checking to make sure the URL
# given in the next request argument is sane. (For example, having next set
# to the login page will cause a redirect loop.) Some more information:
# http://flask.pocoo.org/snippets/62/
if 'next' in flask.request.args:
next_url = flask.request.args['next']
else:
next_url = flask.url_for('index')
# If user is already logged in, return them to where they were last
if flask.g.fas_user:
return flask.redirect(next_url)
return fas.login(return_url=next_url)
@app.route('/logout')
def logout():
if flask.g.fas_user:
fas.logout()
return flask.redirect(flask.url_for('index'))
# This demonstrates the use of the fas_login_required decorator. The
# secret message can only be viewed by those who are logged in.
@app.route('/secret')
@fas_login_required
def secret():
data = TEMPLATE_START + '<p>Be sure to drink your Ovaltine</p>'
return flask.render_template_string(data)
# This demonstrates checking for group membership inside of a function.
# The flask_fas adapter also provides a cla_plus_one_required decorator that
# can restrict a url so that you can only access it from an account that has
# cla +1.
@app.route('/claplusone')
@cla_plus_one_required
def claplusone():
data = TEMPLATE_START
data += '<p>Your account is cla+1.</p>'
return flask.render_template_string(data)
if __name__ == '__main__':
app.run(debug=True)
- Authors
- Luke Macken Toshio Kuratomi
- Date
- 3 September 2011
This plugin provides authentication to the Fedora Account System
using the repoze.who WSGI middleware. It is designed for use with
TurboGears2 but it may be used with any repoze.who using
application. Like TurboGears Identity Provider 2, faswho has builtin
CSRF protection. This protection is implemented as a second piece of
middleware and may be used with other repoze.who authentication
schemes.
Setting up authentication against FAS in TurboGears2 is
very easy. It requires one change to be made to
app/config/app_cfg.py. This change will take care of registering
faswho as the authentication provider, enabling CSRF protection,
switching tg.url() to use fedora.ta2g.utils.url() instead, and
allowing the _csrf_token parameter to be given to any URL.
This section needs to be made clearer so that apps like
mirrormanager can be ported to use this.
The fedora.tg2.utils module contains some templates to help
you write CSRF aware login forms and buttons. You can use the
fedora_template() function to integrate them into your templates:
The templates themselves come in two flavors. One set for use with
mako and one set for use with genshi.