1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/pyinventory_ynh.git synced 2024-09-03 20:16:09 +02:00

Merge pull request #43 from YunoHost-Apps/testing

master <- testing
This commit is contained in:
Jens Diemer 2020-12-30 10:06:17 +01:00 committed by GitHub
commit 45de259db7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 2177 additions and 455 deletions

7
.flake8 Normal file
View file

@ -0,0 +1,7 @@
#
# Move to pyproject.toml after: https://gitlab.com/pycqa/flake8/-/issues/428
#
[flake8]
exclude = .pytest_cache, .tox, dist, htmlcov, local_test
ignore = F405
max-line-length = 119

41
.github/workflows/pytest.yml vendored Normal file
View file

@ -0,0 +1,41 @@
name: pytest
on:
schedule:
- cron: '0 8 * * *'
push:
jobs:
test:
runs-on: ubuntu-latest
strategy:
max-parallel: 2
matrix:
python-version: [3.9, 3.8, 3.7]
steps:
- uses: actions/checkout@v1
- name: 'Set up Python ${{ matrix.python-version }}'
uses: actions/setup-python@v1
with:
python-version: '${{ matrix.python-version }}'
- name: 'Install package'
run: |
pip3 install poetry
make install
- name: 'List installed packages'
run: |
poetry run pip freeze
- name: 'Run tests with Python v${{ matrix.python-version }}'
run: |
make pytest
- name: 'Upload coverage report'
run: bash <(curl -s https://codecov.io/bash)
- name: 'Run linters'
if: matrix.python-version == '3.8'
run: |
make lint

4
.gitignore vendored
View file

@ -1,8 +1,10 @@
.* .*
!.github !.github
!.editorconfig !.editorconfig
!.flake8
!.gitignore !.gitignore
__pycache__ __pycache__
secret.txt secret.txt
/local_test/ /local_test/
/poetry.lock /coverage.xml
/htmlcov/

View file

@ -1,4 +1,5 @@
SHELL := /bin/bash SHELL := /bin/bash
MAX_LINE_LENGTH := 119
all: help all: help
@ -16,16 +17,38 @@ check-poetry:
fi fi
install-poetry: ## install or update poetry install-poetry: ## install or update poetry
pip3 install -U pip
pip3 install -U poetry pip3 install -U poetry
install: check-poetry ## install project via poetry install: check-poetry ## install project via poetry
poetry install poetry install
update: install-poetry ## update the sources and installation update: install-poetry ## update the sources and installation and generate "conf/requirements.txt"
poetry run pip install -U pip
poetry update poetry update
poetry export -f requirements.txt --output conf/requirements.txt
local-test: check-poetry ## Run local_test.py to run the project locally lint: ## Run code formatters and linter
poetry run ./local_test.py poetry run flynt --fail-on-change --line_length=${MAX_LINE_LENGTH} .
poetry run isort --check-only .
poetry run flake8 .
fix-code-style: ## Fix code formatting
poetry run flynt --line_length=${MAX_LINE_LENGTH} .
poetry run black --verbose --safe --line-length=${MAX_LINE_LENGTH} --skip-string-normalization .
poetry run isort .
tox-listenvs: check-poetry ## List all tox test environments
poetry run tox --listenvs
tox: check-poetry ## Run pytest via tox with all environments
poetry run tox
pytest: install ## Run pytest
poetry run python3 ./run_pytest.py
local-test: install ## Run local_test.py to run the project locally
poetry run python3 ./local_test.py
local-diff-settings: ## Run "manage.py diffsettings" with local test local-diff-settings: ## Run "manage.py diffsettings" with local test
poetry run python3 local_test/opt_yunohost/manage.py diffsettings poetry run python3 local_test/opt_yunohost/manage.py diffsettings

View file

@ -21,7 +21,7 @@ Pull requests welcome ;)
## Settings and upgrades ## Settings and upgrades
Almost everything related to PyInventory's configuration is handled in a `"../conf/ynh_pyinventory_settings.py"` file. Almost everything related to PyInventory's configuration is handled in a `"../conf/settings.py"` file.
You can edit the file `$final_path/local_settings.py` to enable or disable features. You can edit the file `$final_path/local_settings.py` to enable or disable features.
# Miscellaneous # Miscellaneous
@ -29,7 +29,7 @@ You can edit the file `$final_path/local_settings.py` to enable or disable featu
## SSO authentication ## SSO authentication
[SSOwat](https://github.com/YunoHost/SSOwat) is fully supported: [SSOwat](https://github.com/YunoHost/SSOwat) is fully supported via [django_ynh](https://github.com/YunoHost-Apps/django_ynh):
* First user (`$YNH_APP_ARG_ADMIN`) will be created as Django's super user * First user (`$YNH_APP_ARG_ADMIN`) will be created as Django's super user
* All new users will be created as normal users * All new users will be created as normal users
@ -100,13 +100,13 @@ drwxr-xr-x 3 root root 3 Dec 8 08:36 ..
-rw-r--r-- 1 pyinventory pyinventory 171 Dec 8 08:39 secret.txt -rw-r--r-- 1 pyinventory pyinventory 171 Dec 8 08:39 secret.txt
drwxr-xr-x 6 pyinventory pyinventory 6 Dec 8 08:37 venv drwxr-xr-x 6 pyinventory pyinventory 6 Dec 8 08:37 venv
-rw-r--r-- 1 pyinventory pyinventory 115 Dec 8 08:39 wsgi.py -rw-r--r-- 1 pyinventory pyinventory 115 Dec 8 08:39 wsgi.py
-rw-r--r-- 1 pyinventory pyinventory 4737 Dec 8 08:39 ynh_pyinventory_settings.py -rw-r--r-- 1 pyinventory pyinventory 4737 Dec 8 08:39 settings.py
root@yunohost:~# cd /opt/yunohost/pyinventory/ root@yunohost:~# cd /opt/yunohost/pyinventory/
root@yunohost:/opt/yunohost/pyinventory# source venv/bin/activate root@yunohost:/opt/yunohost/pyinventory# source venv/bin/activate
(venv) root@yunohost:/opt/yunohost/pyinventory# ./manage.py check (venv) root@yunohost:/opt/yunohost/pyinventory# ./manage.py check
PyInventory v0.8.2 (Django v2.2.17) PyInventory v0.8.2 (Django v2.2.17)
DJANGO_SETTINGS_MODULE='ynh_pyinventory_settings' DJANGO_SETTINGS_MODULE='settings'
PROJECT_PATH:/opt/yunohost/pyinventory/venv/lib/python3.7/site-packages PROJECT_PATH:/opt/yunohost/pyinventory/venv/lib/python3.7/site-packages
BASE_PATH:/opt/yunohost/pyinventory BASE_PATH:/opt/yunohost/pyinventory
System check identified no issues (0 silenced). System check identified no issues (0 silenced).

View file

@ -1,46 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
import sys
def main():
os.environ['DJANGO_SETTINGS_MODULE'] = 'ynh_pyinventory_settings'
parser = argparse.ArgumentParser(
description='Create or update Django super user.'
)
parser.add_argument('--username')
parser.add_argument('--email')
parser.add_argument('--password')
args = parser.parse_args()
username = args.username
email = args.email or ''
password = args.password
import django
django.setup()
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.filter(username=username).first()
if user:
print('Update existing user and set his password.', file=sys.stderr)
user.is_active = True
user.is_staff = True
user.is_superuser = True
user.set_password(password)
user.email = email
user.save()
else:
print('Create new super user', file=sys.stderr)
User.objects.create_superuser(
username=username,
email=email,
password=password
)
if __name__ == '__main__':
main()

View file

@ -3,6 +3,7 @@
""" """
import multiprocessing import multiprocessing
bind = '127.0.0.1:__PORT__' bind = '127.0.0.1:__PORT__'
# https://docs.gunicorn.org/en/latest/settings.html#workers # https://docs.gunicorn.org/en/latest/settings.html#workers

View file

@ -5,8 +5,9 @@ import sys
def main(): def main():
os.environ['DJANGO_SETTINGS_MODULE'] = 'ynh_pyinventory_settings' os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
from django.core.management import execute_from_command_line from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)

