Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tombstoning #598

Merged
merged 49 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
7d04487
adr: Postulate adr006 (combined tombstoning of uid and login)
lukasjuhrich Jan 15, 2023
1a08bc8
[mypy] Add type hints to pycroft.model.ddl
lukasjuhrich Jan 16, 2023
a248875
[model] Add `UnixTombstone` class
lukasjuhrich Jan 28, 2023
5d2eca8
[tests] add some more tests for unix tombstone consistency
lukasjuhrich Jul 6, 2024
dcb5b7d
[model] add missing constraint checks to UnixTombstone
lukasjuhrich Jul 6, 2024
6168a80
tests: add missing tombstone tests to (user + unix_account) case
lukasjuhrich Jul 8, 2024
16ca6aa
tests: Replace `--echo` flag by automatic verbosity interpretation
lukasjuhrich Jul 8, 2024
b540dd0
tombstoning: use `bytea` for `login_hash`, ensure this with meta test
lukasjuhrich Jul 8, 2024
ee52d9c
tests: add unix tombstone lifecycle tests
lukasjuhrich Jul 8, 2024
f34e6f6
model: Implement tombstone lifecycle checks
lukasjuhrich Jul 8, 2024
851c325
model: Extract `pycroft.model.unix_account`
lukasjuhrich Jul 8, 2024
1265363
model: Remove long obsoleted `DDL.on` field
lukasjuhrich Jul 8, 2024
a816e23
model: use dataclasses for custom DDL statements
lukasjuhrich Jul 9, 2024
0984c1e
docs: fix some sphinx warnings
lukasjuhrich Jul 9, 2024
65ef7c6
docs: update sphinx to v7 and add some helpful extensions
lukasjuhrich Jul 10, 2024
497ab62
Document tombstone model
lukasjuhrich Jul 10, 2024
81a0575
Add missing test cases
lukasjuhrich Jul 10, 2024
82c576a
docs: remove nonexistent file `interval_base`
lukasjuhrich Jul 11, 2024
8bb6f77
Fix some docs problems
lukasjuhrich Jul 11, 2024
7761b65
docs: Use a different set of sphinx-toolbox tools
lukasjuhrich Jul 11, 2024
664a2e6
Fix incorrecd docstring regarding `UnixAccount` creation in `move_in`
lukasjuhrich Aug 28, 2024
2a6edb3
Add missing cases to unix account consistency triggers
lukasjuhrich Aug 28, 2024
f767883
model: darker fixes
lukasjuhrich Aug 28, 2024
6ceb978
build: update `bun` to v1.1.26
lukasjuhrich Aug 28, 2024
7f1f807
Use `bun outdated` instead of `npm-check-updates`
lukasjuhrich Aug 28, 2024
d613b43
tooling: add docstring to `just deps-compile`
lukasjuhrich Aug 28, 2024
90db0e1
docs: vendor mermaid.js v11.0.2
lukasjuhrich Aug 28, 2024
4538bf7
tests: move fixtures in TestUserCreation to the bottom
lukasjuhrich Sep 4, 2024
f6a6bc9
Provide separate function for login hash calculation
lukasjuhrich Sep 4, 2024
32658bc
Add function checking for login availability
lukasjuhrich Sep 4, 2024
2305d6f
qa: Use backref instead of custom query
lukasjuhrich Sep 4, 2024
3d25a38
Reorder checks in `check_user_data`
lukasjuhrich Sep 4, 2024
3afcb3f
qa: Improve readability of move_in_date check
lukasjuhrich Sep 4, 2024
2b07ce9
qa: Group similar user data checks
lukasjuhrich Sep 4, 2024
6d6cfe3
Remove behavior-changing parameter `allow_existing`
lukasjuhrich Sep 4, 2024
3a6c9bf
qa: Extract use of `session.utcnow()`
lukasjuhrich Sep 4, 2024
1b607e3
qa: extract `user_from_pre_member`
lukasjuhrich Sep 4, 2024
ebe6fb8
Expand tombstone tests
lukasjuhrich Sep 4, 2024
089fabc
Make new user data login availability check tombstone-aware
lukasjuhrich Sep 4, 2024
fcd6ba2
Fix arg splitting in Justfile
lukasjuhrich Sep 4, 2024
d1a5fd2
add `revision` to alembic wrapper
lukasjuhrich Sep 4, 2024
0e7d232
Upgrade `uv-pre-commit`
lukasjuhrich Sep 8, 2024
d4a6547
Add `rich` to prod requirements
lukasjuhrich Sep 8, 2024
11befc2
Add `alembic check` and a custom `alembic diff`
lukasjuhrich Sep 8, 2024
3ce87bb
add known options to `alembic upgrade` wrapper
lukasjuhrich Sep 9, 2024
94848cf
add tombstone schema migrations
lukasjuhrich Sep 9, 2024
3561e0e
Remove superfluous test
lukasjuhrich Sep 9, 2024
7369076
Don't validate schema version by default
lukasjuhrich Sep 9, 2024
29485cd
Fix `docker-compose.dev.yml` whitespace
lukasjuhrich Sep 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,11 @@ jobs:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.3
bun-version: 1.1.26
- run: bun install --frozen-lockfile
- run: bun run bundle --prod
- name: Check for outdated NPM packages
run: bun x npm-check-updates --root --format group >> "${GITHUB_STEP_SUMMARY}"
run: ./scripts/bun_outdated.sh | tee "${GITHUB_STEP_SUMMARY}"
build:
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ repos:
hooks:
- id: ruff
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.1.29
rev: 0.4.7
hooks:
- id: pip-compile
name: "pip-compile: requirements.txt"
Expand Down
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions doc/_static/js/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
mermaid.min.js* linguist-vendored
mermaid.min.js -diff
2,151 changes: 2,151 additions & 0 deletions doc/_static/js/mermaid.min.js

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions doc/_static/js/mermaid.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion doc/_templates/logo-text.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<a href="{{ homepage() }}" class="text-logo">
<img src="{{ pathto('_static/' + logo, 1) }}" class="logo" alt="Pycroft" height="32px" />
<img src="{{ logo_url }}" class="logo" alt="Pycroft" height="32px" />
{{ theme_project_nav_name or shorttitle }}
</a>
2 changes: 1 addition & 1 deletion doc/api/pycroft.model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ pycroft.model
.. automodule:: pycroft.model.functions
.. automodule:: pycroft.model.hades
.. automodule:: pycroft.model.host
.. automodule:: pycroft.model.interval_base
.. automodule:: pycroft.model.logging
.. automodule:: pycroft.model.net
.. automodule:: pycroft.model.port
Expand All @@ -28,5 +27,6 @@ pycroft.model
.. automodule:: pycroft.model.task_serialization
.. automodule:: pycroft.model.traffic
.. automodule:: pycroft.model.types
.. automodule:: pycroft.model.unix_account
.. automodule:: pycroft.model.user
.. automodule:: pycroft.model.webstorage
148 changes: 148 additions & 0 deletions doc/arch/adr-006.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
ADR006
======

