This document guides you through the process of setting up a development environment for LinOTP. In the end you should have a running LinOTP system that you can easily modify and test.
The steps in a nutshell:
- Get the LinOTP source code
- Set up your LinOTP development environment
- Configure LinOTP
- Run the LinOTP development server
- Run unit, functional and integration tests
- Use MyPy for typechecking
- Use pre-commit hooks for consistent formatting
- Build the LinOTP debian package
Obtain the LinOTP source code from LinOTP GitHub:
git clone https://github.com/LinOTP/LinOTP.git
If you want to develop LinOTP, you first need to install some software packages that LinOTP depends upon.
On a Debian-based system, run as a superuser:
$ apt-get install build-essential python3-dev \
python3-mysqldb mariadb-server libmariadb-dev-compat libmariadb-dev \
libldap2-dev libsasl2-dev \
libssl-dev
On macOS, install the following dependencies to run LinOTP natively and build LinOTP via containers:
brew install libsodium coreutils mysql-client
LinOTP can use a variety of SQL databases but MySQL/MariaDB is most widely used. Other options include PostgreSQL and SQLite, although SQLite is not recommended for production setups.
The libldap2-dev and libsasl2-dev system packages are needed when
installing the python-ldap dependency via pip. Similarly, the
libssl-dev package is needed when installing the cryptography
dependency via pip.
Then, install the development dependencies using uv, a much faster
Python package installer and resolver:
curl -LsSf https://astral.sh/uv/install.sh | sh
uv sync
source .venv/bin/activate
If you prefer another installation method, please refer to the UV installation guide.
For convenience, you can enable shell auto-completion for uv and uvx:
echo 'eval "$(uv generate-shell-completion bash)"' >> ~/.bashrc
echo 'eval "$(uvx --generate-shell-completion bash)"' >> ~/.bashrc
To install all dependencies (e.g. to run tests or build apidocs) run:
uv sync --all-groups
To upgrade all dependencies (e.g. to fix CVEs without breaking changes) run:
uv sync --all-groups --upgrade
For a quickstart using the default configuration, run:
mkdir -p linotp/cache linotp/logs
linotp init database
linotp init audit-keys
linotp init enc-key
linotp local-admins add <your_username>
linotp local-admins password --password <your_password> <your_username>
linotp run
The last command starts a development server. Now you can open the LinOTP
management interface in your browser (http://localhost:5000/manage/) and
login as <your_username>.
init database will create a SQLite database by default. If you want to use a
PostgreSQL or MariaDB database instead, you can override that setting through
the following environment variable before running linotp init database:
export LINOTP_DATABASE_URI="postgresql://user:pass@host/db_name" #gitleaks:allow
or
export LINOTP_DATABASE_URI="mysql+pymysql://user:pass@host/db_name" #gitleaks:allow
Alternatively you can also set this variable in an .env file, as
we explain next.
LinOTP provides three configuration presets for development, testing and production, but you can customize any of the configuration entries by overriding environment variables or specifying additional configuration files.
To inspect the configuration of your LinOTP instance, run linotp config show,
or linotp config explain if you need more information on the configuration
entries. Both commands accept additional parameters, which you can look up by
appending --help.
Configuration settings are hard-coded in linotp/settings.py, which also
defines a small set of "environments" that pre-cook basic configurations:
- development is aimed at LinOTP developers running LinOTP on their local machine. It enables copious log messages and defaults to using a local SQLite database. This is not safe to use in a production setting.
- testing is an environment that facilitates running system tests. Like development, it enables more prolific logging output.
- production is a more streamlined and secure setup to be used on productive servers.
One of these environments can be selected by setting the LINOTP_CONFIG
variable to development, testing, or production. If unset, it
defaults to default, which is identical to development.
Additional configuration settings can be made via environment variables.
We recommend using a .env file to store these settings, which will be loaded when
LinOTP starts.
If a configuration setting inside LinOTP is named XYZ,
then if an environment variable named LINOTP_XYZ is defined,
its value will be used to set XYZ.
To e.g. set the log level to WARNING, define the environment variable
LINOTP_LOG_LEVEL=WARNING.
As a special feature, configuration settings whose names end in _DIR
or _FILE are supposed to contain the names of directories or files
(surprise!). These can either be absolute names (starting with a /)
or else will have the value of the ROOT_DIR variable prepended when
they are used. This means that if the very last configuration setting
you make changes ROOT_DIR, the value assigned there will be the
effective one even for other earlier settings that use relative path
names: After
ROOT_DIR=/var/foo
LOG_FILE_DIR=linotp
ROOT_DIR=/var/barthe effective value of LOG_FILE_DIR will be /var/bar/./linotp. (Note
that we're inserting a /./ to mark where the implicit value of
ROOT_DIR stops and the configured value of the setting starts.) The
only exception to this is ROOT_DIR itself, which must always contain
an absolute directory name, and defaults to Flask's app.root_path
unless it is explicitly set in a configuration file.
LinOTP predefines certain directory names that should be adapted to the conventions of a specific Linux distribution when preparing a LinOTP distribution package for that distribution. These include:
-
ROOT_DIR: The “root directory” of the LinOTP configuration file tree. By default this is the “Flask application root directory”,app.root_path, IOW the directory where LinOTP'sapp.pyfile is located. As mentioned above, the value ofROOT_DIRis prepended to the values of other configuration settings for files and directories if these are relative path names. -
CACHE_DIR: This directory is used for temporary storage of LinOTP data. It defaults toROOT_DIR/cache, but in a distribution will more likely be something like/var/cache/linotp. Note that the actual caches are supposed to be in subdirectories of this directory in order to avoid namespace issues. For example, the resolver cache is found inCACHE_DIR/resolvers, and if Beaker is used with a file-backed cache (not the default method), that cache will be inCACHE_DIR/beaker. These assignments cannot be changed except by changing the LinOTP source code. -
LOG_FILE_DIR: This is where the log file ends up if you're logging to a file (which is something LinOTP does by default). By default this isROOT_DIR/logsbut distribution packages will probably wish to use something like/var/log/linotp.
To make life easier, LinOTP offers a linotp command which you can
run anywhere without having to define FLASK_APP. To enable this on
your development system, execute (if you haven't done so already):
uv sync
(This installs the linotp command in the virtualenv's bin
directory.) After this, a simple
linotp run
will launch the Flask development server. (You can still use environment variables to define the desired environment. see Configure LinOTP)
This starts the Flask development server. Unless you specify otherwise
using the --host and --port options, the development server will
bind to TCP port 5000 on the loopback address (127.0.0.1).
Make sure to create an admin user, otherwise you will not be able to log in to LinOTP's management interface:
linotp local-admins add <your_username>
linotp local-admins password -p <your_password> <your_username>
It is possible to enable the Flask debugger (auto-reload on source code changes and
the interactive debugger) by setting the environment variable FLASK_DEBUG=1 or by
running the linotp run command with the --debug flag.
You can run unit and functional tests by entering the respective commands below from the top-level directory of the LinOTP distribution:
make test # will run all tests
make unittests # will run only unit tests
make functionaltests # will run only functional tests
make integrationtests # will run only integration tests
You can also run the tests directly in their directories:
pytest linotpd/src/linotp/tests/unit
or
pytest linotpd/src/linotp/tests/functional
If you want to run only the tests in a single file, invoke pytest
with the path to that file.
When using make, you can pass command-line arguments to pytest by
assigning them to PYTESTARGS:
make unittests PYTESTARGS="-vv"
See the Pytest documentation for more information about using pytest.
To run integration tests with Selenium, please make sure that your
system has the chromedriver executable installed.
Then start a LinOTP development server and edit
linotpd/src/linotp/tests/integration/server_cfg.ini so that the
[linotp] section contains its hostname/IP address and port number.
You can now execute integration tests with:
pytest --tc-file=linotpd/src/linotp/tests/integration/server_cfg.ini <path_to_test_file>
You can find sample test files under linotpd/src/linotp/tests/integration.
You can run end-to-end integration tests using Docker Compose:
# Start all services
docker compose -f compose.e2etests.yml up -d
# Watch test execution logs
docker compose -f compose.e2etests.yml logs -f runner
# Clean up when done
docker compose -f compose.e2etests.yml down
You can also test against Firefox instead of Chrome (see note in compose file).
For debugging:
- write Screenshots of failed tests to host:
- comment in the volume
# - ./Screenshots:/app/linotp/tests/integration/Screenshots - create the folder
Screenshotsand update its permissions:mkdir Screenshots && chmod 777 Screenshots
- comment in the volume
- Look into Selenium to see tests live:
- (potential) password:
secret - via Browser: http://localhost:7900
- via VNCViewer: http://localhost:5900
- (potential) password:
- Access LinOTP UI: http://localhost:5000/manage/
- username:
admin - password:
admin - You can use this to view which data is shown in Manage-UI
- username:
- View specific service logs:
docker compose -f compose.e2etests.yml logs -f <service-name>- most useful:
docker compose -f compose.e2etests.yml logs -f runner
- most useful:
- Enter e.g. test container:
docker compose -f compose.e2etests.yml run --rm runner /bin/bash - Pass args to pytest via env
PYTESTARGS.- e.g. only run a specific test:
PYTESTARGS=test_emailtoken.py::TestEmailTokenEnroll::test_enroll_token
- e.g. only run a specific test:
To run a type check on the source code, install mypy and sqlalchemy-stubs.
Both requirements are part of the develop requirements:
uv sync
Then run mypy on a directory of your choice like
mypy some/python/dir
If you do not wish to be shown type errors from imported modules, use
the --follow-imports=silent flag.
The --show-column-numbers flag can also be helpful when looking for
the exact location of a problem.
This repository is using the pre-commit framework to ensure a consistent style across the whole project. Inspect .pre-commit-config.yaml for the configured tools and our pyproject.toml file for the configuration.
Install pre-commit manually via uv pip or as part of our develop dependencies:
uv sync
Then install the pre-commit hook in git so that it runs before a commit to ensure correct formatting. The same hook is tested in CI, so we strongly advise to install the hook, even if you use all of the tools in your IDE. This way, you will never push a commit that fails the pre-check.
pre-commit install
You can also run the pre-commit hook manually`:
pre-commit run
Use the arguments --files … or --all-files to change what files are checked.
First install the requirements to generate the api documentation:
uv sync --group apidocs
To build the api documentation enter the following commands in your terminal:
$ cd api-doc
$make apidocs html
Here's how to build a container image for LinOTP which works with container runtimes such as Docker and does not depend on LinOTP's Debian packaging:
From the base directory of the LinOTP distribution, run the following command:
docker build -f docker/Dockerfile.linotp -t linotp .
(If you're a lazy sort of person, omitting -f docker/Dockerfile.linotp
is fine because ./Dockerfile is a symbolic link to
./docker/Dockerfile.linotp.)
To run the previously-built image as a container named my_linotp,
run
docker run -p 5000:5000 --name my_linotp linotp
You may wish to use the --env option to pass configuration settings
to LinOTP (prefixed with LINOTP_, as in LINOTP_DATABASE_URI), or
-v to mount volumes. See the LinOTP Containerisation Guide for
more details.
-
Custom translations can be installed by mounting a translation directory over
/custom-translations. This directory should contain a subdirectory hierarchy like{LANGUAGE_TAG}/LC_MESSAGES/linotp.po, e.g..,eo/LC_MESSAGES/linotp.po. (How to obtain thelinotp.pofile for your desired language is beyond the scope of this document.) -
Custom Mako templates can be installed by mounting a directory over
/custom-templateswhose content follows the directory structure withinlinotp/templates(just so stuff is where LinOTP expects it to be). For example, if you have a customaudit.makotemplate for LinOTP's/manageendpoint, copyaudit.makointo a directory calledmy_templates/manageand invoke LinOTP like$ docker run -p 5000:5000 -name my_linotp \ -v ./my_templates:/custom-templates linotp -
Other files such as image files, CSS style sheets or JavaScript files, can go into a directory that is mounted on
/custom-assets. For example, if you want to customise the appearance of the self-service component using aselfservice-style.cssfile that you wrote, create amy-assetsdirectory, copy your stylesheet into it, and use the-v ./my-assets:/custom-assetsoption when starting the LinOTP container. (Please do this only if you know what you're doing. If you break LinOTP, you get to keep the pieces.) -
To make LinOTP data such as the (SQLite) database, audit keys, and encryption key persistent, mount a Docker volume on
/data:$ docker run -p 5000:5000 -name my_linotp \ -v my_persistent_volume:/data linotp(Refer to the Docker documentation to find out about named volumes, or use a local directory as in the previous examples.)
YOU WILL DEFINITELY WANT TO DO THIS IF YOU PLAN TO DO MORE WITH LINOTP THAN JUST FOOL AROUND FOR A BIT, BECAUSE IF THE ENCRYPTION KEY FOR THE DATABASE EVER GETS LOST, YOU WILL BE, TO USE THE TECHNICAL TERM, WELL AND TRULY F...ED.
(Having said that, LinOTP always puts the content of
/datainto a volume, even if you don't specify one explicitly, so the encryption key shouldn't get lost between runs of the same LinOTP container; it may just be tedious to get at from outside the running container. If you need to, refer to thedocker inspectcommand to find out where Docker hides it.)