View file

@ -12,7 +12,7 @@ location __PATH__/static/ {
# expires 30d; # expires 30d;
#} #}
location / { location __PATH__/ {
# https://github.com/benoitc/gunicorn/blob/master/examples/nginx.conf # https://github.com/benoitc/gunicorn/blob/master/examples/nginx.conf
# this is needed if you have file import via upload enabled # this is needed if you have file import via upload enabled
@ -30,5 +30,5 @@ location / {
proxy_connect_timeout 30; proxy_connect_timeout 30;
proxy_redirect off; proxy_redirect off;
proxy_pass http://127.0.0.1:__PORT__/; proxy_pass http://127.0.0.1:__PORT__;
} }

214
conf/requirements.txt Normal file
View file

@ -0,0 +1,214 @@
bleach==3.2.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd \
--hash=sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080
bx-py-utils==18; python_version >= "3.6" and python_full_version < "4.0.0" \
--hash=sha256:72a6090822544603e3a7547ce07f0120ae67940ca2ec4590ac907b3b09ad70ca \
--hash=sha256:195ea1b3d5d277354ea33e34ec3ebd4fc2a6e8d94d646ede902f80527f06ec75
certifi==2020.12.5; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830 \
--hash=sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c
chardet==4.0.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 \
--hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa
colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or python_version >= "3.7" and python_full_version < "4.0.0" and sys_platform == "win32" and python_full_version >= "3.5.0" \
--hash=sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2 \
--hash=sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b
colorlog==4.6.2; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:edd5ada5de03e880e42b2526f8be5570fd9b692f8eb7cf6b1fdcac3e3fb23976 \
--hash=sha256:54e5f153419c22afc283c130c4201db19a3dbd83221a0f4657d5ee66234a2ea4
defusedxml==0.6.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93 \
--hash=sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5
diff-match-patch==20200713; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:da6f5a01aa586df23dfc89f3827e1cafbb5420be9d87769eeb079ddfd9477a18 \
--hash=sha256:8bf9d9c4e059d917b5c6312bac0c137971a32815ddbda9c682b949f2986b4d34
django-admin-sortable2==0.7.7; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:65c8a02300e178b4a02c904b6f4dce6e528c58f5ecab3907dfba3540da51b862
django-axes==5.10.0; python_version >= "3.7" and python_version < "4.0" and python_full_version < "4.0.0" \
--hash=sha256:8a62cd4cc78ef08007e8102def34be83832995eb6e3e0c814d605741b82a2796 \
--hash=sha256:3c81ddca8a9d7fd0019cb440f711cc873c3039546f7eacb3f2ec616bf0ec1b32
django-ckeditor==6.0.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:29fd1a333cb9741ac2c3fd4e427a5c00115ed33a2389716a09af7656022dcdde \
--hash=sha256:cc2d377f1bdcd4ca1540caeebe85f7e2cd006198d57328ef6c718d3eaa5a0846
django-dbbackup==3.3.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:bb109735cae98b64ad084e5b461b7aca2d7b39992f10c9ed9435e3ebb6fb76c8
django-debug-toolbar==3.2; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:84e2607d900dbd571df0a2acf380b47c088efb787dce9805aefeb407341961d2 \
--hash=sha256:9e5a25d0c965f7e686f6a8ba23613ca9ca30184daa26487706d4829f5cfb697a
django-import-export==2.4.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:401d76eca0a5c6cf43bffed16c06e509b9044ce8f6bcff264b776e3952830f1a \
--hash=sha256:7610f6efff797d65f56c03ba34444507c0b0ccdffe9346c168b9894fc349c55e
django-ipware==3.0.2; python_version >= "3.7" and python_version < "4.0" and python_full_version < "4.0.0" \
--hash=sha256:c7df8e1410a8e5d6b1fbae58728402ea59950f043c3582e033e866f0f0cf5e94
django-js-asset==1.2.2; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:c163ae80d2e0b22d8fb598047cd0dcef31f81830e127cfecae278ad574167260 \
--hash=sha256:8ec12017f26eec524cab436c64ae73033368a372970af4cf42d9354fcb166bdd
django-processinfo==1.0.2; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:3cacad233a5428a5cc6292eafbfddd97948d8d1e4f47e47175a7fcdfcee90f12 \
--hash=sha256:08aefdf7285d1eaa595e46570bcc12f2dfd9e24594d524246110614819076b6c
django-redis==4.12.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:306589c7021e6468b2656edc89f62b8ba67e8d5a1c8877e2688042263daa7a63 \
--hash=sha256:1133b26b75baa3664164c3f44b9d5d133d1b8de45d94d79f38d1adc5b1d502e5
django-reversion-compare==0.13.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:db7c82ade287e172a9e84d6666b1df0b673eb97ab8a81a8cb52212f359e17f9b \
--hash=sha256:087d6951462692e0a477a84e5460d606661be6ea2224e40dc9eded193ab7b928
django-reversion==3.0.8; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:49e9930f90322dc6a2754dd26144285cfcc1c5bd0c1c39ca95d5602c5054ae32 \
--hash=sha256:9cfadeec2df37cb53d795ab79f6792f9eed8e70363dcf3a275dc19a58b971a8f
django-tagulous==1.1.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:9bc9d1d066c486fac1a3ec351531e440bc239c459b043e9180d99d7846e45fd6 \
--hash=sha256:de2a56ed92374b79358275ac0b7910af2c3d2823f44a847bef91ca9e456353ba
django-tools==0.48.3; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:08ed2ae606067f49c2c3949055227a826c8b880e5816114925eca386cf1823af \
--hash=sha256:40444fa16b703b7c6960a800ba76aad42472c9aa70040d549a4d91dbb47a5ddb
django-ynh==0.1.2; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:2efa30444f67252bbb7601e1b5631ce93ddf72a70b2216bb61363990de78ad4f \
--hash=sha256:711b0f9ac183b2507a58cf644081aafefdc025ce585f6a8dad427ca3baf55a19
django==2.2.17; python_version >= "3.7" and python_full_version < "4.0.0" and python_version < "4.0" \
--hash=sha256:558cb27930defd9a6042133258caf797b2d1dee233959f537e3dc475cb49bd7c \
--hash=sha256:cf5370a4d7765a9dd6d42a7b96b53c74f9446cd38209211304b210fe0404b861
et-xmlfile==1.0.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:614d9722d572f6246302c4491846d2c393c199cfa4edc9af593437691683335b
gunicorn==20.0.4; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c \
--hash=sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626
icdiff==1.9.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:66972dd03318da55280991db375d3ef6b66d948c67af96c1ebdb21587e86655e
idna==2.10; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 \
--hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6
jdcal==1.4.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:1abf1305fce18b4e8aa248cf8fe0c56ce2032392bc64bbd61b5dff2a19ec8bba \
--hash=sha256:472872e096eb8df219c23f2689fc336668bdb43d194094b5cc1707e1640acfc8
markuppy==1.14; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:1adee2c0a542af378fe84548ff6f6b0168f3cb7f426b46961038a2bcfaad0d5f
odfpy==1.4.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:fc3b8d1bc098eba4a0fda865a76d9d1e577c4ceec771426bcb169a82c5e9dfe0 \
--hash=sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec
openpyxl==3.0.5; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:f7d666b569f729257082cf7ddc56262431878f602dcc2bc3980775c59439cdab \
--hash=sha256:18e11f9a650128a12580a58e3daba14e00a11d9e907c554a17ea016bf1a2c71b
packaging==20.8; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858 \
--hash=sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093
pillow==8.0.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:b63d4ff734263ae4ce6593798bcfee6dbfb00523c82753a3a03cbc05555a9cc3 \
--hash=sha256:5f9403af9c790cc18411ea398a6950ee2def2a830ad0cfe6dc9122e6d528b302 \
--hash=sha256:6b4a8fd632b4ebee28282a9fef4c341835a1aa8671e2770b6f89adc8e8c2703c \
--hash=sha256:cc3ea6b23954da84dbee8025c616040d9aa5eaf34ea6895a0a762ee9d3e12e11 \
--hash=sha256:d8a96747df78cda35980905bf26e72960cba6d355ace4780d4bdde3b217cdf1e \
--hash=sha256:7ba0ba61252ab23052e642abdb17fd08fdcfdbbf3b74c969a30c58ac1ade7cd3 \
--hash=sha256:795e91a60f291e75de2e20e6bdd67770f793c8605b553cb6e4387ce0cb302e09 \
--hash=sha256:0a2e8d03787ec7ad71dc18aec9367c946ef8ef50e1e78c71f743bc3a770f9fae \
--hash=sha256:006de60d7580d81f4a1a7e9f0173dc90a932e3905cc4d47ea909bc946302311a \
--hash=sha256:bd7bf289e05470b1bc74889d1466d9ad4a56d201f24397557b6f65c24a6844b8 \
--hash=sha256:95edb1ed513e68bddc2aee3de66ceaf743590bf16c023fb9977adc4be15bd3f0 \
--hash=sha256:e38d58d9138ef972fceb7aeec4be02e3f01d383723965bfcef14d174c8ccd039 \
--hash=sha256:d3d07c86d4efa1facdf32aa878bd508c0dc4f87c48125cc16b937baa4e5b5e11 \
--hash=sha256:fbd922f702582cb0d71ef94442bfca57624352622d75e3be7a1e7e9360b07e72 \
--hash=sha256:92c882b70a40c79de9f5294dc99390671e07fc0b0113d472cbea3fde15db1792 \
--hash=sha256:7c9401e68730d6c4245b8e361d3d13e1035cbc94db86b49dc7da8bec235d0015 \
--hash=sha256:6c1aca8231625115104a06e4389fcd9ec88f0c9befbabd80dc206c35561be271 \
--hash=sha256:cc9ec588c6ef3a1325fa032ec14d97b7309db493782ea8c304666fb10c3bd9a7 \
--hash=sha256:eb472586374dc66b31e36e14720747595c2b265ae962987261f044e5cce644b5 \
--hash=sha256:0eeeae397e5a79dc088d8297a4c2c6f901f8fb30db47795113a4a605d0f1e5ce \
--hash=sha256:81f812d8f5e8a09b246515fac141e9d10113229bc33ea073fec11403b016bcf3 \
--hash=sha256:895d54c0ddc78a478c80f9c438579ac15f3e27bf442c2a9aa74d41d0e4d12544 \
--hash=sha256:2fb113757a369a6cdb189f8df3226e995acfed0a8919a72416626af1a0a71140 \
--hash=sha256:59e903ca800c8cfd1ebe482349ec7c35687b95e98cefae213e271c8c7fffa021 \
--hash=sha256:5abd653a23c35d980b332bc0431d39663b1709d64142e3652890df4c9b6970f6 \
--hash=sha256:4b0ef2470c4979e345e4e0cc1bbac65fda11d0d7b789dbac035e4c6ce3f98adb \
--hash=sha256:8de332053707c80963b589b22f8e0229f1be1f3ca862a932c1bcd48dafb18dd8 \
--hash=sha256:11c5c6e9b02c9dac08af04f093eb5a2f84857df70a7d4a6a6ad461aca803fb9e
pprintpp==0.4.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:b6b4dcdd0c0c0d75e4d7b2f21a9e933e5b2ce62b26e1a54537f9651ae5a5c01d \
--hash=sha256:ea826108e2c7f49dc6d66c752973c3fc9749142a798d6b254e1e301cfdbc6403
psycopg2-binary==2.8.6; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.4.0" \
--hash=sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0 \
--hash=sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4 \
--hash=sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db \
--hash=sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5 \
--hash=sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25 \
--hash=sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c \
--hash=sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c \
--hash=sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1 \
--hash=sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2 \
--hash=sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152 \
--hash=sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449 \
--hash=sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859 \
--hash=sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550 \
--hash=sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd \
--hash=sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71 \
--hash=sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4 \
--hash=sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb \
--hash=sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da \
--hash=sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2 \
--hash=sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a \
--hash=sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679 \
--hash=sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf \
--hash=sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b \
--hash=sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67 \
--hash=sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66 \
--hash=sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f \
--hash=sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77 \
--hash=sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94 \
--hash=sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729 \
--hash=sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77 \
--hash=sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83 \
--hash=sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52 \
--hash=sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd \
--hash=sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056 \
--hash=sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6
pyinventory==0.8.3; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:03f028a508b9a4e303b5561083476b5c7099914c792b66f20d94f034a8b0db50 \
--hash=sha256:e7a09820b2aaf1d17e9ca6896e83e69999cce99d7d399141648e0d303cef10c7
pyparsing==2.4.7; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b \
--hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1
python-stdnum==1.14; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:fd3a92b8ec82a159c41dbaa3c5397934d090090c92b04e346412e0fd7e6a1b1c \
--hash=sha256:6389a1e7658e39c37e4f10b42d7a51ce620e031bdeae05158519c218e14ff3b5
pytz==2020.5; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4 \
--hash=sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5
pyyaml==5.3.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f \
--hash=sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76 \
--hash=sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2 \
--hash=sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c \
--hash=sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2 \
--hash=sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648 \
--hash=sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a \
--hash=sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf \
--hash=sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97 \
--hash=sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee \
--hash=sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a \
--hash=sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e \
--hash=sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d
redis==3.5.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24 \
--hash=sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2
requests==2.25.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e \
--hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804
six==1.15.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced \
--hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259
sqlparse==0.4.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0 \
--hash=sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8
tablib==3.0.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:41aa40981cddd7ec4d1fabeae7c38d271601b306386bd05b5c3bcae13e5aeb20 \
--hash=sha256:f83cac08454f225a34a305daa20e2110d5e6335135d505f93bc66583a5f9c10d
urllib3==1.26.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" and python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473 \
--hash=sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08
webencodings==0.5.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
xlrd==2.0.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.6.0" \
--hash=sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd \
--hash=sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88
xlwt==1.3.0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e \
--hash=sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88

View file

@ -11,9 +11,11 @@
from pathlib import Path as __Path from pathlib import Path as __Path
from django_ynh.secret_key import get_or_create_secret as __get_or_create_secret
from inventory_project.settings.base import * # noqa from inventory_project.settings.base import * # noqa
DEBUG = False
DEBUG = False # Don't turn DEBUG on in production!
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
@ -31,40 +33,41 @@ PATH_URL = PATH_URL.strip('/')
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
ROOT_URLCONF = 'ynh_urls' # /opt/yunohost/pyinventory/ynh_urls.py ROOT_URLCONF = 'urls' # /opt/yunohost/pyinventory/ynh_urls.py
# ----------------------------------------------------------------------------- # Function that will be called to finalize a user profile:
YNH_SETUP_USER = 'setup_user.setup_project_user'
SECRET_KEY = __get_or_create_secret(FINAL_HOME_PATH / 'secret.txt') # /opt/yunohost/$app/secret.txt
INSTALLED_APPS.append('django_ynh')
MIDDLEWARE.insert(
MIDDLEWARE.index('django.contrib.auth.middleware.AuthenticationMiddleware') + 1,
# login a user via HTTP_REMOTE_USER header from SSOwat:
'django_ynh.sso_auth.auth_middleware.SSOwatRemoteUserMiddleware',
)
# Keep ModelBackend around for per-user permissions and superuser # Keep ModelBackend around for per-user permissions and superuser
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'axes.backends.AxesBackend', # AxesBackend should be the first backend! 'axes.backends.AxesBackend', # AxesBackend should be the first backend!
#
# Authenticate via SSO and nginx 'HTTP_REMOTE_USER' header: # Authenticate via SSO and nginx 'HTTP_REMOTE_USER' header:
'ynh_authenticate.RemoteUserBackend', 'django_ynh.sso_auth.auth_backend.SSOwatUserBackend',
#
# Fallback to normal Django model backend: # Fallback to normal Django model backend:
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
) )
LOGIN_REDIRECT_URL = None LOGIN_REDIRECT_URL = None
LOGIN_URL = '/yunohost/sso/' LOGIN_URL = '/yunohost/sso/'
LOGOUT_REDIRECT_URL = '/yunohost/sso/' LOGOUT_REDIRECT_URL = '/yunohost/sso/'
# /yunohost/sso/?action=logout # /yunohost/sso/?action=logout
# -----------------------------------------------------------------------------
# https://docs.djangoproject.com/en/2.2/howto/auth-remote-user/
# Add RemoteUserMiddleware after AuthenticationMiddleware
MIDDLEWARE.insert(
MIDDLEWARE.index('django.contrib.auth.middleware.AuthenticationMiddleware') + 1,
'ynh_authenticate.RemoteUserMiddleware',
)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
ADMINS = ( ADMINS = (('__ADMIN__', '__ADMINMAIL__'),)
('__ADMIN__', '__ADMINMAIL__'),
)
MANAGERS = ADMINS MANAGERS = ADMINS
@ -170,6 +173,7 @@ LOGGING = {
'django': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False}, 'django': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
'axes': {'handlers': ['log_file', 'mail_admins'], 'level': 'WARNING', 'propagate': False}, 'axes': {'handlers': ['log_file', 'mail_admins'], 'level': 'WARNING', 'propagate': False},
'django_tools': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False}, 'django_tools': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
'django_ynh': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
'inventory': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False}, 'inventory': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
}, },
} }