:Number: 006
:Title: Combined tombstoning of uid and login
:Author: Lukas Juhrich, Jakob Müller
:Created: 2023-01-15
:Status: Postulated

.. contents:: Table of Contents

Context
-------
In the light of data economy, we should not store the login of a :class:`User` –
which implicitly is a (partial) property of the :class:`UnixAccount` –
more than necessary.

The only reason to keep the login is to avoid assigning the same mail address
to multiple people across time; otherwise,
mails might be send to the wrong addressee.

Thus it has been decided (external from the development team) to store
the login in form of a hash whenever it stops being required for providing
services to a user.

Requirements
~~~~~~~~~~~~
The implementation has to ensure

1. absence of the ``login`` whenever it is not needed *(legal requirement)*
2. that no login is doubly assigned *(legal / business requirement)*

Consequently, we need to track the ``login`` hash in some kind of “tombstone”,
such that

* it is referenced by a ``User`` or a ``UnixAccount``
* it exists in all scenarios and is never deleted

Since the ``login`` and the ``uid`` have the same lifecycle, and both
require tombstoning –
albeit the ``uid`` needs to be kept only for technical reasons,
not for legal ones –
it makes sense to let such a ``tombstone`` maintain both pieces of data.

Current model
~~~~~~~~~~~~~

In the current model, the relevant entities are related as follows:

1. ``User``: has a ``login`` (nullable) and a ``unix_account_id`` (nullable).
2. ``UnixAccount``: Has the columns ``uid``, ``gid``, ``home``, and ``login_shell``,
but no ``login``.
3. An ldap account (Derived entity):
If a ``User`` has a ``UnixAccount`` and the correct permissions,
this causes an LDAP account to be exported by the ldap syncer.

Since ``user.unix_account`` is a foreign key constraint,
we have the following possible states (``U``: User, ``UA``: UnixAccount):

========== === ========= =================== ====
# ∃U? ∃U.login? ∃U.unix_account_id? ∃UA?
========== === ========= =================== ====
1 ✓ ✗ ✗ ✗
2 ✓ ✗ ✓ ✓
3 ✓ ✓ ✗ ✗
4 ✓ ✓ ✓ ✓
---------- --- --------- ------------------- ----
5 ✗ ✗ ✗ ✓
6 ✗ ✗ ✗ ✗
========== === ========= =================== ====

