Merge branch 'admin'

pull/1/head
radex 2023-09-22 22:35:36 +02:00
commit 26c945a91d
19 changed files with 547 additions and 161 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
*pyc
.venv
webapp/config.py

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.11

View File

@ -1,32 +1,46 @@
FROM ubuntu:18.04
# The builder image, used to build the virtual environment
FROM python:3.11-buster as builder
RUN set -e -x ;\
useradd -rm app
RUN apt-get update && \
apt-get install -y libkrb5-dev libsasl2-dev libldap2-dev libssl-dev && \
rm -rf /var/lib/apt/lists/*
RUN set -e -x ;\
export DEBIAN_FRONTEND=noninteractive ;\
apt-get -y update ;\
apt-get -y upgrade ;\
apt-get -y install python3-dev python3-venv build-essential libkrb5-dev libsasl2-dev libldap2-dev libssl-dev krb5-user ;\
rm -rf /var/lib/apt/lists
RUN pip install poetry==1.5.1
ENV POETRY_NO_INTERACTION=1 \
POETRY_VIRTUALENVS_IN_PROJECT=1 \
POETRY_VIRTUALENVS_CREATE=1 \
POETRY_CACHE_DIR=/tmp/poetry_cache
WORKDIR /venv
COPY pyproject.toml ./
COPY poetry.lock ./
RUN touch README.md
RUN poetry install --no-root && \
rm -rf $POETRY_CACHE_DIR
# The runtime image, used to just run the code provided its virtual environment
FROM python:3.11-slim-buster as runtime
RUN apt-get update && \
apt-get install -y libldap2-dev krb5-user libxml2 --no-install-recommends && \
rm -rf /var/lib/apt/lists/*
COPY krb5.conf /etc/krb5.conf
RUN set -e -x ;\
mkdir /app ;\
chown app:app /app
RUN useradd -rm app
USER app
COPY requirements.txt /app/requirements.txt
ENV VIRTUAL_ENV=/venv/.venv \
PATH="/venv/.venv/bin:$PATH"
RUN set -e -x ;\
python3 -m venv /app/venv ;\
/app/venv/bin/pip install -r /app/requirements.txt
COPY --chown=app runserver.py /app/
COPY --chown=app webapp /app/webapp
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
WORKDIR /app
CMD ["/app/venv/bin/uwsgi", "--socket=0.0.0.0:8000", "--protocol=http", "--manage-script-name", "--force-cwd=/app", "--mount", "/=webapp:app"]
COPY . .
STOPSIGNAL SIGINT
CMD ["uwsgi", "--socket=0.0.0.0:8000", "--protocol=http", "--manage-script-name", "--force-cwd=/app", "--mount", "/=webapp:app"]

25
README.md Normal file
View File

@ -0,0 +1,25 @@
# ldap-web aka profile.hackerspace.pl
## quick start locally
```
poetry install
FLASK_ENV=development FLASK_APP=webapp poetry run flask run -p 5001
```
Open app at localhost:5001
To test changing password locally, you must add HACKERSPACE.PL realm to your /etc/krb5.conf. See krb5.conf for an example
## quick start (dockerized)
```
docker build -t ldapweb .
docker run -p 8000:8000 ldapweb
```
Open app at localhost:8000
## deployment
Look for `ldapweb.libsonnet` in hscloud repo

276
poetry.lock generated Normal file
View File

@ -0,0 +1,276 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]]
name = "blinker"
version = "1.6.2"
description = "Fast, simple object-to-object and broadcast signaling"
optional = false
python-versions = ">=3.7"
files = [
{file = "blinker-1.6.2-py3-none-any.whl", hash = "sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0"},
{file = "blinker-1.6.2.tar.gz", hash = "sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "flask"
version = "2.3.3"
description = "A simple framework for building complex web applications."
optional = false
python-versions = ">=3.8"
files = [
{file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"},
{file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"},
]
[package.dependencies]
blinker = ">=1.6.2"
click = ">=8.1.3"
itsdangerous = ">=2.1.2"
Jinja2 = ">=3.1.2"
Werkzeug = ">=2.3.7"
[package.extras]
async = ["asgiref (>=3.2)"]
dotenv = ["python-dotenv"]
[[package]]
name = "flask-wtf"
version = "1.1.1"
description = "Form rendering, validation, and CSRF protection for Flask with WTForms."
optional = false
python-versions = ">=3.7"
files = [
{file = "Flask-WTF-1.1.1.tar.gz", hash = "sha256:41c4244e9ae626d63bed42ae4785b90667b885b1535d5a4095e1f63060d12aa9"},
{file = "Flask_WTF-1.1.1-py3-none-any.whl", hash = "sha256:7887d6f1ebb3e17bf648647422f0944c9a469d0fcf63e3b66fb9a83037e38b2c"},
]
[package.dependencies]
Flask = "*"
itsdangerous = "*"
WTForms = "*"
[package.extras]
email = ["email-validator"]
[[package]]
name = "itsdangerous"
version = "2.1.2"
description = "Safely pass data to untrusted environments and back."
optional = false
python-versions = ">=3.7"
files = [
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
]
[[package]]
name = "jinja2"
version = "3.1.2"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "kerberos"
version = "1.3.1"
description = "Kerberos high-level interface"
optional = false
python-versions = "*"
files = [
{file = "kerberos-1.3.1-cp27-cp27m-macosx_11_1_x86_64.whl", hash = "sha256:98a695c072efef535cb2b5f98e474d00671588859a94ec96c2c1508a113ff3aa"},
{file = "kerberos-1.3.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:2e03c6a9d201d4aab5f899bfb8150de15335955bfce8ca43bfe9a41d7aae54dc"},
{file = "kerberos-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2002b3b1541fc51e2c081ee7048f55e5d9ca63dd09f0d7b951c263920db3a0bb"},
{file = "kerberos-1.3.1.tar.gz", hash = "sha256:cdd046142a4e0060f96a00eb13d82a5d9ebc0f2d7934393ed559bac773460a2c"},
]
[[package]]
name = "markupsafe"
version = "2.1.3"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.7"
files = [
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"},
{file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"},
{file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"},
{file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"},
{file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"},
{file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"},
{file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"},
{file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"},
{file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"},
{file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"},
{file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"},
{file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"},
{file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"},
{file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"},
{file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"},
{file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"},
{file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"},
{file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"},
{file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"},
{file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"},
{file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"},
{file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"},
]
[[package]]
name = "pyasn1"
version = "0.5.0"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
{file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"},
{file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"},
]
[[package]]
name = "pyasn1-modules"
version = "0.3.0"
description = "A collection of ASN.1-based protocols modules"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
files = [
{file = "pyasn1_modules-0.3.0-py2.py3-none-any.whl", hash = "sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d"},
{file = "pyasn1_modules-0.3.0.tar.gz", hash = "sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c"},
]
[package.dependencies]
pyasn1 = ">=0.4.6,<0.6.0"
[[package]]
name = "python-ldap"
version = "3.4.3"
description = "Python modules for implementing LDAP clients"
optional = false
python-versions = ">=3.6"
files = [
{file = "python-ldap-3.4.3.tar.gz", hash = "sha256:ab26c519a0ef2a443a2a10391fa3c5cb52d7871323399db949ebfaa9f25ee2a0"},
]
[package.dependencies]
pyasn1 = ">=0.3.7"
pyasn1_modules = ">=0.1.5"
[[package]]
name = "uwsgi"
version = "2.0.22"
description = "The uWSGI server"
optional = false
python-versions = "*"
files = [
{file = "uwsgi-2.0.22.tar.gz", hash = "sha256:4cc4727258671ac5fa17ab422155e9aaef8a2008ebb86e4404b66deaae965db2"},
]
[[package]]
name = "werkzeug"
version = "2.3.7"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.8"
files = [
{file = "werkzeug-2.3.7-py3-none-any.whl", hash = "sha256:effc12dba7f3bd72e605ce49807bbe692bd729c3bb122a3b91747a6ae77df528"},
{file = "werkzeug-2.3.7.tar.gz", hash = "sha256:2b8c0e447b4b9dbcc85dd97b6eeb4dcbaf6c8b6c3be0bd654e25553e0a2157d8"},
]
[package.dependencies]
MarkupSafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog (>=2.3)"]
[[package]]
name = "wtforms"
version = "3.0.1"
description = "Form validation and rendering for Python web development."
optional = false
python-versions = ">=3.7"
files = [
{file = "WTForms-3.0.1-py3-none-any.whl", hash = "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b"},
{file = "WTForms-3.0.1.tar.gz", hash = "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc"},
]
[package.dependencies]
MarkupSafe = "*"
[package.extras]
email = ["email-validator"]
[metadata]
lock-version = "2.0"
python-versions = "~3.11"
content-hash = "b026e5d2d31ebde4256e7793a62fbba895b049cfc270bf24ab1a0ed352f9d933"

23
pyproject.toml Normal file
View File

@ -0,0 +1,23 @@
[tool.poetry]
name = "ldap-web"
version = "0.1.0"
description = ""
authors = []
readme = "README.md"
[tool.poetry.dependencies]
python = "~3.11"
Flask = "^2.3"
Flask-WTF = "^1"
itsdangerous = "^2.0"
Jinja2 = "^3.0"
kerberos = "^1.3.0"
MarkupSafe = "^2.0"
python-ldap = "^3.2.0"
uWSGI = "^2.0"
Werkzeug = "^2.0"
WTForms = "^3"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@ -1,16 +0,0 @@
Babel==2.7.0
Click==7.0
Flask==1.1.1
Flask-Babel==0.12.2
Flask-WTF==0.14.2
itsdangerous==1.1.0
Jinja2==2.10.3
kerberos==1.3.0
MarkupSafe==1.1.1
pyasn1==0.4.7
pyasn1-modules==0.2.7
python-ldap==3.2.0
pytz==2019.3
uWSGI==2.0.18
Werkzeug==0.16.0
WTForms==2.2.1

View File

@ -1,2 +0,0 @@
from webapp import app
app.run('0.0.0.0', 20007, debug=True)

View File

@ -1,12 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import flask
import flask_wtf
import wtforms
from functools import reduce
from flask_babel import gettext
app = flask.Flask(__name__)
# import views to route them
@ -47,8 +43,7 @@ def initialize_forms():
forms[f] = AddForm
return forms
@app.before_first_request
def start(*a, **kw):
def start():
validation.sanitize_perms()
validation.sanitize_readable()
@ -58,10 +53,11 @@ def start(*a, **kw):
del app.profiles[dn]
app.connections.register_callback('drop', drop_profile)
app.connections.start()
app.tokens = pools.TokenPool(timeout=600.0)
app.tokens.start()
app.profiles = {}
app.forms = initialize_forms()
start()

View File

@ -1,11 +1,20 @@
# -*- coding: utf-8 -*-
import flask_wtf
import wtforms
secret_key = '9c2n8t5nrvbyt7cm3v4n87tnv45'
ldap_url = 'ldap://ldap.hackerspace.pl'
dn_format = "uid=%s,ou=people,dc=hackerspace,dc=pl"
dn_format = "uid=%s,ou=people,dc=hackerspace,dc=pl"
ldapweb_admin_group = 'cn=ldap-admin,ou=Group,dc=hackerspace,dc=pl'
ldap_base = 'dc=hackerspace,dc=pl'
ldap_people = 'ou=People,dc=hackerspace,dc=pl'
admin_groups = {
'Fatty': 'cn=fatty,ou=Group,dc=hackerspace,dc=pl',
'Starving': 'cn=starving,ou=Group,dc=hackerspace,dc=pl',
'Potato': 'cn=potato,ou=Group,dc=hackerspace,dc=pl',
}
admin_dn = 'cn=ldapweb,ou=Services,dc=hackerspace,dc=pl'
admin_pw = 'changeme'
@ -20,6 +29,8 @@ readable_names = {
'loginshell': u'Shell',
'telephonenumber': 'Phone Number',
'mobiletelephonenumber': 'Mobile Number',
'sshpublickey': 'SSH Public Key',
'mifareidhash': 'MIFARE ID Hash',
}
full_name = {
@ -33,8 +44,10 @@ full_name = {
can_add = set([
'telephonenumber',
'mobiletelephonenumber',
'sshpublickey',
'mifareidhash',
])
can_delete = can_add
can_delete = can_add
can_modify = can_add | set([
'givenname', 'surname', 'commonname', 'gecos',
])
@ -59,8 +72,6 @@ default_field = (wtforms.fields.StringField, {})
fields = { 'telephonenumber': (wtforms.fields.StringField, {'validators': [wtforms.validators.Regexp(r'[+0-9 ]+')]})}
kadmin_passwd = True
kadmin_principal = "kadmin/admin@HACKERSPACE.PL"
kadmin_keytab = "/app/mount/keytab"
kadmin_principal_map = "{}@HACKERSPACE.PL"
TOKEN_LENGTH = 32

View File

@ -47,14 +47,3 @@ def refresh_profile(dn=None):
profile[a.uid] = a
app.profiles[dn] = profile
return profile
def generate_token():
dn = get_dn()
token = ''.join(random.choice(string.letters) for i in xrange(config.TOKEN_LENGTH))
app.tokens.insert(dn, token)
def get_token():
dn = get_dn()
return app.tokens[dn]

View File

@ -60,12 +60,6 @@ a {
list-style: none;
}
#headerimg {
width: 1024px;
height: 100px;
background-image: url("/static/servers.jpg");
}
footer {
font-size: 10px;
color: rgb(100, 100, 100);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

View File

@ -0,0 +1,14 @@
{% extends 'basic.html' %}
{% block content %}
<p>Good evening, professor {{ session['username'] }}. All LDAP accounts:</p>
{% for group_name, users in user_groups %}
<h2>{{ group_name }}</h2>
<ul>
{% for uid, name in users %}
<li><a href="/admin/users/{{ uid }}">{{ uid }} ({{ name }})</a></li>
{% endfor %}
</ul>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,49 @@
{% extends 'basic.html' %}
{% block content %}
<h1>User: {{ uid }}</h1>
<div style="margin-bottom: 10px">
<a class="btn btn-default" href="/admin" role="button">Back</a>
<a class="btn btn-default" href="https://kasownik.hackerspace.pl/admin/member/{{ uid }}" role="button" target="_blank">View user in Kasownik</a>
</div>
<p>
<strong>Belongs to groups:</strong>
{% for group_name in groups %}
<a href="/admin/groups/{{ group_name }}">{{ group_name }}</a>,
{% endfor %}
</p>
<p>Full LDAP record:</p>
<table class="table profile-table">
<tr>
<th scope="col">Attribute</th>
<th scope="col">Attribute</th>
<th scope="col" class="profile-table-value">Value</th>
</tr>
{% for attr, attr_readable, value in profile %}
<tr>
<td>{{ attr }}</td>
<td>{{ attr_readable if attr_readable else '' }}</td>
<td class="profile-table-value">{{ value }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}
{% block head %}
<style type="text/css">
.profile-table td, .profile-table th {
overflow: hidden;
overflow-wrap: anywhere;
width: 200px;
}
.profile-table td.profile-table-value,
.profile-table th.profile-table-value {
width: max-content;
}
</style>
{% endblock %}

View File

@ -35,6 +35,9 @@ body {
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
{% if session.username %}
{% if session.is_admin %}
<li><a href="/admin"><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> Admin</a></li>
{% endif %}
<li><a href="/"><span class="glyphicon glyphicon-user" aria-hidden="true"></span> {{ session.username }}</a></li>
<li><a href="/logout"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span> Log Out</a></li>
{% else %}

View File

@ -1,19 +0,0 @@
{% extends 'basic.html' %}
{% block title %}Zarządzaj kluczami{% endblock %}
{% block content %}
<h1>Klucze publiczne SSH</h1>
<table>
{% if not keys %}
<tr><td>Nie masz kluczy publicznych.</td></tr>
{% endif %}
<tr><td><a href="/upload_key">Dodaj nowy</a></td></tr>
{% for k in keys %}
<tr>
<td class="key">{{ k.value }}</td>
<td><a href="/key/{{ k.uid }}/modify">zmień</a></td>
<td><a href="/key/{{ k.uid }}/delete">usuń</a></td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -55,6 +55,7 @@
{{ multifield('telephonenumber') }}
{{ multifield('mobiletelephonenumber') }}
{{ multifield('sshpublickey', code=True, width=12) }}
{{ multifield('mifareidhash', code=True, width=12) }}
</div>
{% endblock %}

View File

@ -1,24 +1,16 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
import urllib
import functools
import ldap
import kerberos
import re
import flask
import flask_wtf
from webapp import app, context, config, validation
lpk_templates = {
'add': 'keys/upload_key.html',
'mod': 'keys/upload_key.html',
'del': 'keys/delete_key.html',
}
def login_required(f):
@functools.wraps(f)
def func(*a, **kw):
@ -30,15 +22,6 @@ def login_required(f):
return f(*a, **kw)
return func
def api_access(f):
@functools.wraps(f)
def func(*a, **kw):
dn = flask.request.environ.get('PEER_DN')
if dn not in config.api_allowed:
return flask.abort(403)
return f(*a, **kw)
return func
def req_to_ctx():
return dict(flask.request.form.items())
@ -127,8 +110,12 @@ def login_action():
if k == 'uid':
username = vs[0].decode()
# Check if user belongs to admin group
is_admin = bool(conn.search_s(dn, ldap.SCOPE_SUBTREE, f'memberOf={config.ldapweb_admin_group}'))
flask.session["username"] = username
flask.session['dn'] = dn
flask.session['is_admin'] = is_admin
context.refresh_profile()
return flask.redirect(goto)
else:
@ -187,61 +174,6 @@ def passwd_action():
flask.flash(u'Wrong password', category='danger')
return flask.render_template('passwd.html')
@app.route('/keys')
@login_required
def keys():
keys = filter(lambda attr: attr.name == validation.sanitize_ldap(config.lpk_attr), context.get_profile().values())
return flask.render_template('keys.html', keys=keys)
@app.route('/upload_key', methods=['GET', 'POST'])
@login_required
def upload_key_form():
return attr_op('add', config.lpk_attr, templates=lpk_templates, success_redirect='/keys',
fatal_redirect='/keys')
@app.route('/key/<uid>/modify', methods=['GET', 'POST'])
@login_required
def modify_key(uid):
return attr_op('mod', config.lpk_attr, uid=uid, templates=lpk_templates, success_redirect='/keys',
fatal_redirect='/keys')
@app.route('/key/<uid>/delete', methods=['GET', 'POST'])
@login_required
def delete_key(uid):
return attr_op('del', config.lpk_attr, uid=uid, templates=lpk_templates, success_redirect='/keys',
fatal_redirect='/keys')
@app.route('/request_token', methods=['GET'])
@login_required
def request_token():
context.generate_token()
return flask.redirect('/vcard')
@app.route('/ldap/claim_nick', methods=['POST'])
@api_access
def claim_nick():
status = 400
error = 'No such token'
token = flask.request.form['token']
nick = flask.request.form['nick']
owner = app.tokens.find_owner(token)
if owner:
conn = context.get_admin_connection()
try:
conn.modify_s(owner, [(ldap.MOD_ADD, config.irc_attr, str_to_ldap(nick))])
app.tokens.drop(owner)
print('Token claimed for {} (nick {}, token {})'.format(owner, nick, token))
status = 200
error = 'Success'
context.refresh_profile(owner)
except ldap.LDAPError as e:
error = 'Could not claim irc nick, contact an admin'
print('LDAP Error:', e)
res = flask.make_response(error, status)
res.mimetype = 'text/plain'
return res
@app.route("/")
@login_required
def root():
@ -253,7 +185,7 @@ def vcard():
data = {}
for v in context.get_profile().values():
data.setdefault(v.name, []).append(v)
return flask.render_template('vcard.html', token=context.get_token(), can_add=config.can['add'],
return flask.render_template('vcard.html', can_add=config.can['add'],
can_modify=config.can['mod'], can_delete=config.can['del'], profile=data)
@app.route('/vcard/add/<attrName>', methods=['GET', 'POST'])
@ -271,4 +203,97 @@ def del_attr(uid):
def mod_attr(uid):
return attr_op('mod', None, uid)
def _ldap_not_in(patterns):
joined_patterns = ''.join(f'({p})' for p in patterns)
one_of_pattern = f'(|{joined_patterns})'
return f'!{one_of_pattern}'
def _ldap_get_users_list(conn, query='&'):
all_users = []
results = conn.search_s(config.ldap_people, ldap.SCOPE_SUBTREE, f'(&(uid=*)(cn=*)({query}))', attrlist=['uid', 'cn'])
for user, attrs in results:
user_uid = attrs['uid'][0].decode()
user_cn = attrs['cn'][0].decode()
all_users.append((user_uid, user_cn))
all_users.sort(key=lambda user: user[0].lower())
return all_users
def _ldap_get_all_users_groupped(conn):
group_queries = [
(group_name, f'memberOf={pattern}')
for group_name, pattern in config.admin_groups.items()
]
groupped_users = [
(group_name, _ldap_get_users_list(conn, query))
for group_name, query in group_queries
]
other_users_query = _ldap_not_in(query for _, query in group_queries)
groupped_users.append(
('Other', _ldap_get_users_list(conn, other_users_query))
)
return groupped_users
@app.route('/admin/')
@login_required
def admin_list():
if not flask.session['is_admin']:
flask.abort(403)
conn = context.get_connection()
user_groups = _ldap_get_all_users_groupped(conn)
return flask.render_template('admin/list.html', user_groups=user_groups)
def _ldap_get_user(conn, uid):
profile = []
for user, attrs in conn.search_s(config.dn_format % uid, ldap.SCOPE_SUBTREE):
for attr, values in attrs.items():
for value in values:
profile.append((attr, value.decode()))
return profile
def _rendered_ldap_profile(profile):
rendered_profile = []
for attr, value in profile:
attr_sanitized = attr.lower()
attr_full_name = config.full_name.get(attr_sanitized, attr_sanitized)
attr_readable_name = config.readable_names.get(attr_full_name)
rendered_profile.append((attr, attr_readable_name, value))
# Attributes with readable names first, then by name
rendered_profile.sort(key=lambda profile: profile[0])
rendered_profile.sort(key=lambda profile: profile[1] is None)
return rendered_profile
def _ldap_get_user_groups(conn, uid):
groups = []
user_dn = config.dn_format % uid
filter = f'(&(objectClass=groupOfUniqueNames)(uniqueMember={user_dn}))'
for group_dn, attrs in conn.search_s(config.ldap_base, ldap.SCOPE_SUBTREE, filter):
groups.append(attrs['cn'][0].decode())
return groups
def _ldap_validate_uid(uid):
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9-_]*\Z', uid):
raise RuntimeError('Invalid uid')
@app.route('/admin/users/<uid>')
@login_required
def admin_user_view(uid):
if not flask.session['is_admin']:
flask.abort(403)
conn = context.get_connection()
_ldap_validate_uid(uid)
profile = _ldap_get_user(conn, uid)
groups = _ldap_get_user_groups(conn, uid)
return flask.render_template('admin/user.html', uid=uid, profile=_rendered_ldap_profile(profile), groups=groups)