8
conf/setup_user.py Normal file
View file

@ -0,0 +1,8 @@
def setup_project_user(user):
"""
All users used the Django admin, so we need to set the "staff" user flag.
Called from django_ynh.sso_auth
"""
user.is_staff = True
user.save()
return user

View file

@ -23,9 +23,7 @@ if settings.PATH_URL:
urlpatterns = [ urlpatterns = [
# path(f'{settings.PATH_URL}/debug/', debug_view), # path(f'{settings.PATH_URL}/debug/', debug_view),
path(f'{settings.PATH_URL}/', admin.site.urls), path(f'{settings.PATH_URL}/', admin.site.urls),
path(f'{settings.PATH_URL}/ckeditor/', include('ckeditor_uploader.urls')), path(f'{settings.PATH_URL}/ckeditor/', include('ckeditor_uploader.urls')),
# MEDIA_URL contains the "PATH_URL" already: # MEDIA_URL contains the "PATH_URL" already:
path(settings.MEDIA_URL.lstrip('/'), include('django_tools.serve_media_app.urls')), path(settings.MEDIA_URL.lstrip('/'), include('django_tools.serve_media_app.urls')),
] ]

View file

@ -2,7 +2,11 @@
WSGI config WSGI config
""" """
import os import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'ynh_pyinventory_settings'
from django.core.wsgi import get_wsgi_application
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
from django.core.wsgi import get_wsgi_application # noqa
application = get_wsgi_application() application = get_wsgi_application()