With regards to tombstoning, the states imply the following requirements:

State 1. No login, no unix account
No requirements.

State 2. No login, but unix account
This is currently allowed, but issues a warning in the ldap syncer.
Indeed, without the login the unix account cannot be exported.
With tombstones however, we require

* :math:`(T_u)` there shall exist a tombstone with the same uid as the account

State 3. Login, but no unix account
* :math:`(T_l)` there shall exist a tombstone with the implied login hash

State 4. Login and unix account
* :math:`(T_u)` there shall exist a tombstone with the same uid as the account
* :math:`(T_l)` there shall exist a tombstone with the implied login hash
* :math:`(E_{ul})` both tombstones in question should be equal

State 5. Unix account without user
* :math:`(T_u)` there shall exist a tombstone with the same uid as the account

State 6. Tautological state
Nothing exists


Decision
--------

There shall be

1. A new entity ``unix_tombstone(uid int, login_hash text)`` satisfying

* ``uid`` is unique
* ``login_hash`` is unique
* Either column may be ``null``, but not both
* ``uid`` and ``login_hash`` form the primary key

2. A generated column ``login_hash`` on the ``user`` relation
(see :class:`sqlalchemy.schema.Computed`)
3. :math:`(T_l)`: A foreign key constraint ``User.login_hash → UnixTombstone.login_hash``
4. :math:`(T_l)`: A foreign key constraint ``UnixAccount.uid → UnixTombstone.uid``
5. :math:`(E_{ul})`: A constraint checking consistency for users with login and unix account:
In this case, the tombstone induced by the ``account.uid`` should agree
with the tombstone induced by the ``user.login_hash``

Consequences
------------

* That the ``login_hash`` is optional allows for
``unix_accounts`` which don't have a ``unix_login`` associated to them
to have valid tombstones as well.
However, this implies that were one to couple these accounts to users again,
the tombstone has to be modified to reflect the user's ``login`` (if it exists).

* using a combined entity instead of an entity for the ``login`` and ``uid``,
respectively, has the advantage that one can identify ``login`` tombstones
which never had a respective ``unix_account``.
Database administrators can then decide on whether to keep these entries or not,
since technically these logins have not been used anywhere.
This might not serve any particular purpose but the

* In the most frequent use case of creating a user with login and unix account,
A tombstone has to be created as well. This is slightly more effort
than the current implementation.
To avoid this, triggers may be created that take care of this automatically.

.. note:: This ADR does not take a stance on whether or not to add triggers
as it is mainly concerned with ensuring the critical legal and business
requirements.

* Tests have to be written to ensure that with any state change of the ``user``
or ``unix_account`` relations,
the information contained in the ``tombstone`` tables is monotonous,
i.e that neither does a tombstone get deleted via a cascade
nor is a field set to ``null`` when it has been non-null before.
7 changes: 7 additions & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
"sphinx_toolbox.more_autodoc.autonamedtuple",
'sphinx.ext.autodoc',
'sphinx.ext.doctest',
'sphinx.ext.todo',
Expand All @@ -40,6 +41,7 @@
"sphinx_paramlinks",
"sphinxcontrib.fulltoc",
"sphinxcontrib.autohttp.flask",
"sphinxcontrib.mermaid",
]

# Add any paths that contain templates here, relative to this directory.
Expand Down Expand Up @@ -168,6 +170,11 @@
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

mermaid_version = ""
html_js_files = [
"js/mermaid.min.js",
]

# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
Expand Down
1 change: 1 addition & 0 deletions doc/guides/docker.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Installing Docker and Docker Compose
------------------------------------
Prerequisites
*nothing*

You need to install

* `Docker-engine <https://docs.docker.com/engine/install/>`__ ``≥19.03.0``
Expand Down
1 change: 1 addition & 0 deletions doc/guides/setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ you only need to do the following:
#. Set up a virtual environment

.. code:: shell

uv venv
uv pip sync requirements.dev.txt && uv pip install -e deps/wtforms-widgets -e '.[dev]'

Expand Down
2 changes: 1 addition & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ services:
"--watch", "doc/_static",
"--watch", "ldap_sync",
"--watch", "web",
]
]
healthcheck:
test: curl --fail http://localhost:8000
ports:
Expand Down
4 changes: 2 additions & 2 deletions docker/dev.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \
apk add unzip curl
RUN <<EOF ash
set -euo pipefail
curl -sSfLO https://github.com/oven-sh/bun/releases/download/bun-v1.1.3/bun-linux-x64-baseline.zip
curl -sSfLO https://github.com/oven-sh/bun/releases/download/bun-v1.1.26/bun-linux-x64-baseline.zip
unzip -j bun-linux-x64-baseline.zip bun-linux-x64-baseline/bun -d /opt
echo "e1c94765691f95ca593cf921c89d7bba951cd6e876d28f67ee37a3feeb288f55 /opt/bun" \
echo "610bf0daf21cbb7a80be18b2bdb67c0cdcb9e83c680afa082e70a970db78f895 /opt/bun" \
| sha256sum -c -
EOF

