Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • uffd/uffd
  • rixx/uffd
  • thies/uffd
  • leona/uffd
  • enbewe/uffd
  • strifel/uffd
  • thies/uffd-2
7 results
Show changes
Commits on Source (246)
......@@ -52,7 +52,6 @@ coverage.xml
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
......
image: registry.git.cccv.de/infra/uffd/docker-images/buster
image: registry.git.cccv.de/uffd/docker-images/bookworm
variables:
DEBIAN_FRONTEND: noninteractive
GIT_SUBMODULE_STRATEGY: normal
PYTHONPATH: deps/ldapalchemy
APT_API_URL: https://packages.cccv.de
APT_REPO: uffd
PYLINT_PIN: pylint~=2.16.2
before_script:
- python3 -V
......@@ -11,20 +13,75 @@ before_script:
- uname -a
- python3 -m pylint --version
- python3 -m coverage --version
- export PACKAGE_VERSION="$(git describe | sed -E -n -e 's/^v([0-9.]*)$/\1/p' -e 's/^v([0-9.]*)-([0-9]*)-g([0-9a-z]*)$/\1.dev+git.\3/p' | grep .)"
.build:
stage: build
build:pip:
extends: .build
script:
- python3 -m build
artifacts:
paths:
- dist/*
build:apt:
extends: .build
script:
- ./debian/create_changelog.py uffd > debian/changelog
- export PYBUILD_INSTALL_ARGS="--install-lib=/usr/share/uffd/ --install-scripts=/usr/share/uffd/"
- export DEB_BUILD_OPTIONS=nocheck
- dpkg-buildpackage -us -uc
- mv ../*.deb ./
- dpkg-deb -I *.deb
- dpkg-deb -c *.deb
artifacts:
paths:
- ./*.deb
db_migrations_updated:
stage: test
needs: []
script:
- FLASK_APP=uffd FLASK_ENV=testing flask db upgrade
- FLASK_APP=uffd FLASK_ENV=testing flask db migrate 2>&1 | grep -q 'No changes in schema detected'
linter:buster:
image: registry.git.cccv.de/uffd/docker-images/buster
stage: test
needs: []
script:
- FLASK_APP=uffd flask db upgrade
- FLASK_APP=uffd flask db migrate 2>&1 | grep -q 'No changes in schema detected'
- pip3 install $PYLINT_PIN pylint-gitlab pylint-flask-sqlalchemy # this force-updates jinja2 and some other packages!
- python3 -m pylint --output-format=pylint_gitlab.GitlabCodeClimateReporter:codeclimate.json,pylint_gitlab.GitlabPagesHtmlReporter:pylint.html,colorized uffd
artifacts:
when: always
paths:
- pylint.html
reports:
codequality: codeclimate.json
linter:bullseye:
image: registry.git.cccv.de/uffd/docker-images/bullseye
stage: test
needs: []
script:
- pip3 install $PYLINT_PIN pylint-gitlab pylint-flask-sqlalchemy # this force-updates jinja2 and some other packages!
- python3 -m pylint --output-format=pylint_gitlab.GitlabCodeClimateReporter:codeclimate.json,pylint_gitlab.GitlabPagesHtmlReporter:pylint.html,colorized uffd
artifacts:
when: always
paths:
- pylint.html
reports:
codequality: codeclimate.json
linter:
linter:bookworm:
image: registry.git.cccv.de/uffd/docker-images/bookworm
stage: test
needs: []
script:
- pip3 install pylint-gitlab # this force-updates jinja2 and some other packages!
- python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabCodeClimateReporter uffd > codeclimate.json
- python3 -m pylint --exit-zero --rcfile .pylintrc --output-format=pylint_gitlab.GitlabPagesHtmlReporter uffd > pylint.html
- python3 -m pylint --rcfile .pylintrc --output-format=text uffd
- pip3 install $PYLINT_PIN pylint-gitlab pylint-flask-sqlalchemy # this force-updates jinja2 and some other packages!
- python3 -m pylint --output-format=pylint_gitlab.GitlabCodeClimateReporter:codeclimate.json,pylint_gitlab.GitlabPagesHtmlReporter:pylint.html,colorized uffd
artifacts:
when: always
paths:
......@@ -32,36 +89,211 @@ linter:
reports:
codequality: codeclimate.json
unittests:
tests:buster:sqlite:
image: registry.git.cccv.de/uffd/docker-images/buster
stage: test
needs: []
script:
- python3 -m pytest --junitxml=report.xml
artifacts:
when: always
reports:
junit: report.xml
tests:buster:mysql:
image: registry.git.cccv.de/uffd/docker-images/buster
stage: test
needs: []
script:
- service mysql start
- TEST_WITH_MYSQL=1 python3 -m pytest --junitxml=report.xml
artifacts:
when: always
reports:
junit: report.xml
tests:bullseye:sqlite:
image: registry.git.cccv.de/uffd/docker-images/bullseye
stage: test
needs: []
script:
- python3 -m pytest --junitxml=report.xml
artifacts:
when: always
reports:
junit: report.xml
tests:bullseye:mysql:
image: registry.git.cccv.de/uffd/docker-images/bullseye
stage: test
needs: []
script:
- service slapd start
- UNITTEST_OPENLDAP=1 python3-coverage run --include 'uffd/*.py' -m pytest --junitxml=report.xml || true
- service mariadb start
- TEST_WITH_MYSQL=1 python3 -m pytest --junitxml=report.xml
artifacts:
when: always
reports:
junit: report.xml
tests:bookworm:sqlite:
image: registry.git.cccv.de/uffd/docker-images/bookworm
stage: test
needs: []
script:
- rm -rf pages
- mkdir -p pages
- cp -r uffd/static pages/static
- DUMP_PAGES=pages python3-coverage run --include 'uffd/*.py' -m pytest --junitxml=report.xml || touch failed
- sed -i -e 's/href="\/static\//href=".\/static\//g' -e 's/src="\/static\//src=".\/static\//g' pages/*.html || true
- python3-coverage report -m
- python3-coverage html
- python3-coverage xml
- test ! -e failed
artifacts:
when: always
paths:
- htmlcov/index.html
- htmlcov
- pages
expose_as: 'Coverage Report'
reports:
cobertura: coverage.xml
coverage_report:
coverage_format: cobertura
path: coverage.xml
junit: report.xml
coverage: '/^TOTAL.*\s+(\d+\%)$/'
tests:bookworm:mysql:
image: registry.git.cccv.de/uffd/docker-images/bookworm
stage: test
needs: []
script:
- service mariadb start
- TEST_WITH_MYSQL=1 python3 -m pytest --junitxml=report.xml
artifacts:
when: always
reports:
junit: report.xml
html5validator:
stage: test
needs:
- job: tests:bookworm:sqlite
script:
- rm -rf pages
- mkdir -p pages
- cp -r uffd/static pages/static
- DUMP_PAGES=pages python3 -m unittest discover tests
- sed -i -e 's/href="\/static\//href=".\/static\//g' -e 's/src="\/static\//src=".\/static\//g' pages/*.html
- html5validator --root pages 2>&1 | tee html5validator.log
artifacts:
when: on_failure
paths:
- pages
- html5validator.log
.trans:
stage: test
needs: []
script:
- ./update_translations.sh $TRANSLATION_LANGUAGE
coverage: '/^TOTAL.*\s+(\d+\%)$/'
trans_de:
extends: .trans
variables:
TRANSLATION_LANGUAGE: de
test:package:pip:buster:
image: registry.git.cccv.de/uffd/docker-images/buster
stage: test
needs:
- job: build:pip
script:
- pip3 install dist/*.tar.gz
test:package:pip:bullseye:
image: registry.git.cccv.de/uffd/docker-images/bullseye
stage: test
needs:
- job: build:pip
script:
- pip3 install dist/*.tar.gz
test:package:pip:bookworm:
image: registry.git.cccv.de/uffd/docker-images/bookworm
stage: test
needs:
- job: build:pip
script:
- pip3 install dist/*.tar.gz
# Since we want to test if the package installs correctly on a fresh Debian
# install (has correct dependencies, etc.), we don't use uffd/docker-images
# here
test:package:apt:buster:
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/debian:buster
stage: test
needs:
- job: build:apt
before_script: []
script:
- apt -y update
- apt -y install curl ./*.deb
- service uwsgi start uffd || ( service uwsgi status uffd ; sleep 15; cat /var/log/uwsgi/app/uffd.log; )
- echo "server { listen 127.0.0.1:5000 default_server; include /etc/uffd/nginx.include.conf; }" > /etc/nginx/sites-enabled/uffd.ini
- service nginx start || ( service nginx status; nginx -t; exit 1; )
- uffd-admin routes
- curl -Lv 127.0.0.1:5000
test:package:apt:bullseye:
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/debian:bullseye
stage: test
needs:
- job: build:apt
before_script: []
script:
- apt -y update
- apt -y install curl ./*.deb
- service uwsgi start uffd || ( service uwsgi status uffd ; sleep 15; cat /var/log/uwsgi/app/uffd.log; )
- echo "server { listen 127.0.0.1:5000 default_server; include /etc/uffd/nginx.include.conf; }" > /etc/nginx/sites-enabled/uffd.ini
- service nginx start || ( service nginx status; nginx -t; exit 1; )
- uffd-admin routes
- curl -Lv 127.0.0.1:5000
test:package:apt:bookworm:
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/debian:bookworm
stage: test
needs:
- job: build:apt
before_script: []
script:
- apt -y update
- apt -y install curl ./*.deb
- service uwsgi start uffd || ( service uwsgi status uffd ; sleep 15; cat /var/log/uwsgi/app/uffd.log; )
- echo "server { listen 127.0.0.1:5000 default_server; include /etc/uffd/nginx.include.conf; }" > /etc/nginx/sites-enabled/uffd.ini
- service nginx start || ( service nginx status; nginx -t; exit 1; )
- uffd-admin routes
- curl -Lv 127.0.0.1:5000
.publish:
stage: deploy
rules:
- if: '$CI_COMMIT_TAG =~ /v[0-9]+[.][0-9]+[.][0-9]+.*/'
publish:pip:
extends: .publish
script:
- TWINE_USERNAME="${GITLABPKGS_USERNAME}" TWINE_PASSWORD="${GITLABPKGS_PASSWORD}" python3 -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/*
- TWINE_USERNAME="${PYPI_USERNAME}" TWINE_PASSWORD="${PYPI_PASSWORD}" python3 -m twine upload dist/*
dependencies:
- build:pip
publish:apt:
extends: .publish
script:
- export DEBPATH="$(echo *.deb)"
- echo Upload deb file, add it to repo and clean up upload
- curl --user "${APTLY_API_USER}:${APTLY_API_PW}" -X POST -F file=@"$DEBPATH" "${APT_API_URL}/api/files/${APT_REPO}-ci-upload-${CI_JOB_ID}"
- curl --user "${APTLY_API_USER}:${APTLY_API_PW}" -X POST "${APT_API_URL}/api/repos/${APT_REPO}/file/${APT_REPO}-ci-upload-${CI_JOB_ID}"
- curl --user "${APTLY_API_USER}:${APTLY_API_PW}" -X DELETE "${APT_API_URL}/api/files/${APT_REPO}-ci-upload-${CI_JOB_ID}"
- echo Update published repo for all distros
- 'curl --user "${APTLY_API_USER}:${APTLY_API_PW}" -X PUT -H "Content-Type: application/json" --data "{ }" "${APT_API_URL}/api/publish/uffd/buster"'
- 'curl --user "${APTLY_API_USER}:${APTLY_API_PW}" -X PUT -H "Content-Type: application/json" --data "{ }" "${APT_API_URL}/api/publish/uffd/bullseye"'
- 'curl --user "${APTLY_API_USER}:${APTLY_API_PW}" -X PUT -H "Content-Type: application/json" --data "{ }" "${APT_API_URL}/api/publish/uffd/bookworm"'
dependencies:
- build:apt
[submodule "deps/ldapalchemy"]
path = deps/ldapalchemy
url = ../ldapalchemy.git
......@@ -28,7 +28,7 @@ limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
load-plugins=pylint_flask_sqlalchemy
# Pickle collected data for later comparisons.
persistent=yes
......@@ -63,92 +63,13 @@ confidence=
disable=missing-module-docstring,
missing-class-docstring,
missing-function-docstring,
print-statement,
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
backtick,
long-suffix,
old-ne-operator,
old-octal-literal,
import-star-module-level,
non-ascii-bytes-literal,
raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
unused-wildcard-import,
apply-builtin,
basestring-builtin,
buffer-builtin,
cmp-builtin,
coerce-builtin,
execfile-builtin,
file-builtin,
long-builtin,
raw_input-builtin,
reduce-builtin,
standarderror-builtin,
unicode-builtin,
xrange-builtin,
coerce-method,
delslice-method,
getslice-method,
setslice-method,
no-absolute-import,
old-division,
dict-iter-method,
dict-view-method,
next-method-called,
metaclass-assignment,
indexing-exception,
raising-string,
reload-builtin,
oct-method,
hex-method,
nonzero-method,
cmp-method,
input-builtin,
round-builtin,
intern-builtin,
unichr-builtin,
map-builtin-not-iterating,
zip-builtin-not-iterating,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
eq-without-hash,
div-method,
idiv-method,
rdiv-method,
exception-message-attribute,
invalid-str-codec,
sys-max-int,
bad-python3-import,
deprecated-string-function,
deprecated-str-translate-call,
deprecated-itertools-function,
deprecated-types-field,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating,
deprecated-operator-function,
deprecated-urllib-function,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape,
too-few-public-methods,
method-hidden,
bad-continuation,
too-many-ancestors,
no-self-use,
duplicate-code,
redefined-builtin,
superfluous-parens,
consider-using-f-string, # Temporary
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
......@@ -467,13 +388,6 @@ max-line-length=160
# Maximum number of lines in a module.
max-module-lines=1000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,
dict-separator
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
......@@ -594,5 +508,5 @@ min-public-methods=2
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
Exception
overgeneral-exceptions=builtin.BaseException,
builtin.Exception
include setup.py
recursive-include uffd *.py *.html *.js *.css *.txt *.po *.cfg
graft uffd/migrations
graft uffd/templates
graft uffd/*/templates
graft uffd/static/
graft uffd/translations
# uffd
# Uffd
This is the UserFerwaltungsFrontend.
A web service to manage LDAP users, groups and permissions.
Uffd (UserFerwaltungsFrontend) is a web-based user management and single sign-on software.
Development chat: [#uffd-development](https://rocket.cccv.de/channel/uffd-development)
## Dependencies
Please note that we refer to Debian packages here and **not** pip packages.
## dependencies
- python3
- python3-ldap3
- python3-flask
- python3-flask-sqlalchemy
- python3-flask-migrate
- python3-qrcode
- python3-fido2 (version 0.5.0, optional)
- python3-flask-oauthlib
- git (cli utility, musst be in path)
- python3-fido2 (version 0.5.0 or 0.9.1, optional)
- python3-prometheus-client (optional, needed for metrics)
- python3-jwt
- python3-cryptography
- python3-flask-babel
- python3-argon2
- python3-itsdangerous (also a dependency of python3-flask)
- python3-mysqldb or python3-pymysql for MariaDB support
- python3-ua-parser (optional, better user agent parsing)
Some of the dependencies (especially fido2 and flask-oauthlib) changed their API in recent versions, so make sure to install the versions from Debian Buster.
You can also use virtualenv with the supplied `requirements.txt`.
Some of the dependencies (especially fido2) changed their API in recent versions, so make sure to install the versions from Debian Bookworm, Bullseye or Buster.
For development, you can also use virtualenv with the supplied `requirements.txt`.
## development
## Development
Before running uffd, you need to create the database with `flask db upgrade`. The database is placed in
`instance/uffd.sqlit3`.
Before running uffd, you need to create the database with `flask db upgrade`.
Then use `flask run` to start the application:
```
......@@ -27,39 +38,56 @@ FLASK_APP=uffd flask db upgrade
FLASK_APP=uffd FLASK_ENV=development flask run
```
During development, you may want to enable LDAP mocking, as you otherwise need to have access to an actual LDAP server with the required schema.
You can do so by setting `LDAP_SERVICE_MOCK=True` in the config.
During development, you may want to create some example data:
```
export FLASK_APP=uffd
flask group create 'uffd_access' --description 'Access to Single-Sign-On and Selfservice'
flask group create 'uffd_admin' --description 'Admin access to uffd'
flask role create 'base' --default --add-group 'uffd_access'
flask role create 'admin' --add-group 'uffd_admin'
flask user create 'testuser' --password 'userpassword' --mail 'test@example.com' --displayname 'Test User'
flask user create 'testadmin' --password 'adminpassword' --mail 'admin@example.com' --displayname 'Test Admin' --add-role 'admin'
```
Afterwards you can login as a normal user with "testuser" and "userpassword", or as an admin with "testadmin" and "adminpassword".
Please note that the mocked LDAP functionality is very limited and many uffd features do not work correctly without a real LDAP server.
## deployment
## Deployment
Use uwsgi. Make sure to run `flask db upgrade` after every update!
Do not use `pip install uffd` for production deployments!
The dependencies of the pip package roughly represent the versions shipped by Debian stable.
We do not keep them updated and we do not test the pip package!
The pip package only exists for local testing/development and to help build the Debian package.
### example uwsgi config
We provide packages for Debian stable, oldstable and oldoldstable (currently Bookworm, Bullseye and Buster).
Since all dependencies are available in the official package mirrors, you will get security updates for everything but uffd itself from Debian.
To install uffd on Debian Bullseye, add our package mirror to `/etc/sources.list`:
```
[uwsgi]
plugin = python3
env = PYTHONIOENCODING=UTF-8
env = LANG=en_GB.utf8
env = TZ=Europe/Berlin
manage-script-name = true
chdir = /var/www/uffd
module = uffd:create_app()
uid = uffd
gid = uffd
vacuum = true
die-on-term = true
hook-pre-app = exec:FLASK_APP=uffd flask db upgrade
deb https://packages.cccv.de/uffd bullseye main
```
## python style conventions
Then download [cccv-archive-key.gpg](https://packages.cccv.de/docs/cccv-archive-key.gpg) and add it to the trusted repository keys in `/etc/apt/trusted.gpg.d/`.
Afterwards run `apt update && apt install uffd` to install the package.
The Debian package uses uwsgi to run uffd and ships an `uffd-admin` script to execute flask commands in the correct context.
If you upgrade, make sure to run `flask db upgrade` after every update! The Debian package takes care of this by itself using uwsgi pre start hooks.
For an example uwsgi config, see our [uswgi.ini](uwsgi.ini). You might find our [nginx include file](nginx.include.conf) helpful to setup a web server in front of uwsgi.
tabs.
Uffd supports SQLite and MariaDB. To use MariaDB, create the database with the options `CHARACTER SET utf8mb4 COLLATE utf8mb4_nopad_bin` and make sure to add the `?charset=utf8mb4` parameter to `SQLALCHEMY_DATABASE_URI`.
## Python Coding Style Conventions
PEP 8 without double new lines, tabs instead of spaces and a max line length of 160 characters.
We ship a [pylint](https://pylint.org/) config to verify changes with.
## Configuration
Uffd reads its default config from `uffd/default_config.cfg`.
You can overwrite config variables by creating a config file in the `instance` folder.
The file must be named `config.cfg` (Python syntax), `config.json` or `config.yml`/`config.yaml`.
You can also set a custom file path with the environment variable `CONFIG_PATH`.
## OAuth2 Single-Sign-On Provider
......@@ -71,7 +99,9 @@ The services need to be setup to use the following URLs with the Authorization C
* `/oauth2/token`: token request endpoint
* `/oauth2/userinfo`: endpoint that provides information about the current user
The userinfo endpoint returns json data with the following structure:
If the service supports server metadata discovery ([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)), configuring the base url of your uffd installation or `/.well-known/openid-configuration` as the discovery endpoint should be sufficient.
The only OAuth2 scope supported is `profile`. The userinfo endpoint returns json data with the following structure:
```
{
......@@ -79,7 +109,6 @@ The userinfo endpoint returns json data with the following structure:
"name": "Test User",
"nickname": "testuser"
"email": "testuser@example.com",
"ldap_dn": "uid=testuser,ou=users,dc=example,dc=com",
"groups": [
"uffd_access",
"users"
......@@ -87,4 +116,79 @@ The userinfo endpoint returns json data with the following structure:
}
```
`id` is the uidNumber, `name` the display name (cn) and `nickname` the uid of the user's LDAP object.
`id` is the numeric (Unix) user id, `name` the display name and `nickname` the loginname of the user.
## OpenID Connect Single-Sign-On Provider
In addition to plain OAuth2, uffd also has basic OpenID Connect support.
Endpoint URLs are the same as for plain OAuth2.
OpenID Connect support is enabled by requesting the `openid` scope.
ID token signing keys are served at `/oauth2/keys`.
See [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html) specification for more details.
Supported flows and response types:
* Only Authorization Code Flow with `code` response type
Supported scopes:
* `openid`: Enables OpenID Connect support and returns mandatory `sub` claim
* `profile`: Returns `name` and `preferred_username` claims
* `email`: Returns `email` and `email_verified` claims
* `groups`: Returns non-standard `groups` claim
Supported claims:
* `sub` (string): Decimal encoded numeric (Unix) user id
* `name` (string): Display name
* `preferred_username`(string): Loginname
* `email` (string): Service-specific or primary email address
* `email_verified` (boolean): Verification status of `email` value (always `true`)
* `groups` (array of strings): Names of groups the user is a member of (non-standard)
uffd supports the optional `claims` authorization request parameter for requesting claims individually.
Note that there is a IANA-registered `groups` claim with a syntax borrowed from [SCIM](https://www.rfc-editor.org/rfc/rfc7643.html).
The syntax used by uffd is different and incompatible, although arguably more common for a claim named "groups" in this context.
uffd aims for complience with OpenID provider conformance profiles Basic and Config.
It is, however, not a certified OpenID provider and it has the following limitations:
* Only the `none` value for the `prompt` authorization request parameter is recognized. Other values (`login`, `consent` and `select_account`) are ignored.
* The `max_age` authorization request parameter is not supported and ignored by uffd.
* The `auth_time` claim is not supported and neither returned if the `max_age` authorization request parameter is present nor if it is requested via the `claims` parameter.
* Requesting the `sub` claim with a specific value for the ID Token (or passing the `id_token_hint` authorization request parameter) is only supported if the `prompt` authorization request parameter is set to `none`. The authorization request is rejected otherwise.
## Metrics
Uffd can export metrics in a prometheus compatible way. It needs python3-prometheus-client for this feature to work.
Metrics can be accessed via `/metrics` and `/api/v1/metrics_prometheus`.
Those endpoints are protected via api credentials. Add prometheus in the uffd UI as a service and create an
api client with the `metrics` permission. Then you can access the metrics like that:
```
$ curl localhost:5000/api/v1/metrics_prometheus --user api-user:api-password
# HELP python_info Python platform information
# TYPE python_info gauge
python_info{implementation="CPython",major="3",minor="9",patchlevel="2",version="3.9.2"} 1.0
# HELP uffd_version_info Various version infos
# TYPE uffd_version_info gauge
uffd_version_info{version="local"} 1.0
[..]
```
## Translation
The web frontend is initially written in English and translated in the following Languages:
![status](https://git.cccv.de/uffd/uffd/badges/master/coverage.svg?job=trans_de&key_text=DE)
The selection uses the language browser header by default but can be overwritten via a UI element.
You can specify the available languages in the config.
Use the `update_translations.sh` to update the translation files.
## License
GNU Affero General Public License v3.0, see [LICENSE](LICENSE).
# Upgrading from v1 to v2
Prior to v2 uffd stored users, groups and mail aliases on an LDAP server.
OAuth2 and API client credentials were defined in the config. Starting with
v2 uffd stores all of this in its database and no longer supports LDAP.
A number of other features and configurations are no longer supported. See the
changelog for details on changed and removed features.
## Preparations
Create a backup of the database before attempting an upgrade. The database
migration scripts are quite complex and somewhat fragile. If anything fails,
you will end up with a broken database that is difficult or impossible to
recover from. Furthermore downgrading from v2 to v1 is not supported.
Make sure no service (besides uffd) directly accesses your LDAP server.
Migrate any remaining services to [uffd-ldapd][] or other solutions that
solely rely on uffds API and OAuth2 endpoints. Uffd will cease to update
any data stored in the LDAP directory.
Migrate all API clients defined with the `API_CLIENTS` config option to the
`API_CLIENTS_2` option. This includes changing authentication from a
token-based mechanism to HTTP Basic Authentication and consequently replacing
affected credentials.
The imported OAuth2 and API clients are grouped by service objects. These
service objects will be auto-created for each client with unique names derived
from the `client_id` parameter. Add the `service_name` parameter to clients to
set a custom name. This name is visible to users in place of the OAuth2
`client_id`. Use the same `service_name` for multiple clients to group them
together. This is recommended for OAuth2 and API credentials used by the same
services, as future features like service-specific email addresses will be
added as service-level options. The OAuth2 client parameter `required_group` is
imported as a service-level option. Make sure that grouped OAuth2 clients have
the same `required_group` value, otherwise nobody will be able to access the
service. Note that values other than a single group name are not supported.
Adjust the ACLs of your LDAP server so uffd can read the `userPassword`
attribute of user objects. Note that uffd will not perform any writes to the
LDAP server during or after the upgrade.
If you use user bind (config option `LDAP_SERVICE_USER_BIND`), i.e. if you
have uffd authenticate with the LDAP server using the credentials of the
currently logged in user, you will have to replace this configuration and
grant uffd full read access to all user, group and mail alias data with
config-defined credentials.
Install the new dependency `python3-argon2`. (Dist-)Upgrading the Debian
package will do that for you. Do not uninstall the removed dependency
`python3-ldap3` (i.e. do not run `apt autoremove`)! It is required to import
data from the LDAP server.
There is a safeguard in place to prevent accidental upgrades. Add the
following line to your config file to disable the safeguard:
```
UPGRADE_V1_TO_V2=True
```
## Running the Upgrade
Upgrade the Debian package to v2. This will restart the uffd UWSGI app. With
the default UWSGI configuration, the database migration scripts will run
automatically.
Otherwise run them manually:
```
uffd-admin db upgrade
```
The database migration scripts import users, groups and mail aliases from the
configured LDAP server. They also import OAuth2 and API clients defined with
the `OAUTH2_CLIENTS` and `API_CLIENTS_2` config options to the database.
Due to data being split between the LDAP server and the database, uffd v1
tended to accumulate orphaned database objects (e.g. role memberships of
deleted users). All orphaned objects are deleted during the upgrade.
As a side-effect upgrading resets all rate limits.
## Follow-up
Rename the following config options:
* `LDAP_USER_GID` -> `USER_GID`
* `LDAP_USER_MIN_UID` -> `USER_MIN_UID`
* `LDAP_USER_MAX_UID` -> `USER_MAX_UID`
* `LDAP_USER_SERVICE_MIN_UID` -> `USER_SERVICE_MIN_UID`
* `LDAP_USER_SERVICE_MAX_UID` -> `USER_SERVICE_MAX_UID`
Add the following config options:
* `GROUP_MIN_GID`
* `GROUP_MAX_GID`
Remove the following config options:
* `UPGRADE_V1_TO_V2`
* `LDAP_USER_SEARCH_BASE`
* `LDAP_USER_SEARCH_FILTER`
* `LDAP_USER_OBJECTCLASSES`
* `LDAP_USER_DN_ATTRIBUTE`
* `LDAP_USER_UID_ATTRIBUTE`
* `LDAP_USER_UID_ALIASES`
* `LDAP_USER_LOGINNAME_ATTRIBUTE`
* `LDAP_USER_LOGINNAME_ALIASES`
* `LDAP_USER_DISPLAYNAME_ATTRIBUTE`
* `LDAP_USER_DISPLAYNAME_ALIASES`
* `LDAP_USER_MAIL_ATTRIBUTE`
* `LDAP_USER_MAIL_ALIASES`
* `LDAP_USER_DEFAULT_ATTRIBUTES`
* `LDAP_GROUP_SEARCH_BASE`
* `LDAP_GROUP_SEARCH_FILTER`
* `LDAP_GROUP_GID_ATTRIBUTE`
* `LDAP_GROUP_NAME_ATTRIBUTE`
* `LDAP_GROUP_DESCRIPTION_ATTRIBUTE`
* `LDAP_GROUP_MEMBER_ATTRIBUTE`
* `LDAP_MAIL_SEARCH_BASE`
* `LDAP_MAIL_SEARCH_FILTER`
* `LDAP_MAIL_OBJECTCLASSES`
* `LDAP_MAIL_DN_ATTRIBUTE`
* `LDAP_MAIL_UID_ATTRIBUTE`
* `LDAP_MAIL_RECEIVERS_ATTRIBUTE`
* `LDAP_MAIL_DESTINATIONS_ATTRIBUTE`
* `LDAP_SERVICE_URL`
* `LDAP_SERVICE_USE_STARTTLS`
* `LDAP_SERVICE_BIND_DN`
* `LDAP_SERVICE_BIND_PASSWORD`
* `LDAP_SERVICE_USER_BIND`
* `ENABLE_INVITE`
* `ENABLE_PASSWORDRESET`
* `ENABLE_ROLESELFSERVICE`
* `OAUTH2_CLIENTS`
* `API_CLIENTS` (should not be set, see "Preperation")
* `API_CLIENTS_2`
* `LDAP_SERVICE_MOCK` (development option, should not be set)
If you set a custom config filename with the environment variable
`CONFIG_FILENAME`, replace it with `CONFIG_PATH`. The new variable must be
set to a full path instead of a filename.
If you set the config option `ACL_SELFSERVICE_GROUP`, but not
`ACL_ACCESS_GROUP`, make sure to set `ACL_ACCESS_GROUP` to the same value as
`ACL_SELFSERVICE_GROUP`.
Add a cron job that runs `uffd-admin cleanup` at least daily. Unless you
modified `/etc/cron.d/uffd`, upgrading the Debian package will do this for you.
Uninstall the previous dependency `python3-ldap3` (i.e. run `apt autoremove`).
[uffd-ldapd]: https://git.cccv.de/uffd/uffd-ldapd
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
JRWMzY9EoBOHB1PubZuzUNIUQeui1vyX
=uRc5
-----END PGP PUBLIC KEY BLOCK-----
\ No newline at end of file
#!/bin/sh
set -eu
export FLASK_APP=/usr/share/uffd/uffd
export CONFIG_PATH=/etc/uffd/uffd.cfg
if [ "$(whoami)" = "uffd" ]; then
flask "$@"
elif command -v sudo > /dev/null 2>&1; then
exec sudo --preserve-env=FLASK_APP,CONFIG_PATH -u uffd flask "$@"
elif command -v runuser > /dev/null 2>&1; then
exec runuser --preserve-environment -u uffd -- flask "$@"
else
echo "Could not not become 'uffd' user, exiting"
exit 255
fi
FLASK_ENV="production"
SQLALCHEMY_DATABASE_URI="sqlite:////var/lib/uffd/db.sqlite"
#SECRET_KEY=autogenerated by postinst script
Source: uffd
Section: python
Priority: optional
Maintainer: CCCV <it@cccv.de>
Build-Depends:
debhelper-compat (= 12),
dh-python,
python3-all,
python3-setuptools,
Standards-Version: 4.5.0
Homepage: https://git.cccv.de/uffd/uffd
Vcs-Git: https://git.cccv.de/uffd/uffd.git
Package: uffd
Architecture: all
Depends:
${misc:Depends},
# Unlike most debian python packages, we depend directly on the deb packages and do not want to populate our dependencies from the setup.py .
# Because of this we do not use the variable from pybuild.
# ${python3:Depends},
python3-flask,
python3-flask-sqlalchemy,
python3-flask-migrate,
python3-qrcode,
python3-fido2,
python3-jwt,
python3-cryptography,
python3-flask-babel,
python3-argon2,
python3-itsdangerous,
uwsgi,
uwsgi-plugin-python3,
Recommends:
nginx,
python3-mysqldb,
python3-prometheus-client,
python3-ua-parser,
Description: Web-based user management and single sign-on software
#!/usr/bin/python3
import sys
import re
import textwrap
import datetime
import email.utils
import git
package_name = 'UNKNOWN'
alias_names = {
'julian': 'Julian Rother',
'Julian': 'Julian Rother',
}
ignore_commit_regexes = [
'^fixup!',
]
def print_release(tag=None, commits=tuple(), last_tag=None):
release_version = '0.0.0'
release_author = git.objects.util.Actor('None', 'undefined@example.com')
release_date = 0
release_status = 'UNRELEASED'
message = ''
if tag:
release_status = 'unstable'
release_version = tag.name[1:] # strip leading "v"
if isinstance(tag.object, git.TagObject):
release_author = tag.object.tagger
release_date = tag.object.tagged_date
message = tag.object.message.split('-----BEGIN PGP SIGNATURE-----')[0].strip()
else:
release_author = tag.object.committer
release_date = tag.object.committed_date
elif commits:
release_author = commits[0].committer
release_date = commits[0].committed_date
date = datetime.datetime.fromtimestamp(release_date).strftime('%Y%m%dT%H%M%S')
last_version = '0.0.0'
if last_tag:
last_version = last_tag.name[1:] # strip leading "v"
release_version = f'{last_version}+git{date}-{commits[0].hexsha[:8]}'
print(f'{package_name} ({release_version}) {release_status}; urgency=medium')
print()
if message:
print(textwrap.indent(message, ' '))
print()
commit_authors = [] # list of (key, author), sorted by first commit date
commit_author_emails = {} # author email -> key
commit_author_names = {} # author name -> key
commit_author_commits = {} # key -> list of commits
for commit in commits:
if any(filter(lambda pattern: re.match(pattern, commit.summary), ignore_commit_regexes)):
continue
if len(commit.parents) > 1:
continue # Ignore merge commits
author_name = alias_names.get(commit.author.name, commit.author.name)
key = commit_author_emails.get(commit.author.email)
if key is None:
key = commit_author_names.get(author_name)
if key is None:
key = commit.author.email
commit_authors.append((key, author_name))
commit_author_emails[commit.author.email] = key
commit_author_names[author_name] = key
commit_author_commits[key] = commit_author_commits.get(key, []) + [commit]
commit_authors.sort(key=lambda args: len(commit_author_commits[args[0]]))
for key, author_name in commit_authors:
print(f' [ {author_name} ]')
for commit in commit_author_commits[key]:
lines = '\n'.join(textwrap.wrap(commit.summary, 90))
lines = ' * ' + textwrap.indent(lines, ' ').strip()
print(lines)
print()
print(f' -- {alias_names.get(release_author.name, release_author.name)} <{release_author.email}> {email.utils.formatdate(release_date)}')
if __name__ == '__main__':
repo = git.Repo('.')
package_name = sys.argv[1]
version_commits = {}
for tag in repo.tags:
if not re.fullmatch('v[0-9]+[.][0-9]+[.][0-9]+.*', tag.name):
continue
if isinstance(tag.object, git.TagObject):
commit_hexsha = tag.object.object.hexsha
else:
commit_hexsha = tag.object.hexsha
version_commits[commit_hexsha] = tag
tag = None
commits = []
for commit in repo.iter_commits('HEAD'):
if commit.hexsha in version_commits:
prev_tag = version_commits[commit.hexsha]
if commits:
print_release(tag, commits, last_tag=prev_tag)
print()
tag = prev_tag
commits = []
commits.append(commit)
print_release(tag, commits)
# Cronjobs for uffd
@daily uffd flock -n /var/run/uffd/cron.roles-update-all.lock /usr/bin/uffd-admin roles-update-all --check-only 2> /dev/null
@daily uffd flock -n /var/run/uffd/cron.cleanup.lock /usr/bin/uffd-admin cleanup > /dev/null
/etc/uffd
/var/lib/uffd
uwsgi.ini /etc/uffd/
nginx.include.conf /etc/uffd/
debian/contrib/uffd.cfg /etc/uffd/
debian/contrib/uffd-admin /usr/bin/
README.md /usr/share/doc/uffd/
UPGRADE.md /usr/share/doc/uffd/
/etc/uffd/uwsgi.ini /etc/uwsgi/apps-available/uffd.ini
/etc/uwsgi/apps-available/uffd.ini /etc/uwsgi/apps-enabled/uffd.ini
#!/bin/sh
set -e
case "$1" in
configure)
getent group uffd >/dev/null 2>&1 || addgroup --system uffd
adduser --system --home /var/lib/uffd --quiet uffd --ingroup uffd || true
chown -R uffd:uffd /var/lib/uffd
chmod 0770 /var/lib/uffd
python3 <<EOF
import secrets
cfg = open('/etc/uffd/uffd.cfg', 'r').read()
cfg = cfg.replace('\n#SECRET_KEY=autogenerated by postinst script\n',
'\nSECRET_KEY="'+secrets.token_hex(128)+'"\n', 1)
open('/etc/uffd/uffd.cfg', 'w').write(cfg)
EOF
chown root:uffd /etc/uffd/uffd.cfg
chmod 0640 /etc/uffd/uffd.cfg
invoke-rc.d uwsgi restart uffd
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0
#!/bin/sh
set -e
case "$1" in
purge)
delgroup uffd || true
userdel uffd || true
rm -rf /var/lib/uffd
;;
remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear)
;;
*)
echo "postrm called with unknown argument \`$1'" >&2
exit 1
esac
#DEBHELPER#
exit 0
#!/usr/bin/make -f
#export DH_VERBOSE = 1
export PYBUILD_NAME=uffd
%:
dh $@ --with python3 --buildsystem pybuild
#Type Path Mode UID GID Age Argument
d /run/uffd 0755 uffd uffd - -