View file

@ -1,185 +0,0 @@
"""
* remote user authentication backend
* remote user middleware
Note: SSOwat/nginx add authentication headers:
'HTTP_AUTHORIZATION': 'Basic XXXXXXXXXXXXXXXX='
'HTTP_AUTH_USER': 'username'
'HTTP_REMOTE_USER': 'username'
Basic auth contains "{username}:{plaintext-password}"
and we get SSOwat cookies like:
'HTTP_COOKIE': 'SSOwAuthUser=username; '
'SSOwAuthHash=593876aa66...99e69f88af1e; '
'SSOwAuthExpire=1609227697.998; '
* Login a user via HTTP_REMOTE_USER header, but check also username in:
* SSOwAuthUser
* HTTP_AUTH_USER
* HTTP_AUTHORIZATION (Basic auth)
* Create new users
* Update Email, First / Last name for existing users
"""
import base64
import logging
from axes.exceptions import AxesBackendPermissionDenied
from django.contrib.auth.backends import RemoteUserBackend as OriginRemoteUserBackend
from django.contrib.auth.middleware import RemoteUserMiddleware as OriginRemoteUserMiddleware
from django.core.exceptions import ValidationError
from inventory.permissions import get_or_create_normal_user_group
logger = logging.getLogger(__name__)
def update_user_profile(request):
"""
Update existing user information:
* Email
* First / Last name
"""
user = request.user
assert user.is_authenticated
update_fields = []
if not user.password:
# Empty password is not valid, so we can't save the model, because of full_clean() call
logger.info('Set unusable password for user: %s', user)
user.set_unusable_password()
update_fields.append('password')
email = request.META.get('HTTP_EMAIL')
if email and user.email != email:
logger.info('Update email: %r -> %r', user.email, email)
user.email = email
update_fields.append('email')
raw_username = request.META.get('HTTP_NAME')
if raw_username:
if ' ' in raw_username:
first_name, last_name = raw_username.split(' ', 1)
else:
first_name = ''
last_name = raw_username
if user.first_name != first_name:
logger.info('Update first name: %r -> %r', user.first_name, first_name)
user.first_name = first_name
update_fields.append('first_name')
if user.last_name != last_name:
logger.info('Update last name: %r -> %r', user.last_name, last_name)
user.last_name = last_name
update_fields.append('last_name')
if update_fields:
try:
user.full_clean()
except ValidationError:
logger.exception('Can not update user: %s', user)
else:
user.save(update_fields=update_fields)
class RemoteUserMiddleware(OriginRemoteUserMiddleware):
"""
Middleware to login a user HTTP_REMOTE_USER header.
Use Django Axes if something is wrong.
Update exising user informations.
"""
header = 'HTTP_REMOTE_USER'
force_logout_if_no_header = True
def process_request(self, request):
# Keep the information if the user is already logged in
was_authenticated = request.user.is_authenticated
super().process_request(request) # login remote user
if not request.user.is_authenticated:
# Not logged in -> nothing to verify here
return
# Check SSOwat cookie informations:
try:
username = request.COOKIES['SSOwAuthUser']
except KeyError:
logger.error('SSOwAuthUser cookie missing!')
# emits a signal indicating user login failed, which is processed by
# axes.signals.log_user_login_failed which logs and flags the failed request.
raise AxesBackendPermissionDenied('Cookie missing')
logger.info('SSOwat username from cookies: %r', username)
if username != request.user.username:
raise AxesBackendPermissionDenied('Wrong username')
# Compare with HTTP_AUTH_USER
try:
username = request.META['HTTP_AUTH_USER']
except KeyError:
logger.error('HTTP_AUTH_USER missing!')
raise AxesBackendPermissionDenied('No HTTP_AUTH_USER')
if username != request.user.username:
raise AxesBackendPermissionDenied('Wrong HTTP_AUTH_USER username')
# Also check 'HTTP_AUTHORIZATION', but only the username ;)
try:
auth = request.META['HTTP_AUTHORIZATION']
except KeyError:
logger.error('HTTP_AUTHORIZATION missing!')
raise AxesBackendPermissionDenied('No HTTP_AUTHORIZATION')
scheme, creds = auth.split(' ', 1)
if scheme.lower() != 'basic':
logger.error('HTTP_AUTHORIZATION with %r not supported', scheme)
raise AxesBackendPermissionDenied('HTTP_AUTHORIZATION scheme not supported')
creds = str(base64.b64decode(creds), encoding='utf-8')
username = creds.split(':', 1)[0]
if username != request.user.username:
raise AxesBackendPermissionDenied('Wrong HTTP_AUTHORIZATION username')
if not was_authenticated:
# First request, after login -> update user informations
logger.info('Remote used was logged in')
update_user_profile(request)
class RemoteUserBackend(OriginRemoteUserBackend):
"""
Authentication backend via SSO/nginx header
"""
create_unknown_user = True
def authenticate(self, request, remote_user):
logger.info('Remote user authenticate: %r', remote_user)
return super().authenticate(request, remote_user)
def configure_user(self, request, user):
"""
Configure a user after creation and return the updated user.
Setup a normal, non-superuser
"""
logger.warning('Configure user %s', user)
user.set_unusable_password() # Always login via SSO
user.is_staff = True
user.is_superuser = False
user.save()
pyinventory_user_group = get_or_create_normal_user_group()[0]
user.groups.set([pyinventory_user_group])
update_user_profile(request)
return user
def user_can_authenticate(self, user):
logger.warning('Remote user login: %s', user)
return True