Expand Down
24 changes: 17 additions & 7 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -93,24 +93,30 @@ _ensure_schema_dir:
run: (_up "dev-app")

# spawn a shell in the `test-app` container
[positional-arguments]
test-shell *args:
{{ drc }} run --rm test-app shell {{ args }}
{{ drc }} run --rm test-app shell "$@"

# spawn a shell in the `dev-app` container
[positional-arguments]
dev-shell *args:
{{ drc }} run --rm dev-app shell {{ args }}
{{ drc }} run --rm dev-app shell "$@"

# run an alembic command against the `dev-db`
alembic command *args:
{{ drc }} --progress=none run --rm dev-migrate shell flask alembic {{ command }} {{ args }}
[positional-arguments]
alembic *args:
{{ drc }} --progress=none run --rm dev-migrate shell flask alembic "$@"


# run an interactive postgres shell in the dev-db container
[positional-arguments]
dev-psql *args: (_up "dev-db")
{{ dev-psql }} {{ args }}
{{ dev-psql }} "$@"

# run an interactive postgres shell in the test-db container
[positional-arguments]
test-psql *args: (_up "test-db")
{{ test-psql }} {{ args }}
{{ test-psql }} "$@"

# give a quick overview over the schema in the dev-db.

Expand All @@ -120,10 +126,14 @@ schema-status: (_up "dev-db")
`{{ dev-psql }} -q -t -c 'table pycroft.alembic_version'`
@echo "Schema version in {{ sql_dump }}: " \
`grep 'COPY.*alembic_version' -A1 {{ sql_dump }} | sed -n '2p'`
{{ drc }} --progress=none run --rm dev-app alembic check 2>&1 | tail -n1
{{ drc }} --progress=none run --rm dev-app flask alembic check 2>&1 | tail -n1

schema-diff: (_up "dev-db") (alembic "diff")

# upgrade the (imported or created) schema to the current revision
schema-upgrade: (_up "dev-db") (alembic "upgrade" "head")

# extract `requirements` lockfiles from `pyproject.toml` dependency spec
deps-compile:
uv pip compile pyproject.toml --generate-hashes -o requirements.txt
uv pip compile pyproject.toml --generate-hashes --extra dev -o requirements.dev.txt
Expand Down
5 changes: 3 additions & 2 deletions ldap_sync/sources/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def _fetch_db_users(
) -> list[_UserProxyType]:
"""Fetch users to be synced, plus whether ``ldap_login_enabled`` is set.

If the `` ldap_login_enabled`` flag is not present,
If the ``ldap_login_enabled`` flag is not present,
we interpret this as ``should_be_blocked``.

:param session: The SQLAlchemy session to use
Expand Down Expand Up @@ -199,7 +199,7 @@ class _PropertyProxyType(NamedTuple):
def _fetch_db_properties(session: Session) -> list[_PropertyProxyType]:
"""Fetch the groups who should be synced.

Explicitly, this returns everything in :ref:`EXPORTED_PROPERTIES` together with
Explicitly, this returns everything in :data:`EXPORTED_PROPERTIES` together with
the current users having the respective property as members.

:param session: The SQLAlchemy session to use
Expand Down Expand Up @@ -246,6 +246,7 @@ def fetch_db_properties(
)


#: The properties of a user we export to LDAP.
EXPORTED_PROPERTIES = frozenset(
[
"network_access",
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
"esbuild-plugin-clean": "^1.0.1",
"esbuild-plugin-summary": "^0.0.2",
"eslint": "^8.16.0",
"npm-check-updates": "^16.14.18",
"typescript": "^4.1.3"
},
"scripts": {
Expand Down
6 changes: 6 additions & 0 deletions pycroft/helpers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import random
import string
import typing as t
from hashlib import sha512

from passlib.apps import ldap_context
from passlib.context import CryptContext
Expand Down Expand Up @@ -66,6 +67,11 @@ def verify_password(plaintext_password: str, hash: str) -> bool:
return False


def login_hash(login: str) -> bytes:
"""Hashes a login with sha512, as is done in the `User.login_hash` generated column."""
return sha512(login.encode()).digest()


def generate_random_str(length: int) -> str:
"""
Generates an aplhanumeric random string
Expand Down
Loading
Loading