Compare commits
117 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
| 03647fa6af | |||
| 5aec581585 | |||
| 68e9b7e140 | |||
| b42bca4e3e | |||
| 42c9ac61b2 | |||
| 7cdc5f0568 | |||
| a063613f4c | |||
| 3af04bf1e4 | |||
| 74f8b68af8 | |||
| 59dbc15be7 | |||
| 9d5dd896f3 | |||
| 02f5f12089 | |||
| 90ea6dba90 | |||
| b0b2c0830b | |||
| acb2b825f3 | |||
| e956b86649 | |||
| 739c66da1c | |||
| e8c7cce68f | |||
| f741d382c2 | |||
| a13d4047b6 | |||
| e0d8189442 | |||
| 760352202e | |||
| 9724ded194 | |||
| 5da4ff4ff1 | |||
| e54b98a80e | |||
| 67b69cb5d3 | |||
| 863111ac57 | |||
| bd78087582 | |||
| 8f4e954160 | |||
| 553f184aad | |||
| b6d7847eae | |||
| ad0d339794 | |||
| 737cd22bb9 | |||
| 6ad1465f8f | |||
| d74fa4abbf | |||
| b24938fc6b | |||
| ea1564548c | |||
| 3663c3c8a1 | |||
| 07e20a2950 | |||
| 6366d50a0e | |||
| c3e64df95b | |||
| d2bf2c8896 | |||
| f27b43507c | |||
| c1058c7438 | |||
| c37901feb9 | |||
| 44b815efae | |||
| 64a71a3663 | |||
| ae435f423e | |||
| 7aa89c6d4f | |||
| 7e9d7e5198 | |||
| 2be6cd70d9 | |||
| 2b9705b33c | |||
| 502e43085f | |||
| 40f1de3b11 | |||
| 899c5b63ea | |||
| e104c74761 | |||
| 5d46c1ea5a | |||
| 7d533889bc | |||
| d9c2b32cba | |||
| 6e4ce8dbaa | |||
| 03d58b439f | |||
| ea38da441b | |||
| bdaf0111c2 | |||
| 974c2ddb11 | |||
| 769ce1c642 | |||
| f294791d41 | |||
| 4ee22f8ec1 | |||
| 74d3cfbba0 | |||
| d278acb83b | |||
| 84da454612 | |||
| 52101007aa | |||
| dc57f433fd | |||
| 3d4c5b8f4e | |||
| e66424cc49 | |||
| 8fa83a8d08 | |||
| 397892b282 | |||
| 7be50c2574 | |||
| 2aad523596 | |||
| 6982b97eb0 | |||
| 3de879496d | |||
| 4e75118a43 | |||
| 52c4fb431f | |||
| d696d854ff | |||
| 6966c119a7 | |||
| 8cf5e647e3 | |||
| 99bc6241f6 | |||
| e5f837ebb7 | |||
| 9d93da3d45 | |||
| 9f6f18f9bb | |||
| 6458b1dbf8 | |||
| 1aff9afca6 | |||
| e0bc7d3932 | |||
| 9fd9b2611c | |||
| 6f3a1dfd08 | |||
| 464b2cce88 | |||
| 4eaa46e717 | |||
| 59e8dca499 | |||
| 945d5bfaf6 | |||
| dbcdab05ff | |||
| e2cc2843d8 | |||
| 241d59be8d | |||
| 74251a8883 | |||
| 585afd1bcd | |||
| 8358574484 | |||
| cbcdaaf532 | |||
| f99eaa85ac | |||
| 5007a6befe | |||
| 50c75087b8 | |||
| 438e4efd49 | |||
| c7ca95ff2b | |||
| 9f403a71ed | |||
| 2f4139df65 | |||
| f3ee8f7d9c | |||
| 5fa3729702 | |||
| 87f44fada4 | |||
| c0026f3e16 | |||
| c1051059f4 |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.10.4-stable
|
current_version = 0.10.8-stable
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||||
|
|||||||
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Logs**
|
||||||
|
Output of docker-compose logs or kubectl logs respectively
|
||||||
|
|
||||||
|
**Version and Deployment (please complete the following information):**
|
||||||
|
- passbook version: [e.g. 0.10.0-stable]
|
||||||
|
- Deployment: [e.g. docker-compose, helm]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
22
.github/dependabot.yml
vendored
Normal file
22
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: gomod
|
||||||
|
directory: "/proxy"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
time: "04:00"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: "/passbook/static/static"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
time: "04:00"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
- package-ecosystem: pip
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: daily
|
||||||
|
time: "04:00"
|
||||||
|
open-pull-requests-limit: 10
|
||||||
|
assignees:
|
||||||
|
- BeryJu
|
||||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
|||||||
- name: Building Docker Image
|
- name: Building Docker Image
|
||||||
run: docker build
|
run: docker build
|
||||||
--no-cache
|
--no-cache
|
||||||
-t beryju/passbook:0.10.4-stable
|
-t beryju/passbook:0.10.8-stable
|
||||||
-t beryju/passbook:latest
|
-t beryju/passbook:latest
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook:0.10.4-stable
|
run: docker push beryju/passbook:0.10.8-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook:latest
|
run: docker push beryju/passbook:latest
|
||||||
build-proxy:
|
build-proxy:
|
||||||
@ -48,11 +48,11 @@ jobs:
|
|||||||
cd proxy
|
cd proxy
|
||||||
docker build \
|
docker build \
|
||||||
--no-cache \
|
--no-cache \
|
||||||
-t beryju/passbook-proxy:0.10.4-stable \
|
-t beryju/passbook-proxy:0.10.8-stable \
|
||||||
-t beryju/passbook-proxy:latest \
|
-t beryju/passbook-proxy:latest \
|
||||||
-f Dockerfile .
|
-f Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook-proxy:0.10.4-stable
|
run: docker push beryju/passbook-proxy:0.10.8-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook-proxy:latest
|
run: docker push beryju/passbook-proxy:latest
|
||||||
build-static:
|
build-static:
|
||||||
@ -77,11 +77,11 @@ jobs:
|
|||||||
run: docker build
|
run: docker build
|
||||||
--no-cache
|
--no-cache
|
||||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||||
-t beryju/passbook-static:0.10.4-stable
|
-t beryju/passbook-static:0.10.8-stable
|
||||||
-t beryju/passbook-static:latest
|
-t beryju/passbook-static:latest
|
||||||
-f static.Dockerfile .
|
-f static.Dockerfile .
|
||||||
- name: Push Docker Container to Registry (versioned)
|
- name: Push Docker Container to Registry (versioned)
|
||||||
run: docker push beryju/passbook-static:0.10.4-stable
|
run: docker push beryju/passbook-static:0.10.8-stable
|
||||||
- name: Push Docker Container to Registry (latest)
|
- name: Push Docker Container to Registry (latest)
|
||||||
run: docker push beryju/passbook-static:latest
|
run: docker push beryju/passbook-static:latest
|
||||||
test-release:
|
test-release:
|
||||||
@ -114,5 +114,5 @@ jobs:
|
|||||||
SENTRY_PROJECT: passbook
|
SENTRY_PROJECT: passbook
|
||||||
SENTRY_URL: https://sentry.beryju.org
|
SENTRY_URL: https://sentry.beryju.org
|
||||||
with:
|
with:
|
||||||
tagName: 0.10.4-stable
|
tagName: 0.10.8-stable
|
||||||
environment: beryjuorg-prod
|
environment: beryjuorg-prod
|
||||||
|
|||||||
@ -17,10 +17,10 @@ COPY --from=locker /app/requirements-dev.txt /
|
|||||||
|
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends postgresql-client-11 build-essential && \
|
apt-get install -y --no-install-recommends postgresql-client-11 build-essential && \
|
||||||
rm -rf /var/lib/apt/ && \
|
apt-get clean && \
|
||||||
pip install -r /requirements.txt --no-cache-dir && \
|
pip install -r /requirements.txt --no-cache-dir && \
|
||||||
apt-get remove --purge -y build-essential && \
|
apt-get remove --purge -y build-essential && \
|
||||||
apt-get autoremove --purge && \
|
apt-get autoremove --purge -y && \
|
||||||
adduser --system --no-create-home --uid 1000 --group --home /passbook passbook
|
adduser --system --no-create-home --uid 1000 --group --home /passbook passbook
|
||||||
|
|
||||||
COPY ./passbook/ /passbook
|
COPY ./passbook/ /passbook
|
||||||
|
|||||||
8
Makefile
8
Makefile
@ -8,12 +8,12 @@ coverage:
|
|||||||
|
|
||||||
lint-fix:
|
lint-fix:
|
||||||
isort -rc .
|
isort -rc .
|
||||||
black .
|
black passbook e2e lifecycle
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
pyright
|
pyright pyright e2e lifecycle
|
||||||
bandit -r .
|
bandit -r passbook e2e lifecycle
|
||||||
pylint passbook
|
pylint passbook e2e lifecycle
|
||||||
prospector
|
prospector
|
||||||
|
|
||||||
gen: coverage
|
gen: coverage
|
||||||
|
|||||||
2
Pipfile
2
Pipfile
@ -17,7 +17,7 @@ django-otp = "*"
|
|||||||
django-prometheus = "*"
|
django-prometheus = "*"
|
||||||
django-recaptcha = "*"
|
django-recaptcha = "*"
|
||||||
django-redis = "*"
|
django-redis = "*"
|
||||||
django-rest-framework = "*"
|
djangorestframework = "==3.11.1"
|
||||||
django-storages = "*"
|
django-storages = "*"
|
||||||
djangorestframework-guardian = "*"
|
djangorestframework-guardian = "*"
|
||||||
drf-yasg = "*"
|
drf-yasg = "*"
|
||||||
|
|||||||
291
Pipfile.lock
generated
291
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "80570636236962f4b934a884817292de9f7bb48520aa964afc2959b0f795fb57"
|
"sha256": "39e0a747699dc7e528a215395cc505b380e40e6bd0295fdf4c373a871a9bde96"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -16,6 +16,24 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
|
"aiohttp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e",
|
||||||
|
"sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326",
|
||||||
|
"sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a",
|
||||||
|
"sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654",
|
||||||
|
"sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a",
|
||||||
|
"sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4",
|
||||||
|
"sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17",
|
||||||
|
"sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec",
|
||||||
|
"sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd",
|
||||||
|
"sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48",
|
||||||
|
"sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59",
|
||||||
|
"sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==3.6.2"
|
||||||
|
},
|
||||||
"aioredis": {
|
"aioredis": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",
|
"sha256:15f8af30b044c771aee6787e5ec24694c048184c7b9e54c3b60c750a4b93273a",
|
||||||
@ -25,10 +43,10 @@
|
|||||||
},
|
},
|
||||||
"amqp": {
|
"amqp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21",
|
"sha256:9881f8e6fe23e3db9faa6cfd8c05390213e1d1b95c0162bc50552cad75bffa5f",
|
||||||
"sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"
|
"sha256:a8fb8151eb9d12204c9f1784c0da920476077609fa0a70f2468001e3a4258484"
|
||||||
],
|
],
|
||||||
"version": "==2.6.1"
|
"version": "==5.0.1"
|
||||||
},
|
},
|
||||||
"asgiref": {
|
"asgiref": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -74,17 +92,18 @@
|
|||||||
},
|
},
|
||||||
"boto3": {
|
"boto3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:44073b1b1823ffc9edcf9027afbca908dad6bd5000f512ca73f929f6a604ae24"
|
"sha256:348cddfd56be6c8b759544f99d20d633cc6a65d346651700ad8ac93a5214c032",
|
||||||
|
"sha256:c4ccd1f260660603f965bcc145de87e09dd1229040784fe119cd08caeb00dbe9"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.15.1"
|
"version": "==1.15.8"
|
||||||
},
|
},
|
||||||
"botocore": {
|
"botocore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:6bdf60281c2e80360fe904851a1a07df3dcfe066fe88dc7fba2b5e626ac05c8c",
|
"sha256:07b399997d8050d3ed1150d4d657b46558999f75246eb5b02cee78b9798b3bd5",
|
||||||
"sha256:d6bdf51c8880aa9974e6b61d2f7d9d1debe407287e2e9e60f36c789fe8ba6790"
|
"sha256:53a778e6b715ad2ae39bf98e088962e8d524133fb458d83f080964254adc9885"
|
||||||
],
|
],
|
||||||
"version": "==1.18.1"
|
"version": "==1.18.8"
|
||||||
},
|
},
|
||||||
"cachetools": {
|
"cachetools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -95,11 +114,11 @@
|
|||||||
},
|
},
|
||||||
"celery": {
|
"celery": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a92e1d56e650781fb747032a3997d16236d037c8199eacd5217d1a72893bca45",
|
"sha256:313930fddde703d8e37029a304bf91429cd11aeef63c57de6daca9d958e1f255",
|
||||||
"sha256:d220b13a8ed57c78149acf82c006785356071844afe0b27012a4991d44026f9f"
|
"sha256:72138dc3887f68dc58e1a2397e477256f80f1894c69fa4337f8ed70be460375b"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.4.7"
|
"version": "==5.0.0"
|
||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -179,6 +198,19 @@
|
|||||||
],
|
],
|
||||||
"version": "==7.1.2"
|
"version": "==7.1.2"
|
||||||
},
|
},
|
||||||
|
"click-didyoumean": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:112229485c9704ff51362fe34b2d4f0b12fc71cc20f6d2b3afabed4b8bfa6aeb"
|
||||||
|
],
|
||||||
|
"version": "==0.0.3"
|
||||||
|
},
|
||||||
|
"click-repl": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9c4c3d022789cae912aad8a3f5e1d7c2cdd016ee1225b5212ad3e8691563cda5",
|
||||||
|
"sha256:b9f29d52abc4d6059f8e276132a111ab8d94980afe6a5432b9d996544afa95d5"
|
||||||
|
],
|
||||||
|
"version": "==0.1.6"
|
||||||
|
},
|
||||||
"constantly": {
|
"constantly": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35",
|
"sha256:586372eb92059873e29eba4f9dec8381541b4d3834660707faf8ba59146dfc35",
|
||||||
@ -272,11 +304,11 @@
|
|||||||
},
|
},
|
||||||
"django-filter": {
|
"django-filter": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:11e63dd759835d9ba7a763926ffb2662cf8a6dcb4c7971a95064de34dbc7e5af",
|
"sha256:84e9d5bb93f237e451db814ed422a3a625751cbc9968b484ecc74964a8696b06",
|
||||||
"sha256:616848eab6fc50193a1b3730140c49b60c57a3eda1f7fc57fa8505ac156c6c75"
|
"sha256:e00d32cebdb3d54273c48f4f878f898dced8d5dfaad009438fe61ebdf535ace1"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.3.0"
|
"version": "==2.4.0"
|
||||||
},
|
},
|
||||||
"django-guardian": {
|
"django-guardian": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -325,13 +357,6 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.12.1"
|
"version": "==4.12.1"
|
||||||
},
|
},
|
||||||
"django-rest-framework": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:47a8f496fa69e3b6bd79f68dd7a1527d907d6b77f009e9db7cf9bb21cc565e4a"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==0.1.0"
|
|
||||||
},
|
|
||||||
"django-storages": {
|
"django-storages": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c",
|
"sha256:12de8fb2605b9b57bfaf54b075280d7cbb3b3ee1ca4bc9b9add147af87fe3a2c",
|
||||||
@ -345,6 +370,7 @@
|
|||||||
"sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32",
|
"sha256:6dd02d5a4bd2516fb93f80360673bf540c3b6641fec8766b1da2870a5aa00b32",
|
||||||
"sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b"
|
"sha256:8b1ac62c581dbc5799b03e535854b92fc4053ecfe74bad3f9c05782063d4196b"
|
||||||
],
|
],
|
||||||
|
"index": "pypi",
|
||||||
"version": "==3.11.1"
|
"version": "==3.11.1"
|
||||||
},
|
},
|
||||||
"djangorestframework-guardian": {
|
"djangorestframework-guardian": {
|
||||||
@ -386,10 +412,10 @@
|
|||||||
},
|
},
|
||||||
"google-auth": {
|
"google-auth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7084c50c03f7a8a5696ef4500e65df0c525a0f6909f3c70b9ee65900a230c755",
|
"sha256:a73e6fb6d232ed1293ef9a5301e6f8aada7880d19c65d7f63e130dc50ec05593",
|
||||||
"sha256:dcf86c5adc3a8a7659be190b12bb8912ae019cfd9ee2a571ea881e289fafbe39"
|
"sha256:e86e72142d939a8d90a772947268aacc127ab7a1d1d6f3e0fecca7a8d74d8257"
|
||||||
],
|
],
|
||||||
"version": "==1.21.2"
|
"version": "==1.22.0"
|
||||||
},
|
},
|
||||||
"gunicorn": {
|
"gunicorn": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -401,10 +427,10 @@
|
|||||||
},
|
},
|
||||||
"h11": {
|
"h11": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:33d4bca7be0fa039f4e84d50ab00531047e53d6ee8ffbc83501ea602c169cae1",
|
"sha256:311dc5478c2568cc07262e0381cdfc5b9c6ba19775905736c87e81ae6662b9fd",
|
||||||
"sha256:4bc6d6a1238b7615b266ada57e0618568066f57dd6fa967d1290ec9309b2f2f1"
|
"sha256:9eecfbafc980976dbff26a01dd3487644dd5d00f8038584451fc64a660f7c502"
|
||||||
],
|
],
|
||||||
"version": "==0.9.0"
|
"version": "==0.10.0"
|
||||||
},
|
},
|
||||||
"hiredis": {
|
"hiredis": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -457,24 +483,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.1.0"
|
"version": "==1.1.0"
|
||||||
},
|
},
|
||||||
"httptools": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be",
|
|
||||||
"sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d",
|
|
||||||
"sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce",
|
|
||||||
"sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2",
|
|
||||||
"sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6",
|
|
||||||
"sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f",
|
|
||||||
"sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009",
|
|
||||||
"sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce",
|
|
||||||
"sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a",
|
|
||||||
"sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c",
|
|
||||||
"sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4",
|
|
||||||
"sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"
|
|
||||||
],
|
|
||||||
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'",
|
|
||||||
"version": "==0.1.1"
|
|
||||||
},
|
|
||||||
"hyperlink": {
|
"hyperlink": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af",
|
"sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af",
|
||||||
@ -533,10 +541,10 @@
|
|||||||
},
|
},
|
||||||
"kombu": {
|
"kombu": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
|
"sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006",
|
||||||
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
|
"sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c"
|
||||||
],
|
],
|
||||||
"version": "==4.6.11"
|
"version": "==5.0.2"
|
||||||
},
|
},
|
||||||
"kubernetes": {
|
"kubernetes": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -652,6 +660,28 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.0.0"
|
"version": "==1.0.0"
|
||||||
},
|
},
|
||||||
|
"multidict": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a",
|
||||||
|
"sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000",
|
||||||
|
"sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2",
|
||||||
|
"sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507",
|
||||||
|
"sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5",
|
||||||
|
"sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7",
|
||||||
|
"sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d",
|
||||||
|
"sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463",
|
||||||
|
"sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19",
|
||||||
|
"sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3",
|
||||||
|
"sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b",
|
||||||
|
"sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c",
|
||||||
|
"sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87",
|
||||||
|
"sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7",
|
||||||
|
"sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430",
|
||||||
|
"sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255",
|
||||||
|
"sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"
|
||||||
|
],
|
||||||
|
"version": "==4.7.6"
|
||||||
|
},
|
||||||
"oauthlib": {
|
"oauthlib": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
||||||
@ -674,6 +704,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.8.0"
|
"version": "==0.8.0"
|
||||||
},
|
},
|
||||||
|
"prompt-toolkit": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:822f4605f28f7d2ba6b0b09a31e25e140871e96364d1d377667b547bb3bf4489",
|
||||||
|
"sha256:83074ee28ad4ba6af190593d4d4c607ff525272a504eb159199b6dd9f950c950"
|
||||||
|
],
|
||||||
|
"version": "==3.0.7"
|
||||||
|
},
|
||||||
"psycopg2-binary": {
|
"psycopg2-binary": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
|
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
|
||||||
@ -748,20 +785,25 @@
|
|||||||
"sha256:54bdedd28476dea8a3cd86cb67c0df1f0e3d71cae8022354b0f879c41a3d27b2",
|
"sha256:54bdedd28476dea8a3cd86cb67c0df1f0e3d71cae8022354b0f879c41a3d27b2",
|
||||||
"sha256:55eb61aca2c883db770999f50d091ff7c14016f2769ad7bca3d9b75d1d7c1b68",
|
"sha256:55eb61aca2c883db770999f50d091ff7c14016f2769ad7bca3d9b75d1d7c1b68",
|
||||||
"sha256:6276478ada411aca97c0d5104916354b3d740d368407912722bd4d11aa9ee4c2",
|
"sha256:6276478ada411aca97c0d5104916354b3d740d368407912722bd4d11aa9ee4c2",
|
||||||
|
"sha256:663f8de2b3df2e744d6e1610506e0ea4e213bde906795953c1e82279c169f0a7",
|
||||||
"sha256:67dcad1b8b201308586a8ca2ffe89df1e4f731d5a4cdd0610cc4ea790351c739",
|
"sha256:67dcad1b8b201308586a8ca2ffe89df1e4f731d5a4cdd0610cc4ea790351c739",
|
||||||
"sha256:709b9f144d23e290b9863121d1ace14a72e01f66ea9c903fbdc690520dfdfcf0",
|
"sha256:709b9f144d23e290b9863121d1ace14a72e01f66ea9c903fbdc690520dfdfcf0",
|
||||||
"sha256:8063a712fba642f78d3c506b0896846601b6de7f5c3d534e388ad0cc07f5a149",
|
"sha256:8063a712fba642f78d3c506b0896846601b6de7f5c3d534e388ad0cc07f5a149",
|
||||||
"sha256:80d57177a0b7c14d4594c62bbb47fe2f6309ad3b0a34348a291d570925c97a82",
|
"sha256:80d57177a0b7c14d4594c62bbb47fe2f6309ad3b0a34348a291d570925c97a82",
|
||||||
|
"sha256:87006cf0d81505408f1ae4f55cf8a5d95a8e029a4793360720ae17c6500f7ecc",
|
||||||
|
"sha256:9f62d21bc693f3d7d444f17ed2ad7a913b4c37c15cd807895d013c39c0517dfd",
|
||||||
"sha256:a207231a52426de3ff20f5608f0687261a3329d97a036c51f7d4c606a6f30c23",
|
"sha256:a207231a52426de3ff20f5608f0687261a3329d97a036c51f7d4c606a6f30c23",
|
||||||
"sha256:abc2e126c9490e58a36a0f83516479e781d83adfb134576a5cbe5c6af2a3e93c",
|
"sha256:abc2e126c9490e58a36a0f83516479e781d83adfb134576a5cbe5c6af2a3e93c",
|
||||||
"sha256:b56638d58a3a4be13229c6a815cd448f9e3ce40c00880a5398471b42ee86f50e",
|
"sha256:b56638d58a3a4be13229c6a815cd448f9e3ce40c00880a5398471b42ee86f50e",
|
||||||
"sha256:bcd5b8416e73e4b0d48afba3704d8c826414764dafaed7a1a93c442188d90ccc",
|
"sha256:bcd5b8416e73e4b0d48afba3704d8c826414764dafaed7a1a93c442188d90ccc",
|
||||||
"sha256:bec2bcdf7c9ce7f04d718e51887f3b05dc5c1cfaf5d2c2e9065ecddd1b2f6c9a",
|
"sha256:bec2bcdf7c9ce7f04d718e51887f3b05dc5c1cfaf5d2c2e9065ecddd1b2f6c9a",
|
||||||
"sha256:c8bf40cf6e281a4378e25846924327e728a887e8bf0ee83b2604a0f4b61692e8",
|
"sha256:c8bf40cf6e281a4378e25846924327e728a887e8bf0ee83b2604a0f4b61692e8",
|
||||||
|
"sha256:cecbf67e81d6144a50dc615629772859463b2e4f815d0c082fa421db362f040e",
|
||||||
"sha256:d8074c8448cfd0705dfa71ca333277fce9786d0b9cac75d120545de6253f996a",
|
"sha256:d8074c8448cfd0705dfa71ca333277fce9786d0b9cac75d120545de6253f996a",
|
||||||
"sha256:dd302b6ae3965afeb5ef1b0d92486f986c0e65183cd7835973f0b593800590e6",
|
"sha256:dd302b6ae3965afeb5ef1b0d92486f986c0e65183cd7835973f0b593800590e6",
|
||||||
"sha256:de6e1cd75677423ff64712c337521e62e3a7a4fc84caabbd93207752e831a85a",
|
"sha256:de6e1cd75677423ff64712c337521e62e3a7a4fc84caabbd93207752e831a85a",
|
||||||
"sha256:ef39c98d9b8c0736d91937d193653e47c3b19ddf4fc3bccdc5e09aaa4b0c5d21",
|
"sha256:ef39c98d9b8c0736d91937d193653e47c3b19ddf4fc3bccdc5e09aaa4b0c5d21",
|
||||||
|
"sha256:f2e045224074d5664dc9cbabbf4f4d4d46f1ee90f24780e3a9a668fd096ff17f",
|
||||||
"sha256:f521178e5a991ffd04182ed08f552daca1affcb826aeda0e1945cd989a9d4345",
|
"sha256:f521178e5a991ffd04182ed08f552daca1affcb826aeda0e1945cd989a9d4345",
|
||||||
"sha256:f78a68c2c820e4731e510a2df3eef0322f24fde1781ced970bf497b6c7d92982",
|
"sha256:f78a68c2c820e4731e510a2df3eef0322f24fde1781ced970bf497b6c7d92982",
|
||||||
"sha256:fbe65d5cfe04ff2f7684160d50f5118bdefb01e3af4718eeb618bfed40f19d94"
|
"sha256:fbe65d5cfe04ff2f7684160d50f5118bdefb01e3af4718eeb618bfed40f19d94"
|
||||||
@ -778,12 +820,14 @@
|
|||||||
"sha256:2275a663c9e744ee4eace816ef2d446b3060554c5773a92fbc79b05bf47debda",
|
"sha256:2275a663c9e744ee4eace816ef2d446b3060554c5773a92fbc79b05bf47debda",
|
||||||
"sha256:2710fc8d83b3352b370db932b3710033b9d630b970ff5aaa3e7458b5336e3b32",
|
"sha256:2710fc8d83b3352b370db932b3710033b9d630b970ff5aaa3e7458b5336e3b32",
|
||||||
"sha256:35b9c9177a9fe7288b19dd41554c9c8ca1063deb426dd5a02e7e2a7416b6bd11",
|
"sha256:35b9c9177a9fe7288b19dd41554c9c8ca1063deb426dd5a02e7e2a7416b6bd11",
|
||||||
|
"sha256:3b23d63030819b7d9ac7db9360305fd1241e6870ca5b7e8d59fee4db4674a490",
|
||||||
"sha256:3caa32cf807422adf33c10c88c22e9e2e08b9d9d042f12e1e25fe23113dd618f",
|
"sha256:3caa32cf807422adf33c10c88c22e9e2e08b9d9d042f12e1e25fe23113dd618f",
|
||||||
"sha256:48cc2cfc251f04a6142badeb666d1ff49ca6fdfc303fd72579f62b768aaa52b9",
|
"sha256:48cc2cfc251f04a6142badeb666d1ff49ca6fdfc303fd72579f62b768aaa52b9",
|
||||||
"sha256:4ae6379350a09339109e9b6f419bb2c3f03d3e441f4b0f5b8ca699d47cc9ff7e",
|
"sha256:4ae6379350a09339109e9b6f419bb2c3f03d3e441f4b0f5b8ca699d47cc9ff7e",
|
||||||
"sha256:4e0b27697fa1621c6d3d3b4edeec723c2e841285de6a8d378c1962da77b349be",
|
"sha256:4e0b27697fa1621c6d3d3b4edeec723c2e841285de6a8d378c1962da77b349be",
|
||||||
"sha256:58e19560814dabf5d788b95a13f6b98279cf41a49b1e49ee6cf6c79a57adb4c9",
|
"sha256:58e19560814dabf5d788b95a13f6b98279cf41a49b1e49ee6cf6c79a57adb4c9",
|
||||||
"sha256:8044eae59301dd392fbb4a7c5d64e1aea8ef0be2540549807ecbe703d6233d68",
|
"sha256:8044eae59301dd392fbb4a7c5d64e1aea8ef0be2540549807ecbe703d6233d68",
|
||||||
|
"sha256:85c108b42e47d4073344ff61d4e019f1d95bb7725ca0fe87d0a2deb237c10e49",
|
||||||
"sha256:89be1bf55e50116fe7e493a7c0c483099770dd7f81b87ac8d04a43b1a203e259",
|
"sha256:89be1bf55e50116fe7e493a7c0c483099770dd7f81b87ac8d04a43b1a203e259",
|
||||||
"sha256:8fcdda24dddf47f716400d54fc7f75cadaaba1dd47cc127e59d752c9c0fc3c48",
|
"sha256:8fcdda24dddf47f716400d54fc7f75cadaaba1dd47cc127e59d752c9c0fc3c48",
|
||||||
"sha256:914fbb18e29c54585e6aa39d300385f90d0fa3b3cc02ed829b08f95c1acf60c2",
|
"sha256:914fbb18e29c54585e6aa39d300385f90d0fa3b3cc02ed829b08f95c1acf60c2",
|
||||||
@ -793,13 +837,16 @@
|
|||||||
"sha256:a2ee8ba99d33e1a434fcd27d7d0aa7964163efeee0730fe2efc9d60edae1fc71",
|
"sha256:a2ee8ba99d33e1a434fcd27d7d0aa7964163efeee0730fe2efc9d60edae1fc71",
|
||||||
"sha256:b2d756620078570d3f940c84bc94dd30aa362b795cce8b2723300a8800b87f1c",
|
"sha256:b2d756620078570d3f940c84bc94dd30aa362b795cce8b2723300a8800b87f1c",
|
||||||
"sha256:c0d085c8187a1e4d3402f626c9e438b5861151ab132d8761d9c5ce6491a87761",
|
"sha256:c0d085c8187a1e4d3402f626c9e438b5861151ab132d8761d9c5ce6491a87761",
|
||||||
|
"sha256:c315262e26d54a9684e323e37ac9254f481d57fcc4fd94002992460898ef5c04",
|
||||||
"sha256:c990f2c58f7c67688e9e86e6557ed05952669ff6f1343e77b459007d85f7df00",
|
"sha256:c990f2c58f7c67688e9e86e6557ed05952669ff6f1343e77b459007d85f7df00",
|
||||||
"sha256:ccbbec59bf4b74226170c54476da5780c9176bae084878fc94d9a2c841218e34",
|
"sha256:ccbbec59bf4b74226170c54476da5780c9176bae084878fc94d9a2c841218e34",
|
||||||
"sha256:dc2bed32c7b138f1331794e454a953360c8cedf3ee62ae31f063822da6007489",
|
"sha256:dc2bed32c7b138f1331794e454a953360c8cedf3ee62ae31f063822da6007489",
|
||||||
|
"sha256:ddb1ae2891c8cb83a25da87a3e00111a9654fc5f0b70f18879c41aece45d6182",
|
||||||
"sha256:e070a1f91202ed34c396be5ea842b886f6fa2b90d2db437dc9fb35a26c80c060",
|
"sha256:e070a1f91202ed34c396be5ea842b886f6fa2b90d2db437dc9fb35a26c80c060",
|
||||||
"sha256:e42860fbe1292668b682f6dabd225fbe2a7a4fa1632f0c39881c019e93dea594",
|
"sha256:e42860fbe1292668b682f6dabd225fbe2a7a4fa1632f0c39881c019e93dea594",
|
||||||
"sha256:e4e1c486bf226822c8dceac81d0ec59c0a2399dbd1b9e04f03c3efa3605db677",
|
"sha256:e4e1c486bf226822c8dceac81d0ec59c0a2399dbd1b9e04f03c3efa3605db677",
|
||||||
"sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46",
|
"sha256:ea4d4b58f9bc34e224ef4b4604a6be03d72ef1f8c486391f970205f6733dbc46",
|
||||||
|
"sha256:f5bd6891380e0fb5467251daf22525644fdf6afd9ae8bc2fe065c78ea1882e0d",
|
||||||
"sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb"
|
"sha256:f60b3484ce4be04f5da3777c51c5140d3fe21cdd6674f2b6568f41c8130bcdeb"
|
||||||
],
|
],
|
||||||
"version": "==3.9.8"
|
"version": "==3.9.8"
|
||||||
@ -951,11 +998,11 @@
|
|||||||
},
|
},
|
||||||
"sentry-sdk": {
|
"sentry-sdk": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1a086486ff9da15791f294f6e9915eb3747d161ef64dee2d038a4d0b4a369b24",
|
"sha256:1d91a0059d2d8bb980bec169578035c2f2d4b93cd8a4fb5b85c81904d33e221a",
|
||||||
"sha256:45486deb031cea6bbb25a540d7adb4dd48cd8a1cc31e6a5ce9fb4f792a572e9a"
|
"sha256:6222cf623e404c3e62b8e0e81c6db866ac2d12a663b7c1f7963350e3f397522a"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.17.6"
|
"version": "==0.18.0"
|
||||||
},
|
},
|
||||||
"service-identity": {
|
"service-identity": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1062,33 +1109,25 @@
|
|||||||
},
|
},
|
||||||
"uvicorn": {
|
"uvicorn": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:46a83e371f37ea7ff29577d00015f02c942410288fb57def6440f2653fff1d26",
|
"sha256:9a8f3501d977dedf77a540a0ec3cfadf409fe48eafca2c100d45d843ac62bc7b",
|
||||||
"sha256:4b70ddb4c1946e39db9f3082d53e323dfd50634b95fd83625d778729ef1730ef"
|
"sha256:fbe9d1b764bc1f4599e1f150a0974feea0fd6380bec889c0d907ebd0a2e896a7"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==0.11.8"
|
"version": "==0.12.0"
|
||||||
},
|
|
||||||
"uvloop": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd",
|
|
||||||
"sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e",
|
|
||||||
"sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09",
|
|
||||||
"sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726",
|
|
||||||
"sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891",
|
|
||||||
"sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7",
|
|
||||||
"sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5",
|
|
||||||
"sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95",
|
|
||||||
"sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362"
|
|
||||||
],
|
|
||||||
"markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'",
|
|
||||||
"version": "==0.14.0"
|
|
||||||
},
|
},
|
||||||
"vine": {
|
"vine": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
|
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
|
||||||
"sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
|
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
|
||||||
],
|
],
|
||||||
"version": "==1.3.0"
|
"version": "==5.0.0"
|
||||||
|
},
|
||||||
|
"wcwidth": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
|
||||||
|
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
|
||||||
|
],
|
||||||
|
"version": "==0.2.5"
|
||||||
},
|
},
|
||||||
"websocket-client": {
|
"websocket-client": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1097,32 +1136,27 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.57.0"
|
"version": "==0.57.0"
|
||||||
},
|
},
|
||||||
"websockets": {
|
"yarl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5",
|
"sha256:04a54f126a0732af75e5edc9addeaa2113e2ca7c6fce8974a63549a70a25e50e",
|
||||||
"sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5",
|
"sha256:3cc860d72ed989f3b1f3abbd6ecf38e412de722fb38b8f1b1a086315cf0d69c5",
|
||||||
"sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308",
|
"sha256:5d84cc36981eb5a8533be79d6c43454c8e6a39ee3118ceaadbd3c029ab2ee580",
|
||||||
"sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb",
|
"sha256:5e447e7f3780f44f890360ea973418025e8c0cdcd7d6a1b221d952600fd945dc",
|
||||||
"sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a",
|
"sha256:61d3ea3c175fe45f1498af868879c6ffeb989d4143ac542163c45538ba5ec21b",
|
||||||
"sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c",
|
"sha256:67c5ea0970da882eaf9efcf65b66792557c526f8e55f752194eff8ec722c75c2",
|
||||||
"sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170",
|
"sha256:6f6898429ec3c4cfbef12907047136fd7b9e81a6ee9f105b45505e633427330a",
|
||||||
"sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422",
|
"sha256:7ce35944e8e61927a8f4eb78f5bc5d1e6da6d40eadd77e3f79d4e9399e263921",
|
||||||
"sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8",
|
"sha256:b7c199d2cbaf892ba0f91ed36d12ff41ecd0dde46cbf64ff4bfe997a3ebc925e",
|
||||||
"sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485",
|
"sha256:c15d71a640fb1f8e98a1423f9c64d7f1f6a3a168f803042eaf3a5b5022fde0c1",
|
||||||
"sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f",
|
"sha256:c22607421f49c0cb6ff3ed593a49b6a99c6ffdeaaa6c944cdda83c2393c8864d",
|
||||||
"sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8",
|
"sha256:c604998ab8115db802cc55cb1b91619b2831a6128a62ca7eea577fc8ea4d3131",
|
||||||
"sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc",
|
"sha256:d088ea9319e49273f25b1c96a3763bf19a882cff774d1792ae6fba34bd40550a",
|
||||||
"sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779",
|
"sha256:db9eb8307219d7e09b33bcb43287222ef35cbcf1586ba9472b0a4b833666ada1",
|
||||||
"sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989",
|
"sha256:e31fef4e7b68184545c3d68baec7074532e077bd1906b040ecfba659737df188",
|
||||||
"sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1",
|
"sha256:e32f0fb443afcfe7f01f95172b66f279938fbc6bdaebe294b0ff6747fb6db020",
|
||||||
"sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092",
|
"sha256:fcbe419805c9b20db9a51d33b942feddbf6e7fb468cb20686fd7089d4164c12a"
|
||||||
"sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824",
|
|
||||||
"sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d",
|
|
||||||
"sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55",
|
|
||||||
"sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36",
|
|
||||||
"sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"
|
|
||||||
],
|
],
|
||||||
"version": "==8.1"
|
"version": "==1.6.0"
|
||||||
},
|
},
|
||||||
"zope.interface": {
|
"zope.interface": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1316,11 +1350,11 @@
|
|||||||
},
|
},
|
||||||
"django-debug-toolbar": {
|
"django-debug-toolbar": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:eabbefe89881bbe4ca7c980ff102e3c35c8e8ad6eb725041f538988f2f39a943",
|
"sha256:a1ce0665f7ef47d27b8df4b5d1058748e1f08500a01421a30d35164f38aaaf4c",
|
||||||
"sha256:ff94725e7aae74b133d0599b9bf89bd4eb8f5d2c964106e61d11750228c8774c"
|
"sha256:c97921a9cd421d392e7860dc4b464db8e06c8628df4dc58fedab012888c293c6"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.2"
|
"version": "==3.1.1"
|
||||||
},
|
},
|
||||||
"docker": {
|
"docker": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1419,13 +1453,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.6.1"
|
"version": "==0.6.1"
|
||||||
},
|
},
|
||||||
"more-itertools": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:6f83822ae94818eae2612063a5101a7311e68ae8002005b5e05f03fd74a86a20",
|
|
||||||
"sha256:9b30f12df9393f0d28af9210ff8efe48d10c94f73e5daf886f10c4b0b0b4f03c"
|
|
||||||
],
|
|
||||||
"version": "==8.5.0"
|
|
||||||
},
|
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
|
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
|
||||||
@ -1541,11 +1568,11 @@
|
|||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0e37f61339c4578776e090c3b8f6b16ce4db333889d65d0efb305243ec544b40",
|
"sha256:1cd09785c0a50f9af72220dd12aa78cfa49cbffc356c61eab009ca189e018a33",
|
||||||
"sha256:c8f57c2a30983f469bf03e68cdfa74dc474ce56b8f280ddcb080dfd91df01043"
|
"sha256:d010e24666435b39a4cf48740b039885642b6c273a3f77be3e7e03554d2806b7"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==6.0.2"
|
"version": "==6.1.0"
|
||||||
},
|
},
|
||||||
"pytest-django": {
|
"pytest-django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1581,29 +1608,29 @@
|
|||||||
},
|
},
|
||||||
"regex": {
|
"regex": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204",
|
"sha256:088afc8c63e7bd187a3c70a94b9e50ab3f17e1d3f52a32750b5b77dbe99ef5ef",
|
||||||
"sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162",
|
"sha256:1fe0a41437bbd06063aa184c34804efa886bcc128222e9916310c92cd54c3b4c",
|
||||||
"sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f",
|
"sha256:41bb65f54bba392643557e617316d0d899ed5b4946dccee1cb6696152b29844b",
|
||||||
"sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb",
|
"sha256:4318d56bccfe7d43e5addb272406ade7a2274da4b70eb15922a071c58ab0108c",
|
||||||
"sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6",
|
"sha256:4707f3695b34335afdfb09be3802c87fa0bc27030471dbc082f815f23688bc63",
|
||||||
"sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7",
|
"sha256:5533a959a1748a5c042a6da71fe9267a908e21eded7a4f373efd23a2cbdb0ecc",
|
||||||
"sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88",
|
"sha256:5f18875ac23d9aa2f060838e8b79093e8bb2313dbaaa9f54c6d8e52a5df097be",
|
||||||
"sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99",
|
"sha256:60b0e9e6dc45683e569ec37c55ac20c582973841927a85f2d8a7d20ee80216ab",
|
||||||
"sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644",
|
"sha256:84e9407db1b2eb368b7ecc283121b5e592c9aaedbe8c78b1a2f1102eb2e21d19",
|
||||||
"sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a",
|
"sha256:8d69cef61fa50c8133382e61fd97439de1ae623fe943578e477e76a9d9471637",
|
||||||
"sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840",
|
"sha256:9a02d0ae31d35e1ec12a4ea4d4cca990800f66a917d0fb997b20fbc13f5321fc",
|
||||||
"sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067",
|
"sha256:9bc13e0d20b97ffb07821aa3e113f9998e84994fe4d159ffa3d3a9d1b805043b",
|
||||||
"sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd",
|
"sha256:a6f32aea4260dfe0e55dc9733ea162ea38f0ea86aa7d0f77b15beac5bf7b369d",
|
||||||
"sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4",
|
"sha256:ae91972f8ac958039920ef6e8769277c084971a142ce2b660691793ae44aae6b",
|
||||||
"sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e",
|
"sha256:c570f6fa14b9c4c8a4924aaad354652366577b4f98213cf76305067144f7b100",
|
||||||
"sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89",
|
"sha256:d23a18037313714fb3bb5a94434d3151ee4300bae631894b1ac08111abeaa4a3",
|
||||||
"sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e",
|
"sha256:eaf548d117b6737df379fdd53bdde4f08870e66d7ea653e230477f071f861121",
|
||||||
"sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc",
|
"sha256:ebbe29186a3d9b0c591e71b7393f1ae08c83cb2d8e517d2a822b8f7ec99dfd8b",
|
||||||
"sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf",
|
"sha256:eda4771e0ace7f67f58bc5b560e27fb20f32a148cbc993b0c3835970935c2707",
|
||||||
"sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341",
|
"sha256:f1b3afc574a3db3b25c89161059d857bd4909a1269b0b3cb3c904677c8c4a3f7",
|
||||||
"sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"
|
"sha256:f2388013e68e750eaa16ccbea62d4130180c26abb1d8e5d584b9baf69672b30f"
|
||||||
],
|
],
|
||||||
"version": "==2020.7.14"
|
"version": "==2020.9.27"
|
||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|||||||
@ -8,6 +8,10 @@ variables:
|
|||||||
POSTGRES_DB: passbook
|
POSTGRES_DB: passbook
|
||||||
POSTGRES_USER: passbook
|
POSTGRES_USER: passbook
|
||||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||||
|
${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/') }}:
|
||||||
|
branchName: ${{ replace(variables['Build.SourceBranchName'], 'refs/heads/', '') }}
|
||||||
|
${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}:
|
||||||
|
branchName: ${{ replace(variables['System.PullRequest.SourceBranch'], 'refs/heads/', '') }}
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- stage: Lint
|
- stage: Lint
|
||||||
@ -26,7 +30,7 @@ stages:
|
|||||||
pipenv install --dev
|
pipenv install --dev
|
||||||
- task: CmdLine@2
|
- task: CmdLine@2
|
||||||
inputs:
|
inputs:
|
||||||
script: pipenv run pylint passbook
|
script: pipenv run pylint passbook e2e lifecycle
|
||||||
- job: black
|
- job: black
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
@ -41,7 +45,7 @@ stages:
|
|||||||
pipenv install --dev
|
pipenv install --dev
|
||||||
- task: CmdLine@2
|
- task: CmdLine@2
|
||||||
inputs:
|
inputs:
|
||||||
script: pipenv run black --check passbook
|
script: pipenv run black --check passbook e2e lifecycle
|
||||||
- job: prospector
|
- job: prospector
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
@ -57,7 +61,7 @@ stages:
|
|||||||
pipenv install --dev prospector --skip-lock
|
pipenv install --dev prospector --skip-lock
|
||||||
- task: CmdLine@2
|
- task: CmdLine@2
|
||||||
inputs:
|
inputs:
|
||||||
script: pipenv run prospector passbook
|
script: pipenv run prospector
|
||||||
- job: bandit
|
- job: bandit
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
@ -72,7 +76,7 @@ stages:
|
|||||||
pipenv install --dev
|
pipenv install --dev
|
||||||
- task: CmdLine@2
|
- task: CmdLine@2
|
||||||
inputs:
|
inputs:
|
||||||
script: pipenv run bandit -r passbook
|
script: pipenv run bandit -r passbook e2e lifecycle
|
||||||
- job: pyright
|
- job: pyright
|
||||||
pool:
|
pool:
|
||||||
vmImage: ubuntu-latest
|
vmImage: ubuntu-latest
|
||||||
@ -93,7 +97,7 @@ stages:
|
|||||||
pipenv install --dev
|
pipenv install --dev
|
||||||
- task: CmdLine@2
|
- task: CmdLine@2
|
||||||
inputs:
|
inputs:
|
||||||
script: pipenv run pyright
|
script: pipenv run pyright e2e lifecycle
|
||||||
- stage: Test
|
- stage: Test
|
||||||
jobs:
|
jobs:
|
||||||
- job: migrations
|
- job: migrations
|
||||||
@ -117,6 +121,41 @@ stages:
|
|||||||
- task: CmdLine@2
|
- task: CmdLine@2
|
||||||
inputs:
|
inputs:
|
||||||
script: pipenv run ./manage.py migrate
|
script: pipenv run ./manage.py migrate
|
||||||
|
- job: migrations_from_previous_release
|
||||||
|
pool:
|
||||||
|
vmImage: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- task: UsePythonVersion@0
|
||||||
|
inputs:
|
||||||
|
versionSpec: '3.8'
|
||||||
|
- task: DockerCompose@0
|
||||||
|
displayName: Run services
|
||||||
|
inputs:
|
||||||
|
dockerComposeFile: 'scripts/ci.docker-compose.yml'
|
||||||
|
action: 'Run services'
|
||||||
|
buildImages: false
|
||||||
|
- task: CmdLine@2
|
||||||
|
displayName: Prepare Last tagged release
|
||||||
|
inputs:
|
||||||
|
script: |
|
||||||
|
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||||
|
sudo pip install -U wheel pipenv
|
||||||
|
pipenv install --dev
|
||||||
|
- task: CmdLine@2
|
||||||
|
displayName: Migrate to last tagged release
|
||||||
|
inputs:
|
||||||
|
script: pipenv run ./manage.py migrate
|
||||||
|
- task: CmdLine@2
|
||||||
|
displayName: Install current branch
|
||||||
|
inputs:
|
||||||
|
script: |
|
||||||
|
set -x
|
||||||
|
git checkout ${{ variables.branchName }}
|
||||||
|
pipenv sync --dev
|
||||||
|
- task: CmdLine@2
|
||||||
|
displayName: Migrate to current branch
|
||||||
|
inputs:
|
||||||
|
script: pipenv run ./manage.py migrate
|
||||||
- job: coverage_unittest
|
- job: coverage_unittest
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
@ -140,6 +179,9 @@ stages:
|
|||||||
inputs:
|
inputs:
|
||||||
script: |
|
script: |
|
||||||
pipenv run coverage run ./manage.py test passbook -v 3
|
pipenv run coverage run ./manage.py test passbook -v 3
|
||||||
|
- task: CmdLine@2
|
||||||
|
inputs:
|
||||||
|
script: |
|
||||||
mkdir output-unittest
|
mkdir output-unittest
|
||||||
mv unittest.xml output-unittest/unittest.xml
|
mv unittest.xml output-unittest/unittest.xml
|
||||||
mv .coverage output-unittest/coverage
|
mv .coverage output-unittest/coverage
|
||||||
@ -182,7 +224,7 @@ stages:
|
|||||||
displayName: Run full test suite
|
displayName: Run full test suite
|
||||||
inputs:
|
inputs:
|
||||||
script: |
|
script: |
|
||||||
pipenv run coverage run ./manage.py test e2e -v 3
|
pipenv run coverage run ./manage.py test e2e -v 3 --failfast
|
||||||
- task: CmdLine@2
|
- task: CmdLine@2
|
||||||
condition: always()
|
condition: always()
|
||||||
displayName: Cleanup
|
displayName: Cleanup
|
||||||
@ -265,7 +307,7 @@ stages:
|
|||||||
repository: 'beryju/passbook'
|
repository: 'beryju/passbook'
|
||||||
command: 'buildAndPush'
|
command: 'buildAndPush'
|
||||||
Dockerfile: 'Dockerfile'
|
Dockerfile: 'Dockerfile'
|
||||||
tags: 'gh-$(Build.SourceBranchName)'
|
tags: "gh-${{ variables.branchName }}"
|
||||||
- job: build_static
|
- job: build_static
|
||||||
pool:
|
pool:
|
||||||
vmImage: 'ubuntu-latest'
|
vmImage: 'ubuntu-latest'
|
||||||
@ -282,14 +324,14 @@ stages:
|
|||||||
repository: 'beryju/passbook-static'
|
repository: 'beryju/passbook-static'
|
||||||
command: 'build'
|
command: 'build'
|
||||||
Dockerfile: 'static.Dockerfile'
|
Dockerfile: 'static.Dockerfile'
|
||||||
tags: 'gh-$(Build.SourceBranchName)'
|
tags: "gh-${{ variables.branchName }}"
|
||||||
arguments: "--network=beryjupassbook_default"
|
arguments: "--network=beryjupassbook_default"
|
||||||
- task: Docker@2
|
- task: Docker@2
|
||||||
inputs:
|
inputs:
|
||||||
containerRegistry: 'dockerhub'
|
containerRegistry: 'dockerhub'
|
||||||
repository: 'beryju/passbook-static'
|
repository: 'beryju/passbook-static'
|
||||||
command: 'push'
|
command: 'push'
|
||||||
tags: 'gh-$(Build.SourceBranchName)'
|
tags: "gh-${{ variables.branchName }}"
|
||||||
- stage: Deploy
|
- stage: Deploy
|
||||||
jobs:
|
jobs:
|
||||||
- job: deploy_dev
|
- job: deploy_dev
|
||||||
|
|||||||
@ -23,13 +23,12 @@ services:
|
|||||||
labels:
|
labels:
|
||||||
- traefik.enable=false
|
- traefik.enable=false
|
||||||
server:
|
server:
|
||||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.4-stable}
|
image: beryju/passbook:${PASSBOOK_TAG:-0.10.8-stable}
|
||||||
command: server
|
command: server
|
||||||
environment:
|
environment:
|
||||||
PASSBOOK_REDIS__HOST: redis
|
PASSBOOK_REDIS__HOST: redis
|
||||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
PASSBOOK_LOG_LEVEL: debug
|
|
||||||
ports:
|
ports:
|
||||||
- 8000
|
- 8000
|
||||||
networks:
|
networks:
|
||||||
@ -41,7 +40,7 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
worker:
|
worker:
|
||||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.4-stable}
|
image: beryju/passbook:${PASSBOOK_TAG:-0.10.8-stable}
|
||||||
command: worker
|
command: worker
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
@ -51,11 +50,10 @@ services:
|
|||||||
PASSBOOK_REDIS__HOST: redis
|
PASSBOOK_REDIS__HOST: redis
|
||||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
PASSBOOK_LOG_LEVEL: debug
|
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
static:
|
static:
|
||||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.4-stable}
|
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.8-stable}
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@ -84,15 +84,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"identifiers": {
|
|
||||||
"pk": "9922212c-47a2-475a-9905-abeb5e621652"
|
|
||||||
},
|
|
||||||
"model": "passbook_policies_expression.expressionpolicy",
|
|
||||||
"attrs": {
|
|
||||||
"name": "policy-enrollment-password-equals",
|
|
||||||
"expression": "# Verifies that the passwords are equal\r\nreturn request.context['password'] == request.context['password_repeat']"
|
|
||||||
}
|
|
||||||
},{
|
|
||||||
"identifiers": {
|
"identifiers": {
|
||||||
"pk": "096e6282-6b30-4695-bd03-3b143eab5580",
|
"pk": "096e6282-6b30-4695-bd03-3b143eab5580",
|
||||||
"name": "default-enrollment-email-verficiation"
|
"name": "default-enrollment-email-verficiation"
|
||||||
@ -135,9 +126,6 @@
|
|||||||
"cb954fd4-65a5-4ad9-b1ee-180ee9559cf4",
|
"cb954fd4-65a5-4ad9-b1ee-180ee9559cf4",
|
||||||
"7db91ee8-4290-4e08-8d39-63f132402515",
|
"7db91ee8-4290-4e08-8d39-63f132402515",
|
||||||
"d30b5eb4-7787-4072-b1ba-65b46e928920"
|
"d30b5eb4-7787-4072-b1ba-65b46e928920"
|
||||||
],
|
|
||||||
"validation_policies": [
|
|
||||||
"9922212c-47a2-475a-9905-abeb5e621652"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -55,16 +55,6 @@
|
|||||||
"order": 1
|
"order": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"identifiers": {
|
|
||||||
"pk": "cd042fc6-cc92-4b98-b7e6-f4729df798d8"
|
|
||||||
},
|
|
||||||
"model": "passbook_policies_expression.expressionpolicy",
|
|
||||||
"attrs": {
|
|
||||||
"name": "default-password-change-password-equal",
|
|
||||||
"expression": "# Check that both passwords are equal.\nreturn request.context['password'] == request.context['password_repeat']"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"identifiers": {
|
"identifiers": {
|
||||||
"pk": "e54045a7-6ecb-4ad9-ad37-28e72d8e565e",
|
"pk": "e54045a7-6ecb-4ad9-ad37-28e72d8e565e",
|
||||||
@ -118,9 +108,6 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
"7db91ee8-4290-4e08-8d39-63f132402515",
|
"7db91ee8-4290-4e08-8d39-63f132402515",
|
||||||
"d30b5eb4-7787-4072-b1ba-65b46e928920"
|
"d30b5eb4-7787-4072-b1ba-65b46e928920"
|
||||||
],
|
|
||||||
"validation_policies": [
|
|
||||||
"cd042fc6-cc92-4b98-b7e6-f4729df798d8"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,7 +13,7 @@ Download the latest `docker-compose.yml` from [here](https://raw.githubuserconte
|
|||||||
|
|
||||||
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env`
|
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING__ENABLED=true >> .env`
|
||||||
|
|
||||||
To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.4-stable >> .env`
|
To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.8-stable >> .env`
|
||||||
|
|
||||||
If this is a fresh passbook install run the following commands to generate a password:
|
If this is a fresh passbook install run the following commands to generate a password:
|
||||||
|
|
||||||
@ -39,4 +39,6 @@ Now you can pull the Docker images needed by running `docker-compose pull`. Afte
|
|||||||
|
|
||||||
passbook will then be reachable via HTTP on port 80, and HTTPS on port 443. You can optionally configure the packaged traefik to use Let's Encrypt certificates for TLS Encryption.
|
passbook will then be reachable via HTTP on port 80, and HTTPS on port 443. You can optionally configure the packaged traefik to use Let's Encrypt certificates for TLS Encryption.
|
||||||
|
|
||||||
|
If you plan to access passbook via a reverse proxy which does SSL Termination, make sure you use the HTTPS port, so passbook is aware of the SSL connection.
|
||||||
|
|
||||||
The initial setup process also creates a default admin user, the username and password for which is `pbadmin`. It is highly recommended to change this password as soon as you log in.
|
The initial setup process also creates a default admin user, the username and password for which is `pbadmin`. It is highly recommended to change this password as soon as you log in.
|
||||||
|
|||||||
@ -11,7 +11,7 @@ This installation automatically applies database migrations on startup. After th
|
|||||||
image:
|
image:
|
||||||
name: beryju/passbook
|
name: beryju/passbook
|
||||||
name_static: beryju/passbook-static
|
name_static: beryju/passbook-static
|
||||||
tag: 0.10.4-stable
|
tag: 0.10.8-stable
|
||||||
|
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
|
|||||||
@ -24,10 +24,49 @@ You can of course use a custom signing certificate, and adjust durations.
|
|||||||
|
|
||||||
Create a role with the permissions you desire, and note the ARN.
|
Create a role with the permissions you desire, and note the ARN.
|
||||||
|
|
||||||
AWS requires two custom PropertyMappings; `Role` and `RoleSessionName`. Create them as following:
|
After you've created the Property Mappings below, add them to the Provider.
|
||||||
|
|
||||||

|
Create an application, assign policies, and assign this provider.
|
||||||
|
|
||||||

|
Export the metadata from passbook, and create an Identity Provider [here](https://console.aws.amazon.com/iam/home#/providers).
|
||||||
|
|
||||||
Afterwards export the metadata from passbook, and create an Identity Provider [here](https://console.aws.amazon.com/iam/home#/providers).
|
#### Role Mapping
|
||||||
|
|
||||||
|
The Role mapping specifies the AWS ARN(s) of the identity provider, and the role the user should assume ([see](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html#saml_role-attribute)).
|
||||||
|
|
||||||
|
This Mapping needs to have the SAML Name field set to "https://aws.amazon.com/SAML/Attributes/Role"
|
||||||
|
|
||||||
|
As expression, you can return a static ARN like so
|
||||||
|
|
||||||
|
```python
|
||||||
|
return "arn:aws:iam::123412341234:role/saml_role,arn:aws:iam::123412341234:saml-provider/passbook"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, if you want to assign AWS Roles based on Group membership, you can add a custom attribute to the Groups, for example "aws_role", and use this snippet below. Groups are sorted by name and later groups overwrite earlier groups' attributes.
|
||||||
|
|
||||||
|
```python
|
||||||
|
role_name = user.group_attributes().get("aws_role", "")
|
||||||
|
return f"arn:aws:iam::123412341234:role/{role_name},arn:aws:iam::123412341234:saml-provider/passbook"
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to allow a user to choose from multiple roles, use this snippet
|
||||||
|
|
||||||
|
```python
|
||||||
|
return [
|
||||||
|
"arn:aws:iam::123412341234:role/role_a,arn:aws:iam::123412341234:saml-provider/passbook",
|
||||||
|
"arn:aws:iam::123412341234:role/role_b,arn:aws:iam::123412341234:saml-provider/passbook",
|
||||||
|
"arn:aws:iam::123412341234:role/role_c,arn:aws:iam::123412341234:saml-provider/passbook",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### RoleSessionName Mapping
|
||||||
|
|
||||||
|
The RoleSessionMapping specifies what identifier will be shown at the top of the Management Console ([see](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_saml_assertions.html#saml_role-session-attribute)).
|
||||||
|
|
||||||
|
This mapping needs to have the SAML Name field set to "https://aws.amazon.com/SAML/Attributes/RoleSessionName".
|
||||||
|
|
||||||
|
To use the user's username, use this snippet
|
||||||
|
|
||||||
|
```python
|
||||||
|
return user.username
|
||||||
|
```
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 66 KiB |
@ -19,6 +19,7 @@ Create an application in passbook and note the slug, as this will be used later.
|
|||||||
- ACS URL: `https://gitlab.company/users/auth/saml/callback`
|
- ACS URL: `https://gitlab.company/users/auth/saml/callback`
|
||||||
- Audience: `https://gitlab.company`
|
- Audience: `https://gitlab.company`
|
||||||
- Issuer: `https://gitlab.company`
|
- Issuer: `https://gitlab.company`
|
||||||
|
- Binding: `Post`
|
||||||
|
|
||||||
You can of course use a custom signing certificate, and adjust durations. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
|
You can of course use a custom signing certificate, and adjust durations. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
|
||||||
|
|
||||||
@ -41,7 +42,7 @@ gitlab_rails['omniauth_providers'] = [
|
|||||||
args: {
|
args: {
|
||||||
assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback',
|
assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback',
|
||||||
idp_cert_fingerprint: '4E:1E:CD:67:4A:67:5A:E9:6A:D0:3C:E6:DD:7A:F2:44:2E:76:00:6A',
|
idp_cert_fingerprint: '4E:1E:CD:67:4A:67:5A:E9:6A:D0:3C:E6:DD:7A:F2:44:2E:76:00:6A',
|
||||||
idp_sso_target_url: 'https://passbook.company/application/saml/<passbook application slug>/login/',
|
idp_sso_target_url: 'https://passbook.company/application/saml/<passbook application slug>/sso/binding/post/',
|
||||||
issuer: 'https://gitlab.company',
|
issuer: 'https://gitlab.company',
|
||||||
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||||
attribute_statements: {
|
attribute_statements: {
|
||||||
|
|||||||
50
docs/integrations/services/tautulli/index.md
Normal file
50
docs/integrations/services/tautulli/index.md
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
# Tautulli Integration
|
||||||
|
|
||||||
|
## What is Tautulli
|
||||||
|
|
||||||
|
From https://tautulli.com/
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Tautulli is a 3rd party application that you can run alongside your Plex Media Server to monitor activity and track various statistics. Most importantly, these statistics include what has been watched, who watched it, when and where they watched it, and how it was watched. The only thing missing is "why they watched it", but who am I to question your 42 plays of Frozen. All statistics are presented in a nice and clean interface with many tables and graphs, which makes it easy to brag about your server to everyone else.
|
||||||
|
|
||||||
|
## Preparation
|
||||||
|
|
||||||
|
The following placeholders will be used:
|
||||||
|
|
||||||
|
- `tautulli.company` is the FQDN of the Tautulli install.
|
||||||
|
- `passbook.company` is the FQDN of the passbook install.
|
||||||
|
|
||||||
|
## passbook Setup
|
||||||
|
|
||||||
|
Because Tautulli requires valid HTTP Basic credentials, you must save your HTTP Basic Credentials in passbook. The recommended way to do this, is to create a Group, called for example "Tautulli Users". For this group, add the following attributes:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tautulli_user: username
|
||||||
|
tautulli_password: password
|
||||||
|
```
|
||||||
|
|
||||||
|
Add all Tautulli users to the Group. You should also create a Group Membership Policy to limit access to the application.
|
||||||
|
|
||||||
|
Create an application in passbook. Create a Proxy provider with the following parameters:
|
||||||
|
|
||||||
|
- Internal host
|
||||||
|
|
||||||
|
If Tautulli is running in docker, and you're deploying the passbook proxy on the same host, set the value to `http://tautulli:3579`, where tautulli is the name of your container.
|
||||||
|
|
||||||
|
If Tautulli is running on a different server than where you are deploying the passbook proxy, set the value to `http://tautulli.company:3579`.
|
||||||
|
|
||||||
|
- External host
|
||||||
|
|
||||||
|
Set this to the external URL you will be accessing Tautulli from.
|
||||||
|
|
||||||
|
Enable the `Set HTTP-Basic Authentication` option. Set and `HTTP-Basic Username` and `HTTP-Basic Password` to `tautulli_user` and `tautulli_password` respectively. These values can be chosen freely, `tautulli_` is just used a prefix for clarity.
|
||||||
|
|
||||||
|
## Tautulli Setup
|
||||||
|
|
||||||
|
In Tautulli, navigate to Settings and enable the "Show Advanced" option. Navigate to "Web Interface" on the sidebar, and ensure the Option `Use Basic Authentication` is checked.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Save the settings, and restart Tautulli if prompted.
|
||||||
|
|
||||||
|
Afterwards, you need to deploy an Outpost in front of Tautulli, just like descried [here](../sonarr/index.md)
|
||||||
BIN
docs/integrations/services/tautulli/tautulli.png
Normal file
BIN
docs/integrations/services/tautulli/tautulli.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
@ -27,4 +27,11 @@ return False
|
|||||||
- `request.context`: A dictionary with dynamic data. This depends on the origin of the execution.
|
- `request.context`: A dictionary with dynamic data. This depends on the origin of the execution.
|
||||||
- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external provider.
|
- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external provider.
|
||||||
- `pb_client_ip`: Client's IP Address or '255.255.255.255' if no IP Address could be extracted. Can be [compared](../expressions/index.md#comparing-ip-addresses)
|
- `pb_client_ip`: Client's IP Address or '255.255.255.255' if no IP Address could be extracted. Can be [compared](../expressions/index.md#comparing-ip-addresses)
|
||||||
- `pb_flow_plan`: Current Plan if Policy is called from the Flow Planner.
|
|
||||||
|
Additionally, when the policy is executed from a flow, every variable from the flow's current context is accessible under the `context` object.
|
||||||
|
|
||||||
|
This includes the following:
|
||||||
|
|
||||||
|
- `prompt_data`: Data which has been saved from a prompt stage or an external source.
|
||||||
|
- `application`: The application the user is in the process of authorizing.
|
||||||
|
- `pending_user`: The currently pending user
|
||||||
|
|||||||
@ -2,7 +2,7 @@ version: '3.7'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
chrome:
|
chrome:
|
||||||
image: selenium/standalone-chrome:3.141.59-20200525
|
image: selenium/standalone-chrome:3.141
|
||||||
volumes:
|
volumes:
|
||||||
- /dev/shm:/dev/shm
|
- /dev/shm:/dev/shm
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
|||||||
@ -2,7 +2,7 @@ version: '3.7'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
chrome:
|
chrome:
|
||||||
image: selenium/standalone-chrome-debug:3.141.59-20200719
|
image: selenium/standalone-chrome-debug:3.141
|
||||||
volumes:
|
volumes:
|
||||||
- /dev/shm:/dev/shm
|
- /dev/shm:/dev/shm
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
|||||||
@ -10,7 +10,6 @@ from selenium.webdriver.support import expected_conditions as ec
|
|||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase
|
||||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from passbook.policies.expression.models import ExpressionPolicy
|
|
||||||
from passbook.stages.email.models import EmailStage, EmailTemplates
|
from passbook.stages.email.models import EmailStage, EmailTemplates
|
||||||
from passbook.stages.identification.models import IdentificationStage
|
from passbook.stages.identification.models import IdentificationStage
|
||||||
from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||||
@ -59,16 +58,9 @@ class TestFlowsEnroll(SeleniumTestCase):
|
|||||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||||
)
|
)
|
||||||
|
|
||||||
# Password checking policy
|
|
||||||
password_policy = ExpressionPolicy.objects.create(
|
|
||||||
name="policy-enrollment-password-equals",
|
|
||||||
expression="return request.context['password'] == request.context['password_repeat']",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Stages
|
# Stages
|
||||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||||
first_stage.validation_policies.set([password_policy])
|
|
||||||
first_stage.save()
|
first_stage.save()
|
||||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||||
second_stage.fields.set([name_field, email])
|
second_stage.fields.set([name_field, email])
|
||||||
@ -112,8 +104,7 @@ class TestFlowsEnroll(SeleniumTestCase):
|
|||||||
|
|
||||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
self.driver.find_element(By.ID, "user-settings").text, "foo",
|
||||||
"foo",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||||
@ -152,16 +143,9 @@ class TestFlowsEnroll(SeleniumTestCase):
|
|||||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||||
)
|
)
|
||||||
|
|
||||||
# Password checking policy
|
|
||||||
password_policy = ExpressionPolicy.objects.create(
|
|
||||||
name="policy-enrollment-password-equals",
|
|
||||||
expression="return request.context['password'] == request.context['password_repeat']",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Stages
|
# Stages
|
||||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||||
first_stage.validation_policies.set([password_policy])
|
|
||||||
first_stage.save()
|
first_stage.save()
|
||||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||||
second_stage.fields.set([name_field, email])
|
second_stage.fields.set([name_field, email])
|
||||||
@ -220,16 +204,11 @@ class TestFlowsEnroll(SeleniumTestCase):
|
|||||||
self.driver.switch_to.window(self.driver.window_handles[0])
|
self.driver.switch_to.window(self.driver.window_handles[0])
|
||||||
|
|
||||||
# We're now logged in
|
# We're now logged in
|
||||||
self.wait.until(
|
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||||
ec.presence_of_element_located(
|
self.driver.find_element(By.ID, "user-settings").click()
|
||||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
self.driver.find_element(By.ID, "user-settings").text, "foo",
|
||||||
"foo",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||||
|
|||||||
@ -21,6 +21,5 @@ class TestFlowsLogin(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
self.driver.find_element(By.ID, "user-settings").text, USER().username,
|
||||||
USER().username,
|
|
||||||
)
|
)
|
||||||
|
|||||||
138
e2e/test_flows_otp.py
Normal file
138
e2e/test_flows_otp.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
"""test flow with otp stages"""
|
||||||
|
from base64 import b32decode
|
||||||
|
from sys import platform
|
||||||
|
from time import sleep
|
||||||
|
from unittest.case import skipUnless
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
|
from django_otp.oath import TOTP
|
||||||
|
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||||
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
|
||||||
|
from e2e.utils import USER, SeleniumTestCase
|
||||||
|
from passbook.flows.models import Flow, FlowStageBinding
|
||||||
|
from passbook.stages.otp_validate.models import OTPValidateStage
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
class TestFlowsOTP(SeleniumTestCase):
|
||||||
|
"""test flow with otp stages"""
|
||||||
|
|
||||||
|
def test_otp_validate(self):
|
||||||
|
"""test flow with otp stages"""
|
||||||
|
sleep(1)
|
||||||
|
# Setup TOTP Device
|
||||||
|
user = USER()
|
||||||
|
device = TOTPDevice.objects.create(user=user, confirmed=True, digits=6)
|
||||||
|
|
||||||
|
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||||
|
# Move the user_login stage to order 3
|
||||||
|
FlowStageBinding.objects.filter(target=flow, order=2).update(order=3)
|
||||||
|
FlowStageBinding.objects.create(
|
||||||
|
target=flow, order=2, stage=OTPValidateStage.objects.create()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
|
# Get expected token
|
||||||
|
totp = TOTP(device.bin_key, device.step, device.t0, device.digits, device.drift)
|
||||||
|
self.driver.find_element(By.ID, "id_code").send_keys(totp.token())
|
||||||
|
self.driver.find_element(By.ID, "id_code").send_keys(Keys.ENTER)
|
||||||
|
self.wait_for_url(self.url("passbook_core:overview"))
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.ID, "user-settings").text, USER().username,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_otp_totp_setup(self):
|
||||||
|
"""test TOTP Setup stage"""
|
||||||
|
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||||
|
|
||||||
|
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.ID, "user-settings").text, USER().username,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
|
||||||
|
self.driver.get(self.url("passbook_core:user-settings"))
|
||||||
|
|
||||||
|
self.driver.find_element(By.LINK_TEXT, "Time-based OTP").click()
|
||||||
|
|
||||||
|
# Remember the current URL as we should end up back here
|
||||||
|
destination_url = self.driver.current_url
|
||||||
|
|
||||||
|
self.driver.find_element(
|
||||||
|
By.CSS_SELECTOR, ".pf-c-card__body a.pf-c-button"
|
||||||
|
).click()
|
||||||
|
|
||||||
|
self.wait.until(ec.presence_of_element_located((By.ID, "qr")))
|
||||||
|
otp_uri = self.driver.find_element(By.ID, "qr").get_attribute("data-otpuri")
|
||||||
|
|
||||||
|
# Parse the OTP URI, extract the secret and get the next token
|
||||||
|
otp_args = urlparse(otp_uri)
|
||||||
|
self.assertEqual(otp_args.scheme, "otpauth")
|
||||||
|
otp_qs = parse_qs(otp_args.query)
|
||||||
|
secret_key = b32decode(otp_qs["secret"][0])
|
||||||
|
|
||||||
|
totp = TOTP(secret_key)
|
||||||
|
|
||||||
|
self.driver.find_element(By.ID, "id_code").send_keys(totp.token())
|
||||||
|
self.driver.find_element(By.ID, "id_code").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
|
self.wait_for_url(destination_url)
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
self.assertTrue(TOTPDevice.objects.filter(user=USER(), confirmed=True).exists())
|
||||||
|
|
||||||
|
def test_otp_static_setup(self):
|
||||||
|
"""test Static OTP Setup stage"""
|
||||||
|
flow: Flow = Flow.objects.get(slug="default-authentication-flow")
|
||||||
|
|
||||||
|
self.driver.get(f"{self.live_server_url}/flows/{flow.slug}/")
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.ID, "user-settings").text, USER().username,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
|
||||||
|
self.driver.find_element(By.ID, "user-settings").click()
|
||||||
|
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||||
|
|
||||||
|
self.driver.find_element(By.LINK_TEXT, "Static OTP").click()
|
||||||
|
|
||||||
|
# Remember the current URL as we should end up back here
|
||||||
|
destination_url = self.driver.current_url
|
||||||
|
|
||||||
|
self.driver.find_element(
|
||||||
|
By.CSS_SELECTOR, ".pf-c-card__body a.pf-c-button"
|
||||||
|
).click()
|
||||||
|
token = self.driver.find_element(
|
||||||
|
By.CSS_SELECTOR, ".pb-otp-tokens li:nth-child(1)"
|
||||||
|
).text
|
||||||
|
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||||
|
|
||||||
|
self.wait_for_url(destination_url)
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
self.assertTrue(
|
||||||
|
StaticDevice.objects.filter(user=USER(), confirmed=True).exists()
|
||||||
|
)
|
||||||
|
device = StaticDevice.objects.filter(user=USER(), confirmed=True).first()
|
||||||
|
self.assertTrue(StaticToken.objects.filter(token=token, device=device).exists())
|
||||||
@ -20,12 +20,12 @@ class TestFlowsStageSetup(SeleniumTestCase):
|
|||||||
"""test password change flow"""
|
"""test password change flow"""
|
||||||
# Ensure that password stage has change_flow set
|
# Ensure that password stage has change_flow set
|
||||||
flow = Flow.objects.get(
|
flow = Flow.objects.get(
|
||||||
slug="default-password-change", designation=FlowDesignation.STAGE_SETUP,
|
slug="default-password-change",
|
||||||
|
designation=FlowDesignation.STAGE_CONFIGURATION,
|
||||||
)
|
)
|
||||||
|
|
||||||
stages = PasswordStage.objects.filter(name="default-authentication-password")
|
stage = PasswordStage.objects.get(name="default-authentication-password")
|
||||||
stage = stages.first()
|
stage.configure_flow = flow
|
||||||
stage.change_flow = flow
|
|
||||||
stage.save()
|
stage.save()
|
||||||
|
|
||||||
new_password = generate_client_secret()
|
new_password = generate_client_secret()
|
||||||
@ -38,7 +38,7 @@ class TestFlowsStageSetup(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
|
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
self.driver.find_element(By.ID, "user-settings").click()
|
||||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||||
self.driver.find_element(By.LINK_TEXT, "Change password").click()
|
self.driver.find_element(By.LINK_TEXT, "Change password").click()
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(new_password)
|
self.driver.find_element(By.ID, "id_password").send_keys(new_password)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"""test OAuth Provider flow"""
|
"""test OAuth Provider flow"""
|
||||||
from sys import platform
|
from sys import platform
|
||||||
|
from time import sleep
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
from unittest.case import skipUnless
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
@ -88,7 +89,7 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
self.driver.get("http://localhost:3000/profile")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||||
USER().username,
|
USER().username,
|
||||||
@ -139,28 +140,19 @@ class TestProviderOAuth2Github(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
self.assertIn(
|
sleep(1)
|
||||||
app.name,
|
|
||||||
self.driver.find_element(
|
self.assertEqual(
|
||||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
|
app.name, self.driver.find_element(By.ID, "application-name").text,
|
||||||
).text,
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"GitHub Compatibility: Access you Email addresses",
|
"GitHub Compatibility: Access you Email addresses",
|
||||||
self.driver.find_element(
|
self.driver.find_element(By.ID, "scope-user:email").text,
|
||||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]"
|
|
||||||
).text,
|
|
||||||
)
|
)
|
||||||
self.driver.find_element(
|
self.driver.find_element(By.CSS_SELECTOR, ("[type=submit]"),).click()
|
||||||
By.CSS_SELECTOR,
|
|
||||||
(
|
|
||||||
"form[action='/flows/b/default-provider-authorization-explicit-consent/'] "
|
|
||||||
"[type=submit]"
|
|
||||||
),
|
|
||||||
).click()
|
|
||||||
|
|
||||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
self.driver.get("http://localhost:3000/profile")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||||
USER().username,
|
USER().username,
|
||||||
|
|||||||
364
e2e/test_provider_oauth2_grafana.py
Normal file
364
e2e/test_provider_oauth2_grafana.py
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
"""test OAuth2 OpenID Provider flow"""
|
||||||
|
from sys import platform
|
||||||
|
from time import sleep
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
|
from docker.types import Healthcheck
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from selenium.webdriver.support import expected_conditions as ec
|
||||||
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from e2e.utils import USER, SeleniumTestCase
|
||||||
|
from passbook.core.models import Application
|
||||||
|
from passbook.crypto.models import CertificateKeyPair
|
||||||
|
from passbook.flows.models import Flow
|
||||||
|
from passbook.policies.expression.models import ExpressionPolicy
|
||||||
|
from passbook.policies.models import PolicyBinding
|
||||||
|
from passbook.providers.oauth2.constants import (
|
||||||
|
SCOPE_OPENID,
|
||||||
|
SCOPE_OPENID_EMAIL,
|
||||||
|
SCOPE_OPENID_PROFILE,
|
||||||
|
)
|
||||||
|
from passbook.providers.oauth2.generators import (
|
||||||
|
generate_client_id,
|
||||||
|
generate_client_secret,
|
||||||
|
)
|
||||||
|
from passbook.providers.oauth2.models import (
|
||||||
|
ClientTypes,
|
||||||
|
OAuth2Provider,
|
||||||
|
ResponseTypes,
|
||||||
|
ScopeMapping,
|
||||||
|
)
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
APPLICATION_SLUG = "grafana"
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
class TestProviderOAuth2OAuth(SeleniumTestCase):
|
||||||
|
"""test OAuth with OAuth Provider flow"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client_id = generate_client_id()
|
||||||
|
self.client_secret = generate_client_secret()
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||||
|
return {
|
||||||
|
"image": "grafana/grafana:7.1.0",
|
||||||
|
"detach": True,
|
||||||
|
"network_mode": "host",
|
||||||
|
"auto_remove": True,
|
||||||
|
"healthcheck": Healthcheck(
|
||||||
|
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
||||||
|
interval=5 * 100 * 1000000,
|
||||||
|
start_period=1 * 100 * 1000000,
|
||||||
|
),
|
||||||
|
"environment": {
|
||||||
|
"GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
|
||||||
|
"GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id,
|
||||||
|
"GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret,
|
||||||
|
"GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile",
|
||||||
|
"GF_AUTH_GENERIC_OAUTH_AUTH_URL": (
|
||||||
|
self.url("passbook_providers_oauth2:authorize")
|
||||||
|
),
|
||||||
|
"GF_AUTH_GENERIC_OAUTH_TOKEN_URL": (
|
||||||
|
self.url("passbook_providers_oauth2:token")
|
||||||
|
),
|
||||||
|
"GF_AUTH_GENERIC_OAUTH_API_URL": (
|
||||||
|
self.url("passbook_providers_oauth2:userinfo")
|
||||||
|
),
|
||||||
|
"GF_AUTH_SIGNOUT_REDIRECT_URL": (
|
||||||
|
self.url(
|
||||||
|
"passbook_providers_oauth2:end-session",
|
||||||
|
application_slug=APPLICATION_SLUG,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"GF_LOG_LEVEL": "debug",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_redirect_uri_error(self):
|
||||||
|
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
||||||
|
sleep(1)
|
||||||
|
# Bootstrap all needed objects
|
||||||
|
authorization_flow = Flow.objects.get(
|
||||||
|
slug="default-provider-authorization-implicit-consent"
|
||||||
|
)
|
||||||
|
provider = OAuth2Provider.objects.create(
|
||||||
|
name="grafana",
|
||||||
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
|
client_id=self.client_id,
|
||||||
|
client_secret=self.client_secret,
|
||||||
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
|
redirect_uris="http://localhost:3000/",
|
||||||
|
authorization_flow=authorization_flow,
|
||||||
|
response_type=ResponseTypes.CODE,
|
||||||
|
)
|
||||||
|
provider.property_mappings.set(
|
||||||
|
ScopeMapping.objects.filter(
|
||||||
|
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
provider.save()
|
||||||
|
Application.objects.create(
|
||||||
|
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.driver.get("http://localhost:3000")
|
||||||
|
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
sleep(2)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.CLASS_NAME, "pf-c-title").text,
|
||||||
|
"Redirect URI Error",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_authorization_consent_implied(self):
|
||||||
|
"""test OpenID Provider flow (default authorization flow with implied consent)"""
|
||||||
|
sleep(1)
|
||||||
|
# Bootstrap all needed objects
|
||||||
|
authorization_flow = Flow.objects.get(
|
||||||
|
slug="default-provider-authorization-implicit-consent"
|
||||||
|
)
|
||||||
|
provider = OAuth2Provider.objects.create(
|
||||||
|
name="grafana",
|
||||||
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
|
client_id=self.client_id,
|
||||||
|
client_secret=self.client_secret,
|
||||||
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
|
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||||
|
authorization_flow=authorization_flow,
|
||||||
|
response_type=ResponseTypes.CODE,
|
||||||
|
)
|
||||||
|
provider.property_mappings.set(
|
||||||
|
ScopeMapping.objects.filter(
|
||||||
|
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
provider.save()
|
||||||
|
Application.objects.create(
|
||||||
|
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.driver.get("http://localhost:3000")
|
||||||
|
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||||
|
self.driver.get("http://localhost:3000/profile")
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||||
|
USER().name,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||||
|
"value"
|
||||||
|
),
|
||||||
|
USER().name,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(
|
||||||
|
By.CSS_SELECTOR, "input[name=email]"
|
||||||
|
).get_attribute("value"),
|
||||||
|
USER().email,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(
|
||||||
|
By.CSS_SELECTOR, "input[name=login]"
|
||||||
|
).get_attribute("value"),
|
||||||
|
USER().email,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_authorization_logout(self):
|
||||||
|
"""test OpenID Provider flow with logout"""
|
||||||
|
sleep(1)
|
||||||
|
# Bootstrap all needed objects
|
||||||
|
authorization_flow = Flow.objects.get(
|
||||||
|
slug="default-provider-authorization-implicit-consent"
|
||||||
|
)
|
||||||
|
provider = OAuth2Provider.objects.create(
|
||||||
|
name="grafana",
|
||||||
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
|
client_id=self.client_id,
|
||||||
|
client_secret=self.client_secret,
|
||||||
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
|
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||||
|
authorization_flow=authorization_flow,
|
||||||
|
response_type=ResponseTypes.CODE,
|
||||||
|
)
|
||||||
|
provider.property_mappings.set(
|
||||||
|
ScopeMapping.objects.filter(
|
||||||
|
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
provider.save()
|
||||||
|
Application.objects.create(
|
||||||
|
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.driver.get("http://localhost:3000")
|
||||||
|
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||||
|
self.driver.get("http://localhost:3000/profile")
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||||
|
USER().name,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||||
|
"value"
|
||||||
|
),
|
||||||
|
USER().name,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(
|
||||||
|
By.CSS_SELECTOR, "input[name=email]"
|
||||||
|
).get_attribute("value"),
|
||||||
|
USER().email,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(
|
||||||
|
By.CSS_SELECTOR, "input[name=login]"
|
||||||
|
).get_attribute("value"),
|
||||||
|
USER().email,
|
||||||
|
)
|
||||||
|
self.driver.get("http://localhost:3000/logout")
|
||||||
|
self.wait_for_url(
|
||||||
|
self.url(
|
||||||
|
"passbook_providers_oauth2:end-session",
|
||||||
|
application_slug=APPLICATION_SLUG,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.find_element(By.ID, "logout").click()
|
||||||
|
|
||||||
|
def test_authorization_consent_explicit(self):
|
||||||
|
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
|
||||||
|
sleep(1)
|
||||||
|
# Bootstrap all needed objects
|
||||||
|
authorization_flow = Flow.objects.get(
|
||||||
|
slug="default-provider-authorization-explicit-consent"
|
||||||
|
)
|
||||||
|
provider = OAuth2Provider.objects.create(
|
||||||
|
name="grafana",
|
||||||
|
authorization_flow=authorization_flow,
|
||||||
|
response_type=ResponseTypes.CODE,
|
||||||
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
|
client_id=self.client_id,
|
||||||
|
client_secret=self.client_secret,
|
||||||
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
|
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||||
|
)
|
||||||
|
provider.property_mappings.set(
|
||||||
|
ScopeMapping.objects.filter(
|
||||||
|
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
provider.save()
|
||||||
|
app = Application.objects.create(
|
||||||
|
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.driver.get("http://localhost:3000")
|
||||||
|
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
app.name, self.driver.find_element(By.ID, "application-name").text,
|
||||||
|
)
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
|
||||||
|
)
|
||||||
|
sleep(1)
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
||||||
|
|
||||||
|
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||||
|
self.driver.get("http://localhost:3000/profile")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
||||||
|
USER().name,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||||
|
"value"
|
||||||
|
),
|
||||||
|
USER().name,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(
|
||||||
|
By.CSS_SELECTOR, "input[name=email]"
|
||||||
|
).get_attribute("value"),
|
||||||
|
USER().email,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(
|
||||||
|
By.CSS_SELECTOR, "input[name=login]"
|
||||||
|
).get_attribute("value"),
|
||||||
|
USER().email,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_authorization_denied(self):
|
||||||
|
"""test OpenID Provider flow (default authorization with access deny)"""
|
||||||
|
sleep(1)
|
||||||
|
# Bootstrap all needed objects
|
||||||
|
authorization_flow = Flow.objects.get(
|
||||||
|
slug="default-provider-authorization-explicit-consent"
|
||||||
|
)
|
||||||
|
provider = OAuth2Provider.objects.create(
|
||||||
|
name="grafana",
|
||||||
|
authorization_flow=authorization_flow,
|
||||||
|
response_type=ResponseTypes.CODE,
|
||||||
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
|
client_id=self.client_id,
|
||||||
|
client_secret=self.client_secret,
|
||||||
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
|
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||||
|
)
|
||||||
|
provider.property_mappings.set(
|
||||||
|
ScopeMapping.objects.filter(
|
||||||
|
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
provider.save()
|
||||||
|
app = Application.objects.create(
|
||||||
|
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
||||||
|
)
|
||||||
|
|
||||||
|
negative_policy = ExpressionPolicy.objects.create(
|
||||||
|
name="negative-static", expression="return False"
|
||||||
|
)
|
||||||
|
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
||||||
|
self.driver.get("http://localhost:3000")
|
||||||
|
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1"))
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, "header > h1").text,
|
||||||
|
"Permission denied",
|
||||||
|
)
|
||||||
@ -1,9 +1,11 @@
|
|||||||
"""test OAuth2 OpenID Provider flow"""
|
"""test OAuth2 OpenID Provider flow"""
|
||||||
|
from json import loads
|
||||||
from sys import platform
|
from sys import platform
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
from unittest.case import skipUnless
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
|
from docker import DockerClient, from_env
|
||||||
|
from docker.models.containers import Container
|
||||||
from docker.types import Healthcheck
|
from docker.types import Healthcheck
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
@ -33,7 +35,6 @@ from passbook.providers.oauth2.models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
APPLICATION_SLUG = "grafana"
|
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
@ -43,42 +44,37 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.client_id = generate_client_id()
|
self.client_id = generate_client_id()
|
||||||
self.client_secret = generate_client_secret()
|
self.client_secret = generate_client_secret()
|
||||||
|
self.application_slug = "test"
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
def setup_client(self) -> Container:
|
||||||
return {
|
"""Setup client saml-sp container which we test SAML against"""
|
||||||
"image": "grafana/grafana:7.1.0",
|
sleep(1)
|
||||||
"detach": True,
|
client: DockerClient = from_env()
|
||||||
"network_mode": "host",
|
client.images.pull("beryju/oidc-test-client")
|
||||||
"auto_remove": True,
|
container = client.containers.run(
|
||||||
"healthcheck": Healthcheck(
|
image="beryju/oidc-test-client",
|
||||||
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
detach=True,
|
||||||
|
network_mode="host",
|
||||||
|
auto_remove=True,
|
||||||
|
healthcheck=Healthcheck(
|
||||||
|
test=["CMD", "wget", "--spider", "http://localhost:9009/health"],
|
||||||
interval=5 * 100 * 1000000,
|
interval=5 * 100 * 1000000,
|
||||||
start_period=1 * 100 * 1000000,
|
start_period=1 * 100 * 1000000,
|
||||||
),
|
),
|
||||||
"environment": {
|
environment={
|
||||||
"GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
|
"OIDC_CLIENT_ID": self.client_id,
|
||||||
"GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id,
|
"OIDC_CLIENT_SECRET": self.client_secret,
|
||||||
"GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret,
|
"OIDC_PROVIDER": f"{self.live_server_url}/application/o/{self.application_slug}/",
|
||||||
"GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile",
|
|
||||||
"GF_AUTH_GENERIC_OAUTH_AUTH_URL": (
|
|
||||||
self.url("passbook_providers_oauth2:authorize")
|
|
||||||
),
|
|
||||||
"GF_AUTH_GENERIC_OAUTH_TOKEN_URL": (
|
|
||||||
self.url("passbook_providers_oauth2:token")
|
|
||||||
),
|
|
||||||
"GF_AUTH_GENERIC_OAUTH_API_URL": (
|
|
||||||
self.url("passbook_providers_oauth2:userinfo")
|
|
||||||
),
|
|
||||||
"GF_AUTH_SIGNOUT_REDIRECT_URL": (
|
|
||||||
self.url(
|
|
||||||
"passbook_providers_oauth2:end-session",
|
|
||||||
application_slug=APPLICATION_SLUG,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"GF_LOG_LEVEL": "debug",
|
|
||||||
},
|
},
|
||||||
}
|
)
|
||||||
|
while True:
|
||||||
|
container.reload()
|
||||||
|
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||||
|
if status == "healthy":
|
||||||
|
return container
|
||||||
|
LOGGER.info("Container failed healthcheck")
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
def test_redirect_uri_error(self):
|
def test_redirect_uri_error(self):
|
||||||
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
||||||
@ -88,12 +84,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
slug="default-provider-authorization-implicit-consent"
|
slug="default-provider-authorization-implicit-consent"
|
||||||
)
|
)
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name="grafana",
|
name=self.application_slug,
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
rsa_key=CertificateKeyPair.objects.first(),
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
redirect_uris="http://localhost:3000/",
|
redirect_uris="http://localhost:9009/",
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
response_type=ResponseTypes.CODE,
|
response_type=ResponseTypes.CODE,
|
||||||
)
|
)
|
||||||
@ -104,11 +100,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
)
|
)
|
||||||
provider.save()
|
provider.save()
|
||||||
Application.objects.create(
|
Application.objects.create(
|
||||||
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
name=self.application_slug, slug=self.application_slug, provider=provider,
|
||||||
)
|
)
|
||||||
|
self.container = self.setup_client()
|
||||||
|
|
||||||
|
self.driver.get("http://localhost:9009")
|
||||||
|
|
||||||
self.driver.get("http://localhost:3000")
|
|
||||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
|
||||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
@ -128,12 +125,12 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
slug="default-provider-authorization-implicit-consent"
|
slug="default-provider-authorization-implicit-consent"
|
||||||
)
|
)
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name="grafana",
|
name=self.application_slug,
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
rsa_key=CertificateKeyPair.objects.first(),
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
redirect_uris="http://localhost:9009/auth/callback",
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
response_type=ResponseTypes.CODE,
|
response_type=ResponseTypes.CODE,
|
||||||
)
|
)
|
||||||
@ -144,105 +141,29 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
)
|
)
|
||||||
provider.save()
|
provider.save()
|
||||||
Application.objects.create(
|
Application.objects.create(
|
||||||
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
name=self.application_slug, slug=self.application_slug, provider=provider,
|
||||||
)
|
)
|
||||||
|
self.container = self.setup_client()
|
||||||
|
|
||||||
|
self.driver.get("http://localhost:9009")
|
||||||
|
|
||||||
self.driver.get("http://localhost:3000")
|
|
||||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
|
||||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
|
||||||
USER().name,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
|
||||||
"value"
|
|
||||||
),
|
|
||||||
USER().name,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(
|
|
||||||
By.CSS_SELECTOR, "input[name=email]"
|
|
||||||
).get_attribute("value"),
|
|
||||||
USER().email,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(
|
|
||||||
By.CSS_SELECTOR, "input[name=login]"
|
|
||||||
).get_attribute("value"),
|
|
||||||
USER().email,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_authorization_logout(self):
|
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
|
||||||
"""test OpenID Provider flow with logout"""
|
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||||
sleep(1)
|
|
||||||
# Bootstrap all needed objects
|
|
||||||
authorization_flow = Flow.objects.get(
|
|
||||||
slug="default-provider-authorization-implicit-consent"
|
|
||||||
)
|
|
||||||
provider = OAuth2Provider.objects.create(
|
|
||||||
name="grafana",
|
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
|
||||||
client_id=self.client_id,
|
|
||||||
client_secret=self.client_secret,
|
|
||||||
rsa_key=CertificateKeyPair.objects.first(),
|
|
||||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
|
||||||
authorization_flow=authorization_flow,
|
|
||||||
response_type=ResponseTypes.CODE,
|
|
||||||
)
|
|
||||||
provider.property_mappings.set(
|
|
||||||
ScopeMapping.objects.filter(
|
|
||||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
provider.save()
|
|
||||||
Application.objects.create(
|
|
||||||
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.driver.get("http://localhost:3000")
|
self.assertEqual(body["IDTokenClaims"]["nickname"], USER().username)
|
||||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
self.assertEqual(body["UserInfo"]["nickname"], USER().username)
|
||||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
|
||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
self.assertEqual(body["IDTokenClaims"]["name"], USER().name)
|
||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.assertEqual(body["UserInfo"]["name"], USER().name)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
self.assertEqual(body["UserInfo"]["email"], USER().email)
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
|
||||||
USER().name,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
|
||||||
"value"
|
|
||||||
),
|
|
||||||
USER().name,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(
|
|
||||||
By.CSS_SELECTOR, "input[name=email]"
|
|
||||||
).get_attribute("value"),
|
|
||||||
USER().email,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(
|
|
||||||
By.CSS_SELECTOR, "input[name=login]"
|
|
||||||
).get_attribute("value"),
|
|
||||||
USER().email,
|
|
||||||
)
|
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "[href='/logout']").click()
|
|
||||||
self.wait_for_url(
|
|
||||||
self.url(
|
|
||||||
"passbook_providers_oauth2:end-session",
|
|
||||||
application_slug=APPLICATION_SLUG,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.driver.find_element(By.ID, "logout").click()
|
|
||||||
|
|
||||||
def test_authorization_consent_explicit(self):
|
def test_authorization_consent_explicit(self):
|
||||||
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
|
"""test OpenID Provider flow (default authorization flow with explicit consent)"""
|
||||||
@ -252,14 +173,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
slug="default-provider-authorization-explicit-consent"
|
slug="default-provider-authorization-explicit-consent"
|
||||||
)
|
)
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name="grafana",
|
name=self.application_slug,
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
response_type=ResponseTypes.CODE,
|
response_type=ResponseTypes.CODE,
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
rsa_key=CertificateKeyPair.objects.first(),
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
redirect_uris="http://localhost:9009/auth/callback",
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
@ -268,22 +189,20 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
)
|
)
|
||||||
provider.save()
|
provider.save()
|
||||||
app = Application.objects.create(
|
app = Application.objects.create(
|
||||||
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
name=self.application_slug, slug=self.application_slug, provider=provider,
|
||||||
)
|
)
|
||||||
|
self.container = self.setup_client()
|
||||||
|
|
||||||
|
self.driver.get("http://localhost:9009")
|
||||||
|
|
||||||
self.driver.get("http://localhost:3000")
|
|
||||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
|
||||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
self.assertIn(
|
self.assertEqual(
|
||||||
app.name,
|
app.name, self.driver.find_element(By.ID, "application-name").text,
|
||||||
self.driver.find_element(
|
|
||||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
|
|
||||||
).text,
|
|
||||||
)
|
)
|
||||||
self.wait.until(
|
self.wait.until(
|
||||||
ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
|
ec.presence_of_element_located((By.CSS_SELECTOR, "[type=submit]"))
|
||||||
@ -291,34 +210,17 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
sleep(1)
|
sleep(1)
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
||||||
|
|
||||||
self.wait.until(
|
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
|
||||||
ec.presence_of_element_located(
|
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||||
(By.XPATH, "//a[contains(@href, '/profile')]")
|
|
||||||
)
|
self.assertEqual(body["IDTokenClaims"]["nickname"], USER().username)
|
||||||
)
|
self.assertEqual(body["UserInfo"]["nickname"], USER().username)
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
|
||||||
self.assertEqual(
|
self.assertEqual(body["IDTokenClaims"]["name"], USER().name)
|
||||||
self.driver.find_element(By.CLASS_NAME, "page-header__title").text,
|
self.assertEqual(body["UserInfo"]["name"], USER().name)
|
||||||
USER().name,
|
|
||||||
)
|
self.assertEqual(body["IDTokenClaims"]["email"], USER().email)
|
||||||
self.assertEqual(
|
self.assertEqual(body["UserInfo"]["email"], USER().email)
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
|
||||||
"value"
|
|
||||||
),
|
|
||||||
USER().name,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(
|
|
||||||
By.CSS_SELECTOR, "input[name=email]"
|
|
||||||
).get_attribute("value"),
|
|
||||||
USER().email,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(
|
|
||||||
By.CSS_SELECTOR, "input[name=login]"
|
|
||||||
).get_attribute("value"),
|
|
||||||
USER().email,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_authorization_denied(self):
|
def test_authorization_denied(self):
|
||||||
"""test OpenID Provider flow (default authorization with access deny)"""
|
"""test OpenID Provider flow (default authorization with access deny)"""
|
||||||
@ -328,14 +230,14 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
slug="default-provider-authorization-explicit-consent"
|
slug="default-provider-authorization-explicit-consent"
|
||||||
)
|
)
|
||||||
provider = OAuth2Provider.objects.create(
|
provider = OAuth2Provider.objects.create(
|
||||||
name="grafana",
|
name=self.application_slug,
|
||||||
authorization_flow=authorization_flow,
|
authorization_flow=authorization_flow,
|
||||||
response_type=ResponseTypes.CODE,
|
response_type=ResponseTypes.CODE,
|
||||||
client_type=ClientTypes.CONFIDENTIAL,
|
client_type=ClientTypes.CONFIDENTIAL,
|
||||||
client_id=self.client_id,
|
client_id=self.client_id,
|
||||||
client_secret=self.client_secret,
|
client_secret=self.client_secret,
|
||||||
rsa_key=CertificateKeyPair.objects.first(),
|
rsa_key=CertificateKeyPair.objects.first(),
|
||||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
redirect_uris="http://localhost:9009/auth/callback",
|
||||||
)
|
)
|
||||||
provider.property_mappings.set(
|
provider.property_mappings.set(
|
||||||
ScopeMapping.objects.filter(
|
ScopeMapping.objects.filter(
|
||||||
@ -344,15 +246,17 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
|
|||||||
)
|
)
|
||||||
provider.save()
|
provider.save()
|
||||||
app = Application.objects.create(
|
app = Application.objects.create(
|
||||||
name="Grafana", slug=APPLICATION_SLUG, provider=provider,
|
name=self.application_slug, slug=self.application_slug, provider=provider,
|
||||||
)
|
)
|
||||||
|
|
||||||
negative_policy = ExpressionPolicy.objects.create(
|
negative_policy = ExpressionPolicy.objects.create(
|
||||||
name="negative-static", expression="return False"
|
name="negative-static", expression="return False"
|
||||||
)
|
)
|
||||||
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
|
||||||
self.driver.get("http://localhost:3000")
|
|
||||||
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
|
self.container = self.setup_client()
|
||||||
|
self.driver.get("http://localhost:9009")
|
||||||
|
|
||||||
self.driver.find_element(By.ID, "id_uid_field").click()
|
self.driver.find_element(By.ID, "id_uid_field").click()
|
||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
|
|||||||
@ -4,12 +4,14 @@ from time import sleep
|
|||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
from unittest.case import skipUnless
|
from unittest.case import skipUnless
|
||||||
|
|
||||||
|
from channels.testing import ChannelsLiveServerTestCase
|
||||||
from docker.client import DockerClient, from_env
|
from docker.client import DockerClient, from_env
|
||||||
from docker.models.containers import Container
|
from docker.models.containers import Container
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
|
||||||
from e2e.utils import USER, SeleniumTestCase
|
from e2e.utils import USER, SeleniumTestCase
|
||||||
|
from passbook import __version__
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.flows.models import Flow
|
from passbook.flows.models import Flow
|
||||||
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
|
from passbook.outposts.models import Outpost, OutpostDeploymentType, OutpostType
|
||||||
@ -37,6 +39,7 @@ class TestProviderProxy(SeleniumTestCase):
|
|||||||
def start_proxy(self, outpost: Outpost) -> Container:
|
def start_proxy(self, outpost: Outpost) -> Container:
|
||||||
"""Start proxy container based on outpost created"""
|
"""Start proxy container based on outpost created"""
|
||||||
client: DockerClient = from_env()
|
client: DockerClient = from_env()
|
||||||
|
client.images.pull("beryju/oidc-test-client")
|
||||||
container = client.containers.run(
|
container = client.containers.run(
|
||||||
image="beryju/passbook-proxy:latest",
|
image="beryju/passbook-proxy:latest",
|
||||||
detach=True,
|
detach=True,
|
||||||
@ -94,3 +97,67 @@ class TestProviderProxy(SeleniumTestCase):
|
|||||||
|
|
||||||
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
full_body_text = self.driver.find_element(By.CSS_SELECTOR, "pre").text
|
||||||
self.assertIn("X-Forwarded-Preferred-Username: pbadmin", full_body_text)
|
self.assertIn("X-Forwarded-Preferred-Username: pbadmin", full_body_text)
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
class TestProviderProxyConnect(ChannelsLiveServerTestCase):
|
||||||
|
"""Test Proxy connectivity over websockets"""
|
||||||
|
|
||||||
|
proxy_container: Container
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
self.proxy_container.kill()
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
def start_proxy(self, outpost: Outpost) -> Container:
|
||||||
|
"""Start proxy container based on outpost created"""
|
||||||
|
client: DockerClient = from_env()
|
||||||
|
client.images.pull("beryju/oidc-test-client")
|
||||||
|
container = client.containers.run(
|
||||||
|
image="beryju/passbook-proxy:latest",
|
||||||
|
detach=True,
|
||||||
|
network_mode="host",
|
||||||
|
auto_remove=True,
|
||||||
|
environment={
|
||||||
|
"PASSBOOK_HOST": self.live_server_url,
|
||||||
|
"PASSBOOK_TOKEN": outpost.token.token_uuid.hex,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return container
|
||||||
|
|
||||||
|
def test_proxy_connectivity(self):
|
||||||
|
"""Test proxy connectivity over websocket"""
|
||||||
|
SeleniumTestCase().apply_default_data()
|
||||||
|
proxy: ProxyProvider = ProxyProvider.objects.create(
|
||||||
|
name="proxy_provider",
|
||||||
|
authorization_flow=Flow.objects.get(
|
||||||
|
slug="default-provider-authorization-implicit-consent"
|
||||||
|
),
|
||||||
|
internal_host="http://localhost:80",
|
||||||
|
external_host="http://localhost:4180",
|
||||||
|
)
|
||||||
|
# Ensure OAuth2 Params are set
|
||||||
|
proxy.set_oauth_defaults()
|
||||||
|
proxy.save()
|
||||||
|
# we need to create an application to actually access the proxy
|
||||||
|
Application.objects.create(name="proxy", slug="proxy", provider=proxy)
|
||||||
|
outpost: Outpost = Outpost.objects.create(
|
||||||
|
name="proxy_outpost",
|
||||||
|
type=OutpostType.PROXY,
|
||||||
|
deployment_type=OutpostDeploymentType.CUSTOM,
|
||||||
|
)
|
||||||
|
outpost.providers.add(proxy)
|
||||||
|
outpost.save()
|
||||||
|
|
||||||
|
self.proxy_container = self.start_proxy(outpost)
|
||||||
|
|
||||||
|
# Wait until outpost healthcheck succeeds
|
||||||
|
healthcheck_retries = 0
|
||||||
|
while healthcheck_retries < 50:
|
||||||
|
if outpost.deployment_health:
|
||||||
|
break
|
||||||
|
healthcheck_retries += 1
|
||||||
|
sleep(0.5)
|
||||||
|
|
||||||
|
self.assertIsNotNone(outpost.deployment_health)
|
||||||
|
self.assertEqual(outpost.deployment_version.get("version"), __version__)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
"""test SAML Provider flow"""
|
"""test SAML Provider flow"""
|
||||||
|
from json import loads
|
||||||
from sys import platform
|
from sys import platform
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from unittest.case import skipUnless
|
from unittest.case import skipUnless
|
||||||
@ -35,6 +36,7 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
def setup_client(self, provider: SAMLProvider) -> Container:
|
def setup_client(self, provider: SAMLProvider) -> Container:
|
||||||
"""Setup client saml-sp container which we test SAML against"""
|
"""Setup client saml-sp container which we test SAML against"""
|
||||||
client: DockerClient = from_env()
|
client: DockerClient = from_env()
|
||||||
|
client.images.pull("beryju/oidc-test-client")
|
||||||
container = client.containers.run(
|
container = client.containers.run(
|
||||||
image="beryju/saml-test-sp",
|
image="beryju/saml-test-sp",
|
||||||
detach=True,
|
detach=True,
|
||||||
@ -92,10 +94,14 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
self.wait_for_url("http://localhost:9009/")
|
self.wait_for_url("http://localhost:9009/")
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.XPATH, "/html/body/pre").text,
|
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||||
f"Hello, {USER().name}!",
|
|
||||||
)
|
self.assertEqual(body["attr"]["cn"], [USER().name])
|
||||||
|
self.assertEqual(body["attr"]["displayName"], [USER().username])
|
||||||
|
self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email])
|
||||||
|
self.assertEqual(body["attr"]["mail"], [USER().email])
|
||||||
|
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
||||||
|
|
||||||
def test_sp_initiated_explicit(self):
|
def test_sp_initiated_explicit(self):
|
||||||
"""test SAML Provider flow SP-initiated flow (explicit consent)"""
|
"""test SAML Provider flow SP-initiated flow (explicit consent)"""
|
||||||
@ -124,19 +130,20 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
self.assertIn(
|
self.assertEqual(
|
||||||
app.name,
|
app.name, self.driver.find_element(By.ID, "application-name").text,
|
||||||
self.driver.find_element(
|
|
||||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/p[1]"
|
|
||||||
).text,
|
|
||||||
)
|
)
|
||||||
sleep(1)
|
sleep(1)
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
||||||
self.wait_for_url("http://localhost:9009/")
|
self.wait_for_url("http://localhost:9009/")
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.XPATH, "/html/body/pre").text,
|
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||||
f"Hello, {USER().name}!",
|
|
||||||
)
|
self.assertEqual(body["attr"]["cn"], [USER().name])
|
||||||
|
self.assertEqual(body["attr"]["displayName"], [USER().username])
|
||||||
|
self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email])
|
||||||
|
self.assertEqual(body["attr"]["mail"], [USER().email])
|
||||||
|
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
||||||
|
|
||||||
def test_idp_initiated_implicit(self):
|
def test_idp_initiated_implicit(self):
|
||||||
"""test SAML Provider flow IdP-initiated flow (implicit consent)"""
|
"""test SAML Provider flow IdP-initiated flow (implicit consent)"""
|
||||||
@ -170,11 +177,16 @@ class TestProviderSAML(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
|
||||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||||
|
sleep(1)
|
||||||
self.wait_for_url("http://localhost:9009/")
|
self.wait_for_url("http://localhost:9009/")
|
||||||
self.assertEqual(
|
|
||||||
self.driver.find_element(By.XPATH, "/html/body/pre").text,
|
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
|
||||||
f"Hello, {USER().name}!",
|
|
||||||
)
|
self.assertEqual(body["attr"]["cn"], [USER().name])
|
||||||
|
self.assertEqual(body["attr"]["displayName"], [USER().username])
|
||||||
|
self.assertEqual(body["attr"]["eduPersonPrincipalName"], [USER().email])
|
||||||
|
self.assertEqual(body["attr"]["mail"], [USER().email])
|
||||||
|
self.assertEqual(body["attr"]["uid"], [str(USER().pk)])
|
||||||
|
|
||||||
def test_sp_initiated_denied(self):
|
def test_sp_initiated_denied(self):
|
||||||
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
|
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
|
||||||
|
|||||||
@ -16,16 +16,18 @@ from yaml import safe_dump
|
|||||||
|
|
||||||
from e2e.utils import SeleniumTestCase
|
from e2e.utils import SeleniumTestCase
|
||||||
from passbook.flows.models import Flow
|
from passbook.flows.models import Flow
|
||||||
from passbook.providers.oauth2.generators import generate_client_secret
|
from passbook.providers.oauth2.generators import (
|
||||||
|
generate_client_id,
|
||||||
|
generate_client_secret,
|
||||||
|
)
|
||||||
from passbook.sources.oauth.models import OAuthSource
|
from passbook.sources.oauth.models import OAuthSource
|
||||||
|
|
||||||
TOKEN_URL = "http://127.0.0.1:5556/dex/token"
|
|
||||||
CONFIG_PATH = "/tmp/dex.yml"
|
CONFIG_PATH = "/tmp/dex.yml"
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
class TestSourceOAuth(SeleniumTestCase):
|
class TestSourceOAuth2(SeleniumTestCase):
|
||||||
"""test OAuth Source flow"""
|
"""test OAuth Source flow"""
|
||||||
|
|
||||||
container: Container
|
container: Container
|
||||||
@ -91,14 +93,14 @@ class TestSourceOAuth(SeleniumTestCase):
|
|||||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||||
|
|
||||||
OAuthSource.objects.create(
|
OAuthSource.objects.create( # nosec
|
||||||
name="dex",
|
name="dex",
|
||||||
slug="dex",
|
slug="dex",
|
||||||
authentication_flow=authentication_flow,
|
authentication_flow=authentication_flow,
|
||||||
enrollment_flow=enrollment_flow,
|
enrollment_flow=enrollment_flow,
|
||||||
provider_type="openid-connect",
|
provider_type="openid-connect",
|
||||||
authorization_url="http://127.0.0.1:5556/dex/auth",
|
authorization_url="http://127.0.0.1:5556/dex/auth",
|
||||||
access_token_url=TOKEN_URL,
|
access_token_url="http://127.0.0.1:5556/dex/token",
|
||||||
profile_url="http://127.0.0.1:5556/dex/userinfo",
|
profile_url="http://127.0.0.1:5556/dex/userinfo",
|
||||||
consumer_key="example-app",
|
consumer_key="example-app",
|
||||||
consumer_secret=self.client_secret,
|
consumer_secret=self.client_secret,
|
||||||
@ -138,13 +140,11 @@ class TestSourceOAuth(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
|
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
# Wait until we've loaded the user info page
|
# Wait until we've loaded the user info page
|
||||||
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
|
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||||
self.driver.find_element(By.LINK_TEXT, "foo").click()
|
self.driver.get(self.url("passbook_core:user-settings"))
|
||||||
|
|
||||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
self.driver.find_element(By.ID, "user-settings").text, "foo",
|
||||||
"foo",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||||
@ -197,7 +197,7 @@ class TestSourceOAuth(SeleniumTestCase):
|
|||||||
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
|
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
|
||||||
self.test_oauth_enroll()
|
self.test_oauth_enroll()
|
||||||
# We're logged in at the end of this, log out and re-login
|
# We're logged in at the end of this, log out and re-login
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "[aria-label=logout]").click()
|
self.driver.find_element(By.ID, "logout").click()
|
||||||
|
|
||||||
self.wait.until(
|
self.wait.until(
|
||||||
ec.presence_of_element_located(
|
ec.presence_of_element_located(
|
||||||
@ -221,14 +221,11 @@ class TestSourceOAuth(SeleniumTestCase):
|
|||||||
)
|
)
|
||||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||||
|
|
||||||
# Wait until we've loaded the user info page
|
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||||
self.wait.until(ec.presence_of_element_located((By.LINK_TEXT, "foo")))
|
self.driver.get(self.url("passbook_core:user-settings"))
|
||||||
self.driver.find_element(By.LINK_TEXT, "foo").click()
|
|
||||||
|
|
||||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").text,
|
self.driver.find_element(By.ID, "user-settings").text, "foo",
|
||||||
"foo",
|
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
self.driver.find_element(By.ID, "id_username").get_attribute("value"), "foo"
|
||||||
@ -240,3 +237,97 @@ class TestSourceOAuth(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||||
"admin@example.com",
|
"admin@example.com",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||||
|
class TestSourceOAuth1(SeleniumTestCase):
|
||||||
|
"""Test OAuth1 Source"""
|
||||||
|
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.client_id = generate_client_id()
|
||||||
|
self.client_secret = generate_client_secret()
|
||||||
|
self.source_slug = "oauth1-test"
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||||
|
return {
|
||||||
|
"image": "beryju/oauth1-test-server",
|
||||||
|
"detach": True,
|
||||||
|
"network_mode": "host",
|
||||||
|
"auto_remove": True,
|
||||||
|
"environment": {
|
||||||
|
"OAUTH1_CLIENT_ID": self.client_id,
|
||||||
|
"OAUTH1_CLIENT_SECRET": self.client_secret,
|
||||||
|
"OAUTH1_REDIRECT_URI": (
|
||||||
|
self.url(
|
||||||
|
"passbook_sources_oauth:oauth-client-callback",
|
||||||
|
source_slug=self.source_slug,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_objects(self):
|
||||||
|
"""Create required objects"""
|
||||||
|
# Bootstrap all needed objects
|
||||||
|
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||||
|
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||||
|
|
||||||
|
OAuthSource.objects.create( # nosec
|
||||||
|
name="oauth1",
|
||||||
|
slug=self.source_slug,
|
||||||
|
authentication_flow=authentication_flow,
|
||||||
|
enrollment_flow=enrollment_flow,
|
||||||
|
provider_type="twitter",
|
||||||
|
request_token_url="http://localhost:5000/oauth/request_token",
|
||||||
|
access_token_url="http://localhost:5000/oauth/access_token",
|
||||||
|
authorization_url="http://localhost:5000/oauth/authorize",
|
||||||
|
profile_url="http://localhost:5000/api/me",
|
||||||
|
consumer_key=self.client_id,
|
||||||
|
consumer_secret=self.client_secret,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_oauth_enroll(self):
|
||||||
|
"""test OAuth Source With With OIDC"""
|
||||||
|
self.create_objects()
|
||||||
|
self.driver.get(self.live_server_url)
|
||||||
|
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located(
|
||||||
|
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.driver.find_element(
|
||||||
|
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||||
|
).click()
|
||||||
|
|
||||||
|
# Now we should be at the IDP, wait for the login field
|
||||||
|
self.wait.until(ec.presence_of_element_located((By.NAME, "username")))
|
||||||
|
self.driver.find_element(By.NAME, "username").send_keys("example-user")
|
||||||
|
self.driver.find_element(By.NAME, "username").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
|
# Wait until we're logged in
|
||||||
|
self.wait.until(
|
||||||
|
ec.presence_of_element_located((By.CSS_SELECTOR, "[name='confirm']"))
|
||||||
|
)
|
||||||
|
self.driver.find_element(By.CSS_SELECTOR, "[name='confirm']").click()
|
||||||
|
|
||||||
|
# Wait until we've loaded the user info page
|
||||||
|
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||||
|
self.driver.get(self.url("passbook_core:user-settings"))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.ID, "user-settings").text, "example-user",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.ID, "id_username").get_attribute("value"),
|
||||||
|
"example-user",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.ID, "id_name").get_attribute("value"),
|
||||||
|
"test name",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.driver.find_element(By.ID, "id_email").get_attribute("value"),
|
||||||
|
"foo@example.com",
|
||||||
|
)
|
||||||
|
|||||||
@ -130,12 +130,8 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
# Wait until we're logged in
|
# Wait until we're logged in
|
||||||
self.wait.until(
|
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||||
ec.presence_of_element_located(
|
self.driver.get(self.url("passbook_core:user-settings"))
|
||||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
|
||||||
|
|
||||||
# Wait until we've loaded the user info page
|
# Wait until we've loaded the user info page
|
||||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||||
@ -183,12 +179,8 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
# Wait until we're logged in
|
# Wait until we're logged in
|
||||||
self.wait.until(
|
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||||
ec.presence_of_element_located(
|
self.driver.get(self.url("passbook_core:user-settings"))
|
||||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
|
||||||
|
|
||||||
# Wait until we've loaded the user info page
|
# Wait until we've loaded the user info page
|
||||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||||
@ -234,12 +226,8 @@ class TestSourceSAML(SeleniumTestCase):
|
|||||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||||
|
|
||||||
# Wait until we're logged in
|
# Wait until we're logged in
|
||||||
self.wait.until(
|
self.wait.until(ec.presence_of_element_located((By.ID, "user-settings")))
|
||||||
ec.presence_of_element_located(
|
self.driver.get(self.url("passbook_core:user-settings"))
|
||||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
|
||||||
|
|
||||||
# Wait until we've loaded the user info page
|
# Wait until we've loaded the user info page
|
||||||
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
self.wait.until(ec.presence_of_element_located((By.ID, "id_username")))
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
"""passbook e2e testing utilities"""
|
"""passbook e2e testing utilities"""
|
||||||
from functools import lru_cache
|
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from importlib.util import module_from_spec, spec_from_file_location
|
from importlib.util import module_from_spec, spec_from_file_location
|
||||||
from inspect import getmembers, isfunction
|
from inspect import getmembers, isfunction
|
||||||
@ -23,7 +22,6 @@ from structlog import get_logger
|
|||||||
from passbook.core.models import User
|
from passbook.core.models import User
|
||||||
|
|
||||||
|
|
||||||
@lru_cache
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
def USER() -> User: # noqa
|
def USER() -> User: # noqa
|
||||||
"""Cached function that always returns pbadmin"""
|
"""Cached function that always returns pbadmin"""
|
||||||
@ -40,8 +38,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
|||||||
makedirs("selenium_screenshots/", exist_ok=True)
|
makedirs("selenium_screenshots/", exist_ok=True)
|
||||||
self.driver = self._get_driver()
|
self.driver = self._get_driver()
|
||||||
self.driver.maximize_window()
|
self.driver.maximize_window()
|
||||||
self.driver.implicitly_wait(10)
|
self.driver.implicitly_wait(30)
|
||||||
self.wait = WebDriverWait(self.driver, 30)
|
self.wait = WebDriverWait(self.driver, 60)
|
||||||
self.apply_default_data()
|
self.apply_default_data()
|
||||||
self.logger = get_logger()
|
self.logger = get_logger()
|
||||||
if specs := self.get_container_specs():
|
if specs := self.get_container_specs():
|
||||||
@ -49,6 +47,7 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
|||||||
|
|
||||||
def _start_container(self, specs: Dict[str, Any]) -> Container:
|
def _start_container(self, specs: Dict[str, Any]) -> Container:
|
||||||
client: DockerClient = from_env()
|
client: DockerClient = from_env()
|
||||||
|
client.images.pull(specs["image"])
|
||||||
container = client.containers.run(**specs)
|
container = client.containers.run(**specs)
|
||||||
if "healthcheck" not in specs:
|
if "healthcheck" not in specs:
|
||||||
return container
|
return container
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
appVersion: "0.10.4-stable"
|
appVersion: "0.10.8-stable"
|
||||||
description: A Helm chart for passbook.
|
description: A Helm chart for passbook.
|
||||||
name: passbook
|
name: passbook
|
||||||
version: "0.10.4-stable"
|
version: "0.10.8-stable"
|
||||||
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
|
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: postgresql
|
- name: postgresql
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
image:
|
image:
|
||||||
name: beryju/passbook
|
name: beryju/passbook
|
||||||
name_static: beryju/passbook-static
|
name_static: beryju/passbook-static
|
||||||
tag: 0.10.4-stable
|
tag: 0.10.8-stable
|
||||||
|
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap",
|
|||||||
if [[ "$1" == "server" ]]; then
|
if [[ "$1" == "server" ]]; then
|
||||||
gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
|
gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
|
||||||
elif [[ "$1" == "worker" ]]; then
|
elif [[ "$1" == "worker" ]]; then
|
||||||
celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule -Q passbook,passbook_scheduled
|
celery -A passbook.root.celery worker --autoscale 10,3 -E -B -s /tmp/celerybeat-schedule -Q passbook,passbook_scheduled
|
||||||
elif [[ "$1" == "migrate" ]]; then
|
elif [[ "$1" == "migrate" ]]; then
|
||||||
# Run system migrations first, run normal migrations after
|
# Run system migrations first, run normal migrations after
|
||||||
python -m lifecycle.migrate
|
python -m lifecycle.migrate
|
||||||
|
|||||||
@ -1,16 +1,28 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
"""This file needs to be run from the root of the project to correctly
|
"""This file needs to be run from the root of the project to correctly
|
||||||
import passbook. This is done by the dockerfile."""
|
import passbook. This is done by the dockerfile."""
|
||||||
|
from json import dumps
|
||||||
|
from sys import stderr
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from psycopg2 import OperationalError, connect
|
from psycopg2 import OperationalError, connect
|
||||||
from redis import Redis
|
from redis import Redis
|
||||||
from redis.exceptions import RedisError
|
from redis.exceptions import RedisError
|
||||||
from structlog import get_logger
|
|
||||||
|
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
def j_print(event: str, log_level: str = "info", **kwargs):
|
||||||
|
"""Print event in the same format as structlog with JSON.
|
||||||
|
Used before structlog is configured."""
|
||||||
|
data = {
|
||||||
|
"event": event,
|
||||||
|
"level": log_level,
|
||||||
|
"logger": __name__,
|
||||||
|
}
|
||||||
|
data.update(**kwargs)
|
||||||
|
print(dumps(data), file=stderr)
|
||||||
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@ -24,7 +36,7 @@ while True:
|
|||||||
break
|
break
|
||||||
except OperationalError:
|
except OperationalError:
|
||||||
sleep(1)
|
sleep(1)
|
||||||
LOGGER.warning("PostgreSQL Connection failed, retrying...")
|
j_print("PostgreSQL Connection failed, retrying...")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
@ -38,4 +50,4 @@ while True:
|
|||||||
break
|
break
|
||||||
except RedisError:
|
except RedisError:
|
||||||
sleep(1)
|
sleep(1)
|
||||||
LOGGER.warning("Redis Connection failed, retrying...")
|
j_print("Redis Connection failed, retrying...")
|
||||||
|
|||||||
@ -56,6 +56,7 @@ nav:
|
|||||||
- VMware vCenter: integrations/services/vmware-vcenter/index.md
|
- VMware vCenter: integrations/services/vmware-vcenter/index.md
|
||||||
- Ubuntu Landscape: integrations/services/ubuntu-landscape/index.md
|
- Ubuntu Landscape: integrations/services/ubuntu-landscape/index.md
|
||||||
- Sonarr: integrations/services/sonarr/index.md
|
- Sonarr: integrations/services/sonarr/index.md
|
||||||
|
- Tautulli: integrations/services/tautulli/index.md
|
||||||
- Upgrading:
|
- Upgrading:
|
||||||
- to 0.9: upgrading/to-0.9.md
|
- to 0.9: upgrading/to-0.9.md
|
||||||
- to 0.10: upgrading/to-0.10.md
|
- to 0.10: upgrading/to-0.10.md
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
"""passbook"""
|
"""passbook"""
|
||||||
__version__ = "0.10.4-stable"
|
__version__ = "0.10.8-stable"
|
||||||
|
|||||||
10
passbook/admin/settings.py
Normal file
10
passbook/admin/settings.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"""passbook admin settings"""
|
||||||
|
from celery.schedules import crontab
|
||||||
|
|
||||||
|
CELERY_BEAT_SCHEDULE = {
|
||||||
|
"admin_latest_version": {
|
||||||
|
"task": "passbook.admin.tasks.update_latest_version",
|
||||||
|
"schedule": crontab(minute=0), # Run every hour
|
||||||
|
"options": {"queue": "passbook_scheduled"},
|
||||||
|
}
|
||||||
|
}
|
||||||
23
passbook/admin/tasks.py
Normal file
23
passbook/admin/tasks.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"""passbook admin tasks"""
|
||||||
|
from django.core.cache import cache
|
||||||
|
from requests import RequestException, get
|
||||||
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.root.celery import CELERY_APP
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
VERSION_CACHE_KEY = "passbook_latest_version"
|
||||||
|
VERSION_CACHE_TIMEOUT = 2 * 60 * 60 # 2 hours
|
||||||
|
|
||||||
|
|
||||||
|
@CELERY_APP.task()
|
||||||
|
def update_latest_version():
|
||||||
|
"""Update latest version info"""
|
||||||
|
try:
|
||||||
|
data = get(
|
||||||
|
"https://api.github.com/repos/beryju/passbook/releases/latest"
|
||||||
|
).json()
|
||||||
|
tag_name = data.get("tag_name")
|
||||||
|
cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], VERSION_CACHE_TIMEOUT)
|
||||||
|
except (RequestException, IndexError):
|
||||||
|
cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
|
||||||
@ -58,7 +58,7 @@
|
|||||||
{% trans 'Property Mappings' %}
|
{% trans 'Property Mappings' %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="pf-c-nav__item pf-m-expandable pf-m-expanded">
|
<li class="pf-c-nav__item pf-m-expanded">
|
||||||
<a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Flows' %}
|
<a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Flows' %}
|
||||||
<span class="pf-c-nav__toggle">
|
<span class="pf-c-nav__toggle">
|
||||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||||
@ -99,7 +99,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</li>
|
</li>
|
||||||
<li class="pf-c-nav__item pf-m-expandable pf-m-expanded">
|
<li class="pf-c-nav__item pf-m-expanded">
|
||||||
<a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Policies' %}
|
<a href="#" class="pf-c-nav__link" aria-expanded="true">{% trans 'Policies' %}
|
||||||
<span class="pf-c-nav__toggle">
|
<span class="pf-c-nav__toggle">
|
||||||
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
<i class="fas fa-angle-right" aria-hidden="true"></i>
|
||||||
|
|||||||
@ -5,18 +5,6 @@
|
|||||||
{% load passbook_utils %}
|
{% load passbook_utils %}
|
||||||
{% load admin_reflection %}
|
{% load admin_reflection %}
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
{{ block.super }}
|
|
||||||
<style>
|
|
||||||
.pf-m-success {
|
|
||||||
color: var(--pf-global--success-color--100);
|
|
||||||
}
|
|
||||||
.pf-m-danger {
|
|
||||||
color: var(--pf-global--danger-color--100);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="pf-c-page__main-section pf-m-light">
|
<section class="pf-c-page__main-section pf-m-light">
|
||||||
<div class="pf-c-content">
|
<div class="pf-c-content">
|
||||||
|
|||||||
@ -9,24 +9,30 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
DeleteMessageView,
|
||||||
|
UserPaginateListMixin,
|
||||||
|
)
|
||||||
from passbook.core.forms.applications import ApplicationForm
|
from passbook.core.forms.applications import ApplicationForm
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
|
|
||||||
|
|
||||||
class ApplicationListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class ApplicationListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all applications"""
|
"""Show list of all applications"""
|
||||||
|
|
||||||
model = Application
|
model = Application
|
||||||
permission_required = "passbook_core.view_application"
|
permission_required = "passbook_core.view_application"
|
||||||
ordering = "name"
|
ordering = "name"
|
||||||
paginate_by = 40
|
|
||||||
template_name = "administration/application/list.html"
|
template_name = "administration/application/list.html"
|
||||||
|
|
||||||
|
|
||||||
class ApplicationCreateView(
|
class ApplicationCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
CreateAssignPermView,
|
CreateAssignPermView,
|
||||||
@ -43,7 +49,11 @@ class ApplicationCreateView(
|
|||||||
|
|
||||||
|
|
||||||
class ApplicationUpdateView(
|
class ApplicationUpdateView(
|
||||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
PermissionRequiredMixin,
|
||||||
|
UpdateView,
|
||||||
):
|
):
|
||||||
"""Update application"""
|
"""Update application"""
|
||||||
|
|
||||||
|
|||||||
@ -9,24 +9,30 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
DeleteMessageView,
|
||||||
|
UserPaginateListMixin,
|
||||||
|
)
|
||||||
from passbook.crypto.forms import CertificateKeyPairForm
|
from passbook.crypto.forms import CertificateKeyPairForm
|
||||||
from passbook.crypto.models import CertificateKeyPair
|
from passbook.crypto.models import CertificateKeyPair
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class CertificateKeyPairListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all keypairs"""
|
"""Show list of all keypairs"""
|
||||||
|
|
||||||
model = CertificateKeyPair
|
model = CertificateKeyPair
|
||||||
permission_required = "passbook_crypto.view_certificatekeypair"
|
permission_required = "passbook_crypto.view_certificatekeypair"
|
||||||
ordering = "name"
|
ordering = "name"
|
||||||
paginate_by = 40
|
|
||||||
template_name = "administration/certificatekeypair/list.html"
|
template_name = "administration/certificatekeypair/list.html"
|
||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairCreateView(
|
class CertificateKeyPairCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
CreateAssignPermView,
|
CreateAssignPermView,
|
||||||
@ -43,7 +49,11 @@ class CertificateKeyPairCreateView(
|
|||||||
|
|
||||||
|
|
||||||
class CertificateKeyPairUpdateView(
|
class CertificateKeyPairUpdateView(
|
||||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
PermissionRequiredMixin,
|
||||||
|
UpdateView,
|
||||||
):
|
):
|
||||||
"""Update certificatekeypair"""
|
"""Update certificatekeypair"""
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,11 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import DetailView, FormView, ListView, UpdateView
|
from django.views.generic import DetailView, FormView, ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
DeleteMessageView,
|
||||||
|
UserPaginateListMixin,
|
||||||
|
)
|
||||||
from passbook.flows.forms import FlowForm, FlowImportForm
|
from passbook.flows.forms import FlowForm, FlowImportForm
|
||||||
from passbook.flows.models import Flow
|
from passbook.flows.models import Flow
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
@ -23,18 +27,20 @@ from passbook.lib.utils.urls import redirect_with_qs
|
|||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
|
|
||||||
|
|
||||||
class FlowListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class FlowListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all flows"""
|
"""Show list of all flows"""
|
||||||
|
|
||||||
model = Flow
|
model = Flow
|
||||||
permission_required = "passbook_flows.view_flow"
|
permission_required = "passbook_flows.view_flow"
|
||||||
ordering = "name"
|
ordering = "name"
|
||||||
paginate_by = 40
|
|
||||||
template_name = "administration/flow/list.html"
|
template_name = "administration/flow/list.html"
|
||||||
|
|
||||||
|
|
||||||
class FlowCreateView(
|
class FlowCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
CreateAssignPermView,
|
CreateAssignPermView,
|
||||||
@ -51,7 +57,11 @@ class FlowCreateView(
|
|||||||
|
|
||||||
|
|
||||||
class FlowUpdateView(
|
class FlowUpdateView(
|
||||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
PermissionRequiredMixin,
|
||||||
|
UpdateView,
|
||||||
):
|
):
|
||||||
"""Update flow"""
|
"""Update flow"""
|
||||||
|
|
||||||
|
|||||||
@ -9,24 +9,30 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
DeleteMessageView,
|
||||||
|
UserPaginateListMixin,
|
||||||
|
)
|
||||||
from passbook.core.forms.groups import GroupForm
|
from passbook.core.forms.groups import GroupForm
|
||||||
from passbook.core.models import Group
|
from passbook.core.models import Group
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
|
|
||||||
|
|
||||||
class GroupListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class GroupListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all groups"""
|
"""Show list of all groups"""
|
||||||
|
|
||||||
model = Group
|
model = Group
|
||||||
permission_required = "passbook_core.view_group"
|
permission_required = "passbook_core.view_group"
|
||||||
ordering = "name"
|
ordering = "name"
|
||||||
paginate_by = 40
|
|
||||||
template_name = "administration/group/list.html"
|
template_name = "administration/group/list.html"
|
||||||
|
|
||||||
|
|
||||||
class GroupCreateView(
|
class GroupCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
CreateAssignPermView,
|
CreateAssignPermView,
|
||||||
@ -43,7 +49,11 @@ class GroupCreateView(
|
|||||||
|
|
||||||
|
|
||||||
class GroupUpdateView(
|
class GroupUpdateView(
|
||||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
PermissionRequiredMixin,
|
||||||
|
UpdateView,
|
||||||
):
|
):
|
||||||
"""Update group"""
|
"""Update group"""
|
||||||
|
|
||||||
|
|||||||
@ -12,24 +12,30 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
DeleteMessageView,
|
||||||
|
UserPaginateListMixin,
|
||||||
|
)
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
from passbook.outposts.forms import OutpostForm
|
from passbook.outposts.forms import OutpostForm
|
||||||
from passbook.outposts.models import Outpost, OutpostConfig
|
from passbook.outposts.models import Outpost, OutpostConfig
|
||||||
|
|
||||||
|
|
||||||
class OutpostListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class OutpostListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all outposts"""
|
"""Show list of all outposts"""
|
||||||
|
|
||||||
model = Outpost
|
model = Outpost
|
||||||
permission_required = "passbook_outposts.view_outpost"
|
permission_required = "passbook_outposts.view_outpost"
|
||||||
ordering = "name"
|
ordering = "name"
|
||||||
paginate_by = 40
|
|
||||||
template_name = "administration/outpost/list.html"
|
template_name = "administration/outpost/list.html"
|
||||||
|
|
||||||
|
|
||||||
class OutpostCreateView(
|
class OutpostCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
CreateAssignPermView,
|
CreateAssignPermView,
|
||||||
@ -53,7 +59,11 @@ class OutpostCreateView(
|
|||||||
|
|
||||||
|
|
||||||
class OutpostUpdateView(
|
class OutpostUpdateView(
|
||||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
PermissionRequiredMixin,
|
||||||
|
UpdateView,
|
||||||
):
|
):
|
||||||
"""Update outpost"""
|
"""Update outpost"""
|
||||||
|
|
||||||
|
|||||||
@ -5,32 +5,16 @@ from django.core.cache import cache
|
|||||||
from django.shortcuts import redirect, reverse
|
from django.shortcuts import redirect, reverse
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from packaging.version import LegacyVersion, Version, parse
|
from packaging.version import LegacyVersion, Version, parse
|
||||||
from requests import RequestException, get
|
|
||||||
|
|
||||||
from passbook import __version__
|
from passbook import __version__
|
||||||
from passbook.admin.mixins import AdminRequiredMixin
|
from passbook.admin.mixins import AdminRequiredMixin
|
||||||
|
from passbook.admin.tasks import VERSION_CACHE_KEY, update_latest_version
|
||||||
from passbook.core.models import Application, Provider, Source, User
|
from passbook.core.models import Application, Provider, Source, User
|
||||||
from passbook.flows.models import Flow, Stage
|
from passbook.flows.models import Flow, Stage
|
||||||
from passbook.policies.models import Policy
|
from passbook.policies.models import Policy
|
||||||
from passbook.root.celery import CELERY_APP
|
from passbook.root.celery import CELERY_APP
|
||||||
from passbook.stages.invitation.models import Invitation
|
from passbook.stages.invitation.models import Invitation
|
||||||
|
|
||||||
VERSION_CACHE_KEY = "passbook_latest_version"
|
|
||||||
|
|
||||||
|
|
||||||
def latest_version() -> Union[LegacyVersion, Version]:
|
|
||||||
"""Get latest release from GitHub, cached"""
|
|
||||||
if not cache.get(VERSION_CACHE_KEY):
|
|
||||||
try:
|
|
||||||
data = get(
|
|
||||||
"https://api.github.com/repos/beryju/passbook/releases/latest"
|
|
||||||
).json()
|
|
||||||
tag_name = data.get("tag_name")
|
|
||||||
cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], 30)
|
|
||||||
except (RequestException, IndexError):
|
|
||||||
cache.set(VERSION_CACHE_KEY, "0.0.0", 30)
|
|
||||||
return parse(cache.get(VERSION_CACHE_KEY))
|
|
||||||
|
|
||||||
|
|
||||||
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||||
"""Overview View"""
|
"""Overview View"""
|
||||||
@ -44,6 +28,14 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
|||||||
return redirect(reverse("passbook_flows:default-authentication"))
|
return redirect(reverse("passbook_flows:default-authentication"))
|
||||||
return self.get(*args, **kwargs)
|
return self.get(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_latest_version(self) -> Union[LegacyVersion, Version]:
|
||||||
|
"""Get latest version from cache"""
|
||||||
|
version_in_cache = cache.get(VERSION_CACHE_KEY)
|
||||||
|
if not version_in_cache:
|
||||||
|
update_latest_version.delay()
|
||||||
|
return parse(__version__)
|
||||||
|
return parse(version_in_cache)
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs["application_count"] = len(Application.objects.all())
|
kwargs["application_count"] = len(Application.objects.all())
|
||||||
kwargs["policy_count"] = len(Policy.objects.all())
|
kwargs["policy_count"] = len(Policy.objects.all())
|
||||||
@ -54,7 +46,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
|||||||
kwargs["flow_count"] = len(Flow.objects.all())
|
kwargs["flow_count"] = len(Flow.objects.all())
|
||||||
kwargs["invitation_count"] = len(Invitation.objects.all())
|
kwargs["invitation_count"] = len(Invitation.objects.all())
|
||||||
kwargs["version"] = parse(__version__)
|
kwargs["version"] = parse(__version__)
|
||||||
kwargs["version_latest"] = latest_version()
|
kwargs["version_latest"] = self.get_latest_version()
|
||||||
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
|
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
|
||||||
kwargs["providers_without_application"] = Provider.objects.filter(
|
kwargs["providers_without_application"] = Provider.objects.filter(
|
||||||
application=None
|
application=None
|
||||||
|
|||||||
@ -17,27 +17,31 @@ from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
|||||||
|
|
||||||
from passbook.admin.forms.policies import PolicyTestForm
|
from passbook.admin.forms.policies import PolicyTestForm
|
||||||
from passbook.admin.views.utils import (
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
DeleteMessageView,
|
DeleteMessageView,
|
||||||
InheritanceCreateView,
|
InheritanceCreateView,
|
||||||
InheritanceListView,
|
InheritanceListView,
|
||||||
InheritanceUpdateView,
|
InheritanceUpdateView,
|
||||||
|
UserPaginateListMixin,
|
||||||
)
|
)
|
||||||
from passbook.policies.models import Policy, PolicyBinding
|
from passbook.policies.models import Policy, PolicyBinding
|
||||||
from passbook.policies.process import PolicyProcess, PolicyRequest
|
from passbook.policies.process import PolicyProcess, PolicyRequest
|
||||||
|
|
||||||
|
|
||||||
class PolicyListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
class PolicyListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
|
||||||
|
):
|
||||||
"""Show list of all policies"""
|
"""Show list of all policies"""
|
||||||
|
|
||||||
model = Policy
|
model = Policy
|
||||||
permission_required = "passbook_policies.view_policy"
|
permission_required = "passbook_policies.view_policy"
|
||||||
paginate_by = 10
|
|
||||||
ordering = "name"
|
ordering = "name"
|
||||||
template_name = "administration/policy/list.html"
|
template_name = "administration/policy/list.html"
|
||||||
|
|
||||||
|
|
||||||
class PolicyCreateView(
|
class PolicyCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
InheritanceCreateView,
|
InheritanceCreateView,
|
||||||
@ -54,6 +58,7 @@ class PolicyCreateView(
|
|||||||
|
|
||||||
class PolicyUpdateView(
|
class PolicyUpdateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
PermissionRequiredMixin,
|
PermissionRequiredMixin,
|
||||||
InheritanceUpdateView,
|
InheritanceUpdateView,
|
||||||
|
|||||||
@ -9,19 +9,25 @@ from django.urls import reverse_lazy
|
|||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
from guardian.shortcuts import get_objects_for_user
|
||||||
|
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
DeleteMessageView,
|
||||||
|
UserPaginateListMixin,
|
||||||
|
)
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
from passbook.policies.forms import PolicyBindingForm
|
from passbook.policies.forms import PolicyBindingForm
|
||||||
from passbook.policies.models import PolicyBinding, PolicyBindingModel
|
from passbook.policies.models import PolicyBinding
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class PolicyBindingListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all policies"""
|
"""Show list of all policies"""
|
||||||
|
|
||||||
model = PolicyBinding
|
model = PolicyBinding
|
||||||
permission_required = "passbook_policies.view_policybinding"
|
permission_required = "passbook_policies.view_policybinding"
|
||||||
paginate_by = 10
|
|
||||||
ordering = ["order", "target"]
|
ordering = ["order", "target"]
|
||||||
template_name = "administration/policy_binding/list.html"
|
template_name = "administration/policy_binding/list.html"
|
||||||
|
|
||||||
@ -29,18 +35,24 @@ class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
|||||||
# Since `select_subclasses` does not work with a foreign key, we have to do two queries here
|
# Since `select_subclasses` does not work with a foreign key, we have to do two queries here
|
||||||
# First, get all pbm objects that have bindings attached
|
# First, get all pbm objects that have bindings attached
|
||||||
objects = (
|
objects = (
|
||||||
PolicyBindingModel.objects.filter(policies__isnull=False)
|
get_objects_for_user(
|
||||||
|
self.request.user, "passbook_policies.view_policybindingmodel"
|
||||||
|
)
|
||||||
|
.filter(policies__isnull=False)
|
||||||
.select_subclasses()
|
.select_subclasses()
|
||||||
.select_related()
|
.select_related()
|
||||||
.order_by("pk")
|
.order_by("pk")
|
||||||
)
|
)
|
||||||
for pbm in objects:
|
for pbm in objects:
|
||||||
pbm.bindings = PolicyBinding.objects.filter(target__pk=pbm.pbm_uuid)
|
pbm.bindings = get_objects_for_user(
|
||||||
|
self.request.user, self.permission_required
|
||||||
|
).filter(target__pk=pbm.pbm_uuid)
|
||||||
return objects
|
return objects
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingCreateView(
|
class PolicyBindingCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
CreateAssignPermView,
|
CreateAssignPermView,
|
||||||
@ -57,7 +69,11 @@ class PolicyBindingCreateView(
|
|||||||
|
|
||||||
|
|
||||||
class PolicyBindingUpdateView(
|
class PolicyBindingUpdateView(
|
||||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
PermissionRequiredMixin,
|
||||||
|
UpdateView,
|
||||||
):
|
):
|
||||||
"""Update policybinding"""
|
"""Update policybinding"""
|
||||||
|
|
||||||
|
|||||||
@ -9,16 +9,18 @@ from django.utils.translation import gettext as _
|
|||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import (
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
DeleteMessageView,
|
DeleteMessageView,
|
||||||
InheritanceCreateView,
|
InheritanceCreateView,
|
||||||
InheritanceListView,
|
InheritanceListView,
|
||||||
InheritanceUpdateView,
|
InheritanceUpdateView,
|
||||||
|
UserPaginateListMixin,
|
||||||
)
|
)
|
||||||
from passbook.core.models import PropertyMapping
|
from passbook.core.models import PropertyMapping
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingListView(
|
class PropertyMappingListView(
|
||||||
LoginRequiredMixin, PermissionListMixin, InheritanceListView
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
|
||||||
):
|
):
|
||||||
"""Show list of all property_mappings"""
|
"""Show list of all property_mappings"""
|
||||||
|
|
||||||
@ -26,11 +28,11 @@ class PropertyMappingListView(
|
|||||||
permission_required = "passbook_core.view_propertymapping"
|
permission_required = "passbook_core.view_propertymapping"
|
||||||
template_name = "administration/property_mapping/list.html"
|
template_name = "administration/property_mapping/list.html"
|
||||||
ordering = "name"
|
ordering = "name"
|
||||||
paginate_by = 40
|
|
||||||
|
|
||||||
|
|
||||||
class PropertyMappingCreateView(
|
class PropertyMappingCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
InheritanceCreateView,
|
InheritanceCreateView,
|
||||||
@ -47,6 +49,7 @@ class PropertyMappingCreateView(
|
|||||||
|
|
||||||
class PropertyMappingUpdateView(
|
class PropertyMappingUpdateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
PermissionRequiredMixin,
|
PermissionRequiredMixin,
|
||||||
InheritanceUpdateView,
|
InheritanceUpdateView,
|
||||||
|
|||||||
@ -9,26 +9,30 @@ from django.utils.translation import gettext as _
|
|||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import (
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
DeleteMessageView,
|
DeleteMessageView,
|
||||||
InheritanceCreateView,
|
InheritanceCreateView,
|
||||||
InheritanceListView,
|
InheritanceListView,
|
||||||
InheritanceUpdateView,
|
InheritanceUpdateView,
|
||||||
|
UserPaginateListMixin,
|
||||||
)
|
)
|
||||||
from passbook.core.models import Provider
|
from passbook.core.models import Provider
|
||||||
|
|
||||||
|
|
||||||
class ProviderListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
class ProviderListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
|
||||||
|
):
|
||||||
"""Show list of all providers"""
|
"""Show list of all providers"""
|
||||||
|
|
||||||
model = Provider
|
model = Provider
|
||||||
permission_required = "passbook_core.add_provider"
|
permission_required = "passbook_core.add_provider"
|
||||||
template_name = "administration/provider/list.html"
|
template_name = "administration/provider/list.html"
|
||||||
paginate_by = 10
|
|
||||||
ordering = "id"
|
ordering = "id"
|
||||||
|
|
||||||
|
|
||||||
class ProviderCreateView(
|
class ProviderCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
InheritanceCreateView,
|
InheritanceCreateView,
|
||||||
@ -45,6 +49,7 @@ class ProviderCreateView(
|
|||||||
|
|
||||||
class ProviderUpdateView(
|
class ProviderUpdateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
PermissionRequiredMixin,
|
PermissionRequiredMixin,
|
||||||
InheritanceUpdateView,
|
InheritanceUpdateView,
|
||||||
|
|||||||
@ -9,26 +9,30 @@ from django.utils.translation import gettext as _
|
|||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import (
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
DeleteMessageView,
|
DeleteMessageView,
|
||||||
InheritanceCreateView,
|
InheritanceCreateView,
|
||||||
InheritanceListView,
|
InheritanceListView,
|
||||||
InheritanceUpdateView,
|
InheritanceUpdateView,
|
||||||
|
UserPaginateListMixin,
|
||||||
)
|
)
|
||||||
from passbook.core.models import Source
|
from passbook.core.models import Source
|
||||||
|
|
||||||
|
|
||||||
class SourceListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
class SourceListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
|
||||||
|
):
|
||||||
"""Show list of all sources"""
|
"""Show list of all sources"""
|
||||||
|
|
||||||
model = Source
|
model = Source
|
||||||
permission_required = "passbook_core.view_source"
|
permission_required = "passbook_core.view_source"
|
||||||
ordering = "name"
|
ordering = "name"
|
||||||
paginate_by = 40
|
|
||||||
template_name = "administration/source/list.html"
|
template_name = "administration/source/list.html"
|
||||||
|
|
||||||
|
|
||||||
class SourceCreateView(
|
class SourceCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
InheritanceCreateView,
|
InheritanceCreateView,
|
||||||
@ -45,6 +49,7 @@ class SourceCreateView(
|
|||||||
|
|
||||||
class SourceUpdateView(
|
class SourceUpdateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
PermissionRequiredMixin,
|
PermissionRequiredMixin,
|
||||||
InheritanceUpdateView,
|
InheritanceUpdateView,
|
||||||
|
|||||||
@ -9,26 +9,30 @@ from django.utils.translation import gettext as _
|
|||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import (
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
DeleteMessageView,
|
DeleteMessageView,
|
||||||
InheritanceCreateView,
|
InheritanceCreateView,
|
||||||
InheritanceListView,
|
InheritanceListView,
|
||||||
InheritanceUpdateView,
|
InheritanceUpdateView,
|
||||||
|
UserPaginateListMixin,
|
||||||
)
|
)
|
||||||
from passbook.flows.models import Stage
|
from passbook.flows.models import Stage
|
||||||
|
|
||||||
|
|
||||||
class StageListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
|
class StageListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, InheritanceListView
|
||||||
|
):
|
||||||
"""Show list of all stages"""
|
"""Show list of all stages"""
|
||||||
|
|
||||||
model = Stage
|
model = Stage
|
||||||
template_name = "administration/stage/list.html"
|
template_name = "administration/stage/list.html"
|
||||||
permission_required = "passbook_flows.view_stage"
|
permission_required = "passbook_flows.view_stage"
|
||||||
ordering = "name"
|
ordering = "name"
|
||||||
paginate_by = 40
|
|
||||||
|
|
||||||
|
|
||||||
class StageCreateView(
|
class StageCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
InheritanceCreateView,
|
InheritanceCreateView,
|
||||||
@ -45,6 +49,7 @@ class StageCreateView(
|
|||||||
|
|
||||||
class StageUpdateView(
|
class StageUpdateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
PermissionRequiredMixin,
|
PermissionRequiredMixin,
|
||||||
InheritanceUpdateView,
|
InheritanceUpdateView,
|
||||||
|
|||||||
@ -9,24 +9,30 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
DeleteMessageView,
|
||||||
|
UserPaginateListMixin,
|
||||||
|
)
|
||||||
from passbook.flows.forms import FlowStageBindingForm
|
from passbook.flows.forms import FlowStageBindingForm
|
||||||
from passbook.flows.models import FlowStageBinding
|
from passbook.flows.models import FlowStageBinding
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
|
|
||||||
|
|
||||||
class StageBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class StageBindingListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all flows"""
|
"""Show list of all flows"""
|
||||||
|
|
||||||
model = FlowStageBinding
|
model = FlowStageBinding
|
||||||
permission_required = "passbook_flows.view_flowstagebinding"
|
permission_required = "passbook_flows.view_flowstagebinding"
|
||||||
paginate_by = 10
|
|
||||||
ordering = ["target", "order"]
|
ordering = ["target", "order"]
|
||||||
template_name = "administration/stage_binding/list.html"
|
template_name = "administration/stage_binding/list.html"
|
||||||
|
|
||||||
|
|
||||||
class StageBindingCreateView(
|
class StageBindingCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
CreateAssignPermView,
|
CreateAssignPermView,
|
||||||
@ -43,7 +49,11 @@ class StageBindingCreateView(
|
|||||||
|
|
||||||
|
|
||||||
class StageBindingUpdateView(
|
class StageBindingUpdateView(
|
||||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
PermissionRequiredMixin,
|
||||||
|
UpdateView,
|
||||||
):
|
):
|
||||||
"""Update FlowStageBinding"""
|
"""Update FlowStageBinding"""
|
||||||
|
|
||||||
|
|||||||
@ -10,25 +10,31 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
DeleteMessageView,
|
||||||
|
UserPaginateListMixin,
|
||||||
|
)
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
from passbook.stages.invitation.forms import InvitationForm
|
from passbook.stages.invitation.forms import InvitationForm
|
||||||
from passbook.stages.invitation.models import Invitation
|
from passbook.stages.invitation.models import Invitation
|
||||||
from passbook.stages.invitation.signals import invitation_created
|
from passbook.stages.invitation.signals import invitation_created
|
||||||
|
|
||||||
|
|
||||||
class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class InvitationListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all invitations"""
|
"""Show list of all invitations"""
|
||||||
|
|
||||||
model = Invitation
|
model = Invitation
|
||||||
permission_required = "passbook_stages_invitation.view_invitation"
|
permission_required = "passbook_stages_invitation.view_invitation"
|
||||||
template_name = "administration/stage_invitation/list.html"
|
template_name = "administration/stage_invitation/list.html"
|
||||||
paginate_by = 10
|
|
||||||
ordering = "-expires"
|
ordering = "-expires"
|
||||||
|
|
||||||
|
|
||||||
class InvitationCreateView(
|
class InvitationCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
CreateAssignPermView,
|
CreateAssignPermView,
|
||||||
|
|||||||
@ -9,24 +9,30 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import ListView, UpdateView
|
from django.views.generic import ListView, UpdateView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
DeleteMessageView,
|
||||||
|
UserPaginateListMixin,
|
||||||
|
)
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
from passbook.stages.prompt.forms import PromptAdminForm
|
from passbook.stages.prompt.forms import PromptAdminForm
|
||||||
from passbook.stages.prompt.models import Prompt
|
from passbook.stages.prompt.models import Prompt
|
||||||
|
|
||||||
|
|
||||||
class PromptListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class PromptListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all prompts"""
|
"""Show list of all prompts"""
|
||||||
|
|
||||||
model = Prompt
|
model = Prompt
|
||||||
permission_required = "passbook_stages_prompt.view_prompt"
|
permission_required = "passbook_stages_prompt.view_prompt"
|
||||||
ordering = "order"
|
ordering = "order"
|
||||||
paginate_by = 40
|
|
||||||
template_name = "administration/stage_prompt/list.html"
|
template_name = "administration/stage_prompt/list.html"
|
||||||
|
|
||||||
|
|
||||||
class PromptCreateView(
|
class PromptCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
CreateAssignPermView,
|
CreateAssignPermView,
|
||||||
@ -43,7 +49,11 @@ class PromptCreateView(
|
|||||||
|
|
||||||
|
|
||||||
class PromptUpdateView(
|
class PromptUpdateView(
|
||||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
PermissionRequiredMixin,
|
||||||
|
UpdateView,
|
||||||
):
|
):
|
||||||
"""Update prompt"""
|
"""Update prompt"""
|
||||||
|
|
||||||
|
|||||||
@ -5,17 +5,18 @@ from django.utils.translation import gettext as _
|
|||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import DeleteMessageView, UserPaginateListMixin
|
||||||
from passbook.core.models import Token
|
from passbook.core.models import Token
|
||||||
|
|
||||||
|
|
||||||
class TokenListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class TokenListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all tokens"""
|
"""Show list of all tokens"""
|
||||||
|
|
||||||
model = Token
|
model = Token
|
||||||
permission_required = "passbook_core.view_token"
|
permission_required = "passbook_core.view_token"
|
||||||
ordering = "expires"
|
ordering = "expires"
|
||||||
paginate_by = 40
|
|
||||||
template_name = "administration/token/list.html"
|
template_name = "administration/token/list.html"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -18,18 +18,23 @@ from guardian.mixins import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from passbook.admin.forms.users import UserForm
|
from passbook.admin.forms.users import UserForm
|
||||||
from passbook.admin.views.utils import DeleteMessageView
|
from passbook.admin.views.utils import (
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
DeleteMessageView,
|
||||||
|
UserPaginateListMixin,
|
||||||
|
)
|
||||||
from passbook.core.models import Token, User
|
from passbook.core.models import Token, User
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
|
|
||||||
|
|
||||||
class UserListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class UserListView(
|
||||||
|
LoginRequiredMixin, PermissionListMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all users"""
|
"""Show list of all users"""
|
||||||
|
|
||||||
model = User
|
model = User
|
||||||
permission_required = "passbook_core.view_user"
|
permission_required = "passbook_core.view_user"
|
||||||
ordering = "username"
|
ordering = "username"
|
||||||
paginate_by = 40
|
|
||||||
template_name = "administration/user/list.html"
|
template_name = "administration/user/list.html"
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -38,6 +43,7 @@ class UserListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
|||||||
|
|
||||||
class UserCreateView(
|
class UserCreateView(
|
||||||
SuccessMessageMixin,
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
LoginRequiredMixin,
|
LoginRequiredMixin,
|
||||||
DjangoPermissionRequiredMixin,
|
DjangoPermissionRequiredMixin,
|
||||||
CreateAssignPermView,
|
CreateAssignPermView,
|
||||||
@ -54,7 +60,11 @@ class UserCreateView(
|
|||||||
|
|
||||||
|
|
||||||
class UserUpdateView(
|
class UserUpdateView(
|
||||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
SuccessMessageMixin,
|
||||||
|
BackSuccessUrlMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
PermissionRequiredMixin,
|
||||||
|
UpdateView,
|
||||||
):
|
):
|
||||||
"""Update user"""
|
"""Update user"""
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
"""passbook admin util views"""
|
"""passbook admin util views"""
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
from django.db.models import QuerySet
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
from django.http.request import HttpRequest
|
||||||
from django.views.generic import DeleteView, ListView, UpdateView
|
from django.views.generic import DeleteView, ListView, UpdateView
|
||||||
|
|
||||||
from passbook.lib.utils.reflection import all_subclasses
|
from passbook.lib.utils.reflection import all_subclasses
|
||||||
@ -40,7 +43,7 @@ class InheritanceCreateView(CreateAssignPermView):
|
|||||||
)
|
)
|
||||||
except StopIteration as exc:
|
except StopIteration as exc:
|
||||||
raise Http404 from exc
|
raise Http404 from exc
|
||||||
return model.form(model)
|
return model().form
|
||||||
|
|
||||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||||
kwargs = super().get_context_data(**kwargs)
|
kwargs = super().get_context_data(**kwargs)
|
||||||
@ -61,7 +64,7 @@ class InheritanceUpdateView(UpdateView):
|
|||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def get_form_class(self):
|
def get_form_class(self):
|
||||||
return self.get_object().form()
|
return self.get_object().form
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
return (
|
return (
|
||||||
@ -69,3 +72,31 @@ class InheritanceUpdateView(UpdateView):
|
|||||||
.select_subclasses()
|
.select_subclasses()
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BackSuccessUrlMixin:
|
||||||
|
"""Checks if a relative URL has been given as ?back param, and redirect to it. Otherwise
|
||||||
|
default to self.success_url."""
|
||||||
|
|
||||||
|
request: HttpRequest
|
||||||
|
|
||||||
|
success_url: Optional[str]
|
||||||
|
|
||||||
|
def get_success_url(self) -> str:
|
||||||
|
"""get_success_url from FormMixin"""
|
||||||
|
back_param = self.request.GET.get("back")
|
||||||
|
if back_param:
|
||||||
|
if not bool(urlparse(back_param).netloc):
|
||||||
|
return back_param
|
||||||
|
return str(self.success_url)
|
||||||
|
|
||||||
|
|
||||||
|
class UserPaginateListMixin:
|
||||||
|
"""Get paginate_by value from user's attributes, defaulting to 15"""
|
||||||
|
|
||||||
|
request: HttpRequest
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def get_paginate_by(self, queryset: QuerySet) -> int:
|
||||||
|
"""get_paginate_by Function of ListView"""
|
||||||
|
return self.request.user.attributes.get("paginate_by", 15)
|
||||||
|
|||||||
85
passbook/audit/middleware.py
Normal file
85
passbook/audit/middleware.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
"""Audit middleware"""
|
||||||
|
from functools import partial
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.db.models import Model
|
||||||
|
from django.db.models.signals import post_save, pre_delete
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
|
||||||
|
from passbook.audit.models import Event, EventAction, model_to_dict
|
||||||
|
from passbook.audit.signals import EventNewThread
|
||||||
|
from passbook.core.middleware import LOCAL
|
||||||
|
|
||||||
|
|
||||||
|
class AuditMiddleware:
|
||||||
|
"""Register handlers for duration of request-response that log creation/update/deletion
|
||||||
|
of models"""
|
||||||
|
|
||||||
|
get_response: Callable[[HttpRequest], HttpResponse]
|
||||||
|
|
||||||
|
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
# Connect signal for automatic logging
|
||||||
|
if hasattr(request, "user") and getattr(
|
||||||
|
request.user, "is_authenticated", False
|
||||||
|
):
|
||||||
|
post_save_handler = partial(
|
||||||
|
self.post_save_handler, user=request.user, request=request
|
||||||
|
)
|
||||||
|
pre_delete_handler = partial(
|
||||||
|
self.pre_delete_handler, user=request.user, request=request
|
||||||
|
)
|
||||||
|
post_save.connect(
|
||||||
|
post_save_handler,
|
||||||
|
dispatch_uid=LOCAL.passbook["request_id"],
|
||||||
|
weak=False,
|
||||||
|
)
|
||||||
|
pre_delete.connect(
|
||||||
|
pre_delete_handler,
|
||||||
|
dispatch_uid=LOCAL.passbook["request_id"],
|
||||||
|
weak=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.get_response(request)
|
||||||
|
|
||||||
|
post_save.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
|
||||||
|
pre_delete.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def process_exception(self, request: HttpRequest, exception: Exception):
|
||||||
|
"""Unregister handlers in case of exception"""
|
||||||
|
post_save.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
|
||||||
|
pre_delete.disconnect(dispatch_uid=LOCAL.passbook["request_id"])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def post_save_handler(
|
||||||
|
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
|
||||||
|
):
|
||||||
|
"""Signal handler for all object's post_save"""
|
||||||
|
if isinstance(instance, Event):
|
||||||
|
return
|
||||||
|
|
||||||
|
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
|
||||||
|
EventNewThread(action, request, user=user, model=model_to_dict(instance)).run()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def pre_delete_handler(
|
||||||
|
user: User, request: HttpRequest, sender, instance: Model, **_
|
||||||
|
):
|
||||||
|
"""Signal handler for all object's pre_delete"""
|
||||||
|
if isinstance(instance, Event):
|
||||||
|
return
|
||||||
|
|
||||||
|
EventNewThread(
|
||||||
|
EventAction.MODEL_DELETED,
|
||||||
|
request,
|
||||||
|
user=user,
|
||||||
|
model=model_to_dict(instance),
|
||||||
|
).run()
|
||||||
59
passbook/audit/migrations/0003_auto_20200917_1155.py
Normal file
59
passbook/audit/migrations/0003_auto_20200917_1155.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Generated by Django 3.1.1 on 2020-09-17 11:55
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
import passbook.audit.models
|
||||||
|
|
||||||
|
|
||||||
|
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
Event = apps.get_model("passbook_audit", "Event")
|
||||||
|
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
for event in Event.objects.all():
|
||||||
|
event.delete()
|
||||||
|
# Because event objects cannot be updated, we have to re-create them
|
||||||
|
event.pk = None
|
||||||
|
event.user_json = (
|
||||||
|
passbook.audit.models.get_user(event.user) if event.user else {}
|
||||||
|
)
|
||||||
|
event._state.adding = True
|
||||||
|
event.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_audit", "0002_auto_20200918_2116"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("LOGIN", "login"),
|
||||||
|
("LOGIN_FAILED", "login_failed"),
|
||||||
|
("LOGOUT", "logout"),
|
||||||
|
("AUTHORIZE_APPLICATION", "authorize_application"),
|
||||||
|
("SUSPICIOUS_REQUEST", "suspicious_request"),
|
||||||
|
("SIGN_UP", "sign_up"),
|
||||||
|
("PASSWORD_RESET", "password_reset"),
|
||||||
|
("INVITE_CREATED", "invitation_created"),
|
||||||
|
("INVITE_USED", "invitation_used"),
|
||||||
|
("IMPERSONATION_STARTED", "impersonation_started"),
|
||||||
|
("IMPERSONATION_ENDED", "impersonation_ended"),
|
||||||
|
("CUSTOM", "custom"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="event", name="user_json", field=models.JSONField(default=dict),
|
||||||
|
),
|
||||||
|
migrations.RunPython(convert_user_to_json),
|
||||||
|
migrations.RemoveField(model_name="event", name="user",),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name="event", old_name="user_json", new_name="user"
|
||||||
|
),
|
||||||
|
]
|
||||||
37
passbook/audit/migrations/0004_auto_20200921_1829.py
Normal file
37
passbook/audit/migrations/0004_auto_20200921_1829.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 3.1.1 on 2020-09-21 18:29
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_audit", "0003_auto_20200917_1155"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="event",
|
||||||
|
name="action",
|
||||||
|
field=models.TextField(
|
||||||
|
choices=[
|
||||||
|
("login", "Login"),
|
||||||
|
("login_failed", "Login Failed"),
|
||||||
|
("logout", "Logout"),
|
||||||
|
("sign_up", "Sign Up"),
|
||||||
|
("authorize_application", "Authorize Application"),
|
||||||
|
("suspicious_request", "Suspicious Request"),
|
||||||
|
("password_set", "Password Set"),
|
||||||
|
("invitation_created", "Invite Created"),
|
||||||
|
("invitation_used", "Invite Used"),
|
||||||
|
("source_linked", "Source Linked"),
|
||||||
|
("impersonation_started", "Impersonation Started"),
|
||||||
|
("impersonation_ended", "Impersonation Ended"),
|
||||||
|
("model_created", "Model Created"),
|
||||||
|
("model_updated", "Model Updated"),
|
||||||
|
("model_deleted", "Model Deleted"),
|
||||||
|
("custom_", "Custom Prefix"),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -1,7 +1,6 @@
|
|||||||
"""passbook audit models"""
|
"""passbook audit models"""
|
||||||
from enum import Enum
|
|
||||||
from inspect import getmodule, stack
|
from inspect import getmodule, stack
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional, Union
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -12,13 +11,17 @@ from django.db.models.base import Model
|
|||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.debug import SafeExceptionReporterFilter
|
from django.views.debug import SafeExceptionReporterFilter
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.utils import get_anonymous_user
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER
|
from passbook.core.middleware import (
|
||||||
|
SESSION_IMPERSONATE_ORIGINAL_USER,
|
||||||
|
SESSION_IMPERSONATE_USER,
|
||||||
|
)
|
||||||
|
from passbook.core.models import User
|
||||||
from passbook.lib.utils.http import get_client_ip
|
from passbook.lib.utils.http import get_client_ip
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger("passbook.audit")
|
||||||
|
|
||||||
|
|
||||||
def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||||
@ -50,6 +53,22 @@ def model_to_dict(model: Model) -> Dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_user(user: User, original_user: Optional[User] = None) -> Dict[str, Any]:
|
||||||
|
"""Convert user object to dictionary, optionally including the original user"""
|
||||||
|
if isinstance(user, AnonymousUser):
|
||||||
|
user = get_anonymous_user()
|
||||||
|
user_data = {
|
||||||
|
"username": user.username,
|
||||||
|
"pk": user.pk,
|
||||||
|
"email": user.email,
|
||||||
|
}
|
||||||
|
if original_user:
|
||||||
|
original_data = get_user(original_user)
|
||||||
|
original_data["on_behalf_of"] = user_data
|
||||||
|
return original_data
|
||||||
|
return user_data
|
||||||
|
|
||||||
|
|
||||||
def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||||
"""clean source of all Models that would interfere with the JSONField.
|
"""clean source of all Models that would interfere with the JSONField.
|
||||||
Models are replaced with a dictionary of {
|
Models are replaced with a dictionary of {
|
||||||
@ -70,38 +89,39 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
|||||||
return final_dict
|
return final_dict
|
||||||
|
|
||||||
|
|
||||||
class EventAction(Enum):
|
class EventAction(models.TextChoices):
|
||||||
"""All possible actions to save into the audit log"""
|
"""All possible actions to save into the audit log"""
|
||||||
|
|
||||||
LOGIN = "login"
|
LOGIN = "login"
|
||||||
LOGIN_FAILED = "login_failed"
|
LOGIN_FAILED = "login_failed"
|
||||||
LOGOUT = "logout"
|
LOGOUT = "logout"
|
||||||
|
|
||||||
|
SIGN_UP = "sign_up"
|
||||||
AUTHORIZE_APPLICATION = "authorize_application"
|
AUTHORIZE_APPLICATION = "authorize_application"
|
||||||
SUSPICIOUS_REQUEST = "suspicious_request"
|
SUSPICIOUS_REQUEST = "suspicious_request"
|
||||||
SIGN_UP = "sign_up"
|
PASSWORD_SET = "password_set" # noqa # nosec
|
||||||
PASSWORD_RESET = "password_reset" # noqa # nosec
|
|
||||||
INVITE_CREATED = "invitation_created"
|
INVITE_CREATED = "invitation_created"
|
||||||
INVITE_USED = "invitation_used"
|
INVITE_USED = "invitation_used"
|
||||||
|
|
||||||
|
SOURCE_LINKED = "source_linked"
|
||||||
|
|
||||||
IMPERSONATION_STARTED = "impersonation_started"
|
IMPERSONATION_STARTED = "impersonation_started"
|
||||||
IMPERSONATION_ENDED = "impersonation_ended"
|
IMPERSONATION_ENDED = "impersonation_ended"
|
||||||
CUSTOM = "custom"
|
|
||||||
|
|
||||||
@staticmethod
|
MODEL_CREATED = "model_created"
|
||||||
def as_choices():
|
MODEL_UPDATED = "model_updated"
|
||||||
"""Generate choices of actions used for database"""
|
MODEL_DELETED = "model_deleted"
|
||||||
return tuple(
|
|
||||||
(x, y.value) for x, y in getattr(EventAction, "__members__").items()
|
CUSTOM_PREFIX = "custom_"
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Event(models.Model):
|
class Event(models.Model):
|
||||||
"""An individual audit log event"""
|
"""An individual audit log event"""
|
||||||
|
|
||||||
event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
|
||||||
user = models.ForeignKey(
|
user = models.JSONField(default=dict)
|
||||||
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
|
action = models.TextField(choices=EventAction.choices)
|
||||||
)
|
|
||||||
action = models.TextField(choices=EventAction.as_choices())
|
|
||||||
date = models.DateTimeField(auto_now_add=True)
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
app = models.TextField()
|
app = models.TextField()
|
||||||
context = models.JSONField(default=dict, blank=True)
|
context = models.JSONField(default=dict, blank=True)
|
||||||
@ -116,20 +136,18 @@ class Event(models.Model):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def new(
|
def new(
|
||||||
action: EventAction,
|
action: Union[str, EventAction],
|
||||||
app: Optional[str] = None,
|
app: Optional[str] = None,
|
||||||
_inspect_offset: int = 1,
|
_inspect_offset: int = 1,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> "Event":
|
) -> "Event":
|
||||||
"""Create new Event instance from arguments. Instance is NOT saved."""
|
"""Create new Event instance from arguments. Instance is NOT saved."""
|
||||||
if not isinstance(action, EventAction):
|
if not isinstance(action, EventAction):
|
||||||
raise ValueError(
|
action = EventAction.CUSTOM_PREFIX + action
|
||||||
f"action must be EventAction instance but was {type(action)}"
|
|
||||||
)
|
|
||||||
if not app:
|
if not app:
|
||||||
app = getmodule(stack()[_inspect_offset][0]).__name__
|
app = getmodule(stack()[_inspect_offset][0]).__name__
|
||||||
cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs))
|
cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs))
|
||||||
event = Event(action=action.value, app=app, context=cleaned_kwargs)
|
event = Event(action=action, app=app, context=cleaned_kwargs)
|
||||||
return event
|
return event
|
||||||
|
|
||||||
def from_http(
|
def from_http(
|
||||||
@ -139,17 +157,18 @@ class Event(models.Model):
|
|||||||
Events independently from requests.
|
Events independently from requests.
|
||||||
`user` arguments optionally overrides user from requests."""
|
`user` arguments optionally overrides user from requests."""
|
||||||
if hasattr(request, "user"):
|
if hasattr(request, "user"):
|
||||||
if isinstance(request.user, AnonymousUser):
|
self.user = get_user(
|
||||||
self.user = get_anonymous_user()
|
request.user,
|
||||||
else:
|
request.session.get(SESSION_IMPERSONATE_ORIGINAL_USER, None),
|
||||||
self.user = request.user
|
)
|
||||||
if user:
|
if user:
|
||||||
self.user = user
|
self.user = get_user(user)
|
||||||
# Check if we're currently impersonating, and add that user
|
# Check if we're currently impersonating, and add that user
|
||||||
if hasattr(request, "session"):
|
if hasattr(request, "session"):
|
||||||
if SESSION_IMPERSONATE_ORIGINAL_USER in request.session:
|
if SESSION_IMPERSONATE_ORIGINAL_USER in request.session:
|
||||||
self.context["on_behalf_of"] = model_to_dict(
|
self.user = get_user(request.session[SESSION_IMPERSONATE_ORIGINAL_USER])
|
||||||
request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
self.user["on_behalf_of"] = get_user(
|
||||||
|
request.session[SESSION_IMPERSONATE_USER]
|
||||||
)
|
)
|
||||||
# User 255.255.255.255 as fallback if IP cannot be determined
|
# User 255.255.255.255 as fallback if IP cannot be determined
|
||||||
self.client_ip = get_client_ip(request) or "255.255.255.255"
|
self.client_ip = get_client_ip(request) or "255.255.255.255"
|
||||||
|
|||||||
@ -20,15 +20,18 @@ from passbook.stages.user_write.signals import user_write
|
|||||||
class EventNewThread(Thread):
|
class EventNewThread(Thread):
|
||||||
"""Create Event in background thread"""
|
"""Create Event in background thread"""
|
||||||
|
|
||||||
action: EventAction
|
action: str
|
||||||
request: HttpRequest
|
request: HttpRequest
|
||||||
kwargs: Dict[str, Any]
|
kwargs: Dict[str, Any]
|
||||||
user: Optional[User] = None
|
user: Optional[User] = None
|
||||||
|
|
||||||
def __init__(self, action: EventAction, request: HttpRequest, **kwargs):
|
def __init__(
|
||||||
|
self, action: str, request: HttpRequest, user: Optional[User] = None, **kwargs
|
||||||
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.action = action
|
self.action = action
|
||||||
self.request = request
|
self.request = request
|
||||||
|
self.user = user
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@ -57,7 +60,7 @@ def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
|
|||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def on_user_write(sender, request: HttpRequest, user: User, data: Dict[str, Any], **_):
|
def on_user_write(sender, request: HttpRequest, user: User, data: Dict[str, Any], **_):
|
||||||
"""Log User write"""
|
"""Log User write"""
|
||||||
thread = EventNewThread(EventAction.CUSTOM, request, **data)
|
thread = EventNewThread("stages/user_write", request, **data)
|
||||||
thread.user = user
|
thread.user = user
|
||||||
thread.run()
|
thread.run()
|
||||||
|
|
||||||
|
|||||||
@ -40,12 +40,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<td role="cell">
|
<td role="cell">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
<code>{{ entry.context }}</code>
|
<code>{{ entry.context }}</code>
|
||||||
|
</div>
|
||||||
|
{% if entry.user.on_behalf_of %}
|
||||||
|
<small>
|
||||||
|
{% blocktrans with username=entry.user.on_behalf_of.username %}
|
||||||
|
On behalf of {{ username }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td role="cell">
|
<td role="cell">
|
||||||
<span>
|
<div>
|
||||||
{{ entry.user }}
|
<div>{{ entry.user.username }}</div>
|
||||||
</span>
|
<small>
|
||||||
|
{% blocktrans with pk=entry.user.pk %}
|
||||||
|
ID: {{ pk }}
|
||||||
|
{% endblocktrans %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td role="cell">
|
<td role="cell">
|
||||||
<span>
|
<span>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
|
||||||
from passbook.audit.models import Event, EventAction
|
from passbook.audit.models import Event
|
||||||
from passbook.policies.dummy.models import DummyPolicy
|
from passbook.policies.dummy.models import DummyPolicy
|
||||||
|
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ class TestAuditEvent(TestCase):
|
|||||||
|
|
||||||
def test_new_with_model(self):
|
def test_new_with_model(self):
|
||||||
"""Create a new Event passing a model as kwarg"""
|
"""Create a new Event passing a model as kwarg"""
|
||||||
event = Event.new(EventAction.CUSTOM, test={"model": get_anonymous_user()})
|
event = Event.new("unittest", test={"model": get_anonymous_user()})
|
||||||
event.save() # We save to ensure nothing is un-saveable
|
event.save() # We save to ensure nothing is un-saveable
|
||||||
model_content_type = ContentType.objects.get_for_model(get_anonymous_user())
|
model_content_type = ContentType.objects.get_for_model(get_anonymous_user())
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -24,7 +24,7 @@ class TestAuditEvent(TestCase):
|
|||||||
def test_new_with_uuid_model(self):
|
def test_new_with_uuid_model(self):
|
||||||
"""Create a new Event passing a model (with UUID PK) as kwarg"""
|
"""Create a new Event passing a model (with UUID PK) as kwarg"""
|
||||||
temp_model = DummyPolicy.objects.create(name="test", result=True)
|
temp_model = DummyPolicy.objects.create(name="test", result=True)
|
||||||
event = Event.new(EventAction.CUSTOM, model=temp_model)
|
event = Event.new("unittest", model=temp_model)
|
||||||
event.save() # We save to ensure nothing is un-saveable
|
event.save() # We save to ensure nothing is un-saveable
|
||||||
model_content_type = ContentType.objects.get_for_model(temp_model)
|
model_content_type = ContentType.objects.get_for_model(temp_model)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|||||||
@ -3,14 +3,16 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
|||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
from guardian.mixins import PermissionListMixin
|
from guardian.mixins import PermissionListMixin
|
||||||
|
|
||||||
|
from passbook.admin.views.utils import UserPaginateListMixin
|
||||||
from passbook.audit.models import Event
|
from passbook.audit.models import Event
|
||||||
|
|
||||||
|
|
||||||
class EventListView(PermissionListMixin, LoginRequiredMixin, ListView):
|
class EventListView(
|
||||||
|
PermissionListMixin, LoginRequiredMixin, UserPaginateListMixin, ListView
|
||||||
|
):
|
||||||
"""Show list of all invitations"""
|
"""Show list of all invitations"""
|
||||||
|
|
||||||
model = Event
|
model = Event
|
||||||
template_name = "audit/list.html"
|
template_name = "audit/list.html"
|
||||||
permission_required = "passbook_audit.view_event"
|
permission_required = "passbook_audit.view_event"
|
||||||
ordering = "-created"
|
ordering = "-created"
|
||||||
paginate_by = 20
|
|
||||||
|
|||||||
@ -20,5 +20,5 @@ def admin_autoregister(app: AppConfig):
|
|||||||
|
|
||||||
for _app in apps.get_app_configs():
|
for _app in apps.get_app_configs():
|
||||||
if _app.label.startswith("passbook_"):
|
if _app.label.startswith("passbook_"):
|
||||||
LOGGER.debug("Registering application for dj-admin", app=_app.label)
|
LOGGER.debug("Registering application for dj-admin", application=_app.label)
|
||||||
admin_autoregister(_app)
|
admin_autoregister(_app)
|
||||||
|
|||||||
37
passbook/core/channels.py
Normal file
37
passbook/core/channels.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"""Channels base classes"""
|
||||||
|
from channels.generic.websocket import JsonWebsocketConsumer
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.core.models import Token, TokenIntents, User
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class AuthJsonConsumer(JsonWebsocketConsumer):
|
||||||
|
"""Authorize a client with a token"""
|
||||||
|
|
||||||
|
user: User
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
headers = dict(self.scope["headers"])
|
||||||
|
if b"authorization" not in headers:
|
||||||
|
LOGGER.warning("WS Request without authorization header")
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
token = headers[b"authorization"]
|
||||||
|
try:
|
||||||
|
token_uuid = token.decode("utf-8")
|
||||||
|
tokens = Token.filter_not_expired(
|
||||||
|
token_uuid=token_uuid, intent=TokenIntents.INTENT_API
|
||||||
|
)
|
||||||
|
if not tokens.exists():
|
||||||
|
LOGGER.warning("WS Request with invalid token")
|
||||||
|
self.close()
|
||||||
|
return False
|
||||||
|
except ValidationError:
|
||||||
|
LOGGER.warning("WS Invalid UUID")
|
||||||
|
self.close()
|
||||||
|
return False
|
||||||
|
self.user = tokens.first().user
|
||||||
|
return True
|
||||||
@ -1,11 +1,14 @@
|
|||||||
"""passbook admin Middleware to impersonate users"""
|
"""passbook admin Middleware to impersonate users"""
|
||||||
|
from logging import Logger
|
||||||
|
from threading import local
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
|
||||||
SESSION_IMPERSONATE_USER = "passbook_impersonate_user"
|
SESSION_IMPERSONATE_USER = "passbook_impersonate_user"
|
||||||
SESSION_IMPERSONATE_ORIGINAL_USER = "passbook_impersonate_original_user"
|
SESSION_IMPERSONATE_ORIGINAL_USER = "passbook_impersonate_original_user"
|
||||||
|
LOCAL = local()
|
||||||
|
|
||||||
|
|
||||||
class ImpersonateMiddleware:
|
class ImpersonateMiddleware:
|
||||||
@ -24,3 +27,30 @@ class ImpersonateMiddleware:
|
|||||||
request.user = request.session[SESSION_IMPERSONATE_USER]
|
request.user = request.session[SESSION_IMPERSONATE_USER]
|
||||||
|
|
||||||
return self.get_response(request)
|
return self.get_response(request)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestIDMiddleware:
|
||||||
|
"""Add a unique ID to every request"""
|
||||||
|
|
||||||
|
get_response: Callable[[HttpRequest], HttpResponse]
|
||||||
|
|
||||||
|
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]):
|
||||||
|
self.get_response = get_response
|
||||||
|
|
||||||
|
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||||
|
if not hasattr(request, "request_id"):
|
||||||
|
request_id = uuid4().hex
|
||||||
|
setattr(request, "request_id", request_id)
|
||||||
|
LOCAL.passbook = {"request_id": request_id}
|
||||||
|
response = self.get_response(request)
|
||||||
|
response["X-passbook-id"] = request.request_id
|
||||||
|
del LOCAL.passbook["request_id"]
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def structlog_add_request_id(logger: Logger, method_name: str, event_dict):
|
||||||
|
"""If threadlocal has passbook defined, add request_id to log"""
|
||||||
|
if hasattr(LOCAL, "passbook"):
|
||||||
|
event_dict["request_id"] = LOCAL.passbook.get("request_id", "")
|
||||||
|
return event_dict
|
||||||
|
|||||||
@ -14,7 +14,7 @@ def create_default_user(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
|||||||
pbadmin, _ = User.objects.using(db_alias).get_or_create(
|
pbadmin, _ = User.objects.using(db_alias).get_or_create(
|
||||||
username="pbadmin", email="root@localhost", name="passbook Default Admin"
|
username="pbadmin", email="root@localhost", name="passbook Default Admin"
|
||||||
)
|
)
|
||||||
pbadmin.set_password("pbadmin") # noqa # nosec
|
pbadmin.set_password("pbadmin", signal=False) # noqa # nosec
|
||||||
pbadmin.save()
|
pbadmin.save()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"""passbook core models"""
|
"""passbook core models"""
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, Optional, Type
|
from typing import Any, Dict, Optional, Type
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
@ -9,6 +9,7 @@ from django.db import models
|
|||||||
from django.db.models import Q, QuerySet
|
from django.db.models import Q, QuerySet
|
||||||
from django.forms import ModelForm
|
from django.forms import ModelForm
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
from django.utils.functional import cached_property
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from guardian.mixins import GuardianUserMixin
|
from guardian.mixins import GuardianUserMixin
|
||||||
@ -80,7 +81,16 @@ class User(GuardianUserMixin, AbstractUser):
|
|||||||
|
|
||||||
objects = UserManager()
|
objects = UserManager()
|
||||||
|
|
||||||
@property
|
def group_attributes(self) -> Dict[str, Any]:
|
||||||
|
"""Get a dictionary containing the attributes from all groups the user belongs to,
|
||||||
|
including the users attributes"""
|
||||||
|
final_attributes = {}
|
||||||
|
for group in self.pb_groups.all().order_by("name"):
|
||||||
|
final_attributes.update(group.attributes)
|
||||||
|
final_attributes.update(self.attributes)
|
||||||
|
return final_attributes
|
||||||
|
|
||||||
|
@cached_property
|
||||||
def is_superuser(self) -> bool:
|
def is_superuser(self) -> bool:
|
||||||
"""Get supseruser status based on membership in a group with superuser status"""
|
"""Get supseruser status based on membership in a group with superuser status"""
|
||||||
return self.pb_groups.filter(is_superuser=True).exists()
|
return self.pb_groups.filter(is_superuser=True).exists()
|
||||||
@ -88,10 +98,10 @@ class User(GuardianUserMixin, AbstractUser):
|
|||||||
@property
|
@property
|
||||||
def is_staff(self) -> bool:
|
def is_staff(self) -> bool:
|
||||||
"""superuser == staff user"""
|
"""superuser == staff user"""
|
||||||
return self.is_superuser
|
return self.is_superuser # type: ignore
|
||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password, signal=True):
|
||||||
if self.pk:
|
if self.pk and signal:
|
||||||
password_changed.send(sender=self, user=self, password=password)
|
password_changed.send(sender=self, user=self, password=password)
|
||||||
self.password_change_date = now()
|
self.password_change_date = now()
|
||||||
return super().set_password(password)
|
return super().set_password(password)
|
||||||
@ -128,6 +138,7 @@ class Provider(models.Model):
|
|||||||
Can return None for providers that are not URL-based"""
|
Can return None for providers that are not URL-based"""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
def form(self) -> Type[ModelForm]:
|
def form(self) -> Type[ModelForm]:
|
||||||
"""Return Form class used to edit this object"""
|
"""Return Form class used to edit this object"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -212,6 +223,7 @@ class Source(PolicyBindingModel):
|
|||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
@property
|
||||||
def form(self) -> Type[ModelForm]:
|
def form(self) -> Type[ModelForm]:
|
||||||
"""Return Form class used to edit this object"""
|
"""Return Form class used to edit this object"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -313,6 +325,7 @@ class PropertyMapping(models.Model):
|
|||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
@property
|
||||||
def form(self) -> Type[ModelForm]:
|
def form(self) -> Type[ModelForm]:
|
||||||
"""Return Form class used to edit this object"""
|
"""Return Form class used to edit this object"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|||||||
@ -44,12 +44,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pf-c-page__header-tools">
|
<div class="pf-c-page__header-tools">
|
||||||
<div class="pf-c-page__header-tools-group pf-m-icons">
|
<div class="pf-c-page__header-tools-group pf-m-icons">
|
||||||
<a href="{% url 'passbook_flows:default-invalidation' %}" class="pf-c-button pf-m-plain" type="button" aria-label="logout">
|
<a href="{% url 'passbook_flows:default-invalidation' %}" class="pf-c-button pf-m-plain" type="button" id="logout">
|
||||||
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
|
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pf-c-page__header-tools-group">
|
<div class="pf-c-page__header-tools-group">
|
||||||
<a href="{% url 'passbook_core:user-settings' %}" class="pf-c-button">
|
<a href="{% url 'passbook_core:user-settings' %}" id="user-settings" class="pf-c-button">
|
||||||
{{ user.username }}
|
{{ user.username }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -53,7 +53,7 @@
|
|||||||
<div class="pf-c-empty-state__body">
|
<div class="pf-c-empty-state__body">
|
||||||
{% trans "Either no applications are defined, or you don't have access to any." %}
|
{% trans "Either no applications are defined, or you don't have access to any." %}
|
||||||
</div>
|
</div>
|
||||||
{% if user.is_superuser %} {# TODO:use guardian permissions instead #}
|
{% if perms.passbook_core.add_application %}
|
||||||
<a href="{% url 'passbook_admin:application-create' %}" class="pf-c-button pf-m-primary" type="button">
|
<a href="{% url 'passbook_admin:application-create' %}" class="pf-c-button pf-m-primary" type="button">
|
||||||
{% trans 'Create Application' %}
|
{% trans 'Create Application' %}
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -62,6 +62,6 @@ class ServerErrorView(TemplateView):
|
|||||||
template_name = "error/generic.html"
|
template_name = "error/generic.html"
|
||||||
|
|
||||||
# pylint: disable=useless-super-delegation
|
# pylint: disable=useless-super-delegation
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs): # pragma: no cover
|
||||||
"""Little wrapper so django accepts this function"""
|
"""Little wrapper so django accepts this function"""
|
||||||
return super().dispatch(*args, **kwargs)
|
return super().dispatch(*args, **kwargs)
|
||||||
|
|||||||
@ -31,7 +31,7 @@ class ImpersonateInitView(View):
|
|||||||
request.session[SESSION_IMPERSONATE_ORIGINAL_USER] = request.user
|
request.session[SESSION_IMPERSONATE_ORIGINAL_USER] = request.user
|
||||||
request.session[SESSION_IMPERSONATE_USER] = user_to_be
|
request.session[SESSION_IMPERSONATE_USER] = user_to_be
|
||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request)
|
Event.new(EventAction.IMPERSONATION_STARTED).from_http(request, user_to_be)
|
||||||
|
|
||||||
return redirect("passbook_core:overview")
|
return redirect("passbook_core:overview")
|
||||||
|
|
||||||
@ -48,9 +48,11 @@ class ImpersonateEndView(View):
|
|||||||
LOGGER.debug("Can't end impersonation", user=request.user)
|
LOGGER.debug("Can't end impersonation", user=request.user)
|
||||||
return redirect("passbook_core:overview")
|
return redirect("passbook_core:overview")
|
||||||
|
|
||||||
|
original_user = request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
|
||||||
del request.session[SESSION_IMPERSONATE_USER]
|
del request.session[SESSION_IMPERSONATE_USER]
|
||||||
del request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
del request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||||
|
|
||||||
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request)
|
Event.new(EventAction.IMPERSONATION_ENDED).from_http(request, original_user)
|
||||||
|
|
||||||
return redirect("passbook_core:overview")
|
return redirect("passbook_core:overview")
|
||||||
|
|||||||
50
passbook/crypto/tests.py
Normal file
50
passbook/crypto/tests.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"""Crypto tests"""
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from passbook.crypto.api import CertificateKeyPairSerializer
|
||||||
|
from passbook.crypto.forms import CertificateKeyPairForm
|
||||||
|
from passbook.crypto.models import CertificateKeyPair
|
||||||
|
|
||||||
|
|
||||||
|
class TestCrypto(TestCase):
|
||||||
|
"""Test Crypto validation"""
|
||||||
|
|
||||||
|
def test_form(self):
|
||||||
|
"""Test form validation"""
|
||||||
|
keypair = CertificateKeyPair.objects.first()
|
||||||
|
self.assertTrue(
|
||||||
|
CertificateKeyPairForm(
|
||||||
|
{
|
||||||
|
"name": keypair.name,
|
||||||
|
"certificate_data": keypair.certificate_data,
|
||||||
|
"key_data": keypair.key_data,
|
||||||
|
}
|
||||||
|
).is_valid()
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
CertificateKeyPairForm(
|
||||||
|
{"name": keypair.name, "certificate_data": "test", "key_data": "test"}
|
||||||
|
).is_valid()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_serializer(self):
|
||||||
|
"""Test API Validation"""
|
||||||
|
keypair = CertificateKeyPair.objects.first()
|
||||||
|
self.assertTrue(
|
||||||
|
CertificateKeyPairSerializer(
|
||||||
|
data={
|
||||||
|
"name": keypair.name,
|
||||||
|
"certificate_data": keypair.certificate_data,
|
||||||
|
"key_data": keypair.key_data,
|
||||||
|
}
|
||||||
|
).is_valid()
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
CertificateKeyPairSerializer(
|
||||||
|
data={
|
||||||
|
"name": keypair.name,
|
||||||
|
"certificate_data": "test",
|
||||||
|
"key_data": "test",
|
||||||
|
}
|
||||||
|
).is_valid()
|
||||||
|
)
|
||||||
116
passbook/flows/management/commands/benchmark.py
Normal file
116
passbook/flows/management/commands/benchmark.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
"""passbook benchmark command"""
|
||||||
|
from csv import DictWriter
|
||||||
|
from multiprocessing import Manager, Process, cpu_count
|
||||||
|
from sys import stdout
|
||||||
|
from time import time
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from django import db
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.test import RequestFactory
|
||||||
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook import __version__
|
||||||
|
from passbook.core.models import User
|
||||||
|
from passbook.flows.models import Flow
|
||||||
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class FlowPlanProcess(Process): # pragma: no cover
|
||||||
|
"""Test process which executes flow planner"""
|
||||||
|
|
||||||
|
def __init__(self, index, return_dict, flow, user) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.index = index
|
||||||
|
self.return_dict = return_dict
|
||||||
|
self.flow = flow
|
||||||
|
self.user = user
|
||||||
|
self.request = RequestFactory().get("/")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
print(f"Proc {self.index} Running")
|
||||||
|
|
||||||
|
def test_inner():
|
||||||
|
planner = FlowPlanner(self.flow)
|
||||||
|
planner.use_cache = False
|
||||||
|
planner.plan(self.request, {PLAN_CONTEXT_PENDING_USER: self.user})
|
||||||
|
|
||||||
|
diffs = []
|
||||||
|
for _ in range(1000):
|
||||||
|
start = time()
|
||||||
|
test_inner()
|
||||||
|
end = time()
|
||||||
|
diffs.append(end - start)
|
||||||
|
self.return_dict[self.index] = diffs
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand): # pragma: no cover
|
||||||
|
"""Benchmark passbook"""
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--processes",
|
||||||
|
default=cpu_count(),
|
||||||
|
action="store",
|
||||||
|
help="How many processes should be started.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--csv", action="store_true", help="Output results as CSV",
|
||||||
|
)
|
||||||
|
|
||||||
|
def benchmark_flows(self, proc_count) -> str:
|
||||||
|
"""Get full recovery link"""
|
||||||
|
flow = Flow.objects.get(slug="default-authentication-flow")
|
||||||
|
user = User.objects.get(username="pbadmin")
|
||||||
|
manager = Manager()
|
||||||
|
return_dict = manager.dict()
|
||||||
|
|
||||||
|
jobs = []
|
||||||
|
db.connections.close_all()
|
||||||
|
for i in range(proc_count):
|
||||||
|
proc = FlowPlanProcess(i, return_dict, flow, user)
|
||||||
|
jobs.append(proc)
|
||||||
|
proc.start()
|
||||||
|
|
||||||
|
for proc in jobs:
|
||||||
|
proc.join()
|
||||||
|
return return_dict.values()
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""Start benchmark"""
|
||||||
|
proc_count = options.get("processes", 1)
|
||||||
|
all_values = self.benchmark_flows(proc_count)
|
||||||
|
if options.get("csv"):
|
||||||
|
self.output_csv(all_values)
|
||||||
|
else:
|
||||||
|
self.output_overview(all_values)
|
||||||
|
|
||||||
|
def output_overview(self, values: List[List[int]]):
|
||||||
|
"""Output results human readable"""
|
||||||
|
total_max = max([max(inner) for inner in values])
|
||||||
|
total_min = min([min(inner) for inner in values])
|
||||||
|
total_avg = sum([sum(inner) for inner in values]) / sum(
|
||||||
|
[len(inner) for inner in values]
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Version: {__version__}")
|
||||||
|
print(f"Processes: {len(values)}")
|
||||||
|
print(f"\tMax: {total_max * 100}ms")
|
||||||
|
print(f"\tMin: {total_min * 100}ms")
|
||||||
|
print(f"\tAvg: {total_avg * 100}ms")
|
||||||
|
|
||||||
|
def output_csv(self, values: List[List[int]]):
|
||||||
|
"""Output results as CSV"""
|
||||||
|
proc_count = len(values)
|
||||||
|
fieldnames = [f"proc_{idx}" for idx in range(proc_count)]
|
||||||
|
writer = DictWriter(stdout, fieldnames=fieldnames)
|
||||||
|
|
||||||
|
writer.writeheader()
|
||||||
|
for run_idx in range(len(values[0])):
|
||||||
|
row_dict = {}
|
||||||
|
for proc_idx in range(proc_count):
|
||||||
|
row_dict[f"proc_{proc_idx}"] = values[proc_idx][run_idx] * 100
|
||||||
|
writer.writerow(row_dict)
|
||||||
@ -12,7 +12,7 @@ FLOW_POLICY_EXPRESSION = """# This policy ensures that this flow can only be use
|
|||||||
return pb_is_sso_flow"""
|
return pb_is_sso_flow"""
|
||||||
PROMPT_POLICY_EXPRESSION = """# Check if we've not been given a username by the external IdP
|
PROMPT_POLICY_EXPRESSION = """# Check if we've not been given a username by the external IdP
|
||||||
# and trigger the enrollment flow
|
# and trigger the enrollment flow
|
||||||
return 'username' not in pb_flow_plan.context.get('prompt_data', {})"""
|
return 'username' not in context.get('prompt_data', {})"""
|
||||||
|
|
||||||
|
|
||||||
def create_default_source_enrollment_flow(
|
def create_default_source_enrollment_flow(
|
||||||
@ -80,7 +80,9 @@ def create_default_source_enrollment_flow(
|
|||||||
)
|
)
|
||||||
|
|
||||||
binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create(
|
binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create(
|
||||||
target=flow, stage=prompt_stage, defaults={"order": 0}
|
target=flow,
|
||||||
|
stage=prompt_stage,
|
||||||
|
defaults={"order": 0, "re_evaluate_policies": True},
|
||||||
)
|
)
|
||||||
PolicyBinding.objects.using(db_alias).update_or_create(
|
PolicyBinding.objects.using(db_alias).update_or_create(
|
||||||
policy=prompt_policy, target=binding, defaults={"order": 0}
|
policy=prompt_policy, target=binding, defaults={"order": 0}
|
||||||
|
|||||||
44
passbook/flows/migrations/0013_auto_20200924_1605.py
Normal file
44
passbook/flows/migrations/0013_auto_20200924_1605.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 3.1.1 on 2020-09-24 16:05
|
||||||
|
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
from passbook.flows.models import FlowDesignation
|
||||||
|
|
||||||
|
|
||||||
|
def update_flow_designation(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||||
|
Flow = apps.get_model("passbook_flows", "Flow")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
for flow in Flow.objects.using(db_alias).all():
|
||||||
|
if flow.designation == "stage_setup":
|
||||||
|
flow.designation = FlowDesignation.STAGE_CONFIGURATION
|
||||||
|
flow.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_flows", "0012_auto_20200908_1542"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flow",
|
||||||
|
name="designation",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("authentication", "Authentication"),
|
||||||
|
("authorization", "Authorization"),
|
||||||
|
("invalidation", "Invalidation"),
|
||||||
|
("enrollment", "Enrollment"),
|
||||||
|
("unenrollment", "Unrenollment"),
|
||||||
|
("recovery", "Recovery"),
|
||||||
|
("stage_configuration", "Stage Configuration"),
|
||||||
|
],
|
||||||
|
max_length=100,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(update_flow_designation),
|
||||||
|
]
|
||||||
51
passbook/flows/migrations/0014_auto_20200925_2332.py
Normal file
51
passbook/flows/migrations/0014_auto_20200925_2332.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Generated by Django 3.1.1 on 2020-09-25 23:32
|
||||||
|
|
||||||
|
from django.apps.registry import Apps
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||||
|
|
||||||
|
|
||||||
|
# First stage for default-source-enrollment flow (prompt stage)
|
||||||
|
# needs to have its policy re-evaluated
|
||||||
|
def update_default_source_enrollment_flow_binding(
|
||||||
|
apps: Apps, schema_editor: BaseDatabaseSchemaEditor
|
||||||
|
):
|
||||||
|
Flow = apps.get_model("passbook_flows", "Flow")
|
||||||
|
FlowStageBinding = apps.get_model("passbook_flows", "FlowStageBinding")
|
||||||
|
db_alias = schema_editor.connection.alias
|
||||||
|
|
||||||
|
flows = Flow.objects.using(db_alias).filter(slug="default-source-enrollment")
|
||||||
|
if not flows.exists():
|
||||||
|
return
|
||||||
|
flow = flows.first()
|
||||||
|
|
||||||
|
binding = FlowStageBinding.objects.get(target=flow, order=0)
|
||||||
|
binding.re_evaluate_policies = True
|
||||||
|
binding.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_flows", "0013_auto_20200924_1605"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="flowstagebinding",
|
||||||
|
options={
|
||||||
|
"ordering": ["target", "order"],
|
||||||
|
"verbose_name": "Flow Stage Binding",
|
||||||
|
"verbose_name_plural": "Flow Stage Bindings",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="flowstagebinding",
|
||||||
|
name="re_evaluate_policies",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text="When this option is enabled, the planner will re-evaluate policies bound to this binding.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(update_default_source_enrollment_flow_binding),
|
||||||
|
]
|
||||||
@ -37,7 +37,7 @@ class FlowDesignation(models.TextChoices):
|
|||||||
ENROLLMENT = "enrollment"
|
ENROLLMENT = "enrollment"
|
||||||
UNRENOLLMENT = "unenrollment"
|
UNRENOLLMENT = "unenrollment"
|
||||||
RECOVERY = "recovery"
|
RECOVERY = "recovery"
|
||||||
STAGE_SETUP = "stage_setup"
|
STAGE_CONFIGURATION = "stage_configuration"
|
||||||
|
|
||||||
|
|
||||||
class Stage(SerializerModel):
|
class Stage(SerializerModel):
|
||||||
@ -50,6 +50,7 @@ class Stage(SerializerModel):
|
|||||||
|
|
||||||
objects = InheritanceManager()
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
@property
|
||||||
def type(self) -> Type["StageView"]:
|
def type(self) -> Type["StageView"]:
|
||||||
"""Return StageView class that implements logic for this stage"""
|
"""Return StageView class that implements logic for this stage"""
|
||||||
# This is a bit of a workaround, since we can't set class methods with setattr
|
# This is a bit of a workaround, since we can't set class methods with setattr
|
||||||
@ -57,6 +58,7 @@ class Stage(SerializerModel):
|
|||||||
return getattr(self, "__in_memory_type")
|
return getattr(self, "__in_memory_type")
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
def form(self) -> Type[ModelForm]:
|
def form(self) -> Type[ModelForm]:
|
||||||
"""Return Form class used to edit this object"""
|
"""Return Form class used to edit this object"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
@ -155,7 +157,10 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
|
|||||||
re_evaluate_policies = models.BooleanField(
|
re_evaluate_policies = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"When this option is enabled, the planner will re-evaluate policies bound to this."
|
(
|
||||||
|
"When this option is enabled, the planner will re-evaluate "
|
||||||
|
"policies bound to this binding."
|
||||||
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -170,12 +175,35 @@ class FlowStageBinding(SerializerModel, PolicyBindingModel):
|
|||||||
return FlowStageBindingSerializer
|
return FlowStageBindingSerializer
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"'{self.target}' -> '{self.stage}' # {self.order}"
|
return f"{self.target} #{self.order} -> {self.stage}"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
ordering = ["order", "target"]
|
ordering = ["target", "order"]
|
||||||
|
|
||||||
verbose_name = _("Flow Stage Binding")
|
verbose_name = _("Flow Stage Binding")
|
||||||
verbose_name_plural = _("Flow Stage Bindings")
|
verbose_name_plural = _("Flow Stage Bindings")
|
||||||
unique_together = (("target", "stage", "order"),)
|
unique_together = (("target", "stage", "order"),)
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurableStage(models.Model):
|
||||||
|
"""Abstract base class for a Stage that can be configured by the enduser.
|
||||||
|
The stage should create a default flow with the configure_stage designation during
|
||||||
|
migration."""
|
||||||
|
|
||||||
|
configure_flow = models.ForeignKey(
|
||||||
|
Flow,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_(
|
||||||
|
(
|
||||||
|
"Flow used by an authenticated user to configure this Stage. "
|
||||||
|
"If empty, user will not be able to configure this stage."
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
abstract = True
|
||||||
|
|||||||
31
passbook/flows/tests/test_models.py
Normal file
31
passbook/flows/tests/test_models.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""flow model tests"""
|
||||||
|
from typing import Callable, Type
|
||||||
|
|
||||||
|
from django.forms import ModelForm
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from passbook.flows.models import Stage
|
||||||
|
from passbook.flows.stage import StageView
|
||||||
|
|
||||||
|
|
||||||
|
class TestStageProperties(TestCase):
|
||||||
|
"""Generic model properties tests"""
|
||||||
|
|
||||||
|
|
||||||
|
def stage_tester_factory(model: Type[Stage]) -> Callable:
|
||||||
|
"""Test a form"""
|
||||||
|
|
||||||
|
def tester(self: TestStageProperties):
|
||||||
|
model_inst = model()
|
||||||
|
self.assertTrue(issubclass(model_inst.form, ModelForm))
|
||||||
|
self.assertTrue(issubclass(model_inst.type, StageView))
|
||||||
|
|
||||||
|
return tester
|
||||||
|
|
||||||
|
|
||||||
|
for stage_type in Stage.__subclasses__():
|
||||||
|
setattr(
|
||||||
|
TestStageProperties,
|
||||||
|
f"test_stage_{stage_type.__name__}",
|
||||||
|
stage_tester_factory(stage_type),
|
||||||
|
)
|
||||||
@ -3,6 +3,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
|||||||
|
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.http import HttpRequest
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import RequestFactory, TestCase
|
from django.test import RequestFactory, TestCase
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
@ -23,6 +24,11 @@ CACHE_MOCK = Mock(wraps=cache)
|
|||||||
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
POLICY_RETURN_TRUE = MagicMock(return_value=PolicyResult(True))
|
||||||
|
|
||||||
|
|
||||||
|
def dummy_get_response(request: HttpRequest): # pragma: no cover
|
||||||
|
"""Dummy get_response for SessionMiddleware"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class TestFlowPlanner(TestCase):
|
class TestFlowPlanner(TestCase):
|
||||||
"""Test planner logic"""
|
"""Test planner logic"""
|
||||||
|
|
||||||
@ -164,7 +170,7 @@ class TestFlowPlanner(TestCase):
|
|||||||
)
|
)
|
||||||
request.user = get_anonymous_user()
|
request.user = get_anonymous_user()
|
||||||
|
|
||||||
middleware = SessionMiddleware()
|
middleware = SessionMiddleware(dummy_get_response)
|
||||||
middleware.process_request(request)
|
middleware.process_request(request)
|
||||||
request.session.save()
|
request.session.save()
|
||||||
|
|
||||||
|
|||||||
@ -105,15 +105,10 @@ class TestFlowTransfer(TransactionTestCase):
|
|||||||
order=2,
|
order=2,
|
||||||
type=FieldTypes.PASSWORD,
|
type=FieldTypes.PASSWORD,
|
||||||
)
|
)
|
||||||
# Password checking policy
|
|
||||||
password_policy = ExpressionPolicy.objects.create(
|
|
||||||
name=generate_client_id(), expression="return True",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Stages
|
# Stages
|
||||||
first_stage = PromptStage.objects.create(name=generate_client_id())
|
first_stage = PromptStage.objects.create(name=generate_client_id())
|
||||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||||
first_stage.validation_policies.set([password_policy])
|
|
||||||
first_stage.save()
|
first_stage.save()
|
||||||
|
|
||||||
flow = Flow.objects.create(
|
flow = Flow.objects.create(
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from django.urls import path
|
|||||||
from passbook.flows.models import FlowDesignation
|
from passbook.flows.models import FlowDesignation
|
||||||
from passbook.flows.views import (
|
from passbook.flows.views import (
|
||||||
CancelView,
|
CancelView,
|
||||||
|
ConfigureFlowInitView,
|
||||||
FlowExecutorShellView,
|
FlowExecutorShellView,
|
||||||
FlowExecutorView,
|
FlowExecutorView,
|
||||||
ToDefaultFlow,
|
ToDefaultFlow,
|
||||||
@ -36,6 +37,11 @@ urlpatterns = [
|
|||||||
name="default-unenrollment",
|
name="default-unenrollment",
|
||||||
),
|
),
|
||||||
path("-/cancel/", CancelView.as_view(), name="cancel"),
|
path("-/cancel/", CancelView.as_view(), name="cancel"),
|
||||||
|
path(
|
||||||
|
"-/configure/<uuid:stage_uuid>/",
|
||||||
|
ConfigureFlowInitView.as_view(),
|
||||||
|
name="configure",
|
||||||
|
),
|
||||||
path("b/<slug:flow_slug>/", FlowExecutorView.as_view(), name="flow-executor"),
|
path("b/<slug:flow_slug>/", FlowExecutorView.as_view(), name="flow-executor"),
|
||||||
path(
|
path(
|
||||||
"<slug:flow_slug>/", FlowExecutorShellView.as_view(), name="flow-executor-shell"
|
"<slug:flow_slug>/", FlowExecutorShellView.as_view(), name="flow-executor-shell"
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
from traceback import format_tb
|
from traceback import format_tb
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import (
|
from django.http import (
|
||||||
Http404,
|
Http404,
|
||||||
HttpRequest,
|
HttpRequest,
|
||||||
@ -19,8 +20,8 @@ from structlog import get_logger
|
|||||||
from passbook.audit.models import cleanse_dict
|
from passbook.audit.models import cleanse_dict
|
||||||
from passbook.core.models import PASSBOOK_USER_DEBUG
|
from passbook.core.models import PASSBOOK_USER_DEBUG
|
||||||
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
from passbook.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||||
from passbook.flows.models import Flow, FlowDesignation, Stage
|
from passbook.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
|
||||||
from passbook.flows.planner import FlowPlan, FlowPlanner
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan, FlowPlanner
|
||||||
from passbook.lib.utils.reflection import class_to_path
|
from passbook.lib.utils.reflection import class_to_path
|
||||||
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
|
from passbook.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||||
from passbook.policies.http import AccessDeniedResponse
|
from passbook.policies.http import AccessDeniedResponse
|
||||||
@ -95,7 +96,7 @@ class FlowExecutorView(View):
|
|||||||
current_stage=self.current_stage,
|
current_stage=self.current_stage,
|
||||||
flow_slug=self.flow.slug,
|
flow_slug=self.flow.slug,
|
||||||
)
|
)
|
||||||
stage_cls = self.current_stage.type()
|
stage_cls = self.current_stage.type
|
||||||
self.current_stage_view = stage_cls(self)
|
self.current_stage_view = stage_cls(self)
|
||||||
self.current_stage_view.args = self.args
|
self.current_stage_view.args = self.args
|
||||||
self.current_stage_view.kwargs = self.kwargs
|
self.current_stage_view.kwargs = self.kwargs
|
||||||
@ -156,10 +157,6 @@ class FlowExecutorView(View):
|
|||||||
stage_class=class_to_path(self.current_stage_view.__class__),
|
stage_class=class_to_path(self.current_stage_view.__class__),
|
||||||
flow_slug=self.flow.slug,
|
flow_slug=self.flow.slug,
|
||||||
)
|
)
|
||||||
# We call plan.next here to check for re-evaluate markers
|
|
||||||
# this is important so we can save the result
|
|
||||||
# and we don't have to re-evaluate the policies each request
|
|
||||||
self.plan.next()
|
|
||||||
self.plan.pop()
|
self.plan.pop()
|
||||||
self.request.session[SESSION_KEY_PLAN] = self.plan
|
self.request.session[SESSION_KEY_PLAN] = self.plan
|
||||||
if self.plan.stages:
|
if self.plan.stages:
|
||||||
@ -295,3 +292,32 @@ def to_stage_response(request: HttpRequest, source: HttpResponse) -> HttpRespons
|
|||||||
{"type": "template", "body": source.content.decode("utf-8")}
|
{"type": "template", "body": source.content.decode("utf-8")}
|
||||||
)
|
)
|
||||||
return source
|
return source
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigureFlowInitView(LoginRequiredMixin, View):
|
||||||
|
"""Initiate planner for selected change flow and redirect to flow executor,
|
||||||
|
or raise Http404 if no configure_flow has been set."""
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, stage_uuid: str) -> HttpResponse:
|
||||||
|
"""Initiate planner for selected change flow and redirect to flow executor,
|
||||||
|
or raise Http404 if no configure_flow has been set."""
|
||||||
|
try:
|
||||||
|
stage: Stage = Stage.objects.get_subclass(pk=stage_uuid)
|
||||||
|
except Stage.DoesNotExist as exc:
|
||||||
|
raise Http404 from exc
|
||||||
|
if not isinstance(stage, ConfigurableStage):
|
||||||
|
LOGGER.debug("Stage does not inherit ConfigurableStage", stage=stage)
|
||||||
|
raise Http404
|
||||||
|
if not stage.configure_flow:
|
||||||
|
LOGGER.debug("Stage has no configure_flow set", stage=stage)
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
plan = FlowPlanner(stage.configure_flow).plan(
|
||||||
|
request, {PLAN_CONTEXT_PENDING_USER: request.user}
|
||||||
|
)
|
||||||
|
request.session[SESSION_KEY_PLAN] = plan
|
||||||
|
return redirect_with_qs(
|
||||||
|
"passbook_flows:flow-executor-shell",
|
||||||
|
self.request.GET,
|
||||||
|
flow_slug=stage.configure_flow.slug,
|
||||||
|
)
|
||||||
|
|||||||
@ -1,9 +1,23 @@
|
|||||||
"""logging helpers"""
|
"""logging helpers"""
|
||||||
|
from logging import Logger
|
||||||
from os import getpid
|
from os import getpid
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def add_process_id(logger, method_name, event_dict):
|
def add_process_id(logger: Logger, method_name: str, event_dict):
|
||||||
"""Add the current process ID"""
|
"""Add the current process ID"""
|
||||||
event_dict["pid"] = getpid()
|
event_dict["pid"] = getpid()
|
||||||
return event_dict
|
return event_dict
|
||||||
|
|
||||||
|
|
||||||
|
def add_common_fields(environment: str) -> Callable:
|
||||||
|
"""Add a common field to easily search for passbook logs"""
|
||||||
|
|
||||||
|
def add_common_field(logger: Logger, method_name: str, event_dict):
|
||||||
|
"""Add a common field to easily search for passbook logs"""
|
||||||
|
event_dict["app"] = "passbook"
|
||||||
|
event_dict["app_environment"] = environment
|
||||||
|
return event_dict
|
||||||
|
|
||||||
|
return add_common_field
|
||||||
|
|||||||
@ -10,7 +10,6 @@ from redis.exceptions import ConnectionError as RedisConnectionError
|
|||||||
from redis.exceptions import RedisError
|
from redis.exceptions import RedisError
|
||||||
from rest_framework.exceptions import APIException
|
from rest_framework.exceptions import APIException
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
from websockets.exceptions import WebSocketException
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
@ -38,7 +37,6 @@ def before_send(event, hint):
|
|||||||
OSError,
|
OSError,
|
||||||
RedisError,
|
RedisError,
|
||||||
SentryIgnoredException,
|
SentryIgnoredException,
|
||||||
WebSocketException,
|
|
||||||
CeleryError,
|
CeleryError,
|
||||||
LDAPException,
|
LDAPException,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,7 +8,7 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
@CELERY_APP.task()
|
||||||
def backup_database():
|
def backup_database(): # pragma: no cover
|
||||||
"""Backup database"""
|
"""Backup database"""
|
||||||
management.call_command("dbbackup")
|
management.call_command("dbbackup")
|
||||||
LOGGER.info("Successfully backed up database.")
|
LOGGER.info("Successfully backed up database.")
|
||||||
|
|||||||
@ -17,7 +17,7 @@ LOGGER = get_logger()
|
|||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def back(context: Context) -> str:
|
def back(context: Context) -> str:
|
||||||
"""Return a link back (either from GET paramter or referer."""
|
"""Return a link back (either from GET parameter or referer."""
|
||||||
if "request" not in context:
|
if "request" not in context:
|
||||||
return ""
|
return ""
|
||||||
request = context.get("request")
|
request = context.get("request")
|
||||||
|
|||||||
30
passbook/lib/tests.py
Normal file
30
passbook/lib/tests.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""base model tests"""
|
||||||
|
from typing import Callable, Type
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework.serializers import BaseSerializer
|
||||||
|
|
||||||
|
from passbook.flows.models import Stage
|
||||||
|
from passbook.lib.models import SerializerModel
|
||||||
|
from passbook.lib.utils.reflection import all_subclasses
|
||||||
|
|
||||||
|
|
||||||
|
class TestModels(TestCase):
|
||||||
|
"""Generic model properties tests"""
|
||||||
|
|
||||||
|
|
||||||
|
def model_tester_factory(test_model: Type[Stage]) -> Callable:
|
||||||
|
"""Test a form"""
|
||||||
|
|
||||||
|
def tester(self: TestModels):
|
||||||
|
model_inst = test_model()
|
||||||
|
try:
|
||||||
|
self.assertTrue(issubclass(model_inst.serializer, BaseSerializer))
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return tester
|
||||||
|
|
||||||
|
|
||||||
|
for model in all_subclasses(SerializerModel):
|
||||||
|
setattr(TestModels, f"test_model_{model.__name__}", model_tester_factory(model))
|
||||||
@ -14,7 +14,7 @@ def _get_client_ip_from_meta(meta: Dict[str, Any]) -> Optional[str]:
|
|||||||
)
|
)
|
||||||
for _header in headers:
|
for _header in headers:
|
||||||
if _header in meta:
|
if _header in meta:
|
||||||
return meta.get(_header)
|
return meta.get(_header).split(", ")[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,14 +4,13 @@ from enum import IntEnum
|
|||||||
from time import time
|
from time import time
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from channels.generic.websocket import JsonWebsocketConsumer
|
|
||||||
from dacite import from_dict
|
from dacite import from_dict
|
||||||
from dacite.data import Data
|
from dacite.data import Data
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.exceptions import ValidationError
|
from guardian.shortcuts import get_objects_for_user
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import Token, TokenIntents
|
from passbook.core.channels import AuthJsonConsumer
|
||||||
from passbook.outposts.models import Outpost
|
from passbook.outposts.models import Outpost
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
@ -38,33 +37,18 @@ class WebsocketMessage:
|
|||||||
args: Dict[str, Any] = field(default_factory=dict)
|
args: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
class OutpostConsumer(JsonWebsocketConsumer):
|
class OutpostConsumer(AuthJsonConsumer):
|
||||||
"""Handler for Outposts that connect over websockets for health checks and live updates"""
|
"""Handler for Outposts that connect over websockets for health checks and live updates"""
|
||||||
|
|
||||||
outpost: Outpost
|
outpost: Outpost
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
# TODO: This authentication block could be handeled in middleware
|
if not super().connect():
|
||||||
headers = dict(self.scope["headers"])
|
return
|
||||||
if b"authorization" not in headers:
|
|
||||||
LOGGER.warning("WS Request without authorization header")
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
token = headers[b"authorization"]
|
|
||||||
try:
|
|
||||||
token_uuid = token.decode("utf-8")
|
|
||||||
tokens = Token.filter_not_expired(
|
|
||||||
token_uuid=token_uuid, intent=TokenIntents.INTENT_API
|
|
||||||
)
|
|
||||||
if not tokens.exists():
|
|
||||||
LOGGER.warning("WS Request with invalid token")
|
|
||||||
self.close()
|
|
||||||
except ValidationError:
|
|
||||||
LOGGER.warning("WS Invalid UUID")
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
uuid = self.scope["url_route"]["kwargs"]["pk"]
|
uuid = self.scope["url_route"]["kwargs"]["pk"]
|
||||||
outpost = Outpost.objects.filter(pk=uuid)
|
outpost = get_objects_for_user(
|
||||||
|
self.user, "passbook_outposts.view_outpost"
|
||||||
|
).filter(pk=uuid)
|
||||||
if not outpost.exists():
|
if not outpost.exists():
|
||||||
self.close()
|
self.close()
|
||||||
return
|
return
|
||||||
|
|||||||
@ -31,6 +31,7 @@ class DummyPolicy(Policy):
|
|||||||
|
|
||||||
return DummyPolicySerializer
|
return DummyPolicySerializer
|
||||||
|
|
||||||
|
@property
|
||||||
def form(self) -> Type[ModelForm]:
|
def form(self) -> Type[ModelForm]:
|
||||||
from passbook.policies.dummy.forms import DummyPolicyForm
|
from passbook.policies.dummy.forms import DummyPolicyForm
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user