149
local_test.py Executable file → Normal file
View file

@ -1,154 +1,29 @@
#!/usr/bin/env python3
""" """
Start PyInventory in YunoHost setup locally. Build a "local_test" YunoHost installation and start the Django dev. server against it.
Note:
You can only run this script, if you are in a activated PyInventory venv! Run via:
make local-test
see README for details ;) see README for details ;)
""" """
import os
import shlex
import subprocess
import sys
from pathlib import Path from pathlib import Path
os.environ['DJANGO_SETTINGS_MODULE'] = 'ynh_pyinventory_settings'
try: try:
import inventory_project # noqa from django_ynh.local_test import create_local_test
except ImportError as err: except ImportError as err:
raise ImportError( raise ImportError('Did you forget to activate a virtual environment?') from err
'Couldn\'t import PyInventory. Did you '
'forget to activate a virtual environment?'
) from err
BASE_PATH = Path(__file__).parent
BASE_PATH = Path(__file__).parent.absolute()
TEST_PATH = BASE_PATH / 'local_test'
CONF_PATH = BASE_PATH / 'conf'
FINAL_HOME_PATH = TEST_PATH / 'opt_yunohost'
FINAL_WWW_PATH = TEST_PATH / 'var_www'
LOG_FILE = TEST_PATH / 'var_log_pyinventory.log'
MANAGE_PY_FILE = CONF_PATH / 'manage.py'
CREATE_SUPERUSER_FILE = CONF_PATH / 'create_superuser.py'
SETTINGS_FILE = CONF_PATH / 'ynh_pyinventory_settings.py'
URLS_FILE = CONF_PATH / 'ynh_urls.py'
REPLACES = {
'__FINAL_HOME_PATH__': str(FINAL_HOME_PATH),
'__FINAL_WWW_PATH__': str(FINAL_WWW_PATH),
'__LOG_FILE__': str(TEST_PATH / 'var_log_pyinventory.log'),
'__PATH_URL__': 'app_path',
'__DOMAIN__': '127.0.0.1',
'django.db.backends.postgresql': 'django.db.backends.sqlite3',
"'NAME': '__APP__',": f"'NAME': '{TEST_PATH / 'test_db.sqlite'}',",
'django_redis.cache.RedisCache': 'django.core.cache.backends.dummy.DummyCache',
'DEBUG = False': 'DEBUG = True',
# Just use the default logging setup from PyInventory project:
'LOGGING = {': 'HACKED_DEACTIVATED_LOGGING = {',
}
def verbose_check_call(command, verbose=True, **kwargs):
""" 'verbose' version of subprocess.check_call() """
if verbose:
print('_' * 100)
msg = f'Call: {command!r}'
verbose_kwargs = ', '.join(f'{k}={v!r}' for k, v in sorted(kwargs.items()))
if verbose_kwargs:
msg += f' (kwargs: {verbose_kwargs})'
print(f'{msg}\n', flush=True)
env = os.environ.copy()
env['PYTHONUNBUFFERED'] = '1'
popenargs = shlex.split(command)
subprocess.check_call(
popenargs,
universal_newlines=True,
env=env,
**kwargs
)
def call_manage_py(args):
verbose_check_call(
command=f'{sys.executable} manage.py {args}',
cwd=FINAL_HOME_PATH,
)
def copy_patch(src_file, replaces=None):
dst_file = FINAL_HOME_PATH / src_file.name
print(f'{src_file.relative_to(BASE_PATH)} -> {dst_file.relative_to(BASE_PATH)}')
with src_file.open('r') as f:
content = f.read()
if replaces:
for old, new in replaces.items():
content = content.replace(old, new)
with dst_file.open('w') as f:
f.write(content)
def main(): def main():
print('-' * 100) create_local_test(
django_settings_path=BASE_PATH / 'conf' / 'settings.py',
assert BASE_PATH.is_dir() destination=BASE_PATH / 'local_test',
assert CONF_PATH.is_dir() runserver=True,
assert SETTINGS_FILE.is_file()
assert URLS_FILE.is_file()
for p in (TEST_PATH, FINAL_HOME_PATH, FINAL_WWW_PATH):
if p.is_dir():
print(f'Already exists: "{p.relative_to(BASE_PATH)}", ok.')
else:
print(f'Create: "{p.relative_to(BASE_PATH)}"')
p.mkdir(parents=True, exist_ok=True)
LOG_FILE.touch(exist_ok=True)
# conf/manage.py -> local_test/manage.py
copy_patch(src_file=MANAGE_PY_FILE)
# conf/create_superuser.py -> local_test/opt_yunohost/create_superuser.py
copy_patch(src_file=CREATE_SUPERUSER_FILE)
# conf/ynh_pyinventory_settings.py -> local_test/ynh_pyinventory_settings.py
copy_patch(src_file=SETTINGS_FILE, replaces=REPLACES)
# conf/ynh_urls.py -> local_test/ynh_urls.py
copy_patch(src_file=URLS_FILE, replaces=REPLACES)
with Path(FINAL_HOME_PATH / 'local_settings.py').open('w') as f:
f.write('# Only for local test run\n')
f.write('SERVE_FILES=True # used in src/inventory_project/urls.py\n')
# call "local_test/manage.py" via subprocess:
call_manage_py('check --deploy')
call_manage_py('migrate --no-input')
call_manage_py('collectstatic --no-input')
verbose_check_call(
command=f'{sys.executable} create_superuser.py --username="test" --password="test"',
cwd=FINAL_HOME_PATH,
) )
try:
call_manage_py('runserver --nostatic')
except KeyboardInterrupt:
print('\nBye ;)')
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -5,7 +5,7 @@
"description": { "description": {
"en": "Web based management to catalog things including state and location etc." "en": "Web based management to catalog things including state and location etc."
}, },
"version": "0.8.2~ynh4", "version": "0.8.3~ynh1",
"url": "https://github.com/jedie/PyInventory", "url": "https://github.com/jedie/PyInventory",
"license": "GPL-3.0", "license": "GPL-3.0",
"maintainer": { "maintainer": {

1485
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,87 @@
[tool.poetry] [tool.poetry]
name = "pyinventory_ynh" name = "pyinventory_ynh"
version = "0.8.2~ynh4" version = "0.8.3~ynh1"
description = "Test pyinventory_ynh via local_test.py" description = "Test pyinventory_ynh via local_test.py"
authors = ["JensDiemer <git@jensdiemer.de>"] authors = ["JensDiemer <git@jensdiemer.de>"]
license = "GPL" license = "GPL"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.7,<4.0.0" python = ">=3.7,<4.0.0"
pyinventory = "*" pyinventory = "==0.8.3"
django_ynh = "*"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
bx_py_utils = "*"
tox = "*"
pytest = "*"
pytest-cov = "*"
pytest-django = "*"
coveralls = "*"
isort = "*"
flake8 = "*"
flynt = "*"
black = "*"
pyupgrade = "*"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.isort]
# https://pycqa.github.io/isort/docs/configuration/config_files/#pyprojecttoml-preferred-format
atomic=true
line_length=120
case_sensitive=false
skip_glob=["*/htmlcov/*","*/migrations/*"]
multi_line_output=3
include_trailing_comma=true
known_first_party=["inventory"]
no_lines_before="LOCALFOLDER"
default_section="THIRDPARTY"
sections=["FUTURE","STDLIB","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"]
lines_after_imports=2
[tool.pytest.ini_options]
# https://docs.pytest.org/en/latest/customize.html#pyproject-toml
minversion = "6.0"
norecursedirs = ".* .git __pycache__ conf coverage* dist htmlcov"
# sometimes helpfull "addopts" arguments:
# -vv
# --verbose
# --capture=no
# --trace-config
# --full-trace
# -p no:warnings
addopts = """
--import-mode=importlib
--reuse-db
--nomigrations
--cov=.
--cov-report term-missing
--cov-report html
--cov-report xml
--no-cov-on-fail
--showlocals
--doctest-modules
--failed-first
--last-failed-no-failures all
--new-first
"""
[tool.tox]
# https://tox.readthedocs.io/en/latest/example/basic.html#pyproject-toml-tox-legacy-ini
legacy_tox_ini = """
[tox]
isolated_build = True
envlist = py39,py38,py37
skip_missing_interpreters = True
[testenv]
passenv = *
whitelist_externals = make
commands =
make pytest
"""

25
run_pytest.py Normal file
View file

@ -0,0 +1,25 @@
"""
Run pytest against local test creation
"""
from pathlib import Path
try:
from django_ynh.pytest_helper import run_pytest
except ImportError as err:
raise ImportError('Did you forget to activate a virtual environment?') from err
BASE_PATH = Path(__file__).parent
def main():
run_pytest(
django_settings_path=BASE_PATH / 'conf' / 'settings.py',
destination=BASE_PATH / 'local_test',
)
if __name__ == '__main__':
main()

View file

@ -25,14 +25,7 @@ log_file="${log_path}/pyinventory.log"
#================================================= #=================================================
# dependencies used by the app # dependencies used by the app
pkg_dependencies="build-essential python3-dev python3-pip python3-venv git \ pkg_dependencies="build-essential python3-dev python3-pip python3-venv git postgresql postgresql-contrib"
postgresql postgresql-contrib"
# PyInventory's version for PIP and settings file
pyinventory_version="0.8.2"
# Extra python packages:
pypi_extras="django-redis"
#================================================= #=================================================
# Redis HELPERS # Redis HELPERS

View file

@ -116,10 +116,10 @@ fi
ynh_script_progression --message="Modify PyInventory's config file..." ynh_script_progression --message="Modify PyInventory's config file..."
# save old settings file # save old settings file
settings="$final_path/ynh_pyinventory_settings.py" settings="$final_path/settings.py"
ynh_backup_if_checksum_is_different --file="$settings" ynh_backup_if_checksum_is_different --file="$settings"
cp "../conf/ynh_pyinventory_settings.py" "$settings" cp "../conf/settings.py" "$settings"
ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings" ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings"
ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings" ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings"

View file

@ -101,9 +101,10 @@ ynh_system_user_create --username="$app" --home_dir="$final_path" --use_shell
#================================================= #=================================================
# PIP INSTALLATION # PIP INSTALLATION
#================================================= #=================================================
ynh_script_progression --message="Install PyInventory using PIP..." --weight=80 ynh_script_progression --message="Install project via pip..." --weight=80
python3 -m venv "${final_path}/venv" python3 -m venv "${final_path}/venv"
cp ../conf/requirements.txt "$final_path/requirements.txt"
chown -R "$app" "$final_path" chown -R "$app" "$final_path"
#run source in a 'sub shell' #run source in a 'sub shell'
@ -112,18 +113,13 @@ chown -R "$app" "$final_path"
source "${final_path}/venv/bin/activate" source "${final_path}/venv/bin/activate"
set -o nounset set -o nounset
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pip ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pip
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade setuptools wheel psycopg2-binary ynh_exec_as $app $final_path/venv/bin/pip install -r "$final_path/requirements.txt"
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pyinventory=="$pyinventory_version"
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade ${pypi_extras}
) )
#================================================= #=================================================
# copy config files # copy config files
# ================================================ # ================================================
ynh_script_progression --message="Create pyinventory configuration file..." ynh_script_progression --message="Create project configuration files..."
cp ../conf/create_superuser.py "$final_path/create_superuser.py"
chmod +x "$final_path/create_superuser.py"
gunicorn_conf="$final_path/gunicorn.conf.py" gunicorn_conf="$final_path/gunicorn.conf.py"
cp "../conf/gunicorn.conf.py" "$gunicorn_conf" cp "../conf/gunicorn.conf.py" "$gunicorn_conf"
@ -135,10 +131,8 @@ ynh_store_file_checksum --file="$gunicorn_conf"
cp ../conf/manage.py "$final_path/manage.py" cp ../conf/manage.py "$final_path/manage.py"
chmod +x "$final_path/manage.py" chmod +x "$final_path/manage.py"
cp ../conf/wsgi.py "$final_path/wsgi.py" settings="$final_path/settings.py"
cp "../conf/settings.py" "$settings"
settings="$final_path/ynh_pyinventory_settings.py"
cp "../conf/ynh_pyinventory_settings.py" "$settings"
ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings" ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings"
ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings" ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings"
@ -157,10 +151,11 @@ ynh_store_file_checksum --file="$settings"
ynh_app_setting_set --app="$app" --key=redis_db --value="$redis_db" ynh_app_setting_set --app="$app" --key=redis_db --value="$redis_db"
touch "$final_path/local_settings.py" cp ../conf/setup_user.py "$final_path/setup_user.py"
cp ../conf/urls.py "$final_path/urls.py"
cp ../conf/wsgi.py "$final_path/wsgi.py"
cp "../conf/ynh_authenticate.py" "$final_path/ynh_authenticate.py" touch "$final_path/local_settings.py"
cp "../conf/ynh_urls.py" "$final_path/ynh_urls.py"
#================================================= #=================================================
# MIGRATE / COLLECTSTATIC / CREATEADMIN # MIGRATE / COLLECTSTATIC / CREATEADMIN
@ -178,7 +173,9 @@ ynh_script_progression --message="migrate/collectstatic/createadmin..." --weight
./manage.py migrate --no-input ./manage.py migrate --no-input
./manage.py collectstatic --no-input ./manage.py collectstatic --no-input
./create_superuser.py --username="$admin" --email="$admin_mail" --password="pyinventory"
# Create/update Django superuser (set unusable password, because auth done via SSOwat):
./manage.py create_superuser --username="$admin" --email="$admin_mail"
# Check the configuration # Check the configuration
# This may fail in some cases with errors, etc., but the app works and the user can fix issues later. # This may fail in some cases with errors, etc., but the app works and the user can fix issues later.

