Compare commits
31 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a105cf5a0 | |||
| 9e384df79e | |||
| c0bfd32d39 | |||
| 7be680cbe5 | |||
| 93bf8eaa82 | |||
| 1248585dca | |||
| 1319c480c4 | |||
| 1911e8e3a9 | |||
| 4198c5363f | |||
| 207aae15a8 | |||
| 50531b8a36 | |||
| e5e4824920 | |||
| 085247e2dc | |||
| f766594ab0 | |||
| d1e469e282 | |||
| 79e4500827 | |||
| 42702fa96a | |||
| 9deb3ad80f | |||
| 9877ef99c4 | |||
| c304b40e1b | |||
| f0e6d6f417 | |||
| 54de5c981e | |||
| a446775fe2 | |||
| 7393d8720b | |||
| 287cb72d6f | |||
| c5eff4bdd6 | |||
| e9a33ed8ab | |||
| 875173a86e | |||
| df7642b365 | |||
| 3bc1c0aa8b | |||
| 8951f5695e |
@ -1,10 +1,10 @@
|
||||
[bumpversion]
|
||||
current_version = 0.12.9-stable
|
||||
current_version = 0.12.11-stable
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
serialize = {major}.{minor}.{patch}-{release}
|
||||
message = new release: {new_version}
|
||||
message = release: {new_version}
|
||||
tag_name = version/{new_version}
|
||||
|
||||
[bumpversion:part:release]
|
||||
@ -15,9 +15,9 @@ values =
|
||||
beta
|
||||
stable
|
||||
|
||||
[bumpversion:file:docs/installation/docker-compose.md]
|
||||
[bumpversion:file:website/docs/installation/docker-compose.md]
|
||||
|
||||
[bumpversion:file:docs/installation/kubernetes.md]
|
||||
[bumpversion:file:website/docs/installation/kubernetes.md]
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ omit =
|
||||
manage.py
|
||||
*/migrations/*
|
||||
*/apps.py
|
||||
docs/
|
||||
website/
|
||||
|
||||
[report]
|
||||
sort = Cover
|
||||
|
||||
@ -3,4 +3,4 @@ helm
|
||||
passbook-ui
|
||||
static
|
||||
*.env.yml
|
||||
node_modules/
|
||||
**/node_modules
|
||||
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@ -18,11 +18,11 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.12.9-stable
|
||||
-t beryju/passbook:0.12.11-stable
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.12.9-stable
|
||||
run: docker push beryju/passbook:0.12.11-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook:latest
|
||||
build-proxy:
|
||||
@ -48,11 +48,11 @@ jobs:
|
||||
cd proxy
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook-proxy:0.12.9-stable \
|
||||
-t beryju/passbook-proxy:0.12.11-stable \
|
||||
-t beryju/passbook-proxy:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-proxy:0.12.9-stable
|
||||
run: docker push beryju/passbook-proxy:0.12.11-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-proxy:latest
|
||||
build-static:
|
||||
@ -77,11 +77,11 @@ jobs:
|
||||
run: docker build
|
||||
--no-cache
|
||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||
-t beryju/passbook-static:0.12.9-stable
|
||||
-t beryju/passbook-static:0.12.11-stable
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.12.9-stable
|
||||
run: docker push beryju/passbook-static:0.12.11-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
@ -114,5 +114,5 @@ jobs:
|
||||
SENTRY_PROJECT: passbook
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 0.12.9-stable
|
||||
tagName: 0.12.11-stable
|
||||
environment: beryjuorg-prod
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -199,3 +199,4 @@ local.env.yml
|
||||
|
||||
# Selenium Screenshots
|
||||
selenium_screenshots/**
|
||||
backups/
|
||||
|
||||
@ -9,3 +9,4 @@ ignore-paths:
|
||||
|
||||
uses:
|
||||
- django
|
||||
- celery
|
||||
|
||||
21
.pylintrc
21
.pylintrc
@ -1,16 +1,29 @@
|
||||
[MASTER]
|
||||
|
||||
disable=arguments-differ,no-self-use,fixme,locally-disabled,too-many-ancestors,too-few-public-methods,import-outside-toplevel,bad-continuation,signature-differs,similarities,cyclic-import
|
||||
disable =
|
||||
arguments-differ,
|
||||
no-self-use,
|
||||
fixme,
|
||||
locally-disabled,
|
||||
too-many-ancestors,
|
||||
too-few-public-methods,
|
||||
import-outside-toplevel,
|
||||
bad-continuation,
|
||||
signature-differs,
|
||||
similarities,
|
||||
cyclic-import,
|
||||
protected-access,
|
||||
unsubscriptable-object # remove when pylint is upgraded to 2.6
|
||||
|
||||
load-plugins=pylint_django,pylint.extensions.bad_builtin
|
||||
|
||||
extension-pkg-whitelist=lxml
|
||||
extension-pkg-whitelist=lxml,xmlsec
|
||||
|
||||
# Allow constants to be shorter than normal (and lowercase, for settings.py)
|
||||
const-rgx=[a-zA-Z0-9_]{1,40}$
|
||||
|
||||
ignored-modules=django-otp
|
||||
generated-members=xmlsec.constants.*,xmlsec.tree.*,xmlsec.template.*
|
||||
ignore=migrations
|
||||
max-attributes=12
|
||||
|
||||
jobs=12
|
||||
max-branches=20
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM python:3.8-slim-buster as locker
|
||||
FROM python:3.9-slim-buster as locker
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
COPY ./Pipfile.lock /app/
|
||||
@ -9,7 +9,7 @@ RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -rd > requirements-dev.txt
|
||||
|
||||
FROM python:3.8-slim-buster
|
||||
FROM python:3.9-slim-buster
|
||||
|
||||
WORKDIR /
|
||||
COPY --from=locker /app/requirements.txt /
|
||||
@ -20,7 +20,7 @@ RUN apt-get update && \
|
||||
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
|
||||
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-12 postgresql-client-11 build-essential && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-12 postgresql-client-11 build-essential libxmlsec1-dev pkg-config && \
|
||||
apt-get clean && \
|
||||
pip install -r /requirements.txt --no-cache-dir && \
|
||||
apt-get remove --purge -y build-essential && \
|
||||
@ -41,5 +41,6 @@ COPY ./manage.py /
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
|
||||
USER passbook
|
||||
STOPSIGNAL SIGINT
|
||||
|
||||
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]
|
||||
|
||||
4
Pipfile
4
Pipfile
@ -35,7 +35,6 @@ qrcode = "*"
|
||||
requests-oauthlib = "*"
|
||||
sentry-sdk = "*"
|
||||
service_identity = "*"
|
||||
signxml = "*"
|
||||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
@ -44,9 +43,10 @@ channels = "*"
|
||||
channels-redis = "*"
|
||||
kubernetes = "*"
|
||||
docker = "*"
|
||||
xmlsec = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
python_version = "3.9"
|
||||
|
||||
[dev-packages]
|
||||
autopep8 = "*"
|
||||
|
||||
379
Pipfile.lock
generated
379
Pipfile.lock
generated
@ -1,11 +1,11 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "d1a9883d864e25f18e34b298b72b58db333a037571c7a20cefb7ba7a4037a434"
|
||||
"sha256": "53ad00e394a5f2e2462837c4ceff837d1e593469af9505726048bed72ce0b81a"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3.8"
|
||||
"python_version": "3.9"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
@ -28,20 +28,23 @@
|
||||
"sha256:5b9062d5c0812335c75434bf17ce33d7a20ecfedaa0733faec7379868eb4068a",
|
||||
"sha256:fcd5b3baeeb7fc19b3486ff6d10543099d40ae1f5c9196eae695d1cde1b2f784"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.0.2"
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:a5098bc870b80e7b872bff60bb363c7f2c2c89078759f6c47b53ff8c525a152e",
|
||||
"sha256:cd88907ecaec59d78e4ac00ea665b03e571cb37e3a0e37b3702af1a9e86c365a"
|
||||
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
|
||||
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
|
||||
],
|
||||
"version": "==3.3.0"
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.3.1"
|
||||
},
|
||||
"async-timeout": {
|
||||
"hashes": [
|
||||
"sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f",
|
||||
"sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"
|
||||
],
|
||||
"markers": "python_full_version >= '3.5.3'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"attrs": {
|
||||
@ -49,6 +52,7 @@
|
||||
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.3.0"
|
||||
},
|
||||
"autobahn": {
|
||||
@ -56,6 +60,7 @@
|
||||
"sha256:24ce276d313e84d68241c3aef30d484f352b90a40168981b3640312c821df77b",
|
||||
"sha256:86bbce30cdd407137c57670993a8f9bfdfe3f8e994b889181d85e844d5aa8dfb"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==20.7.1"
|
||||
},
|
||||
"automat": {
|
||||
@ -74,24 +79,25 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:23d2575b7bd01c4e7153f283c1d1c12d329dabf78a6279d4192f2e752bb67b1a",
|
||||
"sha256:cb3f4c2f2576153b845e5b4f325d54a04f4ca00c156f2063965432bfa5d714dd"
|
||||
"sha256:51c419d890ae216b9b031be31f3182739dc3deb5b64351f286bffca2818ddb35",
|
||||
"sha256:d70d21ea137d786e84124639a62be42f92f4b09472ebfb761156057c92dc5366"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.16.13"
|
||||
"version": "==1.16.18"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:1b1b4cf5efd552ecc7f1ce0fc674d5fba56857db5ffe6394ee76edd1a568d7a6",
|
||||
"sha256:b3b4b8fa33620f015c52e426a92e7db21b5e667ed4785c5fbc484ebfdb2b5153"
|
||||
"sha256:288d43e85f12e3c1d6a0535a585a182ca04e8c6e742ebaaf15357a0e3b37ca7a",
|
||||
"sha256:bba18b5c4eef3eb2dc39b1b1f8959ba01ac27e7e12e413e281b0fb242990c0f5"
|
||||
],
|
||||
"version": "==1.19.13"
|
||||
"version": "==1.19.18"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
"sha256:513d4ff98dd27f85743a8dc0e92f55ddb1b49e060c2d5961512855cda2c01a98",
|
||||
"sha256:bbaa39c3dede00175df2dc2b03d0cf18dd2d32a7de7beb68072d13043c9edb20"
|
||||
],
|
||||
"markers": "python_version ~= '3.5'",
|
||||
"version": "==4.1.1"
|
||||
},
|
||||
"celery": {
|
||||
@ -152,11 +158,11 @@
|
||||
},
|
||||
"channels": {
|
||||
"hashes": [
|
||||
"sha256:5cdd9c6b9ee663cdf1bbb00de7cdab885a3c418f9d32a29f04b09498828020f6",
|
||||
"sha256:b02e150b48704ec3607d4168402ac5c26138dd183fcdb7f2aeb965e6e19fd558"
|
||||
"sha256:74db79c9eca616be69d38013b22083ab5d3f9ccda1ab5e69096b1bb7da2d9b18",
|
||||
"sha256:f50a6e79757a64c1e45e95e144a2ac5f1e99ee44a0718ab182c501f5e5abd268"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.0.1"
|
||||
"version": "==3.0.2"
|
||||
},
|
||||
"channels-redis": {
|
||||
"hashes": [
|
||||
@ -178,6 +184,7 @@
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"click-didyoumean": {
|
||||
@ -251,10 +258,11 @@
|
||||
},
|
||||
"daphne": {
|
||||
"hashes": [
|
||||
"sha256:60856f7efa0b1e1b969efa074e8698bd09de4713ecc06e6a4d19d04c66c4a3bd",
|
||||
"sha256:b43e70d74ff832a634ff6c92badd208824e4530e08b340116517e5aad0aca774"
|
||||
"sha256:0052c9887600c57054a5867d4b0240159fa009faa3bcf6a1627271d9cdcb005a",
|
||||
"sha256:c22b692707f514de9013651ecb687f2abe4f35cf6fe292ece634e9f1737bc7e3"
|
||||
],
|
||||
"version": "==3.0.0"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"defusedxml": {
|
||||
"hashes": [
|
||||
@ -381,13 +389,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.19.4"
|
||||
},
|
||||
"eight": {
|
||||
"hashes": [
|
||||
"sha256:0a7f0e7725f2a478a97676cf9c49266d95f922f8ed621ec314eeccb333927dc2",
|
||||
"sha256:d148aa1fac6cafb5ff806ff634914b05e3f9357aa8dbd82cd7908821d7f93f43"
|
||||
],
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"facebook-sdk": {
|
||||
"hashes": [
|
||||
"sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e",
|
||||
@ -400,6 +401,7 @@
|
||||
"hashes": [
|
||||
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.18.2"
|
||||
},
|
||||
"google-auth": {
|
||||
@ -407,6 +409,7 @@
|
||||
"sha256:5176db85f1e7e837a646cd9cede72c3c404ccf2e3373d9ee14b2db88febad440",
|
||||
"sha256:b728625ff5dfce8f9e56a499c8a4eb51443a67f20f6d28b67d5774c310ec4b6b"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.23.0"
|
||||
},
|
||||
"gunicorn": {
|
||||
@ -473,6 +476,7 @@
|
||||
"sha256:e64be68255234bb489a574c4f2f8df7029c98c81ec4d160d6cd836e7f0679390",
|
||||
"sha256:e82d6b930e02e80e5109b678c663a9ed210680ded81c1abaf54635d88d1da298"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"httptools": {
|
||||
@ -518,6 +522,7 @@
|
||||
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||
"sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.5.1"
|
||||
},
|
||||
"itypes": {
|
||||
@ -532,6 +537,7 @@
|
||||
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.11.2"
|
||||
},
|
||||
"jmespath": {
|
||||
@ -539,6 +545,7 @@
|
||||
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
|
||||
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"jsonschema": {
|
||||
@ -553,20 +560,24 @@
|
||||
"sha256:6dc509178ac4269b0e66ab4881f70a2035c33d3a622e20585f965986a5182006",
|
||||
"sha256:f4965fba0a4718d47d470beeb5d6446e3357a62402b16c510b6a2f251e05ac3c"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.0.2"
|
||||
},
|
||||
"kubernetes": {
|
||||
"hashes": [
|
||||
"sha256:72f095a1cd593401ff26b3b8d71749340394ca6d8413770ea28ce18efd5bcf4c",
|
||||
"sha256:9a339a32d6c79e6461cb6050c3662cb4e33058b508d8d34ee5d5206add395828"
|
||||
"sha256:23c85d8571df8f56e773f1a413bc081537536dc47e2b5e8dc2e6262edb2c57ca",
|
||||
"sha256:ec52ea01d52e2ec3da255992f7e859f3a76f2bdb51cf65ba8cd71dfc309d8daa"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==12.0.0"
|
||||
"version": "==12.0.1"
|
||||
},
|
||||
"ldap3": {
|
||||
"hashes": [
|
||||
"sha256:10bdd23b612e942ce90ea4dbc744dfd88735949833e46c5467a2dcf68e60f469",
|
||||
"sha256:37d633e20fa360c302b1263c96fe932d40622d0119f1bddcb829b03462eeeeb7",
|
||||
"sha256:7c3738570766f5e5e74a56fade15470f339d5c436d821cf476ef27da0a4de8b0"
|
||||
"sha256:7c3738570766f5e5e74a56fade15470f339d5c436d821cf476ef27da0a4de8b0",
|
||||
"sha256:8f59a7b5399555b22db06f153daa76c77ded2dd84bc0f0ffe5b0b33901b6eac4",
|
||||
"sha256:bed71c6ce2f70a00a330eed0c8370664c065239d45bcbe1b82517b6f6eed7f25"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8.1"
|
||||
@ -650,6 +661,7 @@
|
||||
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.1.1"
|
||||
},
|
||||
"msgpack": {
|
||||
@ -680,6 +692,7 @@
|
||||
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
||||
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"packaging": {
|
||||
@ -702,60 +715,83 @@
|
||||
"sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c",
|
||||
"sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63"
|
||||
],
|
||||
"markers": "python_full_version >= '3.6.1'",
|
||||
"version": "==3.0.8"
|
||||
},
|
||||
"psycopg2-binary": {
|
||||
"hashes": [
|
||||
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
|
||||
"sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
|
||||
"sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
|
||||
"sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
|
||||
"sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6",
|
||||
"sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
|
||||
"sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
|
||||
"sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
|
||||
"sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4",
|
||||
"sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056",
|
||||
"sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
|
||||
"sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
|
||||
"sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
|
||||
"sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
|
||||
"sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
|
||||
"sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83",
|
||||
"sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
|
||||
"sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
|
||||
"sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
|
||||
"sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2",
|
||||
"sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
|
||||
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
|
||||
"sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd",
|
||||
"sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859",
|
||||
"sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
|
||||
"sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
|
||||
"sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
|
||||
"sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
|
||||
"sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
|
||||
"sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
|
||||
"sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
|
||||
"sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66",
|
||||
"sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4",
|
||||
"sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449",
|
||||
"sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
|
||||
"sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
|
||||
"sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
|
||||
"sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
|
||||
"sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
|
||||
"sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2",
|
||||
"sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
|
||||
"sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
|
||||
"sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
|
||||
"sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
|
||||
"sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
|
||||
"sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
|
||||
"sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
|
||||
"sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
|
||||
"sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
|
||||
"sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
|
||||
"sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
|
||||
"sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
|
||||
"sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4",
|
||||
"sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"
|
||||
"sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5",
|
||||
"sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
|
||||
"sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
|
||||
"sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
|
||||
"sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859",
|
||||
"sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
|
||||
"sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8.6"
|
||||
},
|
||||
"pyasn1": {
|
||||
"hashes": [
|
||||
"sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359",
|
||||
"sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576",
|
||||
"sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf",
|
||||
"sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7",
|
||||
"sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d",
|
||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"
|
||||
"sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00",
|
||||
"sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8",
|
||||
"sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86",
|
||||
"sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12",
|
||||
"sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776",
|
||||
"sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba",
|
||||
"sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2",
|
||||
"sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"
|
||||
],
|
||||
"version": "==0.4.8"
|
||||
},
|
||||
"pyasn1-modules": {
|
||||
"hashes": [
|
||||
"sha256:0845a5582f6a02bb3e1bde9ecfc4bfcae6ec3210dd270522fee602365430c3f8",
|
||||
"sha256:0fe1b68d1e486a1ed5473f1302bd991c1611d319bba158e98b106ff86e1d7199",
|
||||
"sha256:15b7c67fabc7fc240d87fb9aabf999cf82311a6d6fb2c70d00d3d0604878c811",
|
||||
"sha256:426edb7a5e8879f1ec54a1864f16b882c2837bfd06eee62f2c982315ee2473ed",
|
||||
"sha256:65cebbaffc913f4fe9e4808735c95ea22d7a7775646ab690518c056784bc21b4",
|
||||
"sha256:905f84c712230b2c592c19470d3ca8d552de726050d1d1716282a1f6146be65e",
|
||||
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74"
|
||||
"sha256:a50b808ffeb97cb3601dd25981f6b016cbb3d31fbf57a8b8a87428e6158d0c74",
|
||||
"sha256:a99324196732f53093a84c4369c996713eb8c89d360a496b599fb1a9c47fc3eb",
|
||||
"sha256:b80486a6c77252ea3a3e9b1e360bc9cf28eaac41263d173c032581ad2f20fe45",
|
||||
"sha256:c29a5e5cc7a3f05926aff34e097e84f8589cd790ce0ed41b67aed6857b26aafd",
|
||||
"sha256:cbac4bc38d117f2a49aeedec4407d23e8866ea4ac27ff2cf7fb3e5b570df19e0",
|
||||
"sha256:f39edd8c4ecaa4556e989147ebf219227e2cd2e8a43c7e7fcb1f1c18c5fd6a3d",
|
||||
"sha256:fe0644d9ab041506b62782e92b06b8c68cca799e1a9636ec398675459e031405"
|
||||
],
|
||||
"version": "==0.2.8"
|
||||
},
|
||||
@ -764,6 +800,7 @@
|
||||
"sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0",
|
||||
"sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.20"
|
||||
},
|
||||
"pycryptodome": {
|
||||
@ -845,6 +882,7 @@
|
||||
"sha256:f20a62397e09704049ce9007bea4f6bad965ba9336a760c6f4ef1b4192e12d6d",
|
||||
"sha256:f81f7311250d9480e36dec819127897ae772e7e8de07abfabe931b8566770b8e"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.9.9"
|
||||
},
|
||||
"pyhamcrest": {
|
||||
@ -852,6 +890,7 @@
|
||||
"sha256:412e00137858f04bde0729913874a48485665f2d36fe9ee449f26be864af9316",
|
||||
"sha256:7ead136e03655af85069b6f47b23eb7c3e5c221aa9f022a4fbb499f5b7308f29"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.0.2"
|
||||
},
|
||||
"pyjwkest": {
|
||||
@ -873,12 +912,14 @@
|
||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pyrsistent": {
|
||||
"hashes": [
|
||||
"sha256:2e636185d9eb976a18a8a8e96efce62f2905fea90041958d8cc2a189756ebf3e"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"python-dateutil": {
|
||||
@ -886,6 +927,7 @@
|
||||
"sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
|
||||
"sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"python-dotenv": {
|
||||
@ -932,19 +974,22 @@
|
||||
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
|
||||
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==3.5.3"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
|
||||
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
|
||||
],
|
||||
"version": "==2.24.0"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==2.25.0"
|
||||
},
|
||||
"requests-oauthlib": {
|
||||
"hashes": [
|
||||
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
|
||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
|
||||
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.0"
|
||||
@ -964,36 +1009,6 @@
|
||||
],
|
||||
"version": "==0.16.12"
|
||||
},
|
||||
"ruamel.yaml.clib": {
|
||||
"hashes": [
|
||||
"sha256:058a1cc3df2a8aecc12f983a48bda99315cebf55a3b3a5463e37bb599b05727b",
|
||||
"sha256:2602e91bd5c1b874d6f93d3086f9830f3e907c543c7672cf293a97c3fabdcd91",
|
||||
"sha256:28116f204103cb3a108dfd37668f20abe6e3cafd0d3fd40dba126c732457b3cc",
|
||||
"sha256:2d24bd98af676f4990c4d715bcdc2a60b19c56a3fb3a763164d2d8ca0e806ba7",
|
||||
"sha256:30dca9bbcbb1cc858717438218d11eafb78666759e5094dd767468c0d577a7e7",
|
||||
"sha256:44c7b0498c39f27795224438f1a6be6c5352f82cb887bc33d962c3a3acc00df6",
|
||||
"sha256:464e66a04e740d754170be5e740657a3b3b6d2bcc567f0c3437879a6e6087ff6",
|
||||
"sha256:4df5019e7783d14b79217ad9c56edf1ba7485d614ad5a385d1b3c768635c81c0",
|
||||
"sha256:4e52c96ca66de04be42ea2278012a2342d89f5e82b4512fb6fb7134e377e2e62",
|
||||
"sha256:5254af7d8bdf4d5484c089f929cb7f5bafa59b4f01d4f48adda4be41e6d29f99",
|
||||
"sha256:52ae5739e4b5d6317b52f5b040b1b6639e8af68a5b8fd606a8b08658fbd0cab5",
|
||||
"sha256:53b9dd1abd70e257a6e32f934ebc482dac5edb8c93e23deb663eac724c30b026",
|
||||
"sha256:73b3d43e04cc4b228fa6fa5d796409ece6fcb53a6c270eb2048109cbcbc3b9c2",
|
||||
"sha256:74161d827407f4db9072011adcfb825b5258a5ccb3d2cd518dd6c9edea9e30f1",
|
||||
"sha256:839dd72545ef7ba78fd2aa1a5dd07b33696adf3e68fae7f31327161c1093001b",
|
||||
"sha256:8e8fd0a22c9d92af3a34f91e8a2594eeb35cba90ab643c5e0e643567dc8be43e",
|
||||
"sha256:a873e4d4954f865dcb60bdc4914af7eaae48fb56b60ed6daa1d6251c72f5337c",
|
||||
"sha256:ab845f1f51f7eb750a78937be9f79baea4a42c7960f5a94dde34e69f3cce1988",
|
||||
"sha256:b1e981fe1aff1fd11627f531524826a4dcc1f26c726235a52fcb62ded27d150f",
|
||||
"sha256:b4b0d31f2052b3f9f9b5327024dc629a253a83d8649d4734ca7f35b60ec3e9e5",
|
||||
"sha256:c6ac7e45367b1317e56f1461719c853fd6825226f45b835df7436bb04031fd8a",
|
||||
"sha256:daf21aa33ee9b351f66deed30a3d450ab55c14242cfdfcd377798e2c0d25c9f1",
|
||||
"sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2",
|
||||
"sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f"
|
||||
],
|
||||
"markers": "platform_python_implementation == 'CPython' and python_version < '3.9'",
|
||||
"version": "==0.2.2"
|
||||
},
|
||||
"s3transfer": {
|
||||
"hashes": [
|
||||
"sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13",
|
||||
@ -1003,11 +1018,11 @@
|
||||
},
|
||||
"sentry-sdk": {
|
||||
"hashes": [
|
||||
"sha256:17b725df2258354ccb39618ae4ead29651aa92c01a92acf72f98efe06ee2e45a",
|
||||
"sha256:9040539485226708b5cad0401d76628fba4eed9154bf301c50579767afe344fd"
|
||||
"sha256:81d7a5d8ca0b13a16666e8280127b004565aa988bfeec6481e98a8601804b215",
|
||||
"sha256:fd48f627945511c140546939b4d73815be4860cd1d2b9149577d7f6563e7bd60"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.19.2"
|
||||
"version": "==0.19.3"
|
||||
},
|
||||
"service-identity": {
|
||||
"hashes": [
|
||||
@ -1017,19 +1032,12 @@
|
||||
"index": "pypi",
|
||||
"version": "==18.1.0"
|
||||
},
|
||||
"signxml": {
|
||||
"hashes": [
|
||||
"sha256:b70e151d10d99cbc74a50a3344f508ee481fe3c376d61cd1cae850912d303d19",
|
||||
"sha256:bab03a6823c9a5b225d1e6266ce66b5d08c4ebfb42029fdb5d3e588b8128c86d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.8.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"sqlparse": {
|
||||
@ -1037,6 +1045,7 @@
|
||||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"structlog": {
|
||||
@ -1084,6 +1093,7 @@
|
||||
"sha256:f058bd0168271de4dcdc39845b52dd0a4a2fecf5f1246335f13f5e96eaebb467",
|
||||
"sha256:f3c19e5bd42bbe4bf345704ad7c326c74d3fd7a1b3844987853bef180be638d4"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==20.3.0"
|
||||
},
|
||||
"txaio": {
|
||||
@ -1091,6 +1101,7 @@
|
||||
"sha256:17938f2bca4a9cabce61346758e482ca4e600160cbc28e861493eac74a19539d",
|
||||
"sha256:38a469daf93c37e5527cb062653d6393ae11663147c42fab7ddc3f6d00d434ae"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==20.4.1"
|
||||
},
|
||||
"uritemplate": {
|
||||
@ -1098,6 +1109,7 @@
|
||||
"sha256:07620c3f3f8eed1f12600845892b0e036a2420acf513c53f7de0abd911a5894f",
|
||||
"sha256:5af8ad10cec94f215e3f48112de2022e1d5a37ed427fbd88652fa908f2ab7cae"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.0.1"
|
||||
},
|
||||
"urllib3": {
|
||||
@ -1105,12 +1117,11 @@
|
||||
"secure"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
|
||||
"sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
|
||||
"sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
|
||||
"sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==1.25.11"
|
||||
"version": "==1.26.2"
|
||||
},
|
||||
"uvicorn": {
|
||||
"extras": [
|
||||
@ -1142,6 +1153,7 @@
|
||||
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
|
||||
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.0.0"
|
||||
},
|
||||
"watchgod": {
|
||||
@ -1192,6 +1204,25 @@
|
||||
],
|
||||
"version": "==8.1"
|
||||
},
|
||||
"xmlsec": {
|
||||
"hashes": [
|
||||
"sha256:252f79ed4482d6eefcca62c3bfc99b8d95c07abd846262d854a207ec4d67fac5",
|
||||
"sha256:31884dc97cc34cf1681a0f239f613969e61f9a01f4c2d2a62e53d68216fe42d6",
|
||||
"sha256:32a669dfe447bccecdb4ef79221c0452ce6dad919f3a75daf512792141a54dac",
|
||||
"sha256:3d13d7b6cb921dbc4d60d00ad00081a038df73a1e69f5bcc3695deb1bf2093b0",
|
||||
"sha256:5e2f263a21fd146859911479ec35e40a57f519e650f56c775f91367d2a1b6e15",
|
||||
"sha256:61076be98da4c7cf842a78aa3f129a5039f2ba4992e02480eefe78028d317698",
|
||||
"sha256:69d7f965d6b74b3266f7baa99a0377d9c76acbf26c615b4ee8d2cbe17bf85528",
|
||||
"sha256:6d8bb24c3a4db398011f394e29b58cd34c9c26d76b772c5d418d8579df127234",
|
||||
"sha256:6d9d46d1f6b4985023469a1e334cb35c7c8fc6bd9d8b65ca52b923a7a6869c2a",
|
||||
"sha256:8a7ffdc4f7f760253aa4dd8d2037358eb33915ca1dcf1c2422b19fcf0ab68506",
|
||||
"sha256:927fc5755bb93dc09275bd5d818811e016290c194012d63f8e6f86b7ece3e468",
|
||||
"sha256:dcaa084c3700f775eba09d81a1432444f82d9ad6270320c56c1a733d71cceb3a",
|
||||
"sha256:f59698cc0366395ca79b48b080674973541aae290670c57d88f05d939a4c00da"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.9"
|
||||
},
|
||||
"zope.interface": {
|
||||
"hashes": [
|
||||
"sha256:05a97ba92c1c7c26f25c9f671aa1ef85ffead6cdad13770e5b689cf983adc7e1",
|
||||
@ -1247,6 +1278,7 @@
|
||||
"sha256:f37d45fab14ffef9d33a0dc3bc59ce0c5313e2253323312d47739192da94f5fd",
|
||||
"sha256:f44906f70205d456d503105023041f1e63aece7623b31c390a0103db4de17537"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==5.2.0"
|
||||
}
|
||||
},
|
||||
@ -1260,16 +1292,18 @@
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:a5098bc870b80e7b872bff60bb363c7f2c2c89078759f6c47b53ff8c525a152e",
|
||||
"sha256:cd88907ecaec59d78e4ac00ea665b03e571cb37e3a0e37b3702af1a9e86c365a"
|
||||
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
|
||||
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
|
||||
],
|
||||
"version": "==3.3.0"
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.3.1"
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:4c17cea3e592c21b6e222f673868961bad77e1f985cb1694ed077475a89229c1",
|
||||
"sha256:d8506842a3faf734b81599c8b98dcc423de863adcc1999248480b18bd31a0f38"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.4.1"
|
||||
},
|
||||
"attrs": {
|
||||
@ -1277,6 +1311,7 @@
|
||||
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
|
||||
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.3.0"
|
||||
},
|
||||
"autopep8": {
|
||||
@ -1306,6 +1341,7 @@
|
||||
"sha256:37f927ea17cde7ae2d7baf832f8e80ce3777624554a653006c9144f8017fe410",
|
||||
"sha256:762cb2bfad61f4ec8e2bdf452c7c267416f8c70dd9ecb1653fd0bbb01fa936e6"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"bumpversion": {
|
||||
@ -1321,6 +1357,7 @@
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"colorama": {
|
||||
@ -1399,6 +1436,7 @@
|
||||
"sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839",
|
||||
"sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.8.4"
|
||||
},
|
||||
"flake8-polyfill": {
|
||||
@ -1413,6 +1451,7 @@
|
||||
"sha256:91f36bfb1ab7949b3b40e23736db18231bf7593edada2ba5c3a174a7b23657ac",
|
||||
"sha256:c9e1f2d0db7ddb9a704c2a0217be31214e91a4fe1dea1efad19ae42ba0c285c9"
|
||||
],
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==4.0.5"
|
||||
},
|
||||
"gitpython": {
|
||||
@ -1420,6 +1459,7 @@
|
||||
"sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b",
|
||||
"sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8"
|
||||
],
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==3.1.11"
|
||||
},
|
||||
"iniconfig": {
|
||||
@ -1434,6 +1474,7 @@
|
||||
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
||||
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==4.3.21"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
@ -1460,6 +1501,7 @@
|
||||
"sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
|
||||
"sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.4.3"
|
||||
},
|
||||
"mccabe": {
|
||||
@ -1496,6 +1538,7 @@
|
||||
"sha256:5fad80b613c402d5b7df7bd84812548b2a61e9977387a80a5fc5c396492b13c9",
|
||||
"sha256:b236cde0ac9a6aedd5e3c34517b423cd4fd97ef723849da6b0d2231142d89c00"
|
||||
],
|
||||
"markers": "python_version >= '2.6'",
|
||||
"version": "==5.5.1"
|
||||
},
|
||||
"pep8-naming": {
|
||||
@ -1510,20 +1553,22 @@
|
||||
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
|
||||
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.13.1"
|
||||
},
|
||||
"prospector": {
|
||||
"hashes": [
|
||||
"sha256:43e5e187c027336b0e4c4aa6a82d66d3b923b5ec5b51968126132e32f9d14a2f"
|
||||
"sha256:700d7918d93d73035a2a58fb18c6be0b609a0481fc6e0908843fa856b89e52c6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.0"
|
||||
"version": "==1.3.1"
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
|
||||
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.9.0"
|
||||
},
|
||||
"pycodestyle": {
|
||||
@ -1531,6 +1576,7 @@
|
||||
"sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367",
|
||||
"sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"pydocstyle": {
|
||||
@ -1538,6 +1584,7 @@
|
||||
"sha256:19b86fa8617ed916776a11cd8bc0197e5b9856d5433b777f51a3defe13075325",
|
||||
"sha256:aca749e190a01726a4fb472dd4ef23b5c9da7b9205c0a7857c06533de13fd678"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==5.1.1"
|
||||
},
|
||||
"pyflakes": {
|
||||
@ -1545,15 +1592,16 @@
|
||||
"sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92",
|
||||
"sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:b95e31850f3af163c2283ed40432f053acbc8fc6eba6a069cb518d9dbf71848c",
|
||||
"sha256:dd506acce0427e9e08fb87274bcaa953d38b50a58207170dbf5b36cf3e16957b"
|
||||
"sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc",
|
||||
"sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.5.2"
|
||||
"version": "==2.5.3"
|
||||
},
|
||||
"pylint-celery": {
|
||||
"hashes": [
|
||||
@ -1563,11 +1611,11 @@
|
||||
},
|
||||
"pylint-django": {
|
||||
"hashes": [
|
||||
"sha256:06a64331c498a3f049ba669dc0c174b92209e164198d43e589b1096ee616d5f8",
|
||||
"sha256:3d3436ba8d0fae576ae2db160e33a8f2746a101fda4463f2b3ff3a8b6fccec38"
|
||||
"sha256:b7756844dba0cecd3471056a1ef4154439defedaba38bf3ced9f848d2bf6297c",
|
||||
"sha256:ca32277c77878dd3c2d9e75f3f3f7f0c0712f053f10ff1b946cdc27367a6c911"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0.15"
|
||||
"version": "==2.1.0"
|
||||
},
|
||||
"pylint-flask": {
|
||||
"hashes": [
|
||||
@ -1587,6 +1635,7 @@
|
||||
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.4.7"
|
||||
},
|
||||
"pytest": {
|
||||
@ -1631,51 +1680,49 @@
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a",
|
||||
"sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f",
|
||||
"sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb",
|
||||
"sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5",
|
||||
"sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de",
|
||||
"sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c",
|
||||
"sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0",
|
||||
"sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c",
|
||||
"sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64",
|
||||
"sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53",
|
||||
"sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12",
|
||||
"sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740",
|
||||
"sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c",
|
||||
"sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd",
|
||||
"sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504",
|
||||
"sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427",
|
||||
"sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b",
|
||||
"sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e",
|
||||
"sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582",
|
||||
"sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0",
|
||||
"sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c",
|
||||
"sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9",
|
||||
"sha256:9b6305295b6591e45f069d3553c54d50cc47629eb5c218aac99e0f7fafbf90a1",
|
||||
"sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0",
|
||||
"sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf",
|
||||
"sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898",
|
||||
"sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd",
|
||||
"sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d",
|
||||
"sha256:bd904c0dec29bbd0769887a816657491721d5f545c29e30fd9d7a1a275dc80ab",
|
||||
"sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f",
|
||||
"sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e",
|
||||
"sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786",
|
||||
"sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b",
|
||||
"sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de",
|
||||
"sha256:c454ad88e56e80e44f824ef8366bb7e4c3def12999151fd5c0ea76a18fe9aa3e",
|
||||
"sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789",
|
||||
"sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520",
|
||||
"sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa",
|
||||
"sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b",
|
||||
"sha256:de7fd57765398d141949946c84f3590a68cf5887dac3fc52388df0639b01eda4",
|
||||
"sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625",
|
||||
"sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d",
|
||||
"sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26"
|
||||
"sha256:02951b7dacb123d8ea6da44fe45ddd084aa6777d4b2454fa0da61d569c6fa538",
|
||||
"sha256:0d08e71e70c0237883d0bef12cad5145b84c3705e9c6a588b2a9c7080e5af2a4",
|
||||
"sha256:1862a9d9194fae76a7aaf0150d5f2a8ec1da89e8b55890b1786b8f88a0f619dc",
|
||||
"sha256:1ab79fcb02b930de09c76d024d279686ec5d532eb814fd0ed1e0051eb8bd2daa",
|
||||
"sha256:1fa7ee9c2a0e30405e21031d07d7ba8617bc590d391adfc2b7f1e8b99f46f444",
|
||||
"sha256:262c6825b309e6485ec2493ffc7e62a13cf13fb2a8b6d212f72bd53ad34118f1",
|
||||
"sha256:2a11a3e90bd9901d70a5b31d7dd85114755a581a5da3fc996abfefa48aee78af",
|
||||
"sha256:2c99e97d388cd0a8d30f7c514d67887d8021541b875baf09791a3baad48bb4f8",
|
||||
"sha256:3128e30d83f2e70b0bed9b2a34e92707d0877e460b402faca908c6667092ada9",
|
||||
"sha256:38c8fd190db64f513fe4e1baa59fed086ae71fa45083b6936b52d34df8f86a88",
|
||||
"sha256:3bddc701bdd1efa0d5264d2649588cbfda549b2899dc8d50417e47a82e1387ba",
|
||||
"sha256:4902e6aa086cbb224241adbc2f06235927d5cdacffb2425c73e6570e8d862364",
|
||||
"sha256:49cae022fa13f09be91b2c880e58e14b6da5d10639ed45ca69b85faf039f7a4e",
|
||||
"sha256:56e01daca75eae420bce184edd8bb341c8eebb19dd3bce7266332258f9fb9dd7",
|
||||
"sha256:5862975b45d451b6db51c2e654990c1820523a5b07100fc6903e9c86575202a0",
|
||||
"sha256:6a8ce43923c518c24a2579fda49f093f1397dad5d18346211e46f134fc624e31",
|
||||
"sha256:6c54ce4b5d61a7129bad5c5dc279e222afd00e721bf92f9ef09e4fae28755683",
|
||||
"sha256:6e4b08c6f8daca7d8f07c8d24e4331ae7953333dbd09c648ed6ebd24db5a10ee",
|
||||
"sha256:717881211f46de3ab130b58ec0908267961fadc06e44f974466d1887f865bd5b",
|
||||
"sha256:749078d1eb89484db5f34b4012092ad14b327944ee7f1c4f74d6279a6e4d1884",
|
||||
"sha256:7913bd25f4ab274ba37bc97ad0e21c31004224ccb02765ad984eef43e04acc6c",
|
||||
"sha256:7a25fcbeae08f96a754b45bdc050e1fb94b95cab046bf56b016c25e9ab127b3e",
|
||||
"sha256:83d6b356e116ca119db8e7c6fc2983289d87b27b3fac238cfe5dca529d884562",
|
||||
"sha256:8b882a78c320478b12ff024e81dc7d43c1462aa4a3341c754ee65d857a521f85",
|
||||
"sha256:8f6a2229e8ad946e36815f2a03386bb8353d4bde368fdf8ca5f0cb97264d3b5c",
|
||||
"sha256:9801c4c1d9ae6a70aeb2128e5b4b68c45d4f0af0d1535500884d644fa9b768c6",
|
||||
"sha256:a15f64ae3a027b64496a71ab1f722355e570c3fac5ba2801cafce846bf5af01d",
|
||||
"sha256:a3d748383762e56337c39ab35c6ed4deb88df5326f97a38946ddd19028ecce6b",
|
||||
"sha256:a63f1a07932c9686d2d416fb295ec2c01ab246e89b4d58e5fa468089cab44b70",
|
||||
"sha256:b2b1a5ddae3677d89b686e5c625fc5547c6e492bd755b520de5332773a8af06b",
|
||||
"sha256:b2f4007bff007c96a173e24dcda236e5e83bde4358a557f9ccf5e014439eae4b",
|
||||
"sha256:baf378ba6151f6e272824b86a774326f692bc2ef4cc5ce8d5bc76e38c813a55f",
|
||||
"sha256:bafb01b4688833e099d79e7efd23f99172f501a15c44f21ea2118681473fdba0",
|
||||
"sha256:bba349276b126947b014e50ab3316c027cac1495992f10e5682dc677b3dfa0c5",
|
||||
"sha256:c084582d4215593f2f1d28b65d2a2f3aceff8342aa85afd7be23a9cad74a0de5",
|
||||
"sha256:d1ebb090a426db66dd80df8ca85adc4abfcbad8a7c2e9a5ec7513ede522e0a8f",
|
||||
"sha256:d2d8ce12b7c12c87e41123997ebaf1a5767a5be3ec545f64675388970f415e2e",
|
||||
"sha256:e32f5f3d1b1c663af7f9c4c1e72e6ffe9a78c03a31e149259f531e0fed826512",
|
||||
"sha256:e3faaf10a0d1e8e23a9b51d1900b72e1635c2d5b0e1bea1c18022486a8e2e52d",
|
||||
"sha256:f7d29a6fc4760300f86ae329e3b6ca28ea9c20823df123a2ea8693e967b29917",
|
||||
"sha256:f8f295db00ef5f8bae530fc39af0b40486ca6068733fb860b42115052206466f"
|
||||
],
|
||||
"version": "==2020.10.28"
|
||||
"version": "==2020.11.13"
|
||||
},
|
||||
"requirements-detector": {
|
||||
"hashes": [
|
||||
@ -1702,6 +1749,7 @@
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"smmap": {
|
||||
@ -1709,6 +1757,7 @@
|
||||
"sha256:54c44c197c819d5ef1991799a7e30b662d1e520f2ac75c9efbeb54a742214cf4",
|
||||
"sha256:9c98bbd1f9786d22f14b3d4126894d56befb835ec90cef151af566c7e19b5d24"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
@ -1723,6 +1772,7 @@
|
||||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"stevedore": {
|
||||
@ -1730,6 +1780,7 @@
|
||||
"sha256:5e1ab03eaae06ef6ce23859402de785f08d97780ed774948ef16c4652c41bc62",
|
||||
"sha256:f845868b3a3a77a2489d226568abe7328b5c2d4f6a011cc759dfa99144a521f0"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.2.2"
|
||||
},
|
||||
"toml": {
|
||||
@ -1737,6 +1788,7 @@
|
||||
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
|
||||
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"typed-ast": {
|
||||
@ -1787,12 +1839,11 @@
|
||||
"secure"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
|
||||
"sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
|
||||
"sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
|
||||
"sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": null,
|
||||
"version": "==1.25.11"
|
||||
"version": "==1.26.2"
|
||||
},
|
||||
"wrapt": {
|
||||
"hashes": [
|
||||
|
||||
12
README.md
12
README.md
@ -1,4 +1,4 @@
|
||||
<img src="docs/images/logo.svg" height="50" alt="passbook logo"><img src="docs/images/brand_inverted.svg" height="50" alt="passbook">
|
||||
<img src="website/static/img/logo.svg" height="50" alt="passbook logo"><img src="website/static/img/brand_inverted.svg" height="50" alt="passbook">
|
||||
|
||||
[](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=1)
|
||||

|
||||
@ -13,18 +13,18 @@ passbook is an open-source Identity Provider focused on flexibility and versatil
|
||||
|
||||
## Installation
|
||||
|
||||
For small/test setups it is recommended to use docker-compose, see the [documentation](https://passbook.beryju.org/installation/docker-compose/)
|
||||
For small/test setups it is recommended to use docker-compose, see the [documentation](https://passbook.beryju.org/website/docs/installation/docker-compose/)
|
||||
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org//installation/kubernetes/)
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org/website/docs/installation/kubernetes/)
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Development
|
||||
|
||||
To develop on passbook, you need a system with Python 3.7+ (3.8 is recommended). passbook uses [pipenv](https://pipenv.pypa.io/en/latest/) for managing dependencies.
|
||||
To develop on passbook, you need a system with Python 3.8+ (3.9 is recommended). passbook uses [pipenv](https://pipenv.pypa.io/en/latest/) for managing dependencies.
|
||||
|
||||
To get started, run
|
||||
|
||||
|
||||
@ -22,10 +22,11 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -37,10 +38,11 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -52,10 +54,11 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
pipenv install --dev prospector --skip-lock
|
||||
@ -68,10 +71,11 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -86,13 +90,14 @@ stages:
|
||||
version: '12.x'
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: npm install -g pyright@1.1.79
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -106,7 +111,7 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
@ -116,6 +121,7 @@ stages:
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -128,6 +134,9 @@ stages:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
@ -139,6 +148,7 @@ stages:
|
||||
inputs:
|
||||
script: |
|
||||
git checkout $(git describe --abbrev=0 --match 'version/*')
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -162,7 +172,7 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
@ -179,6 +189,7 @@ stages:
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: CmdLine@2
|
||||
@ -204,7 +215,7 @@ stages:
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: DockerCompose@0
|
||||
displayName: Run services
|
||||
inputs:
|
||||
@ -221,6 +232,7 @@ stages:
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
- task: DockerCompose@0
|
||||
@ -286,10 +298,11 @@ stages:
|
||||
path: "coverage-unittest/"
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
versionSpec: '3.8'
|
||||
versionSpec: '3.9'
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
sudo apt install -y libxmlsec1-dev pkg-config
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
|
||||
|
||||
@ -19,7 +19,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.9-stable}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.11-stable}
|
||||
command: server
|
||||
environment:
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
@ -40,7 +40,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
worker:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.9-stable}
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.12.11-stable}
|
||||
command: worker
|
||||
networks:
|
||||
- internal
|
||||
@ -54,7 +54,7 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
static:
|
||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.9-stable}
|
||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.12.11-stable}
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
|
||||
@ -1,26 +0,0 @@
|
||||
# Passbook User Object
|
||||
|
||||
The User object has the following attributes:
|
||||
|
||||
- `username`: User's username.
|
||||
- `email` User's email.
|
||||
- `name` User's display name.
|
||||
- `is_staff` Boolean field if user is staff.
|
||||
- `is_active` Boolean field if user is active.
|
||||
- `date_joined` Date user joined/was created.
|
||||
- `password_change_date` Date password was last changed.
|
||||
- `attributes` Dynamic attributes.
|
||||
- `pb_groups` This is a queryset of all the user's groups.
|
||||
|
||||
You can do additional filtering like `user.pb_groups.filter(name__startswith='test')`, see [here](https://docs.djangoproject.com/en/3.1/ref/models/querysets/#id4)
|
||||
|
||||
To get the name of all groups, you can do `[group.name for group in user.pb_groups.all()]`
|
||||
|
||||
## Examples
|
||||
|
||||
List all the User's group names:
|
||||
|
||||
```python
|
||||
for group in user.pb_groups.all():
|
||||
yield group.name
|
||||
```
|
||||
@ -1,7 +0,0 @@
|
||||
# OTP Stage
|
||||
|
||||
This stage offers a generic Time-based One-time Password authentication step.
|
||||
|
||||
You can optionally enforce this step, which will force every user without OTP setup to configure it.
|
||||
|
||||
This stage uses a 6-digit Code with a 30 second time-drift. This is currently not changeable.
|
||||
@ -1,8 +0,0 @@
|
||||
# User Delete Stage
|
||||
|
||||
!!! danger
|
||||
This stage deletes the `pending_user` without any confirmation. You have to make sure the user is aware of this.
|
||||
|
||||
This stage is intended for an unenrollment flow. It deletes the currently pending user.
|
||||
|
||||
The pending user is also removed from the current session.
|
||||
@ -1,73 +0,0 @@
|
||||
# Kubernetes
|
||||
|
||||
For a mid to high-load installation, Kubernetes is recommended. passbook is installed using a helm-chart.
|
||||
|
||||
This installation automatically applies database migrations on startup. After the installation is done, you can use `pbadmin` as username and password.
|
||||
|
||||
```yaml
|
||||
###################################
|
||||
# Values directly affecting passbook
|
||||
###################################
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.12.9-stable
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
|
||||
# Enable the Kubernetes integration which lets passbook deploy outposts into kubernetes
|
||||
kubernetesIntegration: true
|
||||
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
errorReporting:
|
||||
enabled: false
|
||||
environment: customer
|
||||
sendPii: false
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
logLevel: warning
|
||||
|
||||
# Enable Database Backups to S3
|
||||
# backup:
|
||||
# accessKey: access-key
|
||||
# secretKey: secret-key
|
||||
# bucket: s3-bucket
|
||||
# region: eu-central-1
|
||||
# host: s3-host
|
||||
|
||||
ingress:
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - passbook.k8s.local
|
||||
|
||||
###################################
|
||||
# Values controlling dependencies
|
||||
###################################
|
||||
|
||||
install:
|
||||
postgresql: true
|
||||
redis: true
|
||||
|
||||
# These values influence the bundled postgresql and redis charts, but are also used by passbook to connect
|
||||
postgresql:
|
||||
postgresqlDatabase: passbook
|
||||
|
||||
redis:
|
||||
cluster:
|
||||
enabled: false
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
```
|
||||
@ -1,75 +0,0 @@
|
||||
# Ansible Tower / AWX Integration
|
||||
|
||||
## What is Tower
|
||||
|
||||
From https://docs.ansible.com/ansible/2.5/reference_appendices/tower.html
|
||||
|
||||
!!! note ""
|
||||
Ansible Tower (formerly ‘AWX’) is a web-based solution that makes Ansible even more easy to use for IT teams of all kinds. It’s designed to be the hub for all of your automation tasks.
|
||||
|
||||
Tower allows you to control access to who can access what, even allowing sharing of SSH credentials without someone being able to transfer those credentials. Inventory can be graphically managed or synced with a wide variety of cloud sources. It logs all of your jobs, integrates well with LDAP, and has an amazing browsable REST API. Command line tools are available for easy integration with Jenkins as well. Provisioning callbacks provide great support for autoscaling topologies.
|
||||
|
||||
!!! note
|
||||
AWX is the open-source version of Tower. The term "AWX" will be used interchangeably throughout this document.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `awx.company` is the FQDN of the AWX/Tower install.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML provider with the following parameters:
|
||||
|
||||
- ACS URL: `https://awx.company/sso/complete/saml/`
|
||||
- Audience: `awx`
|
||||
- Service Provider Binding: Post
|
||||
- Issuer: `https://awx.company/sso/metadata/saml/`
|
||||
|
||||
You can of course use a custom signing certificate, and adjust durations.
|
||||
|
||||
## AWX Configuration
|
||||
|
||||
Navigate to `https://awx.company/#/settings/auth` to configure SAML. Set the Field `SAML SERVICE PROVIDER ENTITY ID` to `awx`.
|
||||
|
||||
For the fields `SAML SERVICE PROVIDER PUBLIC CERTIFICATE` and `SAML SERVICE PROVIDER PRIVATE KEY`, you can either use custom certificates, or use the self-signed pair generated by passbook.
|
||||
|
||||
Provide metadata in the `SAML Service Provider Organization Info` field:
|
||||
|
||||
```json
|
||||
{
|
||||
"en-US": {
|
||||
"name": "passbook",
|
||||
"url": "https://passbook.company",
|
||||
"displayname": "passbook"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Provide metadata in the `SAML Service Provider Technical Contact` and `SAML Service Provider Technical Contact` fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"givenName": "Admin Name",
|
||||
"emailAddress": "admin@company"
|
||||
}
|
||||
```
|
||||
|
||||
In the `SAML Enabled Identity Providers` paste the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"passbook": {
|
||||
"attr_username": "urn:oid:2.16.840.1.113730.3.1.241",
|
||||
"attr_user_permanent_id": "urn:oid:0.9.2342.19200300.100.1.1",
|
||||
"x509cert": "MIIDEjCCAfqgAwIBAgIRAJZ9pOZ1g0xjiHtQAAejsMEwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAwwlcGFzc2Jvb2sgU2VsZi1zaWduZWQgU0FNTCBDZXJ0aWZpY2F0ZTAeFw0xOTEyMjYyMDEwNDFaFw0yMDEyMjYyMDEwNDFaMFkxLjAsBgNVBAMMJXBhc3Nib29rIFNlbGYtc2lnbmVkIFNBTUwgQ2VydGlmaWNhdGUxETAPBgNVBAoMCHBhc3Nib29rMRQwEgYDVQQLDAtTZWxmLXNpZ25lZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO/ktBYZkY9xAijF4acvzX6Q1K8KoIZeyde8fVgcWBz4L5FgDQ4/dni4k2YAcPdwteGL4nKVzetUzjbRCBUNuO6lqU4J4WNNX4Xg4Ir7XLRoAQeo+omTPBdpJ1p02HjtN5jT01umN3bK2yto1e37CJhK6WJiaXqRewPxh4lI4aqdj3BhFkJ3I3r2qxaWOAXQ6X7fg3w/ny7QP53//ouZo7hSLY3GIcRKgvdjjVM3OW5C3WLpOq5Dez5GWVJ17aeFCfGQ8bwFKde6qfYqyGcU9xHB36TtVHB9hSFP/tUFhkiSOxtsrYwCgCyXm4UTSpP+wiNyjKfFw7qGLBvA2hGTNw8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh9PeAqPRQk1/SSygIFADZBi08O/DPCshFwEHvJATIcTzcDD8UGAjXh+H5OlkDyX7KyrcaNvYaafCUo63A+WprdtdY5Ty6SBEwTYyiQyQfwM9BfK+imCoif1Ai7xAelD7p9lNazWq7JU+H/Ep7U7Q7LvpxAbK0JArt+IWTb2NcMb3OWE1r0gFbs44O1l6W9UbJTbyLMzbGbe5i+NHlgnwPwuhtRMh0NUYabGHKcHbhwyFhfGAQv2dAp5KF1E5gu6ZzCiFePzc0FrqXQyb2zpFYcJHXquiqaOeG7cZxRHYcjrl10Vxzki64XVA9BpdELgKSnupDGUEJsRUt3WVOmvZuA==",
|
||||
"url": "https://passbook.company/application/saml/awx/login/",
|
||||
"attr_last_name": "User.LastName",
|
||||
"entity_id": "https://awx.company/sso/metadata/saml/",
|
||||
"attr_email": "urn:oid:0.9.2342.19200300.100.1.3",
|
||||
"attr_first_name": "urn:oid:2.5.4.3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`x509cert` is the certificate configured in passbook. Remove the `--BEGIN CERTIFICATE--` and `--END CERTIFICATE--` headers, then enter the cert as one non-breaking string.
|
||||
@ -1,83 +0,0 @@
|
||||
# VMware vCenter Integration
|
||||
|
||||
## What is vCenter
|
||||
|
||||
From https://en.wikipedia.org/wiki/VCenter
|
||||
|
||||
!!! note ""
|
||||
|
||||
vCenter Server is the centralized management utility for VMware, and is used to manage virtual machines, multiple ESXi hosts, and all dependent components from a single centralized location. VMware vMotion and svMotion require the use of vCenter and ESXi hosts.
|
||||
|
||||
!!! warning
|
||||
|
||||
This requires passbook 0.10.3 or newer.
|
||||
|
||||
!!! warning
|
||||
|
||||
This requires VMware vCenter 7.0.0 or newer.
|
||||
|
||||
!!! note
|
||||
|
||||
It seems that the vCenter still needs to be joined to the Active Directory Domain, otherwise group membership does not work correctly. We're working on a fix for this, for the meantime your vCenter should be part of your Domain.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `vcenter.company` is the FQDN of the vCenter server.
|
||||
- `passbook.company` is the FQDN of the passbook install.
|
||||
|
||||
Since vCenter only allows OpenID-Connect in combination with Active Directory, it is recommended to have passbook sync with the same Active Directory.
|
||||
|
||||
### Step 1
|
||||
|
||||
Under *Property Mappings*, create a *Scope Mapping*. Give it a name like "OIDC-Scope-VMware-vCenter". Set the scope name to `openid` and the expression to the following
|
||||
|
||||
```python
|
||||
return {
|
||||
"domain": "<your active directory domain>",
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2
|
||||
|
||||
!!! note
|
||||
If your Active Directory Schema is the same as your Email address schema, skip to Step 3.
|
||||
|
||||
Under *Sources*, click *Edit* and ensure that "Autogenerated Active Directory Mapping: userPrincipalName -> attributes.upn" has been added to your source.
|
||||
|
||||
### Step 3
|
||||
|
||||
Under *Providers*, create an OAuth2/OpenID Provider with these settings:
|
||||
|
||||
- Client Type: Confidential
|
||||
- Response Type: code (ADFS Compatibility Mode, sends id_token as access_token)
|
||||
- JWT Algorithm: RS256
|
||||
- Redirect URI: `https://vcenter.company/ui/login/oauth2/authcode`
|
||||
- Post Logout Redirect URIs: `https://vcenter.company/ui/login`
|
||||
- Sub Mode: If your Email address Schema matches your UPN, select "Based on the User's Email...", otherwise select "Based on the User's UPN...".
|
||||
- Scopes: Select the Scope Mapping you've created in Step 1
|
||||
|
||||

|
||||
|
||||
### Step 4
|
||||
|
||||
Create an application which uses this provider. Optionally apply access restrictions to the application.
|
||||
|
||||
Set the Launch URL to `https://vcenter.company/ui/login/oauth2`. This will skip vCenter's User Prompt and directly log you in.
|
||||
|
||||
## vCenter Setup
|
||||
|
||||
Login as local Administrator account (most likely ends with vsphere.local). Using the Menu in the Navigation bar, navigate to *Administration -> Single Sing-on -> Configuration*.
|
||||
|
||||
Click on *Change Identity Provider* in the top-right corner.
|
||||
|
||||
In the wizard, select "Microsoft ADFS" and click Next.
|
||||
|
||||
Fill in the Client Identifier and Shared Secret from the Provider in passbook. For the OpenID Address, click on *View Setup URLs* in passbook, and copy the OpenID Configuration URL.
|
||||
|
||||
On the next page, fill in your Active Directory Connection Details. These should be similar to what you have set in passbook.
|
||||
|
||||

|
||||
|
||||
If your vCenter was already setup with LDAP beforehand, your Role assignments will continue to work.
|
||||
@ -1,20 +0,0 @@
|
||||
# Outpost deployment in docker-compose
|
||||
|
||||
To deploy an outpost with docker-compose, use this snippet in your docker-compose file.
|
||||
|
||||
You can also run the outpost in a separate docker-compose project, you just have to ensure that the outpost container can reach your application container.
|
||||
|
||||
```yaml
|
||||
version: '3.5'
|
||||
|
||||
services:
|
||||
passbook_proxy:
|
||||
image: beryju/passbook-proxy:0.10.0-stable
|
||||
ports:
|
||||
- 4180:4180
|
||||
- 4443:4443
|
||||
environment:
|
||||
PASSBOOK_HOST: https://your-passbook.tld
|
||||
PASSBOOK_INSECURE: 'false'
|
||||
PASSBOOK_TOKEN: token-generated-by-passbook
|
||||
```
|
||||
@ -1,99 +0,0 @@
|
||||
# Outpost deployment on Kubernetes
|
||||
|
||||
Use the following manifest, replacing all values surrounded with `__`.
|
||||
|
||||
Afterwards, configure the proxy provider to connect to `<service name>.<namespace>.svc.cluster.local`, and update your Ingress to connect to the `passbook-outpost` service.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
name: passbook-outpost-api
|
||||
stringData:
|
||||
passbook_host: '__PASSBOOK_URL__'
|
||||
passbook_host_insecure: 'true'
|
||||
token: '__PASSBOOK_TOKEN__'
|
||||
type: Opaque
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
name: passbook-outpost
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
port: 4180
|
||||
protocol: TCP
|
||||
targetPort: http
|
||||
- name: https
|
||||
port: 4443
|
||||
protocol: TCP
|
||||
targetPort: https
|
||||
selector:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
name: passbook-outpost
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: test
|
||||
app.kubernetes.io/managed-by: passbook.beryju.org
|
||||
app.kubernetes.io/name: passbook-proxy
|
||||
app.kubernetes.io/version: 0.10.0
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: PASSBOOK_HOST
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: passbook_host
|
||||
name: passbook-outpost-api
|
||||
- name: PASSBOOK_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: token
|
||||
name: passbook-outpost-api
|
||||
- name: PASSBOOK_INSECURE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: passbook_host_insecure
|
||||
name: passbook-outpost-api
|
||||
image: beryju/passbook-proxy:0.10.0-stable
|
||||
name: proxy
|
||||
ports:
|
||||
- containerPort: 4180
|
||||
name: http
|
||||
protocol: TCP
|
||||
- containerPort: 4443
|
||||
name: https
|
||||
protocol: TCP
|
||||
```
|
||||
@ -1,14 +0,0 @@
|
||||
# Outposts
|
||||
|
||||
An outpost is a single deployment of a passbook component, which can be deployed in a completely separate environment. Currently, only the Proxy Provider is supported as outpost.
|
||||
|
||||

|
||||
|
||||
Upon creation, a service account and a token is generated. The service account only has permissions to read the outpost and provider configuration. This token is used by the Outpost to connect to passbook.
|
||||
|
||||
To deploy an outpost, see: <a name="deploy">
|
||||
|
||||
- [Kubernetes](deploy-kubernetes.md)
|
||||
- [docker-compose](deploy-docker-compose.md)
|
||||
|
||||
In future versions, this snippet will be automatically generated. You will also be able to deploy an outpost directly into a kubernetes cluster.
|
||||
@ -1,41 +0,0 @@
|
||||
# Expression Policies
|
||||
|
||||
!!! notice
|
||||
These variables are available in addition to the common variables/functions defined in [**Expressions**](../expressions/index.md)
|
||||
|
||||
The passing of the policy is determined by the return value of the code. Use `return True` to pass a policy and `return False` to fail it.
|
||||
|
||||
### Available Functions
|
||||
|
||||
#### `pb_message(message: str)`
|
||||
|
||||
Add a message, visible by the end user. This can be used to show the reason why they were denied.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
pb_message("Access denied")
|
||||
return False
|
||||
```
|
||||
|
||||
### Context variables
|
||||
|
||||
- `request`: A PolicyRequest object, which has the following properties:
|
||||
- `request.user`: The current user, against which the policy is applied. ([ref](../expressions/reference/user-object.md))
|
||||
- `request.http_request`: The Django HTTP Request. ([ref](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects))
|
||||
- `request.obj`: A Django Model instance. This is only set if the policy is ran against an object.
|
||||
- `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_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), for example
|
||||
|
||||
```python
|
||||
return pb_client_ip in ip_network('10.0.0.0/24')
|
||||
```
|
||||
|
||||
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
|
||||
@ -1,12 +0,0 @@
|
||||
# Property Mapping Expressions
|
||||
|
||||
The property mapping should return a value that is expected by the Provider/Source. Supported types are documented in the individual Provider/Source. Returning `None` is always accepted and would simply skip the mapping for which `None` was returned.
|
||||
|
||||
!!! notice
|
||||
These variables are available in addition to the common variables/functions defined in [**Expressions**](../expressions/index.md)
|
||||
|
||||
### Context Variables
|
||||
|
||||
- `user`: The current user. This may be `None` if there is no contextual user. ([ref](../expressions/reference/user-object.md))
|
||||
- `request`: The current request. This may be `None` if there is no contextual request. ([ref](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects))
|
||||
- Other arbitrary arguments given by the provider, this is documented on the Provider/Source.
|
||||
@ -1,31 +0,0 @@
|
||||
# OAuth2 Provider
|
||||
|
||||
This provider supports both generic OAuth2 as well as OpenID Connect
|
||||
|
||||
Scopes can be configured using Scope Mappings, a type of [Property Mappings](../property-mappings/index.md#scope-mapping).
|
||||
|
||||
Endpoint | URL
|
||||
---------|---
|
||||
Authorization | `/application/o/authorize/`
|
||||
Token | `/application/o/token/`
|
||||
User Info | `/application/o/userinfo/`
|
||||
End Session | `/application/o/end-session/`
|
||||
Introspect | `/application/o/end-session/`
|
||||
JWKS | `/application/o/<application slug>/jwks/`
|
||||
OpenID Configuration | `/application/o/<application slug>/.well-known/openid-configuration`
|
||||
|
||||
## GitHub Compatibility
|
||||
|
||||
This provider also exposes a GitHub-compatible endpoint. This endpoint can be used by applications, which support authenticating against GitHub Enterprise, but not generic OpenID Connect.
|
||||
|
||||
To use any of the GitHub Compatibility scopes, you have to use the GitHub Compatibility Endpoints.
|
||||
|
||||
|
||||
Endpoint | URL
|
||||
---------|---
|
||||
Authorization | `/login/oauth/authorize`
|
||||
Token | `/login/oauth/access_token`
|
||||
User Info | `/user`
|
||||
User Teams Info | `/user/teams`
|
||||
|
||||
To access the user's email address, a scope of `user:email` is required. To access their groups, `read:org` is required. Because these scopes are handled by a different endpoint, they are not customisable as a Scope Mapping.
|
||||
@ -1,24 +0,0 @@
|
||||
# Proxy Provider
|
||||
|
||||
!!! info
|
||||
This provider is to be used in conjunction with [Outposts](../outposts/outposts.md)
|
||||
|
||||
This provider protects applications, which have no built-in support for OAuth2 or SAML. This is done by running a lightweight Reverse Proxy in front of the application, which authenticates the requests.
|
||||
|
||||
passbook Proxy is based on [oauth2_proxy](https://github.com/oauth2-proxy/oauth2-proxy), but has been integrated more tightly with passbook.
|
||||
|
||||
The Proxy these extra headers to the application:
|
||||
|
||||
Header Name | Value
|
||||
-------------|-------
|
||||
X-Forwarded-User | The user's unique identifier (**not the username**)
|
||||
X-Forwarded-Email | The user's email address
|
||||
X-Forwarded-Preferred-Username | The user's username
|
||||
X-Auth-Username | The user's username
|
||||
|
||||
Additionally, you can add more custom headers using `additionalHeaders` in the User or Group Properties, for example
|
||||
|
||||
```yaml
|
||||
additionalHeaders:
|
||||
X-additional-header: bar
|
||||
```
|
||||
@ -1,12 +0,0 @@
|
||||
# SAML Provider
|
||||
|
||||
This provider allows you to integrate enterprise software using the SAML2 Protocol. It supports signed requests and uses [Property Mappings](../property-mappings/index.md#saml-property-mapping) to determine which fields are exposed and what values they return. This makes it possible to expose vendor-specific fields.
|
||||
Default fields are exposed through auto-generated Property Mappings, which are prefixed with "Autogenerated".
|
||||
|
||||
|
||||
Endpoint | URL
|
||||
---------|---
|
||||
SSO (Redirect binding) | `/application/saml/<application slug>/sso/binding/redirect/`
|
||||
SSO (POST binding) | `/application/saml/<application slug>/sso/binding/post/`
|
||||
IdP-initiated login | `/application/saml/<application slug>/sso/binding/init/`
|
||||
Metadata Download | `/application/saml/<application slug>/metadata/`
|
||||
@ -1,2 +0,0 @@
|
||||
mkdocs
|
||||
mkdocs-material
|
||||
@ -1 +0,0 @@
|
||||
3.7
|
||||
@ -1,73 +0,0 @@
|
||||
# Upgrading to 0.10
|
||||
|
||||
This update brings a lot of big features, such as:
|
||||
|
||||
- New OAuth2/OpenID Provider
|
||||
|
||||
This new provider merges both OAuth2 and OpenID. It is based on the codebase of the old provider, which has been simplified and cleaned from the ground up. Support for Property Mappings has also been added. Because of this change, OpenID and OAuth2 Providers will have to be re-created.
|
||||
|
||||
- Proxy Provider
|
||||
|
||||
Due to this new OAuth2 Provider, the Application Gateway Provider, now simply called "Proxy Provider" has been revamped as well. The new passbook Proxy integrates more tightly with passbook via the new Outposts system. The new proxy also supports multiple applications per proxy instance, can configure TLS based on passbook Keypairs, and more.
|
||||
|
||||
See [Proxy](../providers/proxy.md)
|
||||
|
||||
- Outpost System
|
||||
|
||||
This is a new Object type, currently used only by the Proxy Provider. It manages the creation and permissions of service accounts, which are used by the outposts to communicate with passbook.
|
||||
|
||||
See [Outposts](../outposts/outposts.md)
|
||||
|
||||
- Flow Import/Export
|
||||
|
||||
Flows can now be imported and exported. This feature can be used as a backup system, or to share complex flows with other people. Example flows have also been added to the documentation to help you get going with passbook.
|
||||
|
||||
## Under the hood
|
||||
|
||||
- passbook now runs on Django 3.1 and Channels with complete ASGI enabled
|
||||
- uwsgi has been replaced with Gunicorn and uvicorn
|
||||
- Elastic APM has been replaced with Sentry Performance metrics
|
||||
- Flow title is now configurable separately from the name
|
||||
- All logging output is now json
|
||||
|
||||
## Upgrading
|
||||
|
||||
### docker-compose
|
||||
|
||||
The docker-compose file has been updated, please download the latest from `https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml`.
|
||||
By default, the new compose file uses a fixed version to prevent unintended updates.
|
||||
|
||||
Before updating the file, stop all containers. Then download the file, pull the new containers and start the database.
|
||||
|
||||
```
|
||||
docker-compose down
|
||||
docker-compose pull
|
||||
docker-compose up --no-start
|
||||
docker-compose start redis postgrseql
|
||||
docker-compose run --rm server migrate
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Helm
|
||||
|
||||
A few options have changed:
|
||||
|
||||
- `error_reporting` was changed from a simple boolean to a dictionary:
|
||||
|
||||
```yaml
|
||||
error_reporting:
|
||||
enabled: false
|
||||
environment: customer
|
||||
send_pii: false
|
||||
```
|
||||
|
||||
- The `apm` and `monitoring` blocks have been removed.
|
||||
- `serverReplicas` and `workerReplicas` have been added
|
||||
|
||||
### Upgrading
|
||||
|
||||
This upgrade only applies if you are upgrading from a running 0.9 instance. Passbook detects this on startup, and automatically executes this upgrade.
|
||||
|
||||
Because this upgrade brings the new OAuth2 Provider, the old providers will be lost in the process. Make sure to take note of the providers you want to bring over.
|
||||
|
||||
Another side-effect of this upgrade is the change of OAuth2 URLs, see [here](../providers/oauth2.md).
|
||||
@ -1,20 +0,0 @@
|
||||
# Upgrading to 0.11
|
||||
|
||||
This update brings these headline features:
|
||||
|
||||
- Add Backup and Restore, currently only externally schedulable, documented [here](https://passbook.beryju.org/maintenance/backups/)
|
||||
- New Admin Dashboard with more metrics and Charts
|
||||
|
||||
Shows successful and failed logins from the last 24 hours, as well as the most used applications
|
||||
- Add search to all table views
|
||||
- Outpost now supports a Docker Controller, which installs the Outpost on the same host as passbook, updates and manages it
|
||||
- Add Token Identifier
|
||||
|
||||
Tokens now have an identifier which is used to reference to them, so the Primary key is not shown in URLs
|
||||
- `core/applications/list` API now shows applications the user has access to via policies
|
||||
|
||||
## Upgrading
|
||||
|
||||
This upgrade can be done as with minor upgrades, the only external change is the new docker-compose file, which enabled the Docker Integration for Outposts. To use this feature, please download the latest docker-compose from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml).
|
||||
|
||||
Afterwards, you can simply run `docker-compose up -d` and then the normal upgrade command of `docker-compose run --rm server migrate`.
|
||||
@ -1,63 +0,0 @@
|
||||
# Upgrading to 0.12
|
||||
|
||||
This update brings these headline features:
|
||||
|
||||
- Rewrite Outpost state Logic, which now supports multiple concurrent Outpost instances.
|
||||
- Add Kubernetes Integration for Outposts, which deploys and maintains Outposts with High Availability in a Kubernetes Cluster
|
||||
- Add System Task Overview to see all background tasks, their status, the log output, and retry them
|
||||
- Alerts now disappear automatically
|
||||
- Audit Logs are now searchable
|
||||
- Users can now create their own Tokens to access the API
|
||||
- docker-compose deployment now uses traefik 2.3
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fix high CPU Usage of the proxy when Websocket connections fail
|
||||
|
||||
## Upgrading
|
||||
|
||||
### docker-compose
|
||||
|
||||
Docker-compose users should download the latest docker-compose file from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). This includes the new traefik 2.3.
|
||||
|
||||
Afterwards, you can simply run `docker-compose up -d` and then the normal upgrade command of `docker-compose run --rm server migrate`.
|
||||
|
||||
### Kubernetes
|
||||
|
||||
For Kubernetes users, there are some changes to the helm values.
|
||||
|
||||
The values change from
|
||||
|
||||
```yaml
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
error_reporting:
|
||||
enabled: false
|
||||
environment: customer
|
||||
send_pii: false
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
log_level: warning
|
||||
```
|
||||
|
||||
to
|
||||
|
||||
```yaml
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
errorReporting:
|
||||
enabled: false
|
||||
environment: customer
|
||||
sendPii: false
|
||||
# Log level used by web and worker
|
||||
# Can be either debug, info, warning, error
|
||||
logLevel: warning
|
||||
```
|
||||
|
||||
in order to be consistent with the rest of the settings.
|
||||
|
||||
There is also a new setting called `kubernetesIntegration`, which controls the Kubernetes integration for passbook. When enabled (the default), a Service Account is created, which allows passbook to deploy and update Outposts.
|
||||
@ -9,7 +9,7 @@ curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
sudo npm install -g yarn
|
||||
# Setup python
|
||||
sudo apt install -y python3.8 python3-pip
|
||||
sudo apt install -y python3.9 python3-pip libxmlsec1-dev pkg-config
|
||||
# Setup docker
|
||||
sudo pip3 install pipenv
|
||||
|
||||
|
||||
@ -156,8 +156,7 @@ def retry(max_retires=3, exceptions=None):
|
||||
raise exc
|
||||
logger.debug("Retrying on error", exc=exc, test=self)
|
||||
self.tearDown()
|
||||
# pylint: disable=protected-access
|
||||
self._post_teardown()
|
||||
self._post_teardown() # noqa
|
||||
self.setUp()
|
||||
return wrapper(self, *args, **kwargs)
|
||||
|
||||
|
||||
@ -4,8 +4,8 @@ name: passbook
|
||||
home: https://passbook.beryju.org
|
||||
sources:
|
||||
- https://github.com/BeryJu/passbook
|
||||
version: "0.12.9-stable"
|
||||
icon: https://raw.githubusercontent.com/BeryJu/passbook/master/docs/images/logo.svg
|
||||
version: "0.12.11-stable"
|
||||
icon: https://raw.githubusercontent.com/BeryJu/passbook/master/website/static/img/logo.svg
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 9.4.1
|
||||
|
||||
@ -25,4 +25,4 @@
|
||||
| install.redis | true | Enables/disables the packaged Redis Chart
|
||||
| postgresql.postgresqlPassword | | Password used for PostgreSQL, generated automatically.
|
||||
|
||||
For more info, see https://passbook.beryju.org/ and https://passbook.beryju.org/installation/kubernetes/
|
||||
For more info, see https://passbook.beryju.org/ and https://passbook.beryju.org/docs/installation/kubernetes/
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.12.9-stable
|
||||
tag: 0.12.11-stable
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"""Gunicorn config"""
|
||||
import os
|
||||
import warnings
|
||||
from multiprocessing import cpu_count
|
||||
from pathlib import Path
|
||||
@ -14,6 +15,8 @@ worker_class = "uvicorn.workers.UvicornWorker"
|
||||
# Docker containers don't have /tmp as tmpfs
|
||||
worker_tmp_dir = "/dev/shm"
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
|
||||
|
||||
logconfig_dict = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
|
||||
95
mkdocs.yml
95
mkdocs.yml
@ -1,95 +0,0 @@
|
||||
site_name: passbook Docs
|
||||
site_url: https://passbook.beryju.org/
|
||||
copyright: "Copyright © 2019 - 2020 BeryJu.org"
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Terminology: terminology.md
|
||||
- Installation:
|
||||
- docker-compose: installation/docker-compose.md
|
||||
- Kubernetes: installation/kubernetes.md
|
||||
- Reverse Proxy: installation/reverse-proxy.md
|
||||
- Flows:
|
||||
Overview: flow/flows.md
|
||||
Examples: flow/examples/examples.md
|
||||
- Stages:
|
||||
- Captcha Stage: flow/stages/captcha/index.md
|
||||
- Dummy Stage: flow/stages/dummy/index.md
|
||||
- Email Stage: flow/stages/email/index.md
|
||||
- Identification Stage: flow/stages/identification/index.md
|
||||
- Invitation Stage: flow/stages/invitation/index.md
|
||||
- OTP Stage: flow/stages/otp/index.md
|
||||
- Password Stage: flow/stages/password/index.md
|
||||
- Prompt Stage: flow/stages/prompt/index.md
|
||||
- Prompt Stage Validation: flow/stages/prompt/validation.md
|
||||
- User Delete Stage: flow/stages/user_delete.md
|
||||
- User Login Stage: flow/stages/user_login.md
|
||||
- User Logout Stage: flow/stages/user_logout.md
|
||||
- User Write Stage: flow/stages/user_write.md
|
||||
- Sources: sources.md
|
||||
- Providers:
|
||||
- OAuth2: providers/oauth2.md
|
||||
- SAML: providers/saml.md
|
||||
- Proxy: providers/proxy.md
|
||||
- Outposts:
|
||||
- Overview: outposts/outposts.md
|
||||
- Upgrading: outposts/upgrading.md
|
||||
- Deploy on docker-compose: outposts/deploy-docker-compose.md
|
||||
- Deploy on Kubernetes: outposts/deploy-kubernetes.md
|
||||
- Expressions:
|
||||
- Overview: expressions/index.md
|
||||
- Reference:
|
||||
- User Object: expressions/reference/user-object.md
|
||||
- Property Mappings:
|
||||
- Overview: property-mappings/index.md
|
||||
- Expressions: property-mappings/expression.md
|
||||
- Policies:
|
||||
- Overview: policies/index.md
|
||||
- Expression: policies/expression.md
|
||||
- Integrations:
|
||||
- as Source:
|
||||
- Active Directory: integrations/sources/active-directory/index.md
|
||||
- as Provider:
|
||||
- Amazon Web Services: integrations/services/aws/index.md
|
||||
- GitLab: integrations/services/gitlab/index.md
|
||||
- Rancher: integrations/services/rancher/index.md
|
||||
- Harbor: integrations/services/harbor/index.md
|
||||
- Sentry: integrations/services/sentry/index.md
|
||||
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
|
||||
- VMware vCenter: integrations/services/vmware-vcenter/index.md
|
||||
- Ubuntu Landscape: integrations/services/ubuntu-landscape/index.md
|
||||
- Sonarr: integrations/services/sonarr/index.md
|
||||
- Tautulli: integrations/services/tautulli/index.md
|
||||
- Maintenance:
|
||||
- Backups: maintenance/backups/index.md
|
||||
- Upgrading:
|
||||
- to 0.9: upgrading/to-0.9.md
|
||||
- to 0.10: upgrading/to-0.10.md
|
||||
- to 0.11: upgrading/to-0.11.md
|
||||
- to 0.12: upgrading/to-0.12.md
|
||||
- Troubleshooting:
|
||||
- Access problems: troubleshooting/access.md
|
||||
|
||||
repo_name: "BeryJu/passbook"
|
||||
repo_url: https://github.com/BeryJu/passbook
|
||||
theme:
|
||||
name: material
|
||||
logo: images/logo.svg
|
||||
favicon: images/logo.svg
|
||||
palette:
|
||||
scheme: slate
|
||||
primary: white
|
||||
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
permalink: "¶"
|
||||
- admonition
|
||||
- codehilite
|
||||
- pymdownx.betterem:
|
||||
smart_enable: all
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.magiclink
|
||||
- attr_list
|
||||
|
||||
plugins:
|
||||
- search
|
||||
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.12.9-stable"
|
||||
__version__ = "0.12.11-stable"
|
||||
|
||||
@ -147,5 +147,5 @@ class FlowExportView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
flow: Flow = self.get_object()
|
||||
exporter = FlowExporter(flow)
|
||||
response = JsonResponse(exporter.export(), encoder=DataclassEncoder, safe=False)
|
||||
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.json"'
|
||||
response["Content-Disposition"] = f'attachment; filename="{flow.slug}.pbflow"'
|
||||
return response
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""Flow and Stage forms"""
|
||||
|
||||
from django import forms
|
||||
from django.core.validators import FileExtensionValidator
|
||||
from django.forms import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@ -62,7 +63,9 @@ class FlowStageBindingForm(forms.ModelForm):
|
||||
class FlowImportForm(forms.Form):
|
||||
"""Form used for flow importing"""
|
||||
|
||||
flow = forms.FileField()
|
||||
flow = forms.FileField(
|
||||
validators=[FileExtensionValidator(allowed_extensions=["pbflow"])]
|
||||
)
|
||||
|
||||
def clean_flow(self):
|
||||
"""Check if the flow is valid and rewind the file to the start"""
|
||||
|
||||
@ -12,7 +12,7 @@ class TestTransferDocs(TransactionTestCase):
|
||||
"""Empty class, test methods are added dynamically"""
|
||||
|
||||
|
||||
def generic_view_tester(file_name: str) -> Callable:
|
||||
def pbflow_tester(file_name: str) -> Callable:
|
||||
"""This is used instead of subTest for better visibility"""
|
||||
|
||||
def tester(self: TestTransferDocs):
|
||||
@ -24,8 +24,6 @@ def generic_view_tester(file_name: str) -> Callable:
|
||||
return tester
|
||||
|
||||
|
||||
for flow_file in glob("docs/flow/examples/*.json"):
|
||||
for flow_file in glob("website/static/flows/*.pbflow"):
|
||||
method_name = Path(flow_file).stem.replace("-", "_").replace(".", "_")
|
||||
setattr(
|
||||
TestTransferDocs, f"test_flow_{method_name}", generic_view_tester(flow_file)
|
||||
)
|
||||
setattr(TestTransferDocs, f"test_flow_{method_name}", pbflow_tester(flow_file))
|
||||
|
||||
@ -30,6 +30,6 @@ passbook:
|
||||
# Optionally add links to the footer on the login page
|
||||
footer_links:
|
||||
- name: Documentation
|
||||
href: https://passbook.beryju.org/docs/
|
||||
- name: passbook Website
|
||||
href: https://passbook.beryju.org/
|
||||
# - name: test
|
||||
# href: https://test
|
||||
|
||||
@ -27,7 +27,7 @@ class BaseEvaluator:
|
||||
|
||||
def __init__(self):
|
||||
# update passbook/policies/expression/templates/policy/expression/form.html
|
||||
# update docs/policies/expression/index.md
|
||||
# update website/docs/policies/expression.md
|
||||
self._globals = {
|
||||
"regex_match": BaseEvaluator.expr_filter_regex_match,
|
||||
"regex_replace": BaseEvaluator.expr_filter_regex_replace,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
"""passbook sentry integration"""
|
||||
from aioredis.errors import ReplyError
|
||||
from aioredis.errors import ConnectionClosedError, ReplyError
|
||||
from billiard.exceptions import WorkerLostError
|
||||
from botocore.client import ClientError
|
||||
from celery.exceptions import CeleryError
|
||||
@ -40,6 +40,7 @@ def before_send(event, hint):
|
||||
RedisError,
|
||||
ResponseError,
|
||||
ReplyError,
|
||||
ConnectionClosedError,
|
||||
# websocket errors
|
||||
ChannelFull,
|
||||
WebSocketException,
|
||||
|
||||
@ -8,6 +8,7 @@ from passbook.outposts.models import (
|
||||
DockerServiceConnection,
|
||||
KubernetesServiceConnection,
|
||||
Outpost,
|
||||
OutpostServiceConnection,
|
||||
)
|
||||
from passbook.providers.proxy.models import ProxyProvider
|
||||
|
||||
@ -18,6 +19,9 @@ class OutpostForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["providers"].queryset = ProxyProvider.objects.all()
|
||||
self.fields[
|
||||
"service_connection"
|
||||
].queryset = OutpostServiceConnection.objects.select_subclasses()
|
||||
|
||||
class Meta:
|
||||
|
||||
|
||||
@ -25,8 +25,8 @@ def migrate_to_service_connection(apps: Apps, schema_editor: BaseDatabaseSchemaE
|
||||
# Ensure that local connection have been created
|
||||
PassbookOutpostConfig.init_local_connection(None)
|
||||
|
||||
docker = DockerServiceConnection.objects.filter(local=True)
|
||||
k8s = KubernetesServiceConnection.objects.filter(local=True)
|
||||
docker = DockerServiceConnection.objects.filter(local=True).first()
|
||||
k8s = KubernetesServiceConnection.objects.filter(local=True).first()
|
||||
|
||||
try:
|
||||
for outpost in (
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Outpost Deployment Info' %}</h1>
|
||||
</div>
|
||||
<div class="pf-c-modal-box__body" id="modal-description">
|
||||
<p><a href="https://passbook.beryju.org/outposts/outposts/#deploy">{% trans 'View deployment documentation' %}</a></p>
|
||||
<p><a href="https://passbook.beryju.org/docs/outposts/outposts/#deploy">{% trans 'View deployment documentation' %}</a></p>
|
||||
<form class="pf-c-form">
|
||||
<div class="pf-c-form__group">
|
||||
<label class="pf-c-form__label" for="help-text-simple-form-name">
|
||||
|
||||
@ -32,7 +32,7 @@ class PolicyEvaluator(BaseEvaluator):
|
||||
|
||||
def set_policy_request(self, request: PolicyRequest):
|
||||
"""Update context based on policy request (if http request is given, update that too)"""
|
||||
# update docs/policies/expression/index.md
|
||||
# update website/docs/policies/expression.md
|
||||
self._context["pb_is_sso_flow"] = request.context.get(PLAN_CONTEXT_SSO, False)
|
||||
if request.http_request:
|
||||
self.set_http_request(request.http_request)
|
||||
@ -41,7 +41,7 @@ class PolicyEvaluator(BaseEvaluator):
|
||||
|
||||
def set_http_request(self, request: HttpRequest):
|
||||
"""Update context based on http request"""
|
||||
# update docs/policies/expression/index.md
|
||||
# update website/docs/policies/expression.md
|
||||
self._context["pb_client_ip"] = ip_address(
|
||||
get_client_ip(request) or "255.255.255.255"
|
||||
)
|
||||
|
||||
@ -108,7 +108,6 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||
@swagger_serializer_method(serializer_or_field=OpenIDConnectConfigurationSerializer)
|
||||
def get_oidc_configuration(self, obj: ProxyProvider):
|
||||
"""Embed OpenID Connect provider information"""
|
||||
# pylint: disable=protected-access
|
||||
return ProviderInfoView(request=self.context["request"]._request).get_info(obj)
|
||||
|
||||
|
||||
|
||||
@ -24,7 +24,6 @@ class SAMLProviderSerializer(ModelSerializer):
|
||||
"digest_algorithm",
|
||||
"signature_algorithm",
|
||||
"signing_kp",
|
||||
"require_signing",
|
||||
"verification_kp",
|
||||
]
|
||||
|
||||
|
||||
@ -39,7 +39,6 @@ class SAMLProviderForm(forms.ModelForm):
|
||||
"assertion_valid_not_on_or_after",
|
||||
"session_valid_not_on_or_after",
|
||||
"digest_algorithm",
|
||||
"require_signing",
|
||||
"signature_algorithm",
|
||||
"signing_kp",
|
||||
"verification_kp",
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-12 10:36
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_crypto", "0002_create_self_signed_kp"),
|
||||
("passbook_providers_saml", "0007_samlprovider_verification_kp"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="samlprovider",
|
||||
name="require_signing",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlprovider",
|
||||
name="audience",
|
||||
field=models.TextField(
|
||||
default="",
|
||||
help_text="Value of the audience restriction field of the asseration.",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlprovider",
|
||||
name="issuer",
|
||||
field=models.TextField(
|
||||
default="passbook", help_text="Also known as EntityID"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlprovider",
|
||||
name="signing_kp",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Keypair used to sign outgoing Responses going to the Service Provider.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="passbook_crypto.certificatekeypair",
|
||||
verbose_name="Signing Keypair",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlprovider",
|
||||
name="sp_binding",
|
||||
field=models.TextField(
|
||||
choices=[("redirect", "Redirect"), ("post", "Post")],
|
||||
default="redirect",
|
||||
help_text="This determines how passbook sends the response back to the Service Provider.",
|
||||
verbose_name="Service Provider Binding",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlprovider",
|
||||
name="verification_kp",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="When selected, incoming assertion's Signatures will be validated against this certificate. To allow unsigned Requests, leave on default.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="+",
|
||||
to="passbook_crypto.certificatekeypair",
|
||||
verbose_name="Verification Certificate",
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,69 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-12 20:16
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from passbook.sources.saml.processors import constants
|
||||
|
||||
|
||||
def update_algorithms(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
SAMLProvider = apps.get_model("passbook_providers_saml", "SAMLProvider")
|
||||
signature_translation_map = {
|
||||
"rsa-sha1": constants.RSA_SHA1,
|
||||
"rsa-sha256": constants.RSA_SHA256,
|
||||
"ecdsa-sha256": constants.RSA_SHA256,
|
||||
"dsa-sha1": constants.DSA_SHA1,
|
||||
}
|
||||
digest_translation_map = {
|
||||
"sha1": constants.SHA1,
|
||||
"sha256": constants.SHA256,
|
||||
}
|
||||
|
||||
for source in SAMLProvider.objects.all():
|
||||
source.signature_algorithm = signature_translation_map.get(
|
||||
source.signature_algorithm, constants.RSA_SHA256
|
||||
)
|
||||
source.digest_algorithm = digest_translation_map.get(
|
||||
source.digest_algorithm, constants.SHA256
|
||||
)
|
||||
source.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_providers_saml", "0008_auto_20201112_1036"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="samlprovider",
|
||||
name="digest_algorithm",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
(constants.SHA1, "SHA1"),
|
||||
(constants.SHA256, "SHA256"),
|
||||
(constants.SHA384, "SHA384"),
|
||||
(constants.SHA512, "SHA512"),
|
||||
],
|
||||
default=constants.SHA256,
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlprovider",
|
||||
name="signature_algorithm",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
(constants.RSA_SHA1, "RSA-SHA1"),
|
||||
(constants.RSA_SHA256, "RSA-SHA256"),
|
||||
(constants.RSA_SHA384, "RSA-SHA384"),
|
||||
(constants.RSA_SHA512, "RSA-SHA512"),
|
||||
(constants.DSA_SHA1, "DSA-SHA1"),
|
||||
],
|
||||
default=constants.RSA_SHA256,
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
]
|
||||
@ -13,6 +13,17 @@ from passbook.core.models import PropertyMapping, Provider
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.lib.utils.template import render_to_string
|
||||
from passbook.lib.utils.time import timedelta_string_validator
|
||||
from passbook.sources.saml.processors.constants import (
|
||||
DSA_SHA1,
|
||||
RSA_SHA1,
|
||||
RSA_SHA256,
|
||||
RSA_SHA384,
|
||||
RSA_SHA512,
|
||||
SHA1,
|
||||
SHA256,
|
||||
SHA384,
|
||||
SHA512,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
@ -28,12 +39,21 @@ class SAMLProvider(Provider):
|
||||
"""SAML 2.0 Endpoint for applications which support SAML."""
|
||||
|
||||
acs_url = models.URLField(verbose_name=_("ACS URL"))
|
||||
audience = models.TextField(default="")
|
||||
issuer = models.TextField(help_text=_("Also known as EntityID"))
|
||||
audience = models.TextField(
|
||||
default="",
|
||||
help_text=_("Value of the audience restriction field of the asseration."),
|
||||
)
|
||||
issuer = models.TextField(help_text=_("Also known as EntityID"), default="passbook")
|
||||
sp_binding = models.TextField(
|
||||
choices=SAMLBindings.choices,
|
||||
default=SAMLBindings.REDIRECT,
|
||||
verbose_name=_("Service Prodier Binding"),
|
||||
verbose_name=_("Service Provider Binding"),
|
||||
help_text=_(
|
||||
(
|
||||
"This determines how passbook sends the "
|
||||
"response back to the Service Provider."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
assertion_valid_not_before = models.TextField(
|
||||
@ -71,48 +91,52 @@ class SAMLProvider(Provider):
|
||||
digest_algorithm = models.CharField(
|
||||
max_length=50,
|
||||
choices=(
|
||||
("sha1", _("SHA1")),
|
||||
("sha256", _("SHA256")),
|
||||
(SHA1, _("SHA1")),
|
||||
(SHA256, _("SHA256")),
|
||||
(SHA384, _("SHA384")),
|
||||
(SHA512, _("SHA512")),
|
||||
),
|
||||
default="sha256",
|
||||
default=SHA256,
|
||||
)
|
||||
signature_algorithm = models.CharField(
|
||||
max_length=50,
|
||||
choices=(
|
||||
("rsa-sha1", _("RSA-SHA1")),
|
||||
("rsa-sha256", _("RSA-SHA256")),
|
||||
("ecdsa-sha256", _("ECDSA-SHA256")),
|
||||
("dsa-sha1", _("DSA-SHA1")),
|
||||
(RSA_SHA1, _("RSA-SHA1")),
|
||||
(RSA_SHA256, _("RSA-SHA256")),
|
||||
(RSA_SHA384, _("RSA-SHA384")),
|
||||
(RSA_SHA512, _("RSA-SHA512")),
|
||||
(DSA_SHA1, _("DSA-SHA1")),
|
||||
),
|
||||
default="rsa-sha256",
|
||||
default=RSA_SHA256,
|
||||
)
|
||||
|
||||
verification_kp = models.ForeignKey(
|
||||
CertificateKeyPair,
|
||||
default=None,
|
||||
null=True,
|
||||
help_text=_("If selected, incoming assertion's Signatures will be validated."),
|
||||
blank=True,
|
||||
help_text=_(
|
||||
(
|
||||
"When selected, incoming assertion's Signatures will be validated against this "
|
||||
"certificate. To allow unsigned Requests, leave on default."
|
||||
)
|
||||
),
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("Verification Keypair"),
|
||||
verbose_name=_("Verification Certificate"),
|
||||
related_name="+",
|
||||
)
|
||||
signing_kp = models.ForeignKey(
|
||||
CertificateKeyPair,
|
||||
default=None,
|
||||
null=True,
|
||||
help_text=_("Singing is enabled upon selection of a Key Pair."),
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"Keypair used to sign outgoing Responses going to the Service Provider."
|
||||
),
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_("Signing Keypair"),
|
||||
)
|
||||
|
||||
require_signing = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_(
|
||||
"Require Requests to be signed by an X509 Certificate. "
|
||||
"Must match the Certificate selected in `Singing Keypair`."
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def launch_url(self) -> Optional[str]:
|
||||
"""Guess launch_url based on acs URL"""
|
||||
|
||||
@ -2,10 +2,10 @@
|
||||
from hashlib import sha256
|
||||
from types import GeneratorType
|
||||
|
||||
import xmlsec
|
||||
from django.http import HttpRequest
|
||||
from lxml import etree # nosec
|
||||
from lxml.etree import Element, SubElement # nosec
|
||||
from signxml import XMLSigner, XMLVerifier, strip_pem_header
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.exceptions import PropertyMappingExpressionException
|
||||
@ -16,14 +16,15 @@ from passbook.providers.saml.utils import get_random_id
|
||||
from passbook.providers.saml.utils.time import get_time_string
|
||||
from passbook.sources.saml.exceptions import UnsupportedNameIDFormat
|
||||
from passbook.sources.saml.processors.constants import (
|
||||
DIGEST_ALGORITHM_TRANSLATION_MAP,
|
||||
NS_MAP,
|
||||
NS_SAML_ASSERTION,
|
||||
NS_SAML_PROTOCOL,
|
||||
NS_SIGNATURE,
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
SAML_NAME_ID_FORMAT_PERSISTENT,
|
||||
SAML_NAME_ID_FORMAT_TRANSIENT,
|
||||
SAML_NAME_ID_FORMAT_X509,
|
||||
SIGN_ALGORITHM_TRANSFORM_MAP,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
@ -186,12 +187,16 @@ class AssertionProcessor:
|
||||
assertion.append(self.get_issuer())
|
||||
|
||||
if self.provider.signing_kp:
|
||||
# We need a placeholder signature as SAML requires the signature to be between
|
||||
# Issuer and subject
|
||||
signature_placeholder = SubElement(
|
||||
assertion, f"{{{NS_SIGNATURE}}}Signature", nsmap=NS_MAP
|
||||
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
|
||||
self.provider.signature_algorithm, xmlsec.constants.TransformRsaSha1
|
||||
)
|
||||
signature_placeholder.attrib["Id"] = "placeholder"
|
||||
signature = xmlsec.template.create(
|
||||
assertion,
|
||||
xmlsec.constants.TransformExclC14N,
|
||||
sign_algorithm_transform,
|
||||
ns="ds", # type: ignore
|
||||
)
|
||||
assertion.append(signature)
|
||||
|
||||
assertion.append(self.get_assertion_subject())
|
||||
assertion.append(self.get_assertion_conditions())
|
||||
@ -223,20 +228,36 @@ class AssertionProcessor:
|
||||
"""Build string XML Response and sign if signing is enabled."""
|
||||
root_response = self.get_response()
|
||||
if self.provider.signing_kp:
|
||||
signer = XMLSigner(
|
||||
c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#",
|
||||
signature_algorithm=self.provider.signature_algorithm,
|
||||
digest_algorithm=self.provider.digest_algorithm,
|
||||
digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get(
|
||||
self.provider.digest_algorithm, xmlsec.constants.TransformSha1
|
||||
)
|
||||
x509_data = strip_pem_header(
|
||||
self.provider.signing_kp.certificate_data
|
||||
).replace("\n", "")
|
||||
signed = signer.sign(
|
||||
root_response,
|
||||
key=self.provider.signing_kp.private_key,
|
||||
cert=[x509_data],
|
||||
reference_uri=self._assertion_id,
|
||||
assertion = root_response.xpath("//saml:Assertion", namespaces=NS_MAP)[0]
|
||||
xmlsec.tree.add_ids(assertion, ["ID"])
|
||||
signature_node = xmlsec.tree.find_node(
|
||||
assertion, xmlsec.constants.NodeSignature
|
||||
)
|
||||
XMLVerifier().verify(signed, x509_cert=x509_data)
|
||||
return etree.tostring(signed).decode("utf-8") # nosec
|
||||
ref = xmlsec.template.add_reference(
|
||||
signature_node,
|
||||
digest_algorithm_transform,
|
||||
uri="#" + self._assertion_id,
|
||||
)
|
||||
xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped)
|
||||
xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N)
|
||||
key_info = xmlsec.template.ensure_key_info(signature_node)
|
||||
xmlsec.template.add_x509_data(key_info)
|
||||
|
||||
ctx = xmlsec.SignatureContext()
|
||||
|
||||
key = xmlsec.Key.from_memory(
|
||||
self.provider.signing_kp.key_data,
|
||||
xmlsec.constants.KeyDataFormatPem,
|
||||
None,
|
||||
)
|
||||
key.load_cert_from_memory(
|
||||
self.provider.signing_kp.certificate_data,
|
||||
xmlsec.constants.KeyDataFormatCertPem,
|
||||
)
|
||||
ctx.key = key
|
||||
ctx.sign(signature_node)
|
||||
|
||||
return etree.tostring(root_response).decode("utf-8") # nosec
|
||||
|
||||
@ -4,9 +4,9 @@ from typing import Iterator, Optional
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import reverse
|
||||
from lxml.etree import Element, SubElement, tostring # nosec
|
||||
from signxml.util import strip_pem_header
|
||||
|
||||
from passbook.providers.saml.models import SAMLProvider
|
||||
from passbook.providers.saml.utils.encoding import strip_pem_header
|
||||
from passbook.sources.saml.processors.constants import (
|
||||
NS_MAP,
|
||||
NS_SAML_METADATA,
|
||||
@ -42,7 +42,7 @@ class MetadataProcessor:
|
||||
)
|
||||
x509_certificate.text = strip_pem_header(
|
||||
self.provider.signing_kp.certificate_data.replace("\r", "")
|
||||
).replace("\n", "")
|
||||
)
|
||||
return key_descriptor
|
||||
return None
|
||||
|
||||
|
||||
@ -4,22 +4,33 @@ from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
import xmlsec
|
||||
from defusedxml import ElementTree
|
||||
from signxml import XMLVerifier
|
||||
from lxml import etree # nosec
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.providers.saml.exceptions import CannotHandleAssertion
|
||||
from passbook.providers.saml.models import SAMLProvider
|
||||
from passbook.providers.saml.utils.encoding import decode_base64_and_inflate
|
||||
from passbook.sources.saml.processors.constants import (
|
||||
DSA_SHA1,
|
||||
NS_MAP,
|
||||
NS_SAML_PROTOCOL,
|
||||
RSA_SHA1,
|
||||
RSA_SHA256,
|
||||
RSA_SHA384,
|
||||
RSA_SHA512,
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
ERROR_SIGNATURE_REQUIRED_BUT_ABSENT = (
|
||||
"Verification Certificate configured, but request is not signed."
|
||||
)
|
||||
ERROR_SIGNATURE_EXISTS_BUT_NO_VERIFIER = (
|
||||
"Provider does not have a Validation Certificate configured."
|
||||
)
|
||||
ERROR_FAILED_TO_VERIFY = "Failed to verify signature"
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -67,16 +78,38 @@ class AuthNRequestParser:
|
||||
|
||||
def parse(self, saml_request: str, relay_state: Optional[str]) -> AuthNRequest:
|
||||
"""Validate and parse raw request with enveloped signautre."""
|
||||
decoded_xml = decode_base64_and_inflate(saml_request)
|
||||
decoded_xml = b64decode(saml_request.encode()).decode()
|
||||
|
||||
verifier = self.provider.verification_kp
|
||||
|
||||
root = etree.fromstring(decoded_xml) # nosec
|
||||
xmlsec.tree.add_ids(root, ["ID"])
|
||||
signature_nodes = root.xpath(
|
||||
"/samlp:AuthnRequest/ds:Signature", namespaces=NS_MAP
|
||||
)
|
||||
if len(signature_nodes) != 1:
|
||||
raise CannotHandleAssertion(ERROR_SIGNATURE_REQUIRED_BUT_ABSENT)
|
||||
|
||||
signature_node = signature_nodes[0]
|
||||
|
||||
if verifier and signature_node is None:
|
||||
raise CannotHandleAssertion(ERROR_SIGNATURE_REQUIRED_BUT_ABSENT)
|
||||
|
||||
if signature_node is not None:
|
||||
if not verifier:
|
||||
raise CannotHandleAssertion(ERROR_SIGNATURE_EXISTS_BUT_NO_VERIFIER)
|
||||
|
||||
if self.provider.verification_kp:
|
||||
try:
|
||||
XMLVerifier().verify(
|
||||
decoded_xml,
|
||||
x509_cert=self.provider.verification_kp.certificate_data,
|
||||
ctx = xmlsec.SignatureContext()
|
||||
key = xmlsec.Key.from_memory(
|
||||
verifier.certificate_data,
|
||||
xmlsec.constants.KeyDataFormatCertPem,
|
||||
None,
|
||||
)
|
||||
except InvalidSignature as exc:
|
||||
raise CannotHandleAssertion("Failed to verify signature") from exc
|
||||
ctx.key = key
|
||||
ctx.verify(signature_node)
|
||||
except xmlsec.VerificationError as exc:
|
||||
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
|
||||
|
||||
return self._parse_xml(decoded_xml, relay_state)
|
||||
|
||||
@ -90,31 +123,45 @@ class AuthNRequestParser:
|
||||
"""Validate and parse raw request with detached signature"""
|
||||
decoded_xml = decode_base64_and_inflate(saml_request)
|
||||
|
||||
verifier = self.provider.verification_kp
|
||||
|
||||
if verifier and not (signature and sig_alg):
|
||||
raise CannotHandleAssertion(ERROR_SIGNATURE_REQUIRED_BUT_ABSENT)
|
||||
|
||||
if signature and sig_alg:
|
||||
# if sig_alg == "http://www.w3.org/2000/09/xmldsig#rsa-sha1":
|
||||
sig_hash = hashes.SHA1() # nosec
|
||||
if not verifier:
|
||||
raise CannotHandleAssertion(ERROR_SIGNATURE_EXISTS_BUT_NO_VERIFIER)
|
||||
|
||||
querystring = f"SAMLRequest={quote_plus(saml_request)}&"
|
||||
if relay_state is not None:
|
||||
querystring += f"RelayState={quote_plus(relay_state)}&"
|
||||
querystring += f"SigAlg={sig_alg}"
|
||||
querystring += f"SigAlg={quote_plus(sig_alg)}"
|
||||
|
||||
dsig_ctx = xmlsec.SignatureContext()
|
||||
key = xmlsec.Key.from_memory(
|
||||
verifier.certificate_data, xmlsec.constants.KeyDataFormatCertPem, None
|
||||
)
|
||||
dsig_ctx.key = key
|
||||
|
||||
sign_algorithm_transform_map = {
|
||||
DSA_SHA1: xmlsec.constants.TransformDsaSha1,
|
||||
RSA_SHA1: xmlsec.constants.TransformRsaSha1,
|
||||
RSA_SHA256: xmlsec.constants.TransformRsaSha256,
|
||||
RSA_SHA384: xmlsec.constants.TransformRsaSha384,
|
||||
RSA_SHA512: xmlsec.constants.TransformRsaSha512,
|
||||
}
|
||||
sign_algorithm_transform = sign_algorithm_transform_map.get(
|
||||
sig_alg, xmlsec.constants.TransformRsaSha1
|
||||
)
|
||||
|
||||
if not self.provider.verification_kp:
|
||||
raise CannotHandleAssertion(
|
||||
"Provider does not have a Validation Certificate configured."
|
||||
)
|
||||
public_key = self.provider.verification_kp.private_key.public_key()
|
||||
try:
|
||||
public_key.verify(
|
||||
dsig_ctx.verify_binary(
|
||||
querystring.encode("utf-8"),
|
||||
sign_algorithm_transform,
|
||||
b64decode(signature),
|
||||
querystring.encode(),
|
||||
padding.PSS(
|
||||
mgf=padding.MGF1(sig_hash), salt_length=padding.PSS.MAX_LENGTH
|
||||
),
|
||||
sig_hash,
|
||||
)
|
||||
except InvalidSignature as exc:
|
||||
raise CannotHandleAssertion("Failed to verify signature") from exc
|
||||
except xmlsec.VerificationError as exc:
|
||||
raise CannotHandleAssertion(ERROR_FAILED_TO_VERIFY) from exc
|
||||
return self._parse_xml(decoded_xml, relay_state)
|
||||
|
||||
def idp_initiated(self) -> AuthNRequest:
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
"""Test AuthN Request generator and parser"""
|
||||
from base64 import b64encode
|
||||
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http.request import HttpRequest, QueryDict
|
||||
from django.test import RequestFactory, TestCase
|
||||
@ -6,18 +8,55 @@ from guardian.utils import get_anonymous_user
|
||||
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.providers.saml.models import SAMLProvider
|
||||
from passbook.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from passbook.providers.saml.processors.assertion import AssertionProcessor
|
||||
from passbook.providers.saml.processors.request_parser import AuthNRequestParser
|
||||
from passbook.providers.saml.utils.encoding import deflate_and_base64_encode
|
||||
from passbook.sources.saml.exceptions import MismatchedRequestID
|
||||
from passbook.sources.saml.models import SAMLSource
|
||||
from passbook.sources.saml.processors.constants import SAML_NAME_ID_FORMAT_EMAIL
|
||||
from passbook.sources.saml.processors.request import (
|
||||
SESSION_REQUEST_ID,
|
||||
RequestProcessor,
|
||||
)
|
||||
from passbook.sources.saml.processors.response import ResponseProcessor
|
||||
|
||||
REDIRECT_REQUEST = (
|
||||
"fZLNbsIwEIRfJfIdbKeFgEUipXAoEm0jSHvopTLJplhK7NTr9Oft6yRUKhekPdk73+yOdoWyqVuRdu6k9/DRAbrgu6k1iu"
|
||||
"EjJp3VwkhUKLRsAIUrxCF92IlwykRrjTOFqUmQIoJ1yui10dg1YA9gP1UBz/tdTE7OtSgo5WzKQzYditGeP8GW9rSQZk+H"
|
||||
"nAQbb6+07EGj7EI1j8SCeaVs21oVQ9dAoRqcf6OIhh6VLpV+pxZKZaFwlATbTUzeyqKazaqiDCO5WEQwZzKCagkwr8obWc"
|
||||
"qjb0PsYKvRSe1iErKQTTj3lYdc3HLBl68kyM4L340u19M5j4LiPs+zybjgC1gclvMNJFn104vB2P5L/TpW/kVNkqvBrug/"
|
||||
"+mjVikeP224y4/P7CdK6Nl9rC9JBTDihySi5vIbkFw=="
|
||||
)
|
||||
REDIRECT_SIGNATURE = (
|
||||
"UlOe1BItHVHM+io6rUZAenIqfibm7hM6wr9I1rcP5kPJ4N8cbkyqmAMh5LD2lUq3PDERJfjdO/oOKnvJmbD2y9MOObyR2d"
|
||||
"7Udv62KERrA0qM917Q+w8wrLX7w2nHY96EDvkXD4iAomR5EE9dHRuubDy7uRv2syEevc0gfoLi7W/5vp96vJgsaSqxnTp+"
|
||||
"QiYq49KyWyMtxRULF2yd+vYDnHCDME73mNSULEHfwCU71dvbKpnFaej78q7wS20gUk6ysOOXXtvDHbiVcpUb/9oyDgNAxU"
|
||||
"jVvPdh96AhBFj2HCuGZhP0CGotafTciu6YlsiwUpuBkIYgZmNWYa3FR9LS4Q=="
|
||||
)
|
||||
REDIRECT_SIG_ALG = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
REDIRECT_RELAY_STATE = (
|
||||
"ss:mem:7a054b4af44f34f89dd2d973f383c250b6b076e7f06cfa8276008a6504eaf3c7"
|
||||
)
|
||||
REDIRECT_CERT = """-----BEGIN CERTIFICATE-----
|
||||
MIIDCDCCAfCgAwIBAgIRAM5s+bhOHk4ChSpPkGSh0NswDQYJKoZIhvcNAQELBQAw
|
||||
KzEpMCcGA1UEAwwgcGFzc2Jvb2sgU2VsZi1zaWduZWQgQ2VydGlmaWNhdGUwHhcN
|
||||
MjAxMTA3MjAzNDIxWhcNMjExMTA4MjAzNDIxWjBUMSkwJwYDVQQDDCBwYXNzYm9v
|
||||
ayBTZWxmLXNpZ25lZCBDZXJ0aWZpY2F0ZTERMA8GA1UECgwIcGFzc2Jvb2sxFDAS
|
||||
BgNVBAsMC1NlbGYtc2lnbmVkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
|
||||
AQEAuh+Bv6a/ogpic72X/sq86YiLzVjixnGqjc4wpsPPP00GX8jUAZJL4Tjo+sYK
|
||||
IU2DF2/azlVqjkbLho4rGuuc8YkbFXBEXPYc5h3bseO2vk6sbbbWKV0mro1VFhBh
|
||||
T59hBORuMMefmQdhFzsRNOGklIptQdg0quD8ET3+/uNfIT98S2ruZdYteFls46Sa
|
||||
MokZFYVD6pWEYV4P2MKVAFqJX9bqBW0LfCCfFqHAOJjUZj9dtleg86d2WfedUOG2
|
||||
LK0iLrydjhThbI0GUDhv0jWYkRlv04fdJ1WSRANYA3gBOnyw+Iigh2xNnYbVZMXT
|
||||
I0BupIJ4UoODMc4QpD2GYJ6oGwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCCEF3e
|
||||
Y99KxEBSR4H4/TvKbnh4QtHswOf7MaGdjtrld7l4u4Hc4NEklNdDn1XLKhZwnq3Z
|
||||
LRsRlJutDzZ18SRmAJPXPbka7z7D+LA1mbNQElOgiKyQHD9rIJSBr6X5SM9As3CR
|
||||
7QUsb8dg7kc+Jn7WuLZIEVxxMtekt0buWEdMJiklF0tCS3LNsP083FaQk/H1K0z6
|
||||
3PWP26EFdwir3RyTKLY5CBLjKrUAo9O1l/WBVFYbdetnipbGGu5f6nk6nnxbwLLI
|
||||
Dm52Vkq+xFDDUq9IqIoYvLaE86MDvtpMQEx65tIGU19vUf3fL/+sSfdRZ1HDzP4d
|
||||
qNAZMq1DqpibfCBg
|
||||
-----END CERTIFICATE-----"""
|
||||
|
||||
|
||||
def dummy_get_response(request: HttpRequest): # pragma: no cover
|
||||
"""Dummy get_response for SessionMiddleware"""
|
||||
@ -28,18 +67,21 @@ class TestAuthNRequest(TestCase):
|
||||
"""Test AuthN Request generator and parser"""
|
||||
|
||||
def setUp(self):
|
||||
self.provider = SAMLProvider.objects.create(
|
||||
cert = CertificateKeyPair.objects.first()
|
||||
self.provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
acs_url="http://testserver/source/saml/provider/acs/",
|
||||
signing_kp=CertificateKeyPair.objects.first(),
|
||||
verification_kp=CertificateKeyPair.objects.first(),
|
||||
signing_kp=cert,
|
||||
verification_kp=cert,
|
||||
)
|
||||
self.provider.property_mappings.set(SAMLPropertyMapping.objects.all())
|
||||
self.provider.save()
|
||||
self.source = SAMLSource.objects.create(
|
||||
slug="provider",
|
||||
issuer="passbook",
|
||||
signing_kp=CertificateKeyPair.objects.first(),
|
||||
signing_kp=cert,
|
||||
)
|
||||
self.factory = RequestFactory()
|
||||
|
||||
@ -56,14 +98,15 @@ class TestAuthNRequest(TestCase):
|
||||
request = request_proc.build_auth_n()
|
||||
# Now we check the ID and signature
|
||||
parsed_request = AuthNRequestParser(self.provider).parse(
|
||||
deflate_and_base64_encode(request), "test_state"
|
||||
b64encode(request.encode()).decode(), "test_state"
|
||||
)
|
||||
self.assertEqual(parsed_request.id, request_proc.request_id)
|
||||
self.assertEqual(parsed_request.relay_state, "test_state")
|
||||
|
||||
def test_signed_valid_detached(self):
|
||||
"""Test generated AuthNRequest with valid signature (detached)"""
|
||||
def test_request_full_signed(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = get_anonymous_user()
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
@ -71,13 +114,22 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
params = request_proc.build_auth_n_detached()
|
||||
# Now we check the ID and signature
|
||||
parsed_request = AuthNRequestParser(self.provider).parse_detached(
|
||||
params["SAMLRequest"], "test_state", params["Signature"], params["SigAlg"]
|
||||
request = request_proc.build_auth_n()
|
||||
|
||||
# To get an assertion we need a parsed request (parsed by provider)
|
||||
parsed_request = AuthNRequestParser(self.provider).parse(
|
||||
b64encode(request.encode()).decode(), "test_state"
|
||||
)
|
||||
self.assertEqual(parsed_request.id, request_proc.request_id)
|
||||
self.assertEqual(parsed_request.relay_state, "test_state")
|
||||
# Now create a response and convert it to string (provider)
|
||||
response_proc = AssertionProcessor(self.provider, http_request, parsed_request)
|
||||
response = response_proc.build_response()
|
||||
|
||||
# Now parse the response (source)
|
||||
http_request.POST = QueryDict(mutable=True)
|
||||
http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode()
|
||||
|
||||
response_parser = ResponseProcessor(self.source)
|
||||
response_parser.parse(http_request)
|
||||
|
||||
def test_request_id_invalid(self):
|
||||
"""Test generated AuthNRequest with invalid request ID"""
|
||||
@ -98,7 +150,7 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
# To get an assertion we need a parsed request (parsed by provider)
|
||||
parsed_request = AuthNRequestParser(self.provider).parse(
|
||||
deflate_and_base64_encode(request), "test_state"
|
||||
b64encode(request.encode()).decode(), "test_state"
|
||||
)
|
||||
# Now create a response and convert it to string (provider)
|
||||
response_proc = AssertionProcessor(self.provider, http_request, parsed_request)
|
||||
@ -106,9 +158,54 @@ class TestAuthNRequest(TestCase):
|
||||
|
||||
# Now parse the response (source)
|
||||
http_request.POST = QueryDict(mutable=True)
|
||||
http_request.POST["SAMLResponse"] = deflate_and_base64_encode(response)
|
||||
http_request.POST["SAMLResponse"] = b64encode(response.encode()).decode()
|
||||
|
||||
response_parser = ResponseProcessor(self.source)
|
||||
|
||||
with self.assertRaises(MismatchedRequestID):
|
||||
response_parser.parse(http_request)
|
||||
|
||||
def test_signed_valid_detached(self):
|
||||
"""Test generated AuthNRequest with valid signature (detached)"""
|
||||
http_request = self.factory.get("/")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
params = request_proc.build_auth_n_detached()
|
||||
# Now we check the ID and signature
|
||||
parsed_request = AuthNRequestParser(self.provider).parse_detached(
|
||||
params["SAMLRequest"],
|
||||
params["RelayState"],
|
||||
params["Signature"],
|
||||
params["SigAlg"],
|
||||
)
|
||||
self.assertEqual(parsed_request.id, request_proc.request_id)
|
||||
self.assertEqual(parsed_request.relay_state, "test_state")
|
||||
|
||||
def test_signed_detached_static(self):
|
||||
"""Test request with detached signature,
|
||||
taken from https://www.samltool.com/generic_sso_req.php"""
|
||||
static_keypair = CertificateKeyPair.objects.create(
|
||||
name="samltool", certificate_data=REDIRECT_CERT
|
||||
)
|
||||
provider = SAMLProvider(
|
||||
name="samltool",
|
||||
authorization_flow=Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
),
|
||||
acs_url="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
audience="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
issuer="https://10.120.20.200/saml-sp/SAML2/POST",
|
||||
signing_kp=static_keypair,
|
||||
verification_kp=static_keypair,
|
||||
)
|
||||
parsed_request = AuthNRequestParser(provider).parse_detached(
|
||||
REDIRECT_REQUEST, REDIRECT_RELAY_STATE, REDIRECT_SIGNATURE, REDIRECT_SIG_ALG
|
||||
)
|
||||
self.assertEqual(parsed_request.id, "_dcf55fcd27a887e60a7ef9ee6fd3adab")
|
||||
self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_EMAIL)
|
||||
self.assertEqual(parsed_request.relay_state, REDIRECT_RELAY_STATE)
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
import base64
|
||||
import zlib
|
||||
|
||||
PEM_HEADER = "-----BEGIN CERTIFICATE-----"
|
||||
PEM_FOOTER = "-----END CERTIFICATE-----"
|
||||
|
||||
|
||||
def decode_base64_and_inflate(encoded: str, encoding="utf-8") -> str:
|
||||
"""Base64 decode and ZLib decompress b64string"""
|
||||
@ -22,3 +25,8 @@ def deflate_and_base64_encode(inflated: str, encoding="utf-8"):
|
||||
def nice64(src: str) -> str:
|
||||
"""Returns src base64-encoded and formatted nicely for our XML. """
|
||||
return base64.b64encode(src.encode()).decode("utf-8").replace("\n", "")
|
||||
|
||||
|
||||
def strip_pem_header(cert: str) -> str:
|
||||
"""Remove PEM Headers"""
|
||||
return cert.replace(PEM_HEADER, "").replace(PEM_FOOTER, "").replace("\n", "")
|
||||
|
||||
@ -127,7 +127,7 @@ class SAMLSSOBindingPOSTView(SAMLSSOView):
|
||||
def check_saml_request(self) -> Optional[HttpRequest]:
|
||||
"""Handle POST bindings"""
|
||||
if REQUEST_KEY_SAML_REQUEST not in self.request.POST:
|
||||
LOGGER.info("handle_saml_request: SAML payload missing")
|
||||
LOGGER.info("check_saml_request: SAML payload missing")
|
||||
return bad_request_message(
|
||||
self.request, "The SAML request payload is missing."
|
||||
)
|
||||
|
||||
@ -6,23 +6,27 @@ It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
import os
|
||||
import typing
|
||||
from time import time
|
||||
from typing import Any, ByteString, Dict
|
||||
|
||||
import django
|
||||
from asgiref.compatibility import guarantee_single_callable
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from defusedxml import defuse_stdlib
|
||||
from django.core.asgi import get_asgi_application
|
||||
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
|
||||
from structlog import get_logger
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
|
||||
# DJANGO_SETTINGS_MODULE is set in gunicorn.conf.py
|
||||
|
||||
defuse_stdlib()
|
||||
django.setup()
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
from passbook.root import websocket # noqa # isort:skip
|
||||
|
||||
|
||||
# See https://github.com/encode/starlette/blob/master/starlette/types.py
|
||||
Scope = typing.MutableMapping[str, typing.Any]
|
||||
Message = typing.MutableMapping[str, typing.Any]
|
||||
@ -129,5 +133,14 @@ class ASGILogger:
|
||||
|
||||
|
||||
application = ASGILogger(
|
||||
guarantee_single_callable(SentryAsgiMiddleware(get_asgi_application()))
|
||||
guarantee_single_callable(
|
||||
SentryAsgiMiddleware(
|
||||
ProtocolTypeRouter(
|
||||
{
|
||||
"http": get_asgi_application(),
|
||||
"websocket": URLRouter(websocket.websocket_urlpatterns),
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
"""root Websocket URLS"""
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from django.urls import path
|
||||
|
||||
from passbook.outposts.channels import OutpostConsumer
|
||||
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
# (http->django views is added by default)
|
||||
"websocket": URLRouter(
|
||||
[path("ws/outpost/<uuid:pk>/", OutpostConsumer.as_asgi())]
|
||||
),
|
||||
}
|
||||
)
|
||||
@ -208,7 +208,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
ASGI_APPLICATION = "passbook.root.routing.application"
|
||||
ASGI_APPLICATION = "passbook.root.asgi.application"
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
"default": {
|
||||
|
||||
6
passbook/root/websocket.py
Normal file
6
passbook/root/websocket.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""root Websocket URLS"""
|
||||
from django.urls import path
|
||||
|
||||
from passbook.outposts.channels import OutpostConsumer
|
||||
|
||||
websocket_urlpatterns = [path("ws/outpost/<uuid:pk>/", OutpostConsumer.as_asgi())]
|
||||
@ -19,8 +19,10 @@ class SAMLSourceSerializer(ModelSerializer):
|
||||
"allow_idp_initiated",
|
||||
"name_id_policy",
|
||||
"binding_type",
|
||||
"temporary_user_delete_after",
|
||||
"signing_kp",
|
||||
"digest_algorithm",
|
||||
"signature_algorithm",
|
||||
"temporary_user_delete_after",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@ -12,3 +12,7 @@ class UnsupportedNameIDFormat(SentryIgnoredException):
|
||||
|
||||
class MismatchedRequestID(SentryIgnoredException):
|
||||
"""Exception raised when the returned request ID doesn't match the saved ID."""
|
||||
|
||||
|
||||
class InvalidSignature(SentryIgnoredException):
|
||||
"""Signature of XML Object is either missing or invalid"""
|
||||
|
||||
@ -35,8 +35,10 @@ class SAMLSourceForm(forms.ModelForm):
|
||||
"binding_type",
|
||||
"name_id_policy",
|
||||
"allow_idp_initiated",
|
||||
"temporary_user_delete_after",
|
||||
"signing_kp",
|
||||
"digest_algorithm",
|
||||
"signature_algorithm",
|
||||
"temporary_user_delete_after",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
|
||||
51
passbook/sources/saml/migrations/0007_auto_20201112_1055.py
Normal file
51
passbook/sources/saml/migrations/0007_auto_20201112_1055.py
Normal file
@ -0,0 +1,51 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-12 10:55
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_crypto", "0002_create_self_signed_kp"),
|
||||
("passbook_sources_saml", "0006_samlsource_allow_idp_initiated"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="samlsource",
|
||||
name="digest_algorithm",
|
||||
field=models.CharField(
|
||||
choices=[("sha1", "SHA1"), ("sha256", "SHA256")],
|
||||
default="sha256",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="samlsource",
|
||||
name="signature_algorithm",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("rsa-sha1", "RSA-SHA1"),
|
||||
("rsa-sha256", "RSA-SHA256"),
|
||||
("ecdsa-sha256", "ECDSA-SHA256"),
|
||||
("dsa-sha1", "DSA-SHA1"),
|
||||
],
|
||||
default="rsa-sha256",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlsource",
|
||||
name="signing_kp",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
default=None,
|
||||
help_text="Keypair which is used to sign outgoing requests. Leave empty to disable signing.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="passbook_crypto.certificatekeypair",
|
||||
verbose_name="Singing Keypair",
|
||||
),
|
||||
),
|
||||
]
|
||||
70
passbook/sources/saml/migrations/0008_auto_20201112_2016.py
Normal file
70
passbook/sources/saml/migrations/0008_auto_20201112_2016.py
Normal file
@ -0,0 +1,70 @@
|
||||
# Generated by Django 3.1.3 on 2020-11-12 20:16
|
||||
|
||||
from django.apps.registry import Apps
|
||||
from django.db import migrations, models
|
||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
||||
|
||||
from passbook.sources.saml.processors import constants
|
||||
|
||||
|
||||
def update_algorithms(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
|
||||
SAMLSource = apps.get_model("passbook_sources_saml", "SAMLSource")
|
||||
signature_translation_map = {
|
||||
"rsa-sha1": constants.RSA_SHA1,
|
||||
"rsa-sha256": constants.RSA_SHA256,
|
||||
"ecdsa-sha256": constants.RSA_SHA256,
|
||||
"dsa-sha1": constants.DSA_SHA1,
|
||||
}
|
||||
digest_translation_map = {
|
||||
"sha1": constants.SHA1,
|
||||
"sha256": constants.SHA256,
|
||||
}
|
||||
|
||||
for source in SAMLSource.objects.all():
|
||||
source.signature_algorithm = signature_translation_map.get(
|
||||
source.signature_algorithm, constants.RSA_SHA256
|
||||
)
|
||||
source.digest_algorithm = digest_translation_map.get(
|
||||
source.digest_algorithm, constants.SHA256
|
||||
)
|
||||
source.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_sources_saml", "0007_auto_20201112_1055"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="samlsource",
|
||||
name="signature_algorithm",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
(constants.RSA_SHA1, "RSA-SHA1"),
|
||||
(constants.RSA_SHA256, "RSA-SHA256"),
|
||||
(constants.RSA_SHA384, "RSA-SHA384"),
|
||||
(constants.RSA_SHA512, "RSA-SHA512"),
|
||||
(constants.DSA_SHA1, "DSA-SHA1"),
|
||||
],
|
||||
default=constants.RSA_SHA256,
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="samlsource",
|
||||
name="digest_algorithm",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
(constants.SHA1, "SHA1"),
|
||||
(constants.SHA256, "SHA256"),
|
||||
(constants.SHA384, "SHA384"),
|
||||
(constants.SHA512, "SHA512"),
|
||||
],
|
||||
default=constants.SHA256,
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(update_algorithms),
|
||||
]
|
||||
@ -13,11 +13,20 @@ from passbook.core.types import UILoginButton
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.lib.utils.time import timedelta_string_validator
|
||||
from passbook.sources.saml.processors.constants import (
|
||||
DSA_SHA1,
|
||||
RSA_SHA1,
|
||||
RSA_SHA256,
|
||||
RSA_SHA384,
|
||||
RSA_SHA512,
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
SAML_NAME_ID_FORMAT_PERSISTENT,
|
||||
SAML_NAME_ID_FORMAT_TRANSIENT,
|
||||
SAML_NAME_ID_FORMAT_WINDOWS,
|
||||
SAML_NAME_ID_FORMAT_X509,
|
||||
SHA1,
|
||||
SHA256,
|
||||
SHA384,
|
||||
SHA512,
|
||||
)
|
||||
|
||||
|
||||
@ -96,11 +105,36 @@ class SAMLSource(Source):
|
||||
|
||||
signing_kp = models.ForeignKey(
|
||||
CertificateKeyPair,
|
||||
default=None,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Singing Keypair"),
|
||||
help_text=_(
|
||||
"Certificate Key Pair of the IdP which Assertion's Signature is validated against."
|
||||
"Keypair which is used to sign outgoing requests. Leave empty to disable signing."
|
||||
),
|
||||
on_delete=models.PROTECT,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
)
|
||||
|
||||
digest_algorithm = models.CharField(
|
||||
max_length=50,
|
||||
choices=(
|
||||
(SHA1, _("SHA1")),
|
||||
(SHA256, _("SHA256")),
|
||||
(SHA384, _("SHA384")),
|
||||
(SHA512, _("SHA512")),
|
||||
),
|
||||
default=SHA256,
|
||||
)
|
||||
signature_algorithm = models.CharField(
|
||||
max_length=50,
|
||||
choices=(
|
||||
(RSA_SHA1, _("RSA-SHA1")),
|
||||
(RSA_SHA256, _("RSA-SHA256")),
|
||||
(RSA_SHA384, _("RSA-SHA384")),
|
||||
(RSA_SHA512, _("RSA-SHA512")),
|
||||
(DSA_SHA1, _("DSA-SHA1")),
|
||||
),
|
||||
default=RSA_SHA256,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
"""SAML Source processor constants"""
|
||||
import xmlsec
|
||||
|
||||
NS_SAML_PROTOCOL = "urn:oasis:names:tc:SAML:2.0:protocol"
|
||||
NS_SAML_ASSERTION = "urn:oasis:names:tc:SAML:2.0:assertion"
|
||||
NS_SAML_METADATA = "urn:oasis:names:tc:SAML:2.0:metadata"
|
||||
@ -21,3 +23,29 @@ SAML_NAME_ID_FORMAT_TRANSIENT = "urn:oasis:names:tc:SAML:2.0:nameid-format:trans
|
||||
|
||||
SAML_BINDING_POST = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
||||
SAML_BINDING_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
|
||||
|
||||
DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
|
||||
RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
|
||||
RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
|
||||
RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
|
||||
RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
|
||||
|
||||
SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
|
||||
SHA256 = "http://www.w3.org/2001/04/xmlenc#sha256"
|
||||
SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
|
||||
SHA512 = "http://www.w3.org/2001/04/xmlenc#sha512"
|
||||
|
||||
SIGN_ALGORITHM_TRANSFORM_MAP = {
|
||||
DSA_SHA1: xmlsec.constants.TransformDsaSha1,
|
||||
RSA_SHA1: xmlsec.constants.TransformRsaSha1,
|
||||
RSA_SHA256: xmlsec.constants.TransformRsaSha256,
|
||||
RSA_SHA384: xmlsec.constants.TransformRsaSha384,
|
||||
RSA_SHA512: xmlsec.constants.TransformRsaSha512,
|
||||
}
|
||||
|
||||
DIGEST_ALGORITHM_TRANSLATION_MAP = {
|
||||
SHA1: xmlsec.constants.TransformSha1,
|
||||
SHA256: xmlsec.constants.TransformSha256,
|
||||
SHA384: xmlsec.constants.TransformSha384,
|
||||
SHA512: xmlsec.constants.TransformSha512,
|
||||
}
|
||||
|
||||
@ -3,8 +3,8 @@ from typing import Iterator, Optional
|
||||
|
||||
from django.http import HttpRequest
|
||||
from lxml.etree import Element, SubElement, tostring # nosec
|
||||
from signxml.util import strip_pem_header
|
||||
|
||||
from passbook.providers.saml.utils.encoding import strip_pem_header
|
||||
from passbook.sources.saml.models import SAMLSource
|
||||
from passbook.sources.saml.processors.constants import (
|
||||
NS_MAP,
|
||||
|
||||
@ -3,21 +3,21 @@ from base64 import b64encode
|
||||
from typing import Dict
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
import xmlsec
|
||||
from django.http import HttpRequest
|
||||
from lxml import etree # nosec
|
||||
from lxml.etree import Element # nosec
|
||||
from signxml import XMLSigner
|
||||
|
||||
from passbook.providers.saml.utils import get_random_id
|
||||
from passbook.providers.saml.utils.encoding import deflate_and_base64_encode
|
||||
from passbook.providers.saml.utils.time import get_time_string
|
||||
from passbook.sources.saml.models import SAMLSource
|
||||
from passbook.sources.saml.processors.constants import (
|
||||
DIGEST_ALGORITHM_TRANSLATION_MAP,
|
||||
NS_MAP,
|
||||
NS_SAML_ASSERTION,
|
||||
NS_SAML_PROTOCOL,
|
||||
SIGN_ALGORITHM_TRANSFORM_MAP,
|
||||
)
|
||||
|
||||
SESSION_REQUEST_ID = "passbook_source_saml_request_id"
|
||||
@ -51,7 +51,7 @@ class RequestProcessor:
|
||||
def get_name_id_policy(self) -> Element:
|
||||
"""Get NameID Policy Element"""
|
||||
name_id_policy = Element(f"{{{NS_SAML_PROTOCOL}}}NameIDPolicy")
|
||||
name_id_policy.text = self.source.name_id_policy
|
||||
name_id_policy.attrib["Format"] = self.source.name_id_policy
|
||||
return name_id_policy
|
||||
|
||||
def get_auth_n(self) -> Element:
|
||||
@ -67,6 +67,19 @@ class RequestProcessor:
|
||||
auth_n_request.attrib["Version"] = "2.0"
|
||||
# Create issuer object
|
||||
auth_n_request.append(self.get_issuer())
|
||||
|
||||
if self.source.signing_kp:
|
||||
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
|
||||
self.source.signature_algorithm, xmlsec.constants.TransformRsaSha1
|
||||
)
|
||||
signature = xmlsec.template.create(
|
||||
auth_n_request,
|
||||
xmlsec.constants.TransformExclC14N,
|
||||
sign_algorithm_transform,
|
||||
ns="ds", # type: ignore
|
||||
)
|
||||
auth_n_request.append(signature)
|
||||
|
||||
# Create NameID Policy Object
|
||||
auth_n_request.append(self.get_name_id_policy())
|
||||
return auth_n_request
|
||||
@ -77,12 +90,38 @@ class RequestProcessor:
|
||||
auth_n_request = self.get_auth_n()
|
||||
|
||||
if self.source.signing_kp:
|
||||
signed_request = XMLSigner().sign(
|
||||
auth_n_request,
|
||||
cert=self.source.signing_kp.certificate_data,
|
||||
key=self.source.signing_kp.key_data,
|
||||
xmlsec.tree.add_ids(auth_n_request, ["ID"])
|
||||
|
||||
ctx = xmlsec.SignatureContext()
|
||||
|
||||
key = xmlsec.Key.from_memory(
|
||||
self.source.signing_kp.key_data, xmlsec.constants.KeyDataFormatPem, None
|
||||
)
|
||||
return etree.tostring(signed_request).decode()
|
||||
key.load_cert_from_memory(
|
||||
self.source.signing_kp.certificate_data,
|
||||
xmlsec.constants.KeyDataFormatCertPem,
|
||||
)
|
||||
ctx.key = key
|
||||
|
||||
digest_algorithm_transform = DIGEST_ALGORITHM_TRANSLATION_MAP.get(
|
||||
self.source.digest_algorithm, xmlsec.constants.TransformSha1
|
||||
)
|
||||
|
||||
signature_node = xmlsec.tree.find_node(
|
||||
auth_n_request, xmlsec.constants.NodeSignature
|
||||
)
|
||||
|
||||
ref = xmlsec.template.add_reference(
|
||||
signature_node,
|
||||
digest_algorithm_transform,
|
||||
uri="#" + auth_n_request.attrib["ID"],
|
||||
)
|
||||
xmlsec.template.add_transform(ref, xmlsec.constants.TransformEnveloped)
|
||||
xmlsec.template.add_transform(ref, xmlsec.constants.TransformExclC14N)
|
||||
key_info = xmlsec.template.ensure_key_info(signature_node)
|
||||
xmlsec.template.add_x509_data(key_info)
|
||||
|
||||
ctx.sign(signature_node)
|
||||
|
||||
return etree.tostring(auth_n_request).decode()
|
||||
|
||||
@ -103,22 +142,31 @@ class RequestProcessor:
|
||||
response_dict["RelayState"] = self.relay_state
|
||||
|
||||
if self.source.signing_kp:
|
||||
sig_alg = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
|
||||
sig_hash = hashes.SHA1() # nosec
|
||||
sign_algorithm_transform = SIGN_ALGORITHM_TRANSFORM_MAP.get(
|
||||
self.source.signature_algorithm, xmlsec.constants.TransformRsaSha1
|
||||
)
|
||||
|
||||
# Create the full querystring in the correct order to be signed
|
||||
querystring = f"SAMLRequest={quote_plus(saml_request)}&"
|
||||
if self.relay_state != "":
|
||||
querystring += f"RelayState={quote_plus(self.relay_state)}&"
|
||||
querystring += f"SigAlg={sig_alg}"
|
||||
if "RelayState" in response_dict:
|
||||
querystring += f"RelayState={quote_plus(response_dict['RelayState'])}&"
|
||||
querystring += f"SigAlg={quote_plus(self.source.signature_algorithm)}"
|
||||
|
||||
signature = self.source.signing_kp.private_key.sign(
|
||||
querystring.encode(),
|
||||
padding.PSS(
|
||||
mgf=padding.MGF1(sig_hash), salt_length=padding.PSS.MAX_LENGTH
|
||||
),
|
||||
sig_hash,
|
||||
ctx = xmlsec.SignatureContext()
|
||||
|
||||
key = xmlsec.Key.from_memory(
|
||||
self.source.signing_kp.key_data, xmlsec.constants.KeyDataFormatPem, None
|
||||
)
|
||||
key.load_cert_from_memory(
|
||||
self.source.signing_kp.certificate_data,
|
||||
xmlsec.constants.KeyDataFormatPem,
|
||||
)
|
||||
ctx.key = key
|
||||
|
||||
signature = ctx.sign_binary(
|
||||
querystring.encode("utf-8"), sign_algorithm_transform
|
||||
)
|
||||
response_dict["SigAlg"] = sig_alg
|
||||
response_dict["Signature"] = b64encode(signature).decode()
|
||||
response_dict["SigAlg"] = self.source.signature_algorithm
|
||||
|
||||
return response_dict
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
"""passbook saml source processor"""
|
||||
from typing import TYPE_CHECKING, Dict
|
||||
from base64 import b64decode
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
|
||||
from defusedxml import ElementTree
|
||||
import xmlsec
|
||||
from defusedxml.lxml import fromstring
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from signxml import XMLVerifier
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import User
|
||||
@ -18,14 +19,15 @@ from passbook.flows.planner import (
|
||||
from passbook.flows.views import SESSION_KEY_PLAN
|
||||
from passbook.lib.utils.urls import redirect_with_qs
|
||||
from passbook.policies.utils import delete_none_keys
|
||||
from passbook.providers.saml.utils.encoding import decode_base64_and_inflate
|
||||
from passbook.sources.saml.exceptions import (
|
||||
InvalidSignature,
|
||||
MismatchedRequestID,
|
||||
MissingSAMLResponse,
|
||||
UnsupportedNameIDFormat,
|
||||
)
|
||||
from passbook.sources.saml.models import SAMLSource
|
||||
from passbook.sources.saml.processors.constants import (
|
||||
NS_MAP,
|
||||
SAML_NAME_ID_FORMAT_EMAIL,
|
||||
SAML_NAME_ID_FORMAT_PERSISTENT,
|
||||
SAML_NAME_ID_FORMAT_TRANSIENT,
|
||||
@ -49,7 +51,7 @@ class ResponseProcessor:
|
||||
|
||||
_source: SAMLSource
|
||||
|
||||
_root: "Element"
|
||||
_root: Any
|
||||
_root_xml: str
|
||||
|
||||
def __init__(self, source: SAMLSource):
|
||||
@ -61,20 +63,36 @@ class ResponseProcessor:
|
||||
raw_response = request.POST.get("SAMLResponse", None)
|
||||
if not raw_response:
|
||||
raise MissingSAMLResponse("Request does not contain 'SAMLResponse'")
|
||||
# relay_state = request.POST.get('RelayState', None)
|
||||
# Check if response is compressed, b64 decode it
|
||||
self._root_xml = decode_base64_and_inflate(raw_response)
|
||||
self._root = ElementTree.fromstring(self._root_xml)
|
||||
self._root_xml = b64decode(raw_response.encode()).decode()
|
||||
self._root = fromstring(self._root_xml)
|
||||
|
||||
self._verify_signed()
|
||||
if self._source.signing_kp:
|
||||
self._verify_signed()
|
||||
self._verify_request_id(request)
|
||||
|
||||
def _verify_signed(self):
|
||||
"""Verify SAML Response's Signature"""
|
||||
verifier = XMLVerifier()
|
||||
verifier.verify(
|
||||
self._root_xml, x509_cert=self._source.signing_kp.certificate_data
|
||||
signature_nodes = self._root.xpath(
|
||||
"/samlp:Response/saml:Assertion/ds:Signature", namespaces=NS_MAP
|
||||
)
|
||||
if len(signature_nodes) != 1:
|
||||
raise InvalidSignature()
|
||||
signature_node = signature_nodes[0]
|
||||
xmlsec.tree.add_ids(self._root, ["ID"])
|
||||
|
||||
ctx = xmlsec.SignatureContext()
|
||||
key = xmlsec.Key.from_memory(
|
||||
self._source.signing_kp.certificate_data,
|
||||
xmlsec.constants.KeyDataFormatCertPem,
|
||||
)
|
||||
ctx.key = key
|
||||
|
||||
ctx.set_enabled_key_data([xmlsec.constants.KeyDataX509])
|
||||
try:
|
||||
ctx.verify(signature_node)
|
||||
except (xmlsec.InternalError, xmlsec.VerificationError) as exc:
|
||||
raise InvalidSignature from exc
|
||||
LOGGER.debug("Successfully verified signautre")
|
||||
|
||||
def _verify_request_id(self, request: HttpRequest):
|
||||
|
||||
@ -8,7 +8,7 @@ from django.utils.http import urlencode
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from signxml import InvalidSignature
|
||||
from xmlsec import VerificationError
|
||||
|
||||
from passbook.lib.views import bad_request_message
|
||||
from passbook.providers.saml.utils.encoding import nice64
|
||||
@ -77,7 +77,7 @@ class ACSView(View):
|
||||
processor.parse(request)
|
||||
except MissingSAMLResponse as exc:
|
||||
return bad_request_message(request, str(exc))
|
||||
except InvalidSignature as exc:
|
||||
except VerificationError as exc:
|
||||
return bad_request_message(request, str(exc))
|
||||
|
||||
try:
|
||||
|
||||
@ -112,7 +112,6 @@ class PasswordStageView(FormView, StageView):
|
||||
# No user was found -> invalid credentials
|
||||
LOGGER.debug("Invalid credentials")
|
||||
# Manually inject error into form
|
||||
# pylint: disable=protected-access
|
||||
errors = form._errors.setdefault("password", ErrorList())
|
||||
errors.append(_("Invalid password"))
|
||||
return self.form_invalid(form)
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
}
|
||||
|
||||
.pb-brand > img {
|
||||
max-height: 76px;
|
||||
max-height: 68px;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
|
||||
@ -34,11 +34,12 @@ require (
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.7.1 // indirect
|
||||
go.mongodb.org/mongo-driver v1.4.3 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de // indirect
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 // indirect
|
||||
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 // indirect
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
|
||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba // indirect
|
||||
golang.org/x/text v0.3.4 // indirect
|
||||
golang.org/x/tools v0.0.0-20201102043006-b53d4cbd60a6 // indirect
|
||||
golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
|
||||
)
|
||||
|
||||
10
proxy/go.sum
10
proxy/go.sum
@ -664,6 +664,8 @@ go.mongodb.org/mongo-driver v1.3.4/go.mod h1:MSWZXKOynuguX+JSvwP8i+58jYCXxbia8HS
|
||||
go.mongodb.org/mongo-driver v1.3.5/go.mod h1:Ual6Gkco7ZGQw8wE1t4tLnvBsf6yVSM60qW6TgOeJ5c=
|
||||
go.mongodb.org/mongo-driver v1.4.2 h1:WlnEglfTg/PfPq4WXs2Vkl/5ICC6hoG8+r+LraPmGk4=
|
||||
go.mongodb.org/mongo-driver v1.4.2/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
|
||||
go.mongodb.org/mongo-driver v1.4.3 h1:moga+uhicpVshTyaqY9L23E6QqwcHRUv1sqyOsoyOO8=
|
||||
go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
|
||||
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
@ -773,6 +775,8 @@ golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81R
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
@ -845,6 +849,8 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1 h1:a/mKvvZr9Jcc8oKfcmgzyp7OwF73JPWsQLvH1z2Kxck=
|
||||
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba h1:xmhUJGQGbxlod18iJGqVEp9cHIPLl7QiX2aA3to708s=
|
||||
golang.org/x/sys v0.0.0-20201113233024-12cec1faf1ba/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -913,8 +919,8 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f h1:33yHANSyO/TeglgY9rBhUpX43wtonTXoFOsMRtNB6qE=
|
||||
golang.org/x/tools v0.0.0-20200817023811-d00afeaade8f/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20201102043006-b53d4cbd60a6 h1:vTr2e3iWbC27MMR83IzSZeEKQSzJAhwDM+Ld5YSaHA0=
|
||||
golang.org/x/tools v0.0.0-20201102043006-b53d4cbd60a6/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b h1:Ych5r0Z6MLML1fgf5hTg9p5bV56Xqx9xv9hLgMBATWs=
|
||||
golang.org/x/tools v0.0.0-20201114224030-61ea331ec02b/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
package pkg
|
||||
|
||||
const VERSION = "0.12.9-stable"
|
||||
const VERSION = "0.12.11-stable"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#!/bin/bash -xe
|
||||
wget -q -O - https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash
|
||||
|
||||
VERSION=3.8.5
|
||||
VERSION=3.9.0
|
||||
|
||||
wget https://www.python.org/ftp/python/$VERSION/Python-$VERSION.tgz
|
||||
tar xvzf Python-$VERSION.tgz
|
||||
@ -12,5 +12,5 @@ make -j 8
|
||||
sudo make altinstall
|
||||
touch $HOME/_work/_tool/Python/$VERSION/x64.complete
|
||||
|
||||
ln -s $HOME/_work/_tool/Python/3.8.5/x64 $HOME/_work/_tool/Python/3/x64
|
||||
ln -s $HOME/_work/_tool/Python/3.8.5/x64 $HOME/_work/_tool/Python/3.8/x64
|
||||
ln -s $HOME/_work/_tool/Python/3.9.5/x64 $HOME/_work/_tool/Python/3/x64
|
||||
ln -s $HOME/_work/_tool/Python/3.9.5/x64 $HOME/_work/_tool/Python/3.9/x64
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM python:3.8-slim-buster as locker
|
||||
FROM python:3.9-slim-buster as locker
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
COPY ./Pipfile.lock /app/
|
||||
@ -9,7 +9,7 @@ RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -rd > requirements-dev.txt
|
||||
|
||||
FROM python:3.8-slim-buster as static-build
|
||||
FROM python:3.9-slim-buster as static-build
|
||||
|
||||
COPY --from=locker /app/requirements.txt /app/
|
||||
COPY --from=locker /app/requirements-dev.txt /app/
|
||||
@ -17,7 +17,7 @@ COPY --from=locker /app/requirements-dev.txt /app/
|
||||
WORKDIR /app/
|
||||
|
||||
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 libxmlsec1-dev pkg-config && \
|
||||
rm -rf /var/lib/apt/ && \
|
||||
pip install -r requirements.txt --no-cache-dir && \
|
||||
apt-get remove --purge -y build-essential && \
|
||||
|
||||
61
swagger.yaml
61
swagger.yaml
@ -7431,7 +7431,6 @@ definitions:
|
||||
required:
|
||||
- name
|
||||
- acs_url
|
||||
- issuer
|
||||
type: object
|
||||
properties:
|
||||
pk:
|
||||
@ -7450,6 +7449,7 @@ definitions:
|
||||
minLength: 1
|
||||
audience:
|
||||
title: Audience
|
||||
description: Value of the audience restriction field of the asseration.
|
||||
type: string
|
||||
minLength: 1
|
||||
issuer:
|
||||
@ -7485,30 +7485,30 @@ definitions:
|
||||
title: Digest algorithm
|
||||
type: string
|
||||
enum:
|
||||
- sha1
|
||||
- sha256
|
||||
- http://www.w3.org/2000/09/xmldsig#sha1
|
||||
- http://www.w3.org/2001/04/xmlenc#sha256
|
||||
- http://www.w3.org/2001/04/xmldsig-more#sha384
|
||||
- http://www.w3.org/2001/04/xmlenc#sha512
|
||||
signature_algorithm:
|
||||
title: Signature algorithm
|
||||
type: string
|
||||
enum:
|
||||
- rsa-sha1
|
||||
- rsa-sha256
|
||||
- ecdsa-sha256
|
||||
- dsa-sha1
|
||||
- http://www.w3.org/2000/09/xmldsig#rsa-sha1
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
|
||||
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
|
||||
signing_kp:
|
||||
title: Signing Keypair
|
||||
description: Singing is enabled upon selection of a Key Pair.
|
||||
description: Keypair used to sign outgoing Responses going to the Service
|
||||
Provider.
|
||||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
require_signing:
|
||||
title: Require signing
|
||||
description: Require Requests to be signed by an X509 Certificate. Must match
|
||||
the Certificate selected in `Singing Keypair`.
|
||||
type: boolean
|
||||
verification_kp:
|
||||
title: Verification Keypair
|
||||
description: If selected, incoming assertion's Signatures will be validated.
|
||||
title: Verification Certificate
|
||||
description: When selected, incoming assertion's Signatures will be validated
|
||||
against this certificate. To allow unsigned Requests, leave on default.
|
||||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
@ -7782,7 +7782,6 @@ definitions:
|
||||
- name
|
||||
- slug
|
||||
- sso_url
|
||||
- signing_kp
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
@ -7854,6 +7853,30 @@ definitions:
|
||||
- REDIRECT
|
||||
- POST
|
||||
- POST_AUTO
|
||||
signing_kp:
|
||||
title: Singing Keypair
|
||||
description: Keypair which is used to sign outgoing requests. Leave empty
|
||||
to disable signing.
|
||||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
digest_algorithm:
|
||||
title: Digest algorithm
|
||||
type: string
|
||||
enum:
|
||||
- http://www.w3.org/2000/09/xmldsig#sha1
|
||||
- http://www.w3.org/2001/04/xmlenc#sha256
|
||||
- http://www.w3.org/2001/04/xmldsig-more#sha384
|
||||
- http://www.w3.org/2001/04/xmlenc#sha512
|
||||
signature_algorithm:
|
||||
title: Signature algorithm
|
||||
type: string
|
||||
enum:
|
||||
- http://www.w3.org/2000/09/xmldsig#rsa-sha1
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
|
||||
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
|
||||
- http://www.w3.org/2000/09/xmldsig#dsa-sha1
|
||||
temporary_user_delete_after:
|
||||
title: Delete temporary users after
|
||||
description: "Time offset when temporary users should be deleted. This only\
|
||||
@ -7861,12 +7884,6 @@ definitions:
|
||||
\ log out manually. (Format: hours=1;minutes=2;seconds=3)."
|
||||
type: string
|
||||
minLength: 1
|
||||
signing_kp:
|
||||
title: Singing Keypair
|
||||
description: Certificate Key Pair of the IdP which Assertion's Signature is
|
||||
validated against.
|
||||
type: string
|
||||
format: uuid
|
||||
Stage:
|
||||
description: Stage Serializer
|
||||
required:
|
||||
|
||||
20
website/.gitignore
vendored
Normal file
20
website/.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
4
website/.prettierignore
Normal file
4
website/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
# Ignore artifacts:
|
||||
build
|
||||
coverage
|
||||
.docusaurus
|
||||
1
website/.prettierrc.json
Normal file
1
website/.prettierrc.json
Normal file
@ -0,0 +1 @@
|
||||
{}
|
||||
33
website/README.md
Normal file
33
website/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator.
|
||||
|
||||
## Installation
|
||||
|
||||
```console
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
```console
|
||||
yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
## Build
|
||||
|
||||
```console
|
||||
yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
## Deployment
|
||||
|
||||
```console
|
||||
GIT_USER=<Your GitHub username> USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||
3
website/babel.config.js
Normal file
3
website/babel.config.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
|
||||
};
|
||||
@ -1,16 +1,19 @@
|
||||
# Expressions
|
||||
---
|
||||
title: Expressions
|
||||
---
|
||||
|
||||
Expressions allow you to write custom logic using Python code.
|
||||
|
||||
Expressions are used in different places throughout passbook, and can do different things.
|
||||
|
||||
!!! info
|
||||
These functions/objects are available wherever expressions are used. For more specific information, see [Expression Policies](../policies/expression.md) and [Property Mappings](../property-mappings/expression.md)
|
||||
:::info
|
||||
These functions/objects are available wherever expressions are used. For more specific information, see [Expression Policies](../policies/expression.md) and [Property Mappings](../property-mappings/expression.md)
|
||||
:::
|
||||
|
||||
## Global objects
|
||||
|
||||
- `pb_logger`: structlog BoundLogger. ([ref](https://www.structlog.org/en/stable/api.html#structlog.BoundLogger))
|
||||
- `requests`: requests Session object. ([ref](https://requests.readthedocs.io/en/master/user/advanced/))
|
||||
- `pb_logger`: structlog BoundLogger. ([ref](https://www.structlog.org/en/stable/api.html#structlog.BoundLogger))
|
||||
- `requests`: requests Session object. ([ref](https://requests.readthedocs.io/en/master/user/advanced/))
|
||||
|
||||
## Generally available functions
|
||||
|
||||
29
website/docs/expressions/reference/user-object.md
Normal file
29
website/docs/expressions/reference/user-object.md
Normal file
@ -0,0 +1,29 @@
|
||||
---
|
||||
title: User Object
|
||||
---
|
||||
|
||||
The User object has the following attributes:
|
||||
|
||||
- `username`: User's username.
|
||||
- `email` User's email.
|
||||
- `name` User's display name.
|
||||
- `is_staff` Boolean field if user is staff.
|
||||
- `is_active` Boolean field if user is active.
|
||||
- `date_joined` Date user joined/was created.
|
||||
- `password_change_date` Date password was last changed.
|
||||
- `attributes` Dynamic attributes.
|
||||
- `group_attributes` Merged attributes of all groups the user is member of and the user's own attributes.
|
||||
- `pb_groups` This is a queryset of all the user's groups.
|
||||
|
||||
You can do additional filtering like `user.pb_groups.filter(name__startswith='test')`, see [here](https://docs.djangoproject.com/en/3.1/ref/models/querysets/#id4)
|
||||
|
||||
To get the name of all groups, you can do `[group.name for group in user.pb_groups.all()]`
|
||||
|
||||
## Examples
|
||||
|
||||
List all the User's group names:
|
||||
|
||||
```python
|
||||
for group in user.pb_groups.all():
|
||||
yield group.name
|
||||
```
|
||||
@ -1,18 +1,20 @@
|
||||
# Example Flows
|
||||
|
||||
!!! info
|
||||
You can apply theses flows multiple times to stay updated, however this will discard all changes you've made.
|
||||
---
|
||||
title: Example Flows
|
||||
---
|
||||
|
||||
:::info
|
||||
You can apply theses flows multiple times to stay updated, however this will discard all changes you've made.
|
||||
:::
|
||||
|
||||
## Enrollment (2 Stage)
|
||||
|
||||
Flow: right-click [here](enrollment-2-stage.json) and save the file.
|
||||
Flow: right-click [here](/flows/enrollment-2-stage.pbflow) and save the file.
|
||||
|
||||
Sign-up flow for new users, which prompts them for their username, email, password and name. No verification is done. Users are also immediately logged on after this flow.
|
||||
|
||||
## Enrollment with email verification
|
||||
|
||||
Flow: right-click [here](enrollment-email-verification.json) and save the file.
|
||||
Flow: right-click [here](/flows/enrollment-email-verification.pbflow) and save the file.
|
||||
|
||||
Same flow as above, with an extra email verification stage.
|
||||
|
||||
@ -20,13 +22,13 @@ You'll probably have to adjust the Email stage and set your connection details.
|
||||
|
||||
## Two-factor Login
|
||||
|
||||
Flow: right-click [here](login-2fa.json) and save the file.
|
||||
Flow: right-click [here](/flows/login-2fa.pbflow) and save the file.
|
||||
|
||||
Login flow which follows the default pattern (username/email, then password), but also checks for the user's OTP token, if they have one configured
|
||||
|
||||
## Login with conditional Captcha
|
||||
|
||||
Flow: right-click [here](login-conditional-captcha.json) and save the file.
|
||||
Flow: right-click [here](/flows/login-conditional-captcha.pbflow) and save the file.
|
||||
|
||||
Login flow which conditionally shows the users a captcha, based on the reputation of their IP and Username.
|
||||
|
||||
@ -34,16 +36,16 @@ By default, the captcha test keys are used. You can get a proper key [here](http
|
||||
|
||||
## Recovery with email verification
|
||||
|
||||
Flow: right-click [here](recovery-email-verification.json) and save the file.
|
||||
Flow: right-click [here](/flows/recovery-email-verification.pbflow) and save the file.
|
||||
|
||||
Recovery flow, the user is sent an email after they've identified themselves. After they click on the link in the email, they are prompted for a new password and immediately logged on.
|
||||
|
||||
## User deletion
|
||||
|
||||
Flow: right-click [here](unenrollment.json) and save the file.
|
||||
Flow: right-click [here](/flows/unenrollment.pbflow) and save the file.
|
||||
|
||||
Flow for users to delete their account,
|
||||
|
||||
!!! warning
|
||||
This is done without any warning.
|
||||
|
||||
:::warning
|
||||
This is done without any warning.
|
||||
:::
|
||||
@ -1,4 +1,6 @@
|
||||
# Flows
|
||||
---
|
||||
title: Flows
|
||||
---
|
||||
|
||||
Flows are a method of describing a sequence of stages. A stage represents a single verification or logic step. They are used to authenticate users, enroll them, and more.
|
||||
|
||||
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 140 KiB |
@ -1,4 +1,6 @@
|
||||
# Captcha stage
|
||||
---
|
||||
title: Captcha stage
|
||||
---
|
||||
|
||||
This stage adds a form of verification using [Google's ReCaptcha](https://www.google.com/recaptcha/intro/v3.html).
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
@ -1,4 +1,6 @@
|
||||
# Dummy stage
|
||||
---
|
||||
title: Dummy stage
|
||||
---
|
||||
|
||||
This stage is used for development and has no function. It presents the user with a form which requires a single confirmation.
|
||||
|
||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
@ -1,4 +1,6 @@
|
||||
# Email
|
||||
---
|
||||
title: Email stage
|
||||
---
|
||||
|
||||
This stage can be used for email verification. passbook's background worker will send an email using the specified connection details. When an email can't be delivered, delivery is automatically retried periodically.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user