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 an Authorization 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 an Authorization 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)