View file

@ -83,33 +83,27 @@ ynh_script_progression --message="Configuring a systemd service..."
ynh_add_systemd_config --service="$app" --template="pyinventory.service" ynh_add_systemd_config --service="$app" --template="pyinventory.service"
#================================================= #=================================================
# UPGRADE PYINVENTORY # UPGRADE VENV
#================================================= #=================================================
ynh_script_progression --message="Upgrade project via pip..." --weight=80
ynh_script_progression --message="Install pyinventory using PIP..." --weight=15
python3 -m venv "${final_path}/venv" python3 -m venv "${final_path}/venv"
cp ../conf/requirements.txt "$final_path/requirements.txt"
chown -R "$app" "$final_path" chown -R "$app" "$final_path"
#run source in a 'sub shell' #run source in a 'sub shell'
( (
set +o nounset set +o nounset
source "${final_path}/venv/bin/activate" source "${final_path}/venv/bin/activate"
set -o nounset set -o nounset
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pip ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pip
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade setuptools wheel psycopg2-binary ynh_exec_as $app $final_path/venv/bin/pip install -r "$final_path/requirements.txt"
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pyinventory=="$pyinventory_version"
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade ${pypi_extras}
) )
#================================================= #=================================================
# copy config files # copy config files
# ================================================ # ================================================
ynh_script_progression --message="Create pyinventory configuration file..." ynh_script_progression --message="Create project configuration files..."
ynh_backup_if_checksum_is_different --file="$final_path/create_superuser.py"
cp ../conf/create_superuser.py "$final_path/create_superuser.py"
chmod +x "$final_path/create_superuser.py"
gunicorn_conf="$final_path/gunicorn.conf.py" gunicorn_conf="$final_path/gunicorn.conf.py"
ynh_backup_if_checksum_is_different --file="$gunicorn_conf" ynh_backup_if_checksum_is_different --file="$gunicorn_conf"
@ -123,14 +117,11 @@ ynh_backup_if_checksum_is_different --file="$final_path/manage.py"
cp ../conf/manage.py "$final_path/manage.py" cp ../conf/manage.py "$final_path/manage.py"
chmod +x "$final_path/manage.py" chmod +x "$final_path/manage.py"
ynh_backup_if_checksum_is_different --file="$final_path/wsgi.py"
cp ../conf/wsgi.py "$final_path/wsgi.py"
# save old settings file # save old settings file
settings="$final_path/ynh_pyinventory_settings.py" settings="$final_path/settings.py"
ynh_backup_if_checksum_is_different --file="$settings" ynh_backup_if_checksum_is_different --file="$settings"
cp "../conf/ynh_pyinventory_settings.py" "$settings" cp "../conf/settings.py" "$settings"
ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings" ynh_replace_string --match_string="__APP__" --replace_string="$app" --target_file="$settings"
ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings" ynh_replace_string --match_string="__DB_PWD__" --replace_string="$db_pwd" --target_file="$settings"
@ -147,10 +138,16 @@ ynh_replace_string --match_string="__PATH_URL__" --replace_string="$path_url" --
# Recalculate and store the config file checksum into the app settings # Recalculate and store the config file checksum into the app settings
ynh_store_file_checksum --file="$settings" ynh_store_file_checksum --file="$settings"
touch "$final_path/local_settings.py" ynh_backup_if_checksum_is_different --file="$final_path/setup_user.py"
cp ../conf/setup_user.py "$final_path/setup_user.py"
cp "../conf/ynh_authenticate.py" "$final_path/ynh_authenticate.py" ynh_backup_if_checksum_is_different --file="$final_path/urls.py"
cp "../conf/ynh_urls.py" "$final_path/ynh_urls.py" cp ../conf/urls.py "$final_path/urls.py"
ynh_backup_if_checksum_is_different --file="$final_path/wsgi.py"
cp ../conf/wsgi.py "$final_path/wsgi.py"
touch "$final_path/local_settings.py"
#================================================= #=================================================
# MIGRATE PYINVENTORY # MIGRATE PYINVENTORY
@ -168,7 +165,9 @@ ynh_script_progression --message="migrate/collectstatic/createadmin..." --weight
./manage.py migrate --no-input ./manage.py migrate --no-input
./manage.py collectstatic --no-input ./manage.py collectstatic --no-input
./create_superuser.py --username="$admin" --email="$admin_mail" --password="pyinventory"
# Create/update Django superuser (set unusable password, because auth done via SSOwat):
./manage.py create_superuser --username="$admin" --email="$admin_mail"
# Check the configuration # Check the configuration
# This may fail in some cases with errors, etc., but the app works and the user can fix issues later. # This may fail in some cases with errors, etc., but the app works and the user can fix issues later.

