Usage¶
To use Flask-OpenDirectory in a project:
from flask import Flask
from flask_open_directory import OpenDirectory
app = Flask(__name__)
open_directory = OpenDirectory(app)
Or if using application factories:
from flask_open_directory import OpenDirectory
open_directory = OpenDirectory()
open_directory.init_app(app)
Configuration¶
Configuration is done along with your normal Flask
configuration or through
environment variables.
The following variables are used with Flask-OpenDirectory:
OPEN_DIRECTORY_SERVER = 'example.com' # default: 'localhost'
# this is optional, espically for this usecase, as we will parse the
# server url 'example.com' to this same base dn, if not supplied.
# However, if your base dn does not match your server url, then it
# can be useful to supply your own.
OPEN_DIRECTORY_BASE_DN = 'dc=example,dc=com'
app = Flask(__name__)
app.config['OPEN_DIRECTORY_SERVER'] = OPEN_DIRECTORY_SERVER
app.config['OPEN_DIRECTORY_BASE_DN'] = OPEN_DIRECTORY_BASE_DN
open_directory = OpenDirectory(app)
If the above variables are not with the application configuration, then we will look for environment variables (using the same names) as above.
Route Authorization¶
There are several built-in decorators that can be used to mark a route for authorization.
Example Application:¶
#!/usr/bin/env python
"""
route_authorization.py
----------------------
Example application using the authorization decorators. This example would
require an ``OpenDirectory`` environment with an ``office`` and an
``adminstrators`` group.
"""
from flask import Flask
from flask_open_directory import OpenDirectory, requires_group, \
requires_any_group, requires_all_groups, utils
app = Flask(__name__)
open_directory = OpenDirectory(app)
@app.route('/')
def index():
return "Hello, you don't have to be authorized to access this page."
@app.route('/admins')
@requires_group('administrators')
def admins():
return "Hello '{}', you must be an administrator.".format(
utils.username_from_request()
)
@app.route('/office-or-admins')
@requires_any_group('office', 'administrators')
def office_admins():
return "Hello '{}', you must be an office or adminstrator member".format(
utils.username_from_request()
)
@app.route('/only-office-admins')
@requires_all_groups('office', 'administrators')
def only_office_admins():
return "Hello '{}', you must be an office adminstrator.".format(
utils.username_from_request()
)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
Start the application:¶
$ python route_authorization.py
Test with curl:¶
Use the--basic
authentication flag for curl to set anAuthorization
header. So that our methods will have access to the username the request is for.
$ curl --basic -u office_user http://localhost:5000/office-or-admins
Custom Authorization Decorators¶
Flask-OpenDirectory includes a pass_context()
helper when creating your own
custom authorization decorators. This will pass a DecoratorContext
,
which is a specialized dict
like object as the first argument to your
decorator that gives you access to the OpenDirectory
registered with the
current application, as well as the username
from the
request.authorization
header.
Basic Example:¶
#!/usr/bin/env python
"""
custom_decorator.py
-------------------
This example shows how to create a custom authorization decorator using the
``pass_context`` helper.
"""
from functools import wraps
from flask import abort, Flask
from flask_open_directory import pass_context, OpenDirectory
def only_username(username):
"""A silly example, that only allows the specified username to access
the route.
:param username: The authorized username who can access the route.
"""
def inner(fn):
@wraps(fn)
@pass_context
def decorator(ctx, *args, **kwargs):
request_username = ctx.username
if request_username == username:
return fn(*args, **kwargs)
return abort(401)
return decorator
return inner
app = Flask(__name__)
open_directory = OpenDirectory(app)
@app.route('/george')
@only_username('george')
def george_only():
return 'Hello, you must be George'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
Start the Application:¶
$ python custom_decorator.py
Test with curl:¶
Use the--basic
authentication flag for curl to set anAuthorization
header. So that our methods will have access to the username the request is for. The password used doesn’t really matter here, since there is no authentication middleware present.
$ curl --basic -u george http://localhost:5000/george
The above is a pretty silly and simple example, most likely you are going to
want to do more than compare the username. Odds are you are going to need to
query the OpenDirectory
, using the OpenDirectory.query()
. The
query syntax is very similar to the popular SQLAlchemy
package syntax.
Below is an example showing the internals of the requires_group
decorator.
Advanced Example¶
from flask_open_directory import Group, pass_context
def requires_group(group_name):
"""Decorator to ensure the user is a member of the specified group.
"""
def inner(fn):
@wraps(fn)
@pass_context
def decorator(ctx, *args, **kwargs):
open_directory, username = ctx.open_directory, ctx.username
# query the open_directory connection for the specified group.
group = open_directory.query(Group)\
.filter(group_name=group_name)\
.first()
# check that the user is a member of the group
if group.has_user(username):
return fn(*args, **kwargs)
return abort(401)
return decorator
return inner
Custom Model Creation¶
If you need to query for models other than what is already created by this
package, then you will need to create a custom model to map the
OpenDirectory
attributes to your python object. The trickiest part on
creating custom models, is determining the ldap entry key to use to map to your
python object.
Below are some useful resources/commands to look into, to help determine which ldap keys to use:
- (On macOS) /etc/openldap/schema : Contains the ldap schema(s) used for macOS
$ man dscl
: apple’s directory utility command line interface$ man ldapsearch
: ldap search utility syntax is tough to get used to, but tends to be the best resource (for me) to find attribute names.
Example:
#!/usr/bin/env python """ A custom model example for an ``OpenDirectory`` computer group. """ from flask_open_directory import BaseModel, Attribute class ComputerGroup(BaseModel): """Represents an ``OpenDirectory`` computer group """ id = Attribute('apple-generateduid') """The id for a computer group.""" computer_names = Attribute('memberUid', allow_multiple=True) """A list of the computer name(s) that are members of the group.""" computer_ids = Attribute('apple-group-memberguid', allow_multiple=True) """A list of the computer id(s) that are members of the group.""" def has_computer(self, computer: str) -> bool: """Check if the computer is a member of the group. :param computer: A computer name or computer id to check membership. """ if self.computer_names and computer in self.computer_names: return True if self.computer_ids and computer in self.computer_ids: return True return False @classmethod def query_cn(cls) -> str: """Return the query cn used for searches. """ return 'cn=computer_groups' if __name__ == '__main__': from flask_open_directory import OpenDirectory open_directory = OpenDirectory() computers = open_directory.query(ComputerGroup).all() print('computers', computers) if len(computers) > 0: for c in computers: assert isinstance(c, ComputerGroup) assert isinstance(c.id, str) assert isinstance(c.computer_ids, list) assert isinstance(c.computer_names, list)