0
tests/__init__.py Normal file
View file

View file

@ -0,0 +1,145 @@
from axes.models import AccessLog
from bx_py_utils.test_utils.html_assertion import HtmlAssertionMixin
from django.conf import settings
from django.contrib.auth.models import User
from django.test import override_settings
from django.test.testcases import TestCase
from django.urls import NoReverseMatch
from django.urls.base import reverse
from django_ynh.test_utils import generate_basic_auth
from django_ynh.views import request_media_debug_view
import inventory
@override_settings(DEBUG=False)
class DjangoYnhTestCase(HtmlAssertionMixin, TestCase):
def setUp(self):
super().setUp()
# Always start a fresh session:
self.client = self.client_class()
def test_settings(self):
assert settings.PATH_URL == 'app_path'
assert str(settings.FINAL_HOME_PATH).endswith('/local_test/opt_yunohost')
assert str(settings.FINAL_WWW_PATH).endswith('/local_test/var_www')
assert str(settings.LOG_FILE).endswith('/local_test/var_log_django_ynh.log')
assert settings.ROOT_URLCONF == 'urls'
def test_urls(self):
assert reverse('admin:index') == '/app_path/'
# The django_ynh debug view should not be avaiable:
with self.assertRaises(NoReverseMatch):
reverse(request_media_debug_view)
# Serve user uploads via django_tools.serve_media_app:
assert settings.MEDIA_URL == '/app_path/media/'
assert reverse('serve_media_app:serve-media', kwargs={'user_token': 'token', 'path': 'foo/bar/'}) == (
'/app_path/media/token/foo/bar/'
)
def test_auth(self):
response = self.client.get('/app_path/')
self.assertRedirects(response, expected_url='/app_path/login/?next=/app_path/')
def test_create_unknown_user(self):
assert User.objects.count() == 0
self.client.cookies['SSOwAuthUser'] = 'test'
response = self.client.get(
path='/app_path/',
HTTP_REMOTE_USER='test',
HTTP_AUTH_USER='test',
HTTP_AUTHORIZATION='basic dGVzdDp0ZXN0MTIz',
)
assert User.objects.count() == 1
user = User.objects.first()
assert user.username == 'test'
assert user.is_active is True
assert user.is_staff is True # Set by: conf.django_ynh_demo_urls.setup_user_handler
assert user.is_superuser is False
self.assert_html_parts(
response,
parts=(
f'<title>Site administration | PyInventory v{inventory.__version__}</title>',
'<strong>test</strong>',
),
)
def test_wrong_auth_user(self):
assert User.objects.count() == 0
assert AccessLog.objects.count() == 0
self.client.cookies['SSOwAuthUser'] = 'test'
response = self.client.get(
path='/app_path/',
HTTP_REMOTE_USER='test',
HTTP_AUTH_USER='foobar', # <<< wrong user name
HTTP_AUTHORIZATION='basic dGVzdDp0ZXN0MTIz',
)
assert User.objects.count() == 1
user = User.objects.first()
assert user.username == 'test'
assert user.is_active is True
assert user.is_staff is True # Set by: conf.django_ynh_demo_urls.setup_user_handler
assert user.is_superuser is False
assert AccessLog.objects.count() == 1
assert response.status_code == 403 # Forbidden
def test_wrong_cookie(self):
assert User.objects.count() == 0
assert AccessLog.objects.count() == 0
self.client.cookies['SSOwAuthUser'] = 'foobar' # <<< wrong user name
response = self.client.get(
path='/app_path/',
HTTP_REMOTE_USER='test',
HTTP_AUTH_USER='test',
HTTP_AUTHORIZATION='basic dGVzdDp0ZXN0MTIz',
)
assert User.objects.count() == 1
user = User.objects.first()
assert user.username == 'test'
assert user.is_active is True
assert user.is_staff is True # Set by: conf.django_ynh_demo_urls.setup_user_handler
assert user.is_superuser is False
assert AccessLog.objects.count() == 1
assert response.status_code == 403 # Forbidden
def test_wrong_authorization_user(self):
assert User.objects.count() == 0
self.client.cookies['SSOwAuthUser'] = 'test'
response = self.client.get(
path='/app_path/',
HTTP_REMOTE_USER='test',
HTTP_AUTH_USER='test',
HTTP_AUTHORIZATION=generate_basic_auth(username='foobar', password='test123'), # <<< wrong user name
)
assert User.objects.count() == 1
user = User.objects.first()
assert user.username == 'test'
assert user.is_active is True
assert user.is_staff is True # Set by: conf.django_ynh_demo_urls.setup_user_handler
assert user.is_superuser is False
assert AccessLog.objects.count() == 1
assert response.status_code == 403 # Forbidden

13
tests/test_lint.py Normal file
View file

@ -0,0 +1,13 @@
import shutil
import subprocess
from pathlib import Path
BASE_PATH = Path(__file__).parent.parent
def test_lint():
assert Path(BASE_PATH, 'Makefile').is_file()
make_bin = shutil.which('make')
assert make_bin is not None
subprocess.check_call([make_bin, 'lint'], cwd=BASE_PATH)

View file

@ -0,0 +1,39 @@
import os
import shutil
import subprocess
from pathlib import Path
import inventory
PACKAGE_ROOT = Path(__file__).parent.parent
def assert_file_contains_string(file_path, string):
with file_path.open('r') as f:
for line in f:
if string in line:
return
raise AssertionError(f'File {file_path} does not contain {string!r} !')
def test_version():
version = inventory.__version__
assert_file_contains_string(file_path=Path(PACKAGE_ROOT, 'pyproject.toml'), string=f'version = "{version}~ynh')
assert_file_contains_string(file_path=Path(PACKAGE_ROOT, 'pyproject.toml'), string=f'pyinventory = "=={version}"')
assert_file_contains_string(file_path=Path(PACKAGE_ROOT, 'manifest.json'), string=f'"version": "{version}~ynh')
def test_poetry_check():
poerty_bin = shutil.which('poetry')
output = subprocess.check_output(
[poerty_bin, 'check'],
universal_newlines=True,
env=os.environ,
stderr=subprocess.STDOUT,
cwd=str(PACKAGE_ROOT),
)
print(output)
assert output == 'All set!\n'

8
tests/test_utils.py Normal file
View file

@ -0,0 +1,8 @@
from unittest.case import TestCase
from django_ynh.test_utils import generate_basic_auth
class TestUtilsTestCase(TestCase):
def test_generate_basic_auth(self):
assert generate_basic_auth(username='test', password='test123') == 'basic dGVzdDp0ZXN0MTIz'