Compare commits
273 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
4d51295db2 | |||
3bbded3555 | |||
b3262e2a82 | |||
40614a65fc | |||
3cf558d594 | |||
812cc0d2f1 | |||
e21ed92848 | |||
5184c4b7ef | |||
2c07859b68 | |||
ae6304c05e | |||
501683e3cb | |||
cc8afa8706 | |||
17a9e02bc0 | |||
6a669992a8 | |||
7ea5c22b6c | |||
b11d6a5891 | |||
49830367a7 | |||
e69ca5a229 | |||
a57d21f5e8 | |||
c7026407c6 | |||
69eecd6b60 | |||
810f10edfe | |||
1c57128f11 | |||
82eade3eb1 | |||
56a9dcc88d | |||
fe70d80189 | |||
e97e22c58a | |||
bb4e39aab6 | |||
a8744f443c | |||
7fe9b8f0b4 | |||
696aa7e5f6 | |||
e1d82aee1d | |||
151374f565 | |||
bebeff9f7f | |||
8b99afa34d | |||
b317852e8a | |||
24ae35c35a | |||
8e6bb48227 | |||
7a4e8af1ae | |||
0161205c82 | |||
ca0ba85023 | |||
c2ebaa7f64 | |||
23cccebb96 | |||
3f5d30e6fe | |||
ca735349f9 | |||
25ce8c6dc7 | |||
081ac0bcdb | |||
8a07b349ee | |||
b3468bc265 | |||
4edfad869f | |||
404f5d7912 | |||
8bea99a953 | |||
0b0ba33dce | |||
e3627b2cd9 | |||
37fac3ae00 | |||
17a90adf3e | |||
7c3590f8ef | |||
7471415e7f | |||
9339d496f9 | |||
e72000eb06 | |||
ec5ff7c14d | |||
43cb08b433 | |||
95a1c7b6d5 | |||
031a3d8719 | |||
430905295d | |||
1356a8108b | |||
37dcf264e5 | |||
296e1f4962 | |||
a0e81650d7 | |||
894cee6123 | |||
a7be0379f4 | |||
2d6b57839d | |||
455e39a8bd | |||
a7d8ac888a | |||
349e536d14 | |||
cddc9bc1b7 | |||
6d27408a10 | |||
50a5959f6c | |||
18f42a0edf | |||
860ba994a6 | |||
1776b72356 | |||
8db60b3e83 | |||
3b6341bf41 | |||
3b97389833 | |||
102d536a72 | |||
9712be847c | |||
f0b5e8143e | |||
cc061e5b16 | |||
fd8514331b | |||
1dc63776a5 | |||
87b14e8761 | |||
28893b9695 | |||
bb9ae28be8 | |||
0c05fd47f5 | |||
fea44486c3 | |||
bf4763d946 | |||
219e16f8e5 | |||
6ebefc9f17 | |||
80e8a3d63c | |||
dd017e7190 | |||
268de20872 | |||
14e47f3195 | |||
6d289aea48 | |||
529fd081a0 | |||
02e3c78720 | |||
abc78d6633 | |||
3f3dfc0a28 | |||
5bd27bce3f | |||
c39d136383 | |||
a977184577 | |||
b7ca40d98e | |||
b2cb794865 | |||
874f03e4dd | |||
8f08d78bf1 | |||
2661f2bbb3 | |||
7d321e8aa8 | |||
a732beb72b | |||
0996775ebf | |||
4147e8d1a7 | |||
983bbb622d | |||
885f8bae9f | |||
aaa662199c | |||
0e0898c3cf | |||
8b17e8be99 | |||
a082222b58 | |||
9826bb4d01 | |||
f7c629ec9b | |||
e2aeb96a6a | |||
ff810c689f | |||
0eb94df1f7 | |||
86597df159 | |||
0394adaf46 | |||
c7a2410b1d | |||
b9076b5fd4 | |||
c07a45083f | |||
4b10fa3d93 | |||
c910dc9a3c | |||
882dc60292 | |||
7923468a01 | |||
9ebbb51cf7 | |||
bd25cadb71 | |||
7334599efd | |||
54f0728005 | |||
e0c7637382 | |||
086a3c0548 | |||
65efbbd7ee | |||
5cc045e3c9 | |||
56d259ce75 | |||
8919bade55 | |||
703d511089 | |||
aabedfc3e4 | |||
0f154dee11 | |||
a5c46d7e72 | |||
bbd59698e1 | |||
3b0216bc00 | |||
b4fc32afac | |||
45df127f18 | |||
55cf49bb8a | |||
00ce2a90f1 | |||
de77e1e41e | |||
e40c07e997 | |||
d4b0bbb368 | |||
d05f077ba0 | |||
ca322d1e2c | |||
3c9631b287 | |||
16c2332c14 | |||
2723b2091f | |||
7b454ff72a | |||
4578bf6f29 | |||
a991632396 | |||
81d2f8c728 | |||
b42164a6b6 | |||
5857552b73 | |||
0645dde90c | |||
494a8226a4 | |||
aedd5f3f99 | |||
8a1ff7cb5b | |||
e0a9cc0e26 | |||
8f240b5303 | |||
ea39a5e952 | |||
ac539268cb | |||
ed72a2c959 | |||
e5cd9a4a2a | |||
d4f530f80b | |||
282a518e00 | |||
5d50d99f59 | |||
d56a98e561 | |||
0cfdbd92d8 | |||
6262923398 | |||
f96b1b58f3 | |||
fdf372912a | |||
2e517258fa | |||
316ac78e49 | |||
de2b67b111 | |||
e1bbbe6671 | |||
8b3839343c | |||
7897ca4744 | |||
2fd00c6c9d | |||
80f7f82fa4 | |||
1a21012911 | |||
d4a5269bf1 | |||
fcf70a3cd4 | |||
e9411d856c | |||
1a6dd00681 | |||
330bd0932b | |||
250e77f40f | |||
ef71aba544 | |||
567a8f53da | |||
88c87aa205 | |||
90ac3d56ca | |||
a298e9e2ca | |||
abdf86d9c9 | |||
c58658d820 | |||
a9b5e6ea13 | |||
ddb0fdee98 | |||
83205f1b49 | |||
7221800a16 | |||
4515cb6bbe | |||
7f9da11eba | |||
da69d2611d | |||
3b4be5695a | |||
9d68c9550b | |||
3b2d469780 | |||
ae629d1159 | |||
72a6f9cbe0 | |||
9793b7461b | |||
9c1a824dc4 | |||
738ced3327 | |||
ed1ee1fa55 | |||
95776bbc56 | |||
62a4beb3d6 | |||
466a825f5b | |||
3ffed279d7 | |||
4b6b36b2d2 | |||
2a8f63bf86 | |||
3c12cf96a9 | |||
d787caf0e4 | |||
0fc2f32d3d | |||
894d5da1d8 | |||
985d20d025 | |||
94f3e6d0c5 | |||
0a196608c7 | |||
d33f0fb2cf | |||
ffff69ada0 | |||
37a432267d | |||
88029a4335 | |||
4040eb9619 | |||
c9663a08da | |||
a3d92ebc0a | |||
6fa825e372 | |||
6aefd072c8 | |||
ac2dd3611f | |||
74e628ce9c | |||
d4ee18ee32 | |||
9ff3ee7c0c | |||
418b94a45a | |||
1393078fe6 | |||
50612991fa | |||
37b2400cdb | |||
05c3393669 | |||
c60d1e1f9a | |||
2be7d3191f | |||
aa692fdacb | |||
c163637bfd | |||
5552aca079 | |||
ff2456dcfa | |||
539264c396 | |||
1acfaf1562 | |||
a81e277cfa | |||
b4cb78f33f | |||
35c0a9532f | |||
aff074420b | |||
edbea9ccff |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.9.0-rc1
|
||||
current_version = 0.10.1-stable
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
@ -19,6 +19,10 @@ values =
|
||||
|
||||
[bumpversion:file:docs/installation/docker-compose.md]
|
||||
|
||||
[bumpversion:file:docs/installation/kubernetes.md]
|
||||
|
||||
[bumpversion:file:docker-compose.yml]
|
||||
|
||||
[bumpversion:file:helm/values.yaml]
|
||||
|
||||
[bumpversion:file:helm/Chart.yaml]
|
||||
|
@ -1,7 +1,8 @@
|
||||
[run]
|
||||
source = passbook
|
||||
relative_files = true
|
||||
omit =
|
||||
*/wsgi.py
|
||||
*/asgi.py
|
||||
manage.py
|
||||
*/migrations/*
|
||||
*/apps.py
|
||||
|
20
.fossa.yml
20
.fossa.yml
@ -1,20 +0,0 @@
|
||||
# Generated by FOSSA CLI (https://github.com/fossas/fossa-cli)
|
||||
# Visit https://fossa.com to learn more
|
||||
|
||||
version: 2
|
||||
cli:
|
||||
server: https://app.fossa.com
|
||||
fetcher: custom
|
||||
project: git@github.com:BeryJu/passbook.git
|
||||
analyze:
|
||||
modules:
|
||||
- name: static
|
||||
type: npm
|
||||
target: passbook/static/static
|
||||
path: passbook/static/static
|
||||
- name: .
|
||||
type: pip
|
||||
target: .
|
||||
path: .
|
||||
options:
|
||||
strategy: pipenv
|
54
.github/workflows/codeql-analysis.yml
vendored
Normal file
54
.github/workflows/codeql-analysis.yml
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, admin-more-info, ci-deploy-dev, gh-pages, provider-saml-v2]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 20 * * 2'
|
||||
|
||||
jobs:
|
||||
analyse:
|
||||
name: Analyse
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
44
.github/workflows/release.yml
vendored
44
.github/workflows/release.yml
vendored
@ -1,6 +1,8 @@
|
||||
name: passbook-release
|
||||
name: passbook-on-release
|
||||
|
||||
on:
|
||||
release
|
||||
release:
|
||||
types: [published, created]
|
||||
|
||||
jobs:
|
||||
# Build
|
||||
@ -16,17 +18,26 @@ jobs:
|
||||
- name: Building Docker Image
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.9.0-rc1
|
||||
-t beryju/passbook:0.10.1-stable
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.9.0-rc1
|
||||
run: docker push beryju/passbook:0.10.1-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook:latest
|
||||
build-gatekeeper:
|
||||
build-proxy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: "^1.15"
|
||||
- name: prepare go api client
|
||||
run: |
|
||||
cd proxy
|
||||
go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
||||
swagger generate client -f ../swagger.yaml -A passbook -t pkg/
|
||||
go build -v .
|
||||
- name: Docker Login Registry
|
||||
env:
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
@ -34,16 +45,16 @@ jobs:
|
||||
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
|
||||
- name: Building Docker Image
|
||||
run: |
|
||||
cd gatekeeper
|
||||
cd proxy
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook-gatekeeper:0.9.0-rc1 \
|
||||
-t beryju/passbook-gatekeeper:latest \
|
||||
-t beryju/passbook-proxy:0.10.1-stable \
|
||||
-t beryju/passbook-proxy:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-gatekeeper:0.9.0-rc1
|
||||
run: docker push beryju/passbook-proxy:0.10.1-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-gatekeeper:latest
|
||||
run: docker push beryju/passbook-proxy:latest
|
||||
build-static:
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
@ -66,11 +77,11 @@ jobs:
|
||||
run: docker build
|
||||
--no-cache
|
||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||
-t beryju/passbook-static:0.9.0-rc1
|
||||
-t beryju/passbook-static:0.10.1-stable
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.9.0-rc1
|
||||
run: docker push beryju/passbook-static:0.10.1-stable
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
@ -82,10 +93,13 @@ jobs:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
sudo apt-get install -y pwgen
|
||||
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
||||
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||
docker-compose pull -q
|
||||
docker-compose up --no-start
|
||||
docker-compose start postgresql redis
|
||||
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
|
||||
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test passbook"
|
||||
sentry-release:
|
||||
needs:
|
||||
- test-release
|
||||
@ -100,5 +114,5 @@ jobs:
|
||||
SENTRY_PROJECT: passbook
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
tagName: 0.9.0-rc1
|
||||
environment: production
|
||||
tagName: 0.10.1-stable
|
||||
environment: beryjuorg-prod
|
||||
|
12
.github/workflows/tag.yml
vendored
12
.github/workflows/tag.yml
vendored
@ -1,10 +1,10 @@
|
||||
name: passbook-on-tag
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'version/*'
|
||||
|
||||
name: passbook-version-tag
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Create Release from Tag
|
||||
@ -13,6 +13,10 @@ jobs:
|
||||
- uses: actions/checkout@master
|
||||
- name: Pre-release test
|
||||
run: |
|
||||
sudo apt-get install -y pwgen
|
||||
echo "PASSBOOK_TAG=latest" >> .env
|
||||
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
||||
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||
docker-compose pull -q
|
||||
docker build \
|
||||
--no-cache \
|
||||
@ -20,7 +24,7 @@ jobs:
|
||||
-f Dockerfile .
|
||||
docker-compose up --no-start
|
||||
docker-compose start postgresql redis
|
||||
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
|
||||
docker-compose run -u root --entrypoint /bin/bash server -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test passbook"
|
||||
- name: Install Helm
|
||||
run: |
|
||||
apt update && apt install -y curl
|
||||
@ -30,7 +34,7 @@ jobs:
|
||||
helm dependency update helm/
|
||||
helm package helm/
|
||||
mv passbook-*.tgz passbook-chart.tgz
|
||||
- name: Extract verison number
|
||||
- name: Extract version number
|
||||
id: get_version
|
||||
uses: actions/github-script@0.2.0
|
||||
with:
|
||||
|
@ -1,6 +1,6 @@
|
||||
[MASTER]
|
||||
|
||||
disable=redefined-outer-name,arguments-differ,no-self-use,cyclic-import,fixme,locally-disabled,too-many-ancestors,too-few-public-methods,import-outside-toplevel,bad-continuation,signature-differs
|
||||
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
|
||||
load-plugins=pylint_django,pylint.extensions.bad_builtin
|
||||
extension-pkg-whitelist=lxml
|
||||
const-rgx=[a-zA-Z0-9_]{1,40}$
|
||||
|
27
Dockerfile
27
Dockerfile
@ -11,25 +11,22 @@ RUN pip install pipenv && \
|
||||
|
||||
FROM python:3.8-slim-buster
|
||||
|
||||
COPY --from=locker /app/requirements.txt /app/
|
||||
COPY --from=locker /app/requirements-dev.txt /app/
|
||||
|
||||
WORKDIR /app/
|
||||
WORKDIR /
|
||||
COPY --from=locker /app/requirements.txt /
|
||||
COPY --from=locker /app/requirements-dev.txt /
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-11 && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-11 build-essential && \
|
||||
rm -rf /var/lib/apt/ && \
|
||||
pip install -r requirements.txt --no-cache-dir && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /app passbook
|
||||
pip install -r /requirements.txt --no-cache-dir && \
|
||||
apt-get remove --purge -y build-essential && \
|
||||
apt-get autoremove --purge && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /passbook passbook
|
||||
|
||||
COPY ./passbook/ /app/passbook
|
||||
COPY ./manage.py /app/
|
||||
COPY ./docker/uwsgi.ini /app/
|
||||
COPY ./docker/bootstrap.sh /bootstrap.sh
|
||||
COPY ./docker/wait_for_db.py /app/wait_for_db.py
|
||||
|
||||
WORKDIR /app/
|
||||
COPY ./passbook/ /passbook
|
||||
COPY ./manage.py /
|
||||
COPY ./lifecycle/ /lifecycle
|
||||
|
||||
USER passbook
|
||||
|
||||
ENTRYPOINT [ "/bootstrap.sh" ]
|
||||
ENTRYPOINT [ "/lifecycle/bootstrap.sh" ]
|
||||
|
26
Makefile
Normal file
26
Makefile
Normal file
@ -0,0 +1,26 @@
|
||||
all: lint-fix lint coverage gen
|
||||
|
||||
coverage:
|
||||
coverage run --concurrency=multiprocessing manage.py test --failfast -v 3
|
||||
coverage combine
|
||||
coverage html
|
||||
coverage report
|
||||
|
||||
lint-fix:
|
||||
isort -rc .
|
||||
black .
|
||||
|
||||
lint:
|
||||
pyright
|
||||
bandit -r .
|
||||
pylint passbook
|
||||
prospector
|
||||
|
||||
gen: coverage
|
||||
./manage.py generate_swagger -o swagger.yaml -f yaml
|
||||
|
||||
local-stack:
|
||||
export PASSBOOK_TAG=testing
|
||||
docker build -t beryju/passbook:testng .
|
||||
docker-compose up -d
|
||||
docker-compose run --rm server migrate
|
26
Pipfile
26
Pipfile
@ -13,8 +13,6 @@ django-dbbackup = "*"
|
||||
django-filter = "*"
|
||||
django-guardian = "*"
|
||||
django-model-utils = "*"
|
||||
django-oauth-toolkit = "*"
|
||||
django-oidc-provider = "*"
|
||||
django-otp = "*"
|
||||
django-prometheus = "*"
|
||||
django-recaptcha = "*"
|
||||
@ -23,14 +21,15 @@ django-rest-framework = "*"
|
||||
django-storages = "*"
|
||||
djangorestframework-guardian = "*"
|
||||
drf-yasg = "*"
|
||||
kombu = "*"
|
||||
facebook-sdk = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
oauthlib = "*"
|
||||
packaging = "*"
|
||||
psycopg2-binary = "*"
|
||||
pycryptodome = "*"
|
||||
pyuwsgi = "*"
|
||||
pyjwkest = "*"
|
||||
uvicorn = "*"
|
||||
gunicorn = "*"
|
||||
pyyaml = "*"
|
||||
qrcode = "*"
|
||||
requests-oauthlib = "*"
|
||||
@ -40,8 +39,10 @@ signxml = "*"
|
||||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
facebook-sdk = "*"
|
||||
elastic-apm = "*"
|
||||
dacite = "*"
|
||||
channels = "*"
|
||||
channels-redis = "*"
|
||||
kubernetes = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
@ -49,16 +50,15 @@ python_version = "3.8"
|
||||
[dev-packages]
|
||||
autopep8 = "*"
|
||||
bandit = "*"
|
||||
black = "==19.10b0"
|
||||
bumpversion = "*"
|
||||
colorama = "*"
|
||||
coverage = "*"
|
||||
django-debug-toolbar = "*"
|
||||
docker = "*"
|
||||
pylint = "*"
|
||||
pylint-django = "*"
|
||||
unittest-xml-reporting = "*"
|
||||
black = "*"
|
||||
selenium = "*"
|
||||
docker = "*"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
prospector = "*"
|
||||
pytest = "*"
|
||||
pytest-django = "*"
|
||||
|
1164
Pipfile.lock
generated
1164
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,9 @@
|
||||
<img src="passbook/static/static/passbook/logo.svg" height="50" alt="passbook logo"><img src="passbook/static/static/passbook/brand_inverted.svg" height="50" alt="passbook">
|
||||
<img src="docs/images/logo.svg" height="50" alt="passbook logo"><img src="docs/images/brand_inverted.svg" height="50" alt="passbook">
|
||||
|
||||
[](https://dev.azure.com/beryjuorg/passbook/_build?definitionId=1)
|
||||

|
||||
[](https://codecov.io/gh/BeryJu/passbook)
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@ -21,12 +20,12 @@ wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
||||
# Optionally enable Error-reporting
|
||||
# export PASSBOOK_ERROR_REPORTING=true
|
||||
# Optionally deploy a different version
|
||||
# export PASSBOOK_TAG=0.9.0-rc1
|
||||
# export PASSBOOK_TAG=0.10.1-stable
|
||||
# If this is a productive installation, set a different PostgreSQL Password
|
||||
# export PG_PASS=$(pwgen 40 1)
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker-compose exec server ./manage.py migrate
|
||||
docker-compose run --rm server migrate
|
||||
```
|
||||
|
||||
For bigger setups, there is a Helm Chart in the `helm/` directory. This is documented [here](https://passbook.beryju.org//installation/kubernetes/)
|
||||
@ -61,7 +60,6 @@ postgresql:
|
||||
user: postgres
|
||||
|
||||
log_level: debug
|
||||
error_reporting: false
|
||||
```
|
||||
|
||||
## Security
|
||||
|
@ -10,4 +10,4 @@ As passbook is currently in a pre-stable, only the latest "stable" version is su
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, send am email to [security@beryju.org](mailto:security@beryju.org)
|
||||
To report a vulnerability, send an email to [security@beryju.org](mailto:security@beryju.org)
|
||||
|
@ -139,7 +139,7 @@ stages:
|
||||
displayName: Run full test suite
|
||||
inputs:
|
||||
script: |
|
||||
pipenv run coverage run ./manage.py test passbook
|
||||
pipenv run coverage run ./manage.py test passbook -v 3
|
||||
mkdir output-unittest
|
||||
mv unittest.xml output-unittest/unittest.xml
|
||||
mv .coverage output-unittest/coverage
|
||||
@ -150,7 +150,7 @@ stages:
|
||||
publishLocation: 'pipeline'
|
||||
- job: coverage_e2e
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
name: coventry
|
||||
steps:
|
||||
- task: UsePythonVersion@0
|
||||
inputs:
|
||||
@ -181,7 +181,7 @@ stages:
|
||||
- task: CmdLine@2
|
||||
displayName: Run full test suite
|
||||
inputs:
|
||||
script: pipenv run coverage run ./manage.py test e2e
|
||||
script: pipenv run coverage run ./manage.py test e2e -v 3
|
||||
- task: CmdLine@2
|
||||
displayName: Prepare unittests and coverage for upload
|
||||
inputs:
|
||||
@ -225,11 +225,9 @@ stages:
|
||||
script: |
|
||||
sudo pip install -U wheel pipenv
|
||||
pipenv install --dev
|
||||
find .
|
||||
pipenv run coverage combine coverage-e2e/coverage coverage-unittest/coverage
|
||||
pipenv run coverage xml
|
||||
pipenv run coverage html
|
||||
find .
|
||||
- task: PublishCodeCoverageResults@1
|
||||
inputs:
|
||||
codeCoverageTool: 'Cobertura'
|
||||
@ -261,18 +259,6 @@ stages:
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'Dockerfile'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- job: build_gatekeeper
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: Docker@2
|
||||
inputs:
|
||||
containerRegistry: 'dockerhub'
|
||||
repository: 'beryju/passbook-gatekeeper'
|
||||
command: 'buildAndPush'
|
||||
Dockerfile: 'gatekeeper/Dockerfile'
|
||||
buildContext: 'gatekeeper/'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- job: build_static
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
@ -297,3 +283,19 @@ stages:
|
||||
repository: 'beryju/passbook-static'
|
||||
command: 'push'
|
||||
tags: 'gh-$(Build.SourceBranchName)'
|
||||
- stage: Deploy
|
||||
jobs:
|
||||
- job: deploy_dev
|
||||
pool:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: HelmDeploy@0
|
||||
inputs:
|
||||
connectionType: 'Kubernetes Service Connection'
|
||||
kubernetesServiceConnection: 'k8s-beryjuorg-prd'
|
||||
namespace: 'passbook-dev'
|
||||
command: 'upgrade'
|
||||
chartType: 'FilePath'
|
||||
chartPath: 'helm/'
|
||||
releaseName: 'passbook-dev'
|
||||
recreate: true
|
||||
|
@ -14,6 +14,8 @@ services:
|
||||
- POSTGRES_DB=passbook
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
env_file:
|
||||
- .env
|
||||
redis:
|
||||
image: redis
|
||||
networks:
|
||||
@ -21,15 +23,13 @@ services:
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
server:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-latest}
|
||||
command:
|
||||
- uwsgi
|
||||
- uwsgi.ini
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.1-stable}
|
||||
command: server
|
||||
environment:
|
||||
- PASSBOOK_REDIS__HOST=redis
|
||||
- PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false}
|
||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
PASSBOOK_LOG_LEVEL: debug
|
||||
ports:
|
||||
- 8000
|
||||
networks:
|
||||
@ -38,27 +38,24 @@ services:
|
||||
- traefik.port=8000
|
||||
- traefik.docker.network=internal
|
||||
- traefik.frontend.rule=PathPrefix:/
|
||||
env_file:
|
||||
- .env
|
||||
worker:
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-latest}
|
||||
command:
|
||||
- celery
|
||||
- worker
|
||||
- --autoscale=10,3
|
||||
- -E
|
||||
- -B
|
||||
- -A=passbook.root.celery
|
||||
- -s=/tmp/celerybeat-schedule
|
||||
image: beryju/passbook:${PASSBOOK_TAG:-0.10.1-stable}
|
||||
command: worker
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
environment:
|
||||
- PASSBOOK_REDIS__HOST=redis
|
||||
- PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false}
|
||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||
PASSBOOK_REDIS__HOST: redis
|
||||
PASSBOOK_POSTGRESQL__HOST: postgresql
|
||||
PASSBOOK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||
PASSBOOK_LOG_LEVEL: debug
|
||||
env_file:
|
||||
- .env
|
||||
static:
|
||||
image: beryju/passbook-static:latest
|
||||
image: beryju/passbook-static:${PASSBOOK_TAG:-0.10.1-stable}
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
|
@ -1,3 +0,0 @@
|
||||
#!/bin/bash -ex
|
||||
/app/wait_for_db.py
|
||||
"$@"
|
@ -1,10 +0,0 @@
|
||||
[uwsgi]
|
||||
http = 0.0.0.0:8000
|
||||
wsgi-file = passbook/root/wsgi.py
|
||||
processes = 2
|
||||
master = true
|
||||
threads = 2
|
||||
enable-threads = true
|
||||
uid = passbook
|
||||
gid = passbook
|
||||
disable-logging = True
|
@ -1,3 +0,0 @@
|
||||
#!/bin/bash -x
|
||||
pip install -U mkdocs mkdocs-material
|
||||
mkdocs gh-deploy
|
180
docs/flow/examples/enrollment-2-stage.json
Normal file
180
docs/flow/examples/enrollment-2-stage.json
Normal file
@ -0,0 +1,180 @@
|
||||
{
|
||||
"version": 1,
|
||||
"entries": [
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"slug": "default-enrollment-flow"
|
||||
},
|
||||
"model": "passbook_flows.flow",
|
||||
"attrs": {
|
||||
"name": "Default enrollment Flow",
|
||||
"title": "Welcome to passbook!",
|
||||
"designation": "enrollment"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "cb954fd4-65a5-4ad9-b1ee-180ee9559cf4"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "username",
|
||||
"label": "Username",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"placeholder": "Username",
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "7db91ee8-4290-4e08-8d39-63f132402515"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "password",
|
||||
"label": "Password",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Password",
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "d30b5eb4-7787-4072-b1ba-65b46e928920"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "password_repeat",
|
||||
"label": "Password (repeat)",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Password (repeat)",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "f78d977a-efa6-4cc2-9a0f-2621a9fd94d2"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "name",
|
||||
"label": "Name",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"placeholder": "Name",
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "1ff91927-e33d-4615-95b0-c258e5f0df62"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "email",
|
||||
"label": "Email",
|
||||
"type": "email",
|
||||
"required": true,
|
||||
"placeholder": "Email",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "6c342b94-790d-425a-ae31-6196b6570722",
|
||||
"name": "default-enrollment-prompt-second"
|
||||
},
|
||||
"model": "passbook_stages_prompt.promptstage",
|
||||
"attrs": {
|
||||
"fields": [
|
||||
"f78d977a-efa6-4cc2-9a0f-2621a9fd94d2",
|
||||
"1ff91927-e33d-4615-95b0-c258e5f0df62"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "20375f30-7fa7-4562-8f6e-0f61889f2963",
|
||||
"name": "default-enrollment-prompt-first"
|
||||
},
|
||||
"model": "passbook_stages_prompt.promptstage",
|
||||
"attrs": {
|
||||
"fields": [
|
||||
"cb954fd4-65a5-4ad9-b1ee-180ee9559cf4",
|
||||
"7db91ee8-4290-4e08-8d39-63f132402515",
|
||||
"d30b5eb4-7787-4072-b1ba-65b46e928920"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "77090897-eb3f-40db-81e6-b4074b1998c4",
|
||||
"name": "default-enrollment-user-login"
|
||||
},
|
||||
"model": "passbook_stages_user_login.userloginstage",
|
||||
"attrs": {
|
||||
"session_duration": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "a4090add-f483-4ac6-8917-10b493ef843e",
|
||||
"name": "default-enrollment-user-write"
|
||||
},
|
||||
"model": "passbook_stages_user_write.userwritestage",
|
||||
"attrs": {}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "34e1e7d5-8eed-4549-bc7a-305069ff7df0",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "20375f30-7fa7-4562-8f6e-0f61889f2963",
|
||||
"order": 0
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "e40467a6-3052-488c-a1b5-1ad7a80fe7b3",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "6c342b94-790d-425a-ae31-6196b6570722",
|
||||
"order": 1
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "76bc594e-2715-49ab-bd40-994abd9a7b70",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "a4090add-f483-4ac6-8917-10b493ef843e",
|
||||
"order": 2
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "2f324f6d-7646-4108-a6e2-e7f90985477f",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "77090897-eb3f-40db-81e6-b4074b1998c4",
|
||||
"order": 3
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
223
docs/flow/examples/enrollment-email-verification.json
Normal file
223
docs/flow/examples/enrollment-email-verification.json
Normal file
@ -0,0 +1,223 @@
|
||||
{
|
||||
"version": 1,
|
||||
"entries": [
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"slug": "default-enrollment-flow"
|
||||
},
|
||||
"model": "passbook_flows.flow",
|
||||
"attrs": {
|
||||
"name": "Default enrollment Flow",
|
||||
"title": "Welcome to passbook!",
|
||||
"designation": "enrollment"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "cb954fd4-65a5-4ad9-b1ee-180ee9559cf4"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "username",
|
||||
"label": "Username",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"placeholder": "Username",
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "7db91ee8-4290-4e08-8d39-63f132402515"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "password",
|
||||
"label": "Password",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Password",
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "d30b5eb4-7787-4072-b1ba-65b46e928920"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "password_repeat",
|
||||
"label": "Password (repeat)",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Password (repeat)",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "f78d977a-efa6-4cc2-9a0f-2621a9fd94d2"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "name",
|
||||
"label": "Name",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"placeholder": "Name",
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "1ff91927-e33d-4615-95b0-c258e5f0df62"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "email",
|
||||
"label": "Email",
|
||||
"type": "email",
|
||||
"required": true,
|
||||
"placeholder": "Email",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "9922212c-47a2-475a-9905-abeb5e621652"
|
||||
},
|
||||
"model": "passbook_policies_expression.expressionpolicy",
|
||||
"attrs": {
|
||||
"name": "policy-enrollment-password-equals",
|
||||
"expression": "# Verifies that the passwords are equal\r\nreturn request.context['password'] == request.context['password_repeat']"
|
||||
}
|
||||
},{
|
||||
"identifiers": {
|
||||
"pk": "096e6282-6b30-4695-bd03-3b143eab5580",
|
||||
"name": "default-enrollment-email-verficiation"
|
||||
},
|
||||
"model": "passbook_stages_email.emailstage",
|
||||
"attrs": {
|
||||
"host": "localhost",
|
||||
"port": 25,
|
||||
"username": "",
|
||||
"use_tls": false,
|
||||
"use_ssl": false,
|
||||
"timeout": 10,
|
||||
"from_address": "system@passbook.local",
|
||||
"token_expiry": 30,
|
||||
"subject": "passbook",
|
||||
"template": "stages/email/for_email/account_confirmation.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "6c342b94-790d-425a-ae31-6196b6570722",
|
||||
"name": "default-enrollment-prompt-second"
|
||||
},
|
||||
"model": "passbook_stages_prompt.promptstage",
|
||||
"attrs": {
|
||||
"fields": [
|
||||
"f78d977a-efa6-4cc2-9a0f-2621a9fd94d2",
|
||||
"1ff91927-e33d-4615-95b0-c258e5f0df62"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "20375f30-7fa7-4562-8f6e-0f61889f2963",
|
||||
"name": "default-enrollment-prompt-first"
|
||||
},
|
||||
"model": "passbook_stages_prompt.promptstage",
|
||||
"attrs": {
|
||||
"fields": [
|
||||
"cb954fd4-65a5-4ad9-b1ee-180ee9559cf4",
|
||||
"7db91ee8-4290-4e08-8d39-63f132402515",
|
||||
"d30b5eb4-7787-4072-b1ba-65b46e928920"
|
||||
],
|
||||
"validation_policies": [
|
||||
"9922212c-47a2-475a-9905-abeb5e621652"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "77090897-eb3f-40db-81e6-b4074b1998c4",
|
||||
"name": "default-enrollment-user-login"
|
||||
},
|
||||
"model": "passbook_stages_user_login.userloginstage",
|
||||
"attrs": {
|
||||
"session_duration": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "a4090add-f483-4ac6-8917-10b493ef843e",
|
||||
"name": "default-enrollment-user-write"
|
||||
},
|
||||
"model": "passbook_stages_user_write.userwritestage",
|
||||
"attrs": {}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "34e1e7d5-8eed-4549-bc7a-305069ff7df0",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "20375f30-7fa7-4562-8f6e-0f61889f2963",
|
||||
"order": 0
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "e40467a6-3052-488c-a1b5-1ad7a80fe7b3",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "6c342b94-790d-425a-ae31-6196b6570722",
|
||||
"order": 1
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "76bc594e-2715-49ab-bd40-994abd9a7b70",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "a4090add-f483-4ac6-8917-10b493ef843e",
|
||||
"order": 2
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "1db34a14-8985-4184-b5c9-254cd585d94f",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "096e6282-6b30-4695-bd03-3b143eab5580",
|
||||
"order": 3
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "2f324f6d-7646-4108-a6e2-e7f90985477f",
|
||||
"target": "773c6673-e4a2-423f-8d32-95b7b4a41cf3",
|
||||
"stage": "77090897-eb3f-40db-81e6-b4074b1998c4",
|
||||
"order": 4
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
49
docs/flow/examples/examples.md
Normal file
49
docs/flow/examples/examples.md
Normal file
@ -0,0 +1,49 @@
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
Same flow as above, with an extra email verification stage.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Login flow which conditionally shows the users a captcha, based on the reputation of their IP and Username.
|
||||
|
||||
By default, the captcha test keys are used. You can get a proper key [here](https://www.google.com/recaptcha/intro/v3.html)
|
||||
|
||||
## Recovery with email verification
|
||||
|
||||
Flow: right-click [here](recovery-email-verification.json) 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 for users to delete their account,
|
||||
|
||||
!!! warning
|
||||
This is done without any warning.
|
||||
|
111
docs/flow/examples/login-2fa.json
Normal file
111
docs/flow/examples/login-2fa.json
Normal file
@ -0,0 +1,111 @@
|
||||
{
|
||||
"version": 1,
|
||||
"entries": [
|
||||
{
|
||||
"identifiers": {
|
||||
"slug": "default-authentication-flow",
|
||||
"pk": "563ece21-e9a4-47e5-a264-23ffd923e393"
|
||||
},
|
||||
"model": "passbook_flows.flow",
|
||||
"attrs": {
|
||||
"name": "Default Authentication Flow",
|
||||
"title": "Welcome to passbook!",
|
||||
"designation": "authentication"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "69d41125-3987-499b-8d74-ef27b54b88c8",
|
||||
"name": "default-authentication-login"
|
||||
},
|
||||
"model": "passbook_stages_user_login.userloginstage",
|
||||
"attrs": {
|
||||
"session_duration": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "5f594f27-0def-488d-9855-fe604eb13de5",
|
||||
"name": "default-authentication-identification"
|
||||
},
|
||||
"model": "passbook_stages_identification.identificationstage",
|
||||
"attrs": {
|
||||
"user_fields": [
|
||||
"email",
|
||||
"username"
|
||||
],
|
||||
"template": "stages/identification/login.html",
|
||||
"enrollment_flow": null,
|
||||
"recovery_flow": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "37f709c3-8817-45e8-9a93-80a925d293c2",
|
||||
"name": "default-authentication-flow-totp"
|
||||
},
|
||||
"model": "passbook_stages_otp_validate.otpvalidatestage",
|
||||
"attrs": {}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "d8affa62-500c-4c5c-a01f-5835e1ffdf40",
|
||||
"name": "default-authentication-password"
|
||||
},
|
||||
"model": "passbook_stages_password.passwordstage",
|
||||
"attrs": {
|
||||
"backends": [
|
||||
"django.contrib.auth.backends.ModelBackend"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "a3056482-b692-4e3a-93f1-7351c6a351c7",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "5f594f27-0def-488d-9855-fe604eb13de5",
|
||||
"order": 0
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "4e8538cf-3e18-4a68-82ae-6df6725fa2e6",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "d8affa62-500c-4c5c-a01f-5835e1ffdf40",
|
||||
"order": 1
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "688aec6f-5622-42c6-83a5-d22072d7e798",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "37f709c3-8817-45e8-9a93-80a925d293c2",
|
||||
"order": 2
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "f3fede3a-a9b5-4232-9ec7-be7ff4194b27",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "69d41125-3987-499b-8d74-ef27b54b88c8",
|
||||
"order": 3
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
139
docs/flow/examples/login-conditional-captcha.json
Normal file
139
docs/flow/examples/login-conditional-captcha.json
Normal file
@ -0,0 +1,139 @@
|
||||
{
|
||||
"version": 1,
|
||||
"entries": [
|
||||
{
|
||||
"identifiers": {
|
||||
"slug": "default-authentication-flow",
|
||||
"pk": "563ece21-e9a4-47e5-a264-23ffd923e393"
|
||||
},
|
||||
"model": "passbook_flows.flow",
|
||||
"attrs": {
|
||||
"name": "Default Authentication Flow",
|
||||
"title": "Welcome to passbook!",
|
||||
"designation": "authentication"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"name": "default-authentication-login",
|
||||
"pk": "69d41125-3987-499b-8d74-ef27b54b88c8"
|
||||
},
|
||||
"model": "passbook_stages_user_login.userloginstage",
|
||||
"attrs": {
|
||||
"session_duration": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"name": "default-authentication-flow-captcha",
|
||||
"pk": "a368cafc-1494-45e9-b75b-b5e7ac2bd3e4"
|
||||
},
|
||||
"model": "passbook_stages_captcha.captchastage",
|
||||
"attrs": {
|
||||
"public_key": "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
|
||||
"private_key": "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"name": "default-authentication-identification",
|
||||
"pk": "5f594f27-0def-488d-9855-fe604eb13de5"
|
||||
},
|
||||
"model": "passbook_stages_identification.identificationstage",
|
||||
"attrs": {
|
||||
"user_fields": [
|
||||
"email",
|
||||
"username"
|
||||
],
|
||||
"template": "stages/identification/login.html",
|
||||
"enrollment_flow": null,
|
||||
"recovery_flow": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"name": "default-authentication-password",
|
||||
"pk": "d8affa62-500c-4c5c-a01f-5835e1ffdf40"
|
||||
},
|
||||
"model": "passbook_stages_password.passwordstage",
|
||||
"attrs": {
|
||||
"backends": [
|
||||
"django.contrib.auth.backends.ModelBackend"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "a3056482-b692-4e3a-93f1-7351c6a351c7",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "5f594f27-0def-488d-9855-fe604eb13de5",
|
||||
"order": 0
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "4e8538cf-3e18-4a68-82ae-6df6725fa2e6",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "d8affa62-500c-4c5c-a01f-5835e1ffdf40",
|
||||
"order": 1
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "3bcd6af0-48a6-4e18-87f3-d251a1a58226",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "a368cafc-1494-45e9-b75b-b5e7ac2bd3e4",
|
||||
"order": 2
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "f3fede3a-a9b5-4232-9ec7-be7ff4194b27",
|
||||
"target": "563ece21-e9a4-47e5-a264-23ffd923e393",
|
||||
"stage": "69d41125-3987-499b-8d74-ef27b54b88c8",
|
||||
"order": 3
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "688c9890-47ad-4327-a9e5-380e88d34be5"
|
||||
},
|
||||
"model": "passbook_policies_reputation.reputationpolicy",
|
||||
"attrs": {
|
||||
"name": "default-authentication-flow-conditional-captcha",
|
||||
"check_ip": true,
|
||||
"check_username": true,
|
||||
"threshold": -5
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "02e4d220-3448-44db-822e-c5255cf7c250",
|
||||
"policy": "688c9890-47ad-4327-a9e5-380e88d34be5",
|
||||
"target": "3bcd6af0-48a6-4e18-87f3-d251a1a58226",
|
||||
"order": 0
|
||||
},
|
||||
"model": "passbook_policies.policybinding",
|
||||
"attrs": {
|
||||
"enabled": true,
|
||||
"timeout": 30
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
# Login Flow
|
||||
|
||||
This document describes how a simple authentication flow can be created.
|
||||
|
||||
This flow is created automatically when passbook is installed.
|
||||
|
||||
1. Create an **Identification** stage
|
||||
|
||||
> Here you can select whichever fields the user can identify themselves with
|
||||
> Select the Template **Default Login**, as this template shows the (optional) Flows
|
||||
> Here you can also link optional enrollment and recovery flows.
|
||||
|
||||
2. Create a **Password** stage
|
||||
|
||||
> Select the Backend you want the password to be checked against. Select "passbook-internal Userdatabase".
|
||||
|
||||
3. Create a **User Login** stage
|
||||
|
||||
> This stage doesn't have any options.
|
||||
|
||||
4. Create a flow
|
||||
|
||||
> Create a flow with the delegation of **Authentication**
|
||||
> Assign a name and a slug. The slug is used in the URL when the flow is executed.
|
||||
|
||||
5. Bind the stages to the flow
|
||||
|
||||
> Bind the **Identification** Stage with an order of 0
|
||||
> Bind the **Password** Stage with an order of 1
|
||||
> Bind the **User Login** Stage with an order of 2
|
||||
|
||||

|
||||
|
||||
!!! notice
|
||||
|
||||
This flow can used by any user, authenticated and un-authenticated. This means any authenticated user that visits this flow can login again.
|
Binary file not shown.
Before Width: | Height: | Size: 110 KiB |
198
docs/flow/examples/recovery-email-verification.json
Normal file
198
docs/flow/examples/recovery-email-verification.json
Normal file
@ -0,0 +1,198 @@
|
||||
{
|
||||
"version": 1,
|
||||
"entries": [
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
|
||||
"slug": "default-recovery-flow"
|
||||
},
|
||||
"model": "passbook_flows.flow",
|
||||
"attrs": {
|
||||
"name": "Default recovery flow",
|
||||
"title": "Reset your password",
|
||||
"designation": "recovery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "1ff91927-e33d-4615-95b0-c258e5f0df62"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "email",
|
||||
"label": "Email",
|
||||
"type": "email",
|
||||
"required": true,
|
||||
"placeholder": "Email",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "7db91ee8-4290-4e08-8d39-63f132402515"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "password",
|
||||
"label": "Password",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Password",
|
||||
"order": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "d30b5eb4-7787-4072-b1ba-65b46e928920"
|
||||
},
|
||||
"model": "passbook_stages_prompt.prompt",
|
||||
"attrs": {
|
||||
"field_key": "password_repeat",
|
||||
"label": "Password (repeat)",
|
||||
"type": "password",
|
||||
"required": true,
|
||||
"placeholder": "Password (repeat)",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "cd042fc6-cc92-4b98-b7e6-f4729df798d8"
|
||||
},
|
||||
"model": "passbook_policies_expression.expressionpolicy",
|
||||
"attrs": {
|
||||
"name": "default-password-change-password-equal",
|
||||
"expression": "# Check that both passwords are equal.\nreturn request.context['password'] == request.context['password_repeat']"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "e54045a7-6ecb-4ad9-ad37-28e72d8e565e",
|
||||
"name": "default-recovery-identification"
|
||||
},
|
||||
"model": "passbook_stages_identification.identificationstage",
|
||||
"attrs": {
|
||||
"user_fields": [
|
||||
"email",
|
||||
"username"
|
||||
],
|
||||
"template": "stages/identification/recovery.html",
|
||||
"enrollment_flow": null,
|
||||
"recovery_flow": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "3909fd60-b013-4668-8806-12e9507dab97",
|
||||
"name": "default-recovery-user-write"
|
||||
},
|
||||
"model": "passbook_stages_user_write.userwritestage",
|
||||
"attrs": {}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "66f948dc-3f74-42b2-b26b-b8b9df109efb",
|
||||
"name": "default-recovery-email"
|
||||
},
|
||||
"model": "passbook_stages_email.emailstage",
|
||||
"attrs": {
|
||||
"host": "localhost",
|
||||
"port": 25,
|
||||
"username": "",
|
||||
"use_tls": false,
|
||||
"use_ssl": false,
|
||||
"timeout": 10,
|
||||
"from_address": "system@passbook.local",
|
||||
"token_expiry": 30,
|
||||
"subject": "passbook",
|
||||
"template": "stages/email/for_email/password_reset.html"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "975d5502-1e22-4d10-b560-fbc5bd70ff4d",
|
||||
"name": "default-password-change-prompt"
|
||||
},
|
||||
"model": "passbook_stages_prompt.promptstage",
|
||||
"attrs": {
|
||||
"fields": [
|
||||
"7db91ee8-4290-4e08-8d39-63f132402515",
|
||||
"d30b5eb4-7787-4072-b1ba-65b46e928920"
|
||||
],
|
||||
"validation_policies": [
|
||||
"cd042fc6-cc92-4b98-b7e6-f4729df798d8"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "fcdd4206-0d35-4ad2-a59f-5a72422936bb",
|
||||
"name": "default-recovery-user-login"
|
||||
},
|
||||
"model": "passbook_stages_user_login.userloginstage",
|
||||
"attrs": {
|
||||
"session_duration": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "7af7558e-2196-4b9f-a08e-d38420b7cfbb",
|
||||
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
|
||||
"stage": "e54045a7-6ecb-4ad9-ad37-28e72d8e565e",
|
||||
"order": 0
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "29446fd6-dd93-4e92-9830-2d81debad5ae",
|
||||
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
|
||||
"stage": "66f948dc-3f74-42b2-b26b-b8b9df109efb",
|
||||
"order": 1
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "1219d06e-2c06-4c5b-a162-78e3959c6cf0",
|
||||
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
|
||||
"stage": "975d5502-1e22-4d10-b560-fbc5bd70ff4d",
|
||||
"order": 2
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "66de86ba-0707-46a0-8475-ff2e260d6935",
|
||||
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
|
||||
"stage": "3909fd60-b013-4668-8806-12e9507dab97",
|
||||
"order": 3
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "9cec2334-d4a2-4895-a2b2-bc5ae4e9639a",
|
||||
"target": "a5993183-89c0-43d2-a7f4-ddffb17baba7",
|
||||
"stage": "fcdd4206-0d35-4ad2-a59f-5a72422936bb",
|
||||
"order": 4
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
37
docs/flow/examples/unenrollment.json
Normal file
37
docs/flow/examples/unenrollment.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 1,
|
||||
"entries": [
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "59a576ce-2f23-4a63-b63a-d18dc7e550f5",
|
||||
"slug": "default-unenrollment-flow"
|
||||
},
|
||||
"model": "passbook_flows.flow",
|
||||
"attrs": {
|
||||
"name": "Default unenrollment flow",
|
||||
"title": "Delete your account",
|
||||
"designation": "unenrollment"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "c62ac2a4-2735-4a0f-abd0-8523d68c1209",
|
||||
"name": "default-unenrollment-user-delete"
|
||||
},
|
||||
"model": "passbook_stages_user_delete.userdeletestage",
|
||||
"attrs": {}
|
||||
},
|
||||
{
|
||||
"identifiers": {
|
||||
"pk": "eb9aff2b-b95d-40b3-ad08-233aa77bbcf3",
|
||||
"target": "59a576ce-2f23-4a63-b63a-d18dc7e550f5",
|
||||
"stage": "c62ac2a4-2735-4a0f-abd0-8523d68c1209",
|
||||
"order": 0
|
||||
},
|
||||
"model": "passbook_flows.flowstagebinding",
|
||||
"attrs": {
|
||||
"re_evaluate_policies": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -11,20 +11,27 @@ This installation method is for test-setups and small-scale productive setups.
|
||||
|
||||
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). Place it in a directory of your choice.
|
||||
|
||||
To optionally enable error-reporting, run `echo PASSBOOK_ERROR_REPORTING=true >> .env`
|
||||
|
||||
To optionally deploy a different version run `echo PASSBOOK_TAG=0.10.1-stable >> .env`
|
||||
|
||||
If this is a fresh passbook install run the following commands to generate a password:
|
||||
|
||||
```
|
||||
wget https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml
|
||||
# Optionally enable Error-reporting
|
||||
# export PASSBOOK_ERROR_REPORTING=true
|
||||
# Optionally deploy a different version
|
||||
# export PASSBOOK_TAG=0.9.0-rc1
|
||||
# If this is a productive installation, set a different PostgreSQL Password
|
||||
# export PG_PASS=$(pwgen 40 1)
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker-compose exec server ./manage.py migrate
|
||||
sudo apt-get install -y pwgen
|
||||
echo "PG_PASS=$(pwgen 40 1)" >> .env
|
||||
echo "PASSBOOK_SECRET_KEY=$(pwgen 50 1)" >> .env
|
||||
```
|
||||
|
||||
The compose file references the current latest version, which can be overridden with the `SERVER_TAG` environment variable.
|
||||
Afterwards, run these commands to finish
|
||||
|
||||
```
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker-compose run --rm server migrate
|
||||
```
|
||||
|
||||
The compose file statically references the latest version available at the time of downloading, which can be overridden with the `SERVER_TAG` environment variable.
|
||||
|
||||
If you plan to use this setup for production, it is also advised to change the PostgreSQL password by setting `PG_PASS` to a password of your choice.
|
||||
|
||||
|
@ -5,34 +5,30 @@ For a mid to high-load installation, Kubernetes is recommended. passbook is inst
|
||||
This installation automatically applies database migrations on startup. After the installation is done, you can use `pbadmin` as username and password.
|
||||
|
||||
```
|
||||
# Default values for passbook.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
# passbook version to use. Defaults to latest stable version
|
||||
# image:
|
||||
# tag:
|
||||
###################################
|
||||
# Values directly affecting passbook
|
||||
###################################
|
||||
image:
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.10.1-stable
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
error_reporting: false
|
||||
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
|
||||
# Optionally enable Elastic APM Support
|
||||
apm:
|
||||
enabled: false
|
||||
server_url: ""
|
||||
secret_token: ""
|
||||
verify_server_cert: true
|
||||
|
||||
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
|
||||
# This requires the CoreOS Prometheus Operator.
|
||||
monitoring:
|
||||
enabled: false
|
||||
|
||||
# Enable Database Backups to S3
|
||||
# backup:
|
||||
@ -41,20 +37,15 @@ monitoring:
|
||||
# bucket: s3-bucket
|
||||
# host: s3-host
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - passbook.k8s.local
|
||||
###################################
|
||||
# Values controlling dependencies
|
||||
###################################
|
||||
|
||||
# These settings configure the packaged PostgreSQL and Redis chart.
|
||||
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
|
||||
|
||||
@ -66,4 +57,16 @@ redis:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
|
||||
ingress:
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - passbook.k8s.local
|
||||
```
|
||||
|
@ -16,6 +16,7 @@ Create an application in passbook and note the slug, as this will be used later.
|
||||
- ACS URL: `https://signin.aws.amazon.com/saml`
|
||||
- Audience: `urn:amazon:webservices`
|
||||
- Issuer: `passbook`
|
||||
- Binding: `Post`
|
||||
|
||||
You can of course use a custom signing certificate, and adjust durations.
|
||||
|
||||
|
20
docs/outposts/deploy-docker-compose.md
Normal file
20
docs/outposts/deploy-docker-compose.md
Normal file
@ -0,0 +1,20 @@
|
||||
# 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: 'true'
|
||||
PASSBOOK_TOKEN: token-generated-by-passbook
|
||||
```
|
99
docs/outposts/deploy-kubernetes.md
Normal file
99
docs/outposts/deploy-kubernetes.md
Normal file
@ -0,0 +1,99 @@
|
||||
# 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
|
||||
```
|
14
docs/outposts/outposts.md
Normal file
14
docs/outposts/outposts.md
Normal file
@ -0,0 +1,14 @@
|
||||
# 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.
|
BIN
docs/outposts/outposts.png
Normal file
BIN
docs/outposts/outposts.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 122 KiB |
@ -19,3 +19,7 @@ LDAP Property Mappings are used when you define a LDAP Source. These mappings de
|
||||
- Autogenerated LDAP Mapping: sn -> last_name
|
||||
|
||||
These are configured with most common LDAP setups.
|
||||
|
||||
## Scope Mapping
|
||||
|
||||
Scope Mappings are used by the OAuth2 Provider to map information from passbook to OAuth2/OpenID Claims.
|
||||
|
@ -1,24 +0,0 @@
|
||||
# Providers
|
||||
|
||||
Providers allow external applications to authenticate against passbook and use its user information.
|
||||
|
||||
## OpenID Provider
|
||||
|
||||
This provider utilises the commonly used OpenID Connect variation of OAuth2.
|
||||
|
||||
## OAuth2 Provider
|
||||
|
||||
This provider is slightly different than the OpenID Provider. While it uses the same basic OAuth2 Protocol, it provides a GitHub-compatible endpoint. This allows you to integrate applications which don't support custom OpenID providers.
|
||||
The API exposes username, email, name, and groups in a GitHub-compatible format.
|
||||
This provider currently supports the following scopes:
|
||||
|
||||
- `openid`: Access OpenID Userinfo
|
||||
- `userinfo`: Access OpenID Userinfo
|
||||
- `email`: Access OpenID Email
|
||||
- `user:email`: GitHub Compatibility: User Email
|
||||
- `read:org`: GitHub Compatibility: User Groups
|
||||
|
||||
## 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".
|
31
docs/providers/oauth2.md
Normal file
31
docs/providers/oauth2.md
Normal file
@ -0,0 +1,31 @@
|
||||
# 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.
|
16
docs/providers/proxy.md
Normal file
16
docs/providers/proxy.md
Normal file
@ -0,0 +1,16 @@
|
||||
# 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-Auth-Request-User | The user's unique identifier
|
||||
X-Auth-Request-Email | The user's email address
|
||||
X-Auth-Request-Preferred-Username | The user's username
|
12
docs/providers/saml.md
Normal file
12
docs/providers/saml.md
Normal file
@ -0,0 +1,12 @@
|
||||
# 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/`
|
11
docs/troubleshooting/access.md
Normal file
11
docs/troubleshooting/access.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Troubleshooting access problems
|
||||
|
||||
## I get an access denied error when trying to access an application.
|
||||
|
||||
If your user is a superuser, or has the attribute `passbook_user_debug` set to true:
|
||||
|
||||

|
||||
|
||||
Afterwards, try to access the application again. You will now see a message explaining which policy denied you access:
|
||||
|
||||

|
BIN
docs/troubleshooting/access_denied_message.png
Normal file
BIN
docs/troubleshooting/access_denied_message.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
BIN
docs/troubleshooting/passbook_user_debug.png
Normal file
BIN
docs/troubleshooting/passbook_user_debug.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
73
docs/upgrading/to-0.10.md
Normal file
73
docs/upgrading/to-0.10.md
Normal file
@ -0,0 +1,73 @@
|
||||
# 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,4 +1,4 @@
|
||||
# Upgrading from 0.8.x
|
||||
# Upgrading to 0.9
|
||||
|
||||
Due to some database changes that had to be rather sooner than later, there is no possibility to directly upgrade. You must extract the data before hand and import it again. It is recommended to spin up a second instance of passbook to do this.
|
||||
|
@ -2,7 +2,7 @@ version: '3.7'
|
||||
|
||||
services:
|
||||
chrome:
|
||||
image: selenium/standalone-chrome
|
||||
image: selenium/standalone-chrome:3.141.59-20200525
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
|
@ -2,7 +2,8 @@ version: '3.7'
|
||||
|
||||
services:
|
||||
chrome:
|
||||
image: selenium/standalone-chrome-debug:3.141.59-20200525
|
||||
image: selenium/standalone-chrome-debug:3.141.59-20200719
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
restart: always
|
||||
|
@ -1,17 +1,16 @@
|
||||
"""Test Enroll flow"""
|
||||
from time import sleep
|
||||
from sys import platform
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from django.test import override_settings
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.stages.email.models import EmailStage, EmailTemplates
|
||||
from passbook.stages.identification.models import IdentificationStage
|
||||
from passbook.stages.prompt.models import FieldTypes, Prompt, PromptStage
|
||||
@ -19,37 +18,22 @@ from passbook.stages.user_login.models import UserLoginStage
|
||||
from passbook.stages.user_write.models import UserWriteStage
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestFlowsEnroll(SeleniumTestCase):
|
||||
"""Test Enroll flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test IdP container"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="mailhog/mailhog",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
test=["CMD", "wget", "-s", "http://localhost:8025"],
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "mailhog/mailhog:v1.0.1",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:8025"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
}
|
||||
|
||||
def test_enroll_2_step(self):
|
||||
"""Test 2-step enroll flow"""
|
||||
@ -75,9 +59,16 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||
)
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.validation_policies.set([password_policy])
|
||||
first_stage.save()
|
||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||
second_stage.fields.set([name_field, email])
|
||||
@ -85,15 +76,6 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
user_write = UserWriteStage.objects.create(name="enroll-user-write")
|
||||
user_login = UserLoginStage.objects.create(name="enroll-user-login")
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
PolicyBinding.objects.create(
|
||||
target=first_stage, policy=password_policy, order=0
|
||||
)
|
||||
|
||||
flow = Flow.objects.create(
|
||||
name="default-enrollment-flow",
|
||||
slug="default-enrollment-flow",
|
||||
@ -170,9 +152,16 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
field_key="email", label="E-Mail", order=1, type=FieldTypes.EMAIL
|
||||
)
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
|
||||
# Stages
|
||||
first_stage = PromptStage.objects.create(name="prompt-stage-first")
|
||||
first_stage.fields.set([username_prompt, password, password_repeat])
|
||||
first_stage.validation_policies.set([password_policy])
|
||||
first_stage.save()
|
||||
second_stage = PromptStage.objects.create(name="prompt-stage-second")
|
||||
second_stage.fields.set([name_field, email])
|
||||
@ -186,15 +175,6 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
user_write = UserWriteStage.objects.create(name="enroll-user-write")
|
||||
user_login = UserLoginStage.objects.create(name="enroll-user-login")
|
||||
|
||||
# Password checking policy
|
||||
password_policy = ExpressionPolicy.objects.create(
|
||||
name="policy-enrollment-password-equals",
|
||||
expression="return request.context['password'] == request.context['password_repeat']",
|
||||
)
|
||||
PolicyBinding.objects.create(
|
||||
target=first_stage, policy=password_policy, order=0
|
||||
)
|
||||
|
||||
flow = Flow.objects.create(
|
||||
name="default-enrollment-flow",
|
||||
slug="default-enrollment-flow",
|
||||
@ -221,21 +201,25 @@ class TestFlowsEnroll(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_name").send_keys("some name")
|
||||
self.driver.find_element(By.ID, "id_email").send_keys("foo@bar.baz")
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
sleep(3)
|
||||
# Wait for the success message so we know the email is sent
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-form > p"))
|
||||
)
|
||||
|
||||
# Open Mailhog
|
||||
self.driver.get("http://localhost:8025")
|
||||
|
||||
# Click on first message
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CLASS_NAME, "msglist-message"))
|
||||
)
|
||||
self.driver.find_element(By.CLASS_NAME, "msglist-message").click()
|
||||
sleep(3)
|
||||
self.driver.switch_to.frame(self.driver.find_element(By.CLASS_NAME, "tab-pane"))
|
||||
self.driver.find_element(By.ID, "confirm").click()
|
||||
self.driver.close()
|
||||
self.driver.switch_to.window(self.driver.window_handles[0])
|
||||
|
||||
# We're now logged in
|
||||
sleep(3)
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.XPATH, "//a[contains(@href, '/-/user/')]")
|
||||
|
@ -1,10 +1,14 @@
|
||||
"""test default login flow"""
|
||||
from sys import platform
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestFlowsLogin(SeleniumTestCase):
|
||||
"""test default login flow"""
|
||||
|
||||
|
@ -1,25 +1,35 @@
|
||||
"""test stage setup flows (password change)"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
from time import sleep
|
||||
from sys import platform
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.core.models import User
|
||||
from passbook.flows.models import Flow, FlowDesignation
|
||||
from passbook.providers.oauth2.generators import generate_client_secret
|
||||
from passbook.stages.password.models import PasswordStage
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestFlowsStageSetup(SeleniumTestCase):
|
||||
"""test stage setup flows"""
|
||||
|
||||
def test_password_change(self):
|
||||
"""test password change flow"""
|
||||
new_password = "".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits)
|
||||
for _ in range(8)
|
||||
# Ensure that password stage has change_flow set
|
||||
flow = Flow.objects.get(
|
||||
slug="default-password-change", designation=FlowDesignation.STAGE_SETUP,
|
||||
)
|
||||
|
||||
stages = PasswordStage.objects.filter(name="default-authentication-password")
|
||||
stage = stages.first()
|
||||
stage.change_flow = flow
|
||||
stage.save()
|
||||
|
||||
new_password = generate_client_secret()
|
||||
|
||||
self.driver.get(
|
||||
f"{self.live_server_url}/flows/default-authentication-flow/?next=%2F"
|
||||
)
|
||||
@ -29,13 +39,14 @@ class TestFlowsStageSetup(SeleniumTestCase):
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-page__header").click()
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/-/user/')]").click()
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
self.driver.find_element(By.LINK_TEXT, "Change password").click()
|
||||
self.driver.find_element(By.ID, "id_password").send_keys(new_password)
|
||||
self.driver.find_element(By.ID, "id_password_repeat").click()
|
||||
self.driver.find_element(By.ID, "id_password_repeat").send_keys(new_password)
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-button").click()
|
||||
|
||||
sleep(2)
|
||||
self.wait_for_url(self.url("passbook_core:user-settings"))
|
||||
# Because USER() is cached, we need to get the user manually here
|
||||
user = User.objects.get(username=USER().username)
|
||||
self.assertTrue(user.check_password(new_password))
|
||||
|
@ -1,87 +1,77 @@
|
||||
"""test OAuth Provider flow"""
|
||||
from time import sleep
|
||||
from sys import platform
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from oauth2_provider.generators import generate_client_id, generate_client_secret
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.core.models import Application
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.providers.oauth.models import OAuth2Provider
|
||||
from passbook.providers.oauth2.generators import (
|
||||
generate_client_id,
|
||||
generate_client_secret,
|
||||
)
|
||||
from passbook.providers.oauth2.models import ClientTypes, OAuth2Provider, ResponseTypes
|
||||
|
||||
|
||||
class TestProviderOAuth(SeleniumTestCase):
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderOAuth2Github(SeleniumTestCase):
|
||||
"""test OAuth Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.client_id = generate_client_id()
|
||||
self.client_secret = generate_client_secret()
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
"""Setup client grafana container which we test OAuth against"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="grafana/grafana:latest",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
return {
|
||||
"image": "grafana/grafana:7.1.0",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
environment={
|
||||
"environment": {
|
||||
"GF_AUTH_GITHUB_ENABLED": "true",
|
||||
"GF_AUTH_GITHUB_allow_sign_up": "true",
|
||||
"GF_AUTH_GITHUB_ALLOW_SIGN_UP": "true",
|
||||
"GF_AUTH_GITHUB_CLIENT_ID": self.client_id,
|
||||
"GF_AUTH_GITHUB_CLIENT_SECRET": self.client_secret,
|
||||
"GF_AUTH_GITHUB_SCOPES": "user:email,read:org",
|
||||
"GF_AUTH_GITHUB_AUTH_URL": self.url(
|
||||
"passbook_providers_oauth:github-authorize"
|
||||
"passbook_providers_oauth2_github:github-authorize"
|
||||
),
|
||||
"GF_AUTH_GITHUB_TOKEN_URL": self.url(
|
||||
"passbook_providers_oauth:github-access-token"
|
||||
"passbook_providers_oauth2_github:github-access-token"
|
||||
),
|
||||
"GF_AUTH_GITHUB_API_URL": self.url(
|
||||
"passbook_providers_oauth:github-user"
|
||||
"passbook_providers_oauth2_github:github-user"
|
||||
),
|
||||
"GF_LOG_LEVEL": "debug",
|
||||
},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
}
|
||||
|
||||
def test_authorization_consent_implied(self):
|
||||
"""test OAuth Provider flow (default authorization flow with implied consent)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type=OAuth2Provider.CLIENT_CONFIDENTIAL,
|
||||
authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
response_type=ResponseTypes.CODE,
|
||||
redirect_uris="http://localhost:3000/login/github",
|
||||
skip_authorization=True,
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
Application.objects.create(
|
||||
@ -103,42 +93,37 @@ class TestProviderOAuth(SeleniumTestCase):
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().username,
|
||||
)
|
||||
|
||||
def test_authorization_consent_explicit(self):
|
||||
"""test OAuth Provider flow (default authorization flow with explicit consent)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type=OAuth2Provider.CLIENT_CONFIDENTIAL,
|
||||
authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
response_type=ResponseTypes.CODE,
|
||||
redirect_uris="http://localhost:3000/login/github",
|
||||
skip_authorization=True,
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
app = Application.objects.create(
|
||||
@ -160,13 +145,18 @@ class TestProviderOAuth(SeleniumTestCase):
|
||||
).text,
|
||||
)
|
||||
self.assertEqual(
|
||||
"GitHub Compatibility: User Email",
|
||||
"GitHub Compatibility: Access you Email addresses",
|
||||
self.driver.find_element(
|
||||
By.XPATH, "/html/body/div[2]/div/main/div/form/div[2]/ul/li[1]"
|
||||
).text,
|
||||
)
|
||||
sleep(1)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "[type=submit]").click()
|
||||
self.driver.find_element(
|
||||
By.CSS_SELECTOR,
|
||||
(
|
||||
"form[action='/flows/b/default-provider-authorization-explicit-consent/'] "
|
||||
"[type=submit]"
|
||||
),
|
||||
).click()
|
||||
|
||||
self.wait_for_url("http://localhost:3000/?orgId=1")
|
||||
self.driver.find_element(By.XPATH, "//a[contains(@href, '/profile')]").click()
|
||||
@ -175,42 +165,37 @@ class TestProviderOAuth(SeleniumTestCase):
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().username,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().username,
|
||||
)
|
||||
|
||||
def test_denied(self):
|
||||
"""test OAuth Provider flow (default authorization flow, denied)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type=OAuth2Provider.CLIENT_CONFIDENTIAL,
|
||||
authorization_grant_type=OAuth2Provider.GRANT_AUTHORIZATION_CODE,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
response_type=ResponseTypes.CODE,
|
||||
redirect_uris="http://localhost:3000/login/github",
|
||||
skip_authorization=True,
|
||||
authorization_flow=authorization_flow,
|
||||
)
|
||||
app = Application.objects.create(
|
@ -1,73 +1,77 @@
|
||||
"""test OpenID Provider flow"""
|
||||
"""test OAuth2 OpenID Provider flow"""
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from django.shortcuts import reverse
|
||||
from oauth2_provider.generators import generate_client_id, generate_client_secret
|
||||
from oidc_provider.models import Client, ResponseType
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from structlog import get_logger
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import USER, SeleniumTestCase, ensure_rsa_key
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.core.models import Application
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.models import PolicyBinding
|
||||
from passbook.providers.oidc.models import OpenIDProvider
|
||||
from passbook.providers.oauth2.constants import (
|
||||
SCOPE_OPENID,
|
||||
SCOPE_OPENID_EMAIL,
|
||||
SCOPE_OPENID_PROFILE,
|
||||
)
|
||||
from passbook.providers.oauth2.generators import (
|
||||
generate_client_id,
|
||||
generate_client_secret,
|
||||
)
|
||||
from passbook.providers.oauth2.models import (
|
||||
ClientTypes,
|
||||
OAuth2Provider,
|
||||
ResponseTypes,
|
||||
ScopeMapping,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class TestProviderOIDC(SeleniumTestCase):
|
||||
"""test OpenID Provider flow"""
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderOAuth2OIDC(SeleniumTestCase):
|
||||
"""test OAuth with OpenID Provider flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.client_id = generate_client_id()
|
||||
self.client_secret = generate_client_secret()
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup client grafana container which we test OIDC against"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="grafana/grafana:latest",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "grafana/grafana:7.1.0",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:3000"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
environment={
|
||||
"environment": {
|
||||
"GF_AUTH_GENERIC_OAUTH_ENABLED": "true",
|
||||
"GF_AUTH_GENERIC_OAUTH_CLIENT_ID": self.client_id,
|
||||
"GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET": self.client_secret,
|
||||
"GF_AUTH_GENERIC_OAUTH_SCOPES": "openid email profile",
|
||||
"GF_AUTH_GENERIC_OAUTH_AUTH_URL": (
|
||||
self.live_server_url + reverse("passbook_providers_oidc:authorize")
|
||||
self.url("passbook_providers_oauth2:authorize")
|
||||
),
|
||||
"GF_AUTH_GENERIC_OAUTH_TOKEN_URL": (
|
||||
self.live_server_url + reverse("oidc_provider:token")
|
||||
self.url("passbook_providers_oauth2:token")
|
||||
),
|
||||
"GF_AUTH_GENERIC_OAUTH_API_URL": (
|
||||
self.live_server_url + reverse("oidc_provider:userinfo")
|
||||
self.url("passbook_providers_oauth2:userinfo")
|
||||
),
|
||||
"GF_LOG_LEVEL": "debug",
|
||||
},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
}
|
||||
|
||||
def test_redirect_uri_error(self):
|
||||
"""test OpenID Provider flow (invalid redirect URI, check error message)"""
|
||||
@ -76,23 +80,22 @@ class TestProviderOIDC(SeleniumTestCase):
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
client = Client.objects.create(
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type="confidential",
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
_redirect_uris="http://localhost:3000/",
|
||||
_scope="openid userinfo",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
)
|
||||
# At least one of these objects must exist
|
||||
ensure_rsa_key()
|
||||
# This response_code object might exist or not, depending on the order the tests are run
|
||||
rp_type, _ = ResponseType.objects.get_or_create(value="code")
|
||||
client.response_types.set([rp_type])
|
||||
client.save()
|
||||
provider = OpenIDProvider.objects.create(
|
||||
oidc_client=client, authorization_flow=authorization_flow,
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||
)
|
||||
)
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
||||
@ -117,25 +120,22 @@ class TestProviderOIDC(SeleniumTestCase):
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-implicit-consent"
|
||||
)
|
||||
client = Client.objects.create(
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type="confidential",
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
_redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
_scope="openid profile email",
|
||||
reuse_consent=False,
|
||||
require_consent=False,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
)
|
||||
# At least one of these objects must exist
|
||||
ensure_rsa_key()
|
||||
# This response_code object might exist or not, depending on the order the tests are run
|
||||
rp_type, _ = ResponseType.objects.get_or_create(value="code")
|
||||
client.response_types.set([rp_type])
|
||||
client.save()
|
||||
provider = OpenIDProvider.objects.create(
|
||||
oidc_client=client, authorization_flow=authorization_flow,
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||
)
|
||||
)
|
||||
provider.save()
|
||||
Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
||||
@ -153,23 +153,20 @@ class TestProviderOIDC(SeleniumTestCase):
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
@ -181,25 +178,22 @@ class TestProviderOIDC(SeleniumTestCase):
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
client = Client.objects.create(
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type="confidential",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
_redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
_scope="openid profile email",
|
||||
reuse_consent=False,
|
||||
require_consent=False,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
)
|
||||
# At least one of these objects must exist
|
||||
ensure_rsa_key()
|
||||
# This response_code object might exist or not, depending on the order the tests are run
|
||||
rp_type, _ = ResponseType.objects.get_or_create(value="code")
|
||||
client.response_types.set([rp_type])
|
||||
client.save()
|
||||
provider = OpenIDProvider.objects.create(
|
||||
oidc_client=client, authorization_flow=authorization_flow,
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||
)
|
||||
)
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
||||
@ -235,23 +229,20 @@ class TestProviderOIDC(SeleniumTestCase):
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[1]/div/input",
|
||||
).get_attribute("value"),
|
||||
self.driver.find_element(By.CSS_SELECTOR, "input[name=name]").get_attribute(
|
||||
"value"
|
||||
),
|
||||
USER().name,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[2]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=email]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(
|
||||
By.XPATH,
|
||||
"/html/body/grafana-app/div/div/div/react-profile-wrapper/form[1]/div[3]/div/input",
|
||||
By.CSS_SELECTOR, "input[name=login]"
|
||||
).get_attribute("value"),
|
||||
USER().email,
|
||||
)
|
||||
@ -263,25 +254,22 @@ class TestProviderOIDC(SeleniumTestCase):
|
||||
authorization_flow = Flow.objects.get(
|
||||
slug="default-provider-authorization-explicit-consent"
|
||||
)
|
||||
client = Client.objects.create(
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="grafana",
|
||||
client_type="confidential",
|
||||
authorization_flow=authorization_flow,
|
||||
response_type=ResponseTypes.CODE,
|
||||
client_type=ClientTypes.CONFIDENTIAL,
|
||||
client_id=self.client_id,
|
||||
client_secret=self.client_secret,
|
||||
_redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
_scope="openid profile email",
|
||||
reuse_consent=False,
|
||||
require_consent=False,
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
redirect_uris="http://localhost:3000/login/generic_oauth",
|
||||
)
|
||||
# At least one of these objects must exist
|
||||
ensure_rsa_key()
|
||||
# This response_code object might exist or not, depending on the order the tests are run
|
||||
rp_type, _ = ResponseType.objects.get_or_create(value="code")
|
||||
client.response_types.set([rp_type])
|
||||
client.save()
|
||||
provider = OpenIDProvider.objects.create(
|
||||
oidc_client=client, authorization_flow=authorization_flow,
|
||||
provider.property_mappings.set(
|
||||
ScopeMapping.objects.filter(
|
||||
scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE]
|
||||
)
|
||||
)
|
||||
provider.save()
|
||||
app = Application.objects.create(
|
||||
name="Grafana", slug="grafana", provider=provider,
|
||||
)
|
@ -1,12 +1,15 @@
|
||||
"""test SAML Provider flow"""
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from structlog import get_logger
|
||||
|
||||
from e2e.utils import USER, SeleniumTestCase
|
||||
from passbook.core.models import Application
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
@ -19,7 +22,10 @@ from passbook.providers.saml.models import (
|
||||
SAMLProvider,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestProviderSAML(SeleniumTestCase):
|
||||
"""test SAML Provider flow"""
|
||||
|
||||
@ -54,12 +60,9 @@ class TestProviderSAML(SeleniumTestCase):
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
LOGGER.info("Container failed healthcheck")
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
def test_sp_initiated_implicit(self):
|
||||
"""test SAML Provider flow SP-initiated flow (implicit consent)"""
|
||||
# Bootstrap all needed objects
|
||||
|
@ -1,24 +1,30 @@
|
||||
"""test OAuth Source"""
|
||||
from os.path import abspath
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from oauth2_provider.generators import generate_client_secret
|
||||
from django.test import override_settings
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from structlog import get_logger
|
||||
from yaml import safe_dump
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import SeleniumTestCase
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.providers.oauth2.generators import generate_client_secret
|
||||
from passbook.sources.oauth.models import OAuthSource
|
||||
|
||||
TOKEN_URL = "http://127.0.0.1:5556/dex/token"
|
||||
CONFIG_PATH = "/tmp/dex.yml"
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestSourceOAuth(SeleniumTestCase):
|
||||
"""test OAuth Source flow"""
|
||||
|
||||
@ -26,7 +32,7 @@ class TestSourceOAuth(SeleniumTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.client_secret = generate_client_secret()
|
||||
self.container = self.setup_client()
|
||||
self.prepare_dex_config()
|
||||
super().setUp()
|
||||
|
||||
def prepare_dex_config(self):
|
||||
@ -64,33 +70,23 @@ class TestSourceOAuth(SeleniumTestCase):
|
||||
with open(CONFIG_PATH, "w+") as _file:
|
||||
safe_dump(config, _file)
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test Dex container"""
|
||||
self.prepare_dex_config()
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="quay.io/dexidp/dex:v2.24.0",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
command="serve /config.yml",
|
||||
healthcheck=Healthcheck(
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "quay.io/dexidp/dex:v2.24.0",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
"command": "serve /config.yml",
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "wget", "--spider", "http://localhost:5556/dex/healthz"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
volumes={abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro",}},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
"volumes": {abspath(CONFIG_PATH): {"bind": "/config.yml", "mode": "ro"}},
|
||||
}
|
||||
|
||||
def create_objects(self):
|
||||
"""Create required objects"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
@ -108,10 +104,6 @@ class TestSourceOAuth(SeleniumTestCase):
|
||||
consumer_secret=self.client_secret,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
|
||||
def test_oauth_enroll(self):
|
||||
"""test OAuth Source With With OIDC"""
|
||||
self.create_objects()
|
||||
@ -138,6 +130,7 @@ class TestSourceOAuth(SeleniumTestCase):
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
self.wait.until(ec.presence_of_element_located((By.NAME, "username")))
|
||||
# At this point we've been redirected back
|
||||
# and we're asked for the username
|
||||
self.driver.find_element(By.NAME, "username").click()
|
||||
@ -164,6 +157,42 @@ class TestSourceOAuth(SeleniumTestCase):
|
||||
"admin@example.com",
|
||||
)
|
||||
|
||||
@override_settings(SESSION_COOKIE_SAMESITE="strict")
|
||||
def test_oauth_samesite_strict(self):
|
||||
"""test OAuth Source With SameSite set to strict
|
||||
(=will fail because session is not carried over)"""
|
||||
self.create_objects()
|
||||
self.driver.get(self.live_server_url)
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located(
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
||||
self.driver.find_element(By.ID, "password").send_keys("password")
|
||||
self.driver.find_element(By.ID, "password").send_keys(Keys.ENTER)
|
||||
|
||||
# Wait until we're logged in
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button[type=submit]"))
|
||||
)
|
||||
self.driver.find_element(By.CSS_SELECTOR, "button[type=submit]").click()
|
||||
|
||||
self.wait.until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, ".pf-c-alert__title"))
|
||||
)
|
||||
self.assertEqual(
|
||||
self.driver.find_element(By.CSS_SELECTOR, ".pf-c-alert__title").text,
|
||||
"Authentication Failed.",
|
||||
)
|
||||
|
||||
def test_oauth_enroll_auth(self):
|
||||
"""test OAuth Source With With OIDC (enroll and authenticate again)"""
|
||||
self.test_oauth_enroll()
|
||||
@ -175,10 +204,11 @@ class TestSourceOAuth(SeleniumTestCase):
|
||||
(By.CLASS_NAME, "pf-c-login__main-footer-links-item-link")
|
||||
)
|
||||
)
|
||||
sleep(1)
|
||||
self.driver.find_element(
|
||||
By.CLASS_NAME, "pf-c-login__main-footer-links-item-link"
|
||||
).click()
|
||||
|
||||
sleep(1)
|
||||
# Now we should be at the IDP, wait for the login field
|
||||
self.wait.until(ec.presence_of_element_located((By.ID, "login")))
|
||||
self.driver.find_element(By.ID, "login").send_keys("admin@example.com")
|
@ -1,18 +1,22 @@
|
||||
"""test SAML Source"""
|
||||
from sys import platform
|
||||
from time import sleep
|
||||
from typing import Any, Dict, Optional
|
||||
from unittest.case import skipUnless
|
||||
|
||||
from docker.types import Healthcheck
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.common.keys import Keys
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from structlog import get_logger
|
||||
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from docker.types import Healthcheck
|
||||
from e2e.utils import SeleniumTestCase
|
||||
from passbook.crypto.models import CertificateKeyPair
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.sources.saml.models import SAMLBindingTypes, SAMLSource
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
IDP_CERT = """-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
@ -65,47 +69,31 @@ Sm75WXsflOxuTn08LbgGc4s=
|
||||
-----END PRIVATE KEY-----"""
|
||||
|
||||
|
||||
@skipUnless(platform.startswith("linux"), "requires local docker")
|
||||
class TestSourceSAML(SeleniumTestCase):
|
||||
"""test SAML Source flow"""
|
||||
|
||||
def setUp(self):
|
||||
self.container = self.setup_client()
|
||||
super().setUp()
|
||||
|
||||
def setup_client(self) -> Container:
|
||||
"""Setup test IdP container"""
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(
|
||||
image="kristophjunge/test-saml-idp",
|
||||
detach=True,
|
||||
network_mode="host",
|
||||
auto_remove=True,
|
||||
healthcheck=Healthcheck(
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
return {
|
||||
"image": "kristophjunge/test-saml-idp:1.15",
|
||||
"detach": True,
|
||||
"network_mode": "host",
|
||||
"auto_remove": True,
|
||||
"healthcheck": Healthcheck(
|
||||
test=["CMD", "curl", "http://localhost:8080"],
|
||||
interval=5 * 100 * 1000000,
|
||||
start_period=1 * 100 * 1000000,
|
||||
),
|
||||
environment={
|
||||
"environment": {
|
||||
"SIMPLESAMLPHP_SP_ENTITY_ID": "entity-id",
|
||||
"SIMPLESAMLPHP_SP_ASSERTION_CONSUMER_SERVICE": (
|
||||
f"{self.live_server_url}/source/saml/saml-idp-test/acs/"
|
||||
),
|
||||
},
|
||||
)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
sleep(1)
|
||||
|
||||
def tearDown(self):
|
||||
self.container.kill()
|
||||
super().tearDown()
|
||||
}
|
||||
|
||||
def test_idp_redirect(self):
|
||||
"""test SAML Source With redirect binding"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
@ -157,7 +145,6 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
def test_idp_post(self):
|
||||
"""test SAML Source With post binding"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
@ -211,7 +198,6 @@ class TestSourceSAML(SeleniumTestCase):
|
||||
|
||||
def test_idp_post_auto(self):
|
||||
"""test SAML Source With post binding (auto redirect)"""
|
||||
sleep(1)
|
||||
# Bootstrap all needed objects
|
||||
authentication_flow = Flow.objects.get(slug="default-source-authentication")
|
||||
enrollment_flow = Flow.objects.get(slug="default-source-enrollment")
|
||||
|
44
e2e/utils.py
44
e2e/utils.py
@ -4,14 +4,16 @@ from glob import glob
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from inspect import getmembers, isfunction
|
||||
from os import environ, makedirs
|
||||
from time import time
|
||||
from time import sleep, time
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from django.apps import apps
|
||||
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
|
||||
from django.db import connection, transaction
|
||||
from django.db.utils import IntegrityError
|
||||
from django.shortcuts import reverse
|
||||
from docker import DockerClient, from_env
|
||||
from docker.models.containers import Container
|
||||
from selenium import webdriver
|
||||
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
@ -28,28 +30,38 @@ def USER() -> User: # noqa
|
||||
return User.objects.get(username="pbadmin")
|
||||
|
||||
|
||||
def ensure_rsa_key():
|
||||
"""Ensure that at least one RSAKey Object exists, create one if none exist"""
|
||||
from oidc_provider.models import RSAKey
|
||||
|
||||
if not RSAKey.objects.exists():
|
||||
key = RSA.generate(2048)
|
||||
rsakey = RSAKey(key=key.exportKey("PEM").decode("utf8"))
|
||||
rsakey.save()
|
||||
|
||||
|
||||
class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
"""StaticLiveServerTestCase which automatically creates a Webdriver instance"""
|
||||
|
||||
container: Optional[Container] = None
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
makedirs("selenium_screenshots/", exist_ok=True)
|
||||
self.driver = self._get_driver()
|
||||
self.driver.maximize_window()
|
||||
self.driver.implicitly_wait(30)
|
||||
self.wait = WebDriverWait(self.driver, 50)
|
||||
self.driver.implicitly_wait(10)
|
||||
self.wait = WebDriverWait(self.driver, 30)
|
||||
self.apply_default_data()
|
||||
self.logger = get_logger()
|
||||
if specs := self.get_container_specs():
|
||||
self.container = self._start_container(specs)
|
||||
|
||||
def _start_container(self, specs: Dict[str, Any]) -> Container:
|
||||
client: DockerClient = from_env()
|
||||
container = client.containers.run(**specs)
|
||||
while True:
|
||||
container.reload()
|
||||
status = container.attrs.get("State", {}).get("Health", {}).get("Status")
|
||||
if status == "healthy":
|
||||
return container
|
||||
self.logger.info("Container failed healthcheck")
|
||||
sleep(1)
|
||||
|
||||
def get_container_specs(self) -> Optional[Dict[str, Any]]:
|
||||
"""Optionally get container specs which will launched on setup, wait for the container to
|
||||
be healthy, and deleted again on tearDown"""
|
||||
return None
|
||||
|
||||
def _get_driver(self) -> WebDriver:
|
||||
return webdriver.Remote(
|
||||
@ -58,7 +70,7 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
if "CI" in environ:
|
||||
if "TF_BUILD" in environ:
|
||||
screenshot_file = (
|
||||
f"selenium_screenshots/{self.__class__.__name__}_{time()}.png"
|
||||
)
|
||||
@ -68,6 +80,8 @@ class SeleniumTestCase(StaticLiveServerTestCase):
|
||||
self.logger.warning(
|
||||
line["message"], source=line["source"], level=line["level"]
|
||||
)
|
||||
if self.container:
|
||||
self.container.kill()
|
||||
self.driver.quit()
|
||||
super().tearDown()
|
||||
|
||||
|
@ -1,8 +0,0 @@
|
||||
FROM quay.io/oauth2-proxy/oauth2-proxy
|
||||
|
||||
COPY templates /templates
|
||||
|
||||
ENV OAUTH2_PROXY_EMAIL_DOMAINS=*
|
||||
ENV OAUTH2_PROXY_PROVIDER=oidc
|
||||
ENV OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR=/templates
|
||||
ENV OAUTH2_PROXY_HTTP_ADDRESS=:4180
|
@ -1,18 +0,0 @@
|
||||
{{define "error.html"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
|
||||
<head>
|
||||
<title>{{.Title}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h2>{{.Title}}</h2>
|
||||
<p>{{.Message}}</p>
|
||||
<hr>
|
||||
<p><a href="{{.ProxyPrefix}}/sign_in">Sign In</a></p>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
@ -1,119 +0,0 @@
|
||||
{{define "sign_in.html"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" charset="utf-8">
|
||||
<head>
|
||||
<title>Sign In with passbook</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<style>
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
color: #333;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.signin {
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
max-width: 400px;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
color: #fff;
|
||||
background-color: #428bca;
|
||||
border: 1px solid #357ebd;
|
||||
-webkit-border-radius: 4;
|
||||
-moz-border-radius: 4;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 6px 12px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background-color: #3071a9;
|
||||
border-color: #285e8e;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.42857143;
|
||||
color: #555;
|
||||
background-color: #fff;
|
||||
background-image: none;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
|
||||
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
|
||||
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: #aaa;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
footer a {
|
||||
display: inline-block;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
color: #aaa;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="signin center">
|
||||
<form method="GET" action="{{.ProxyPrefix}}/start">
|
||||
<input type="hidden" name="rd" value="{{.Redirect}}">
|
||||
<button type="submit" class="btn">Sign in with passbook</button><br />
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
if (window.location.hash) {
|
||||
(function () {
|
||||
var inputs = document.getElementsByName('rd');
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
inputs[i].value += window.location.hash;
|
||||
}
|
||||
})();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{end}}
|
@ -1,9 +1,9 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
||||
version: 6.5.8
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 9.3.2
|
||||
- name: redis
|
||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
||||
version: 9.5.1
|
||||
digest: sha256:f18b5dc8d0be13d584407405c60d10b6b84d25f7fa8aaa3dd0e5385c38f5c516
|
||||
generated: "2019-12-14T13:33:48.4341939Z"
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
version: 10.7.16
|
||||
digest: sha256:fd31e2e2b9ff17a5ed906a77a4f15ffa1ab7f5aecaea1e5db77f0d199ae4f19e
|
||||
generated: "2020-08-25T17:57:49.684549+02:00"
|
||||
|
@ -1,6 +1,15 @@
|
||||
apiVersion: v1
|
||||
appVersion: "0.9.0-rc1"
|
||||
apiVersion: v2
|
||||
appVersion: "0.10.1-stable"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.9.0-rc1"
|
||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
||||
version: "0.10.1-stable"
|
||||
icon: https://github.com/BeryJu/passbook/blob/master/docs/images/logo.svg
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 9.3.2
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: install.postgresql
|
||||
- name: redis
|
||||
version: 10.7.16
|
||||
repository: https://charts.bitnami.com/bitnami
|
||||
condition: install.redis
|
||||
|
@ -1,9 +0,0 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
||||
version: 6.5.8
|
||||
- name: redis
|
||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
||||
version: 9.5.1
|
||||
digest: sha256:476834fb82f66bc7242c4a5e7343d0a859d8307cb301256beb0eb749983014e4
|
||||
generated: "2019-11-07T10:21:30.902415+01:00"
|
@ -1,7 +0,0 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 6.5.8
|
||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
||||
- name: redis
|
||||
version: 9.5.1
|
||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
@ -1,19 +1,5 @@
|
||||
1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
1. Access passbook using the following URL:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "passbook.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get svc -w {{ include "passbook.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "passbook.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "passbook.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl port-forward $POD_NAME 8080:8000
|
||||
{{- end }}
|
||||
2. Login to passbook using the user "pbadmin" and the password "pbadmin".
|
||||
|
@ -3,26 +3,17 @@ kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
data:
|
||||
config.yml: |
|
||||
postgresql:
|
||||
host: "{{ .Release.Name }}-postgresql"
|
||||
name: "{{ .Values.postgresql.postgresqlDatabase }}"
|
||||
user: postgres
|
||||
{{- if .Values.backup }}
|
||||
backup:
|
||||
access_key: "{{ .Values.backup.access_key }}"
|
||||
secret_key: "{{ .Values.backup.secret_key }}"
|
||||
bucket: "{{ .Values.backup.bucket }}"
|
||||
host: "{{ .Values.backup.host }}"
|
||||
{{- end}}
|
||||
redis:
|
||||
host: "{{ .Release.Name }}-redis-master"
|
||||
cache_db: 0
|
||||
message_queue_db: 1
|
||||
error_reporting: {{ .Values.config.error_reporting }}
|
||||
log_level: "{{ .Values.config.log_level }}"
|
||||
apm:
|
||||
enabled: {{ .Values.config.apm.enabled }}
|
||||
server_url: "{{ .Values.config.apm.server_url }}"
|
||||
secret_token: "{{ .Values.config.apm.server_token }}"
|
||||
verify_server_cert: {{ .Values.config.apm.verify_server_cert }}
|
||||
POSTGRESQL__HOST: "{{ .Release.Name }}-postgresql"
|
||||
POSTGRESQL__NAME: "{{ .Values.postgresql.postgresqlDatabase }}"
|
||||
POSTGRESQL__USER: "{{ .Values.postgresql.postgresqlUsername }}"
|
||||
{{- if .Values.backup }}
|
||||
POSTGRESQL__BACKUP__ACCESS_KEY: "{{ .Values.backup.access_key }}"
|
||||
POSTGRESQL__BACKUP__SECRET_KEY: "{{ .Values.backup.secret_key }}"
|
||||
POSTGRESQL__BACKUP__BUCKET: "{{ .Values.backup.bucket }}"
|
||||
POSTGRESQL__BACKUP__HOST: "{{ .Values.backup.host }}"
|
||||
{{- end}}
|
||||
REDIS__HOST: "{{ .Release.Name }}-redis-master"
|
||||
ERROR_REPORTING__ENABLED: "{{ .Values.config.error_reporting.enabled }}"
|
||||
ERROR_REPORTING__ENVIRONMENT: "{{ .Values.config.error_reporting.environment }}"
|
||||
ERROR_REPORTING__SEND_PII: "{{ .Values.config.error_reporting.send_pii }}"
|
||||
LOG_LEVEL: "{{ .Values.config.log_level }}"
|
||||
|
@ -1,4 +1,3 @@
|
||||
{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "passbook.fullname" . -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
@ -46,4 +45,3 @@ spec:
|
||||
serviceName: {{ $fullName }}-static
|
||||
servicePort: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
@ -1,121 +0,0 @@
|
||||
{{- if .Values.monitoring.enabled -}}
|
||||
---
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PrometheusRule
|
||||
metadata:
|
||||
name: {{ include "passbook.fullname" . }}-static-rules
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
groups:
|
||||
- name: Aggregate request counters
|
||||
rules:
|
||||
- record: job:django_http_requests_before_middlewares_total:sum_rate30s
|
||||
expr: sum(rate(django_http_requests_before_middlewares_total[30s])) by (job)
|
||||
- record: job:django_http_requests_unknown_latency_total:sum_rate30s
|
||||
expr: sum(rate(django_http_requests_unknown_latency_total[30s])) by (job)
|
||||
- record: job:django_http_ajax_requests_total:sum_rate30s
|
||||
expr: sum(rate(django_http_ajax_requests_total[30s])) by (job)
|
||||
- record: job:django_http_responses_before_middlewares_total:sum_rate30s
|
||||
expr: sum(rate(django_http_responses_before_middlewares_total[30s])) by (job)
|
||||
- record: job:django_http_requests_unknown_latency_including_middlewares_total:sum_rate30s
|
||||
expr: sum(rate(django_http_requests_unknown_latency_including_middlewares_total[30s])) by (job)
|
||||
- record: job:django_http_requests_body_total_bytes:sum_rate30s
|
||||
expr: sum(rate(django_http_requests_body_total_bytes[30s])) by (job)
|
||||
- record: job:django_http_responses_streaming_total:sum_rate30s
|
||||
expr: sum(rate(django_http_responses_streaming_total[30s])) by (job)
|
||||
- record: job:django_http_responses_body_total_bytes:sum_rate30s
|
||||
expr: sum(rate(django_http_responses_body_total_bytes[30s])) by (job)
|
||||
- record: job:django_http_requests_total:sum_rate30s
|
||||
expr: sum(rate(django_http_requests_total_by_method[30s])) by (job)
|
||||
- record: job:django_http_requests_total_by_method:sum_rate30s
|
||||
expr: sum(rate(django_http_requests_total_by_method[30s])) by (job,method)
|
||||
- record: job:django_http_requests_total_by_transport:sum_rate30s
|
||||
expr: sum(rate(django_http_requests_total_by_transport[30s])) by (job,transport)
|
||||
- record: job:django_http_requests_total_by_view:sum_rate30s
|
||||
expr: sum(rate(django_http_requests_total_by_view_transport_method[30s])) by (job,view)
|
||||
- record: job:django_http_requests_total_by_view_transport_method:sum_rate30s
|
||||
expr: sum(rate(django_http_requests_total_by_view_transport_method[30s])) by (job,view,transport,method)
|
||||
- record: job:django_http_responses_total_by_templatename:sum_rate30s
|
||||
expr: sum(rate(django_http_responses_total_by_templatename[30s])) by (job,templatename)
|
||||
- record: job:django_http_responses_total_by_status:sum_rate30s
|
||||
expr: sum(rate(django_http_responses_total_by_status[30s])) by (job,status)
|
||||
- record: job:django_http_responses_total_by_status_name_method:sum_rate30s
|
||||
expr: sum(rate(django_http_responses_total_by_status_name_method[30s])) by (job,status,name,method)
|
||||
- record: job:django_http_responses_total_by_charset:sum_rate30s
|
||||
expr: sum(rate(django_http_responses_total_by_charset[30s])) by (job,charset)
|
||||
- record: job:django_http_exceptions_total_by_type:sum_rate30s
|
||||
expr: sum(rate(django_http_exceptions_total_by_type[30s])) by (job,type)
|
||||
- record: job:django_http_exceptions_total_by_view:sum_rate30s
|
||||
expr: sum(rate(django_http_exceptions_total_by_view[30s])) by (job,view)
|
||||
- name: Aggregate latency histograms
|
||||
rules:
|
||||
- record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s
|
||||
expr: histogram_quantile(0.50, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le))
|
||||
labels:
|
||||
quantile: "50"
|
||||
- record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s
|
||||
expr: histogram_quantile(0.95, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le))
|
||||
labels:
|
||||
quantile: "95"
|
||||
- record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s
|
||||
expr: histogram_quantile(0.99, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le))
|
||||
labels:
|
||||
quantile: "99"
|
||||
- record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s
|
||||
expr: histogram_quantile(0.999, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le))
|
||||
labels:
|
||||
quantile: "99.9"
|
||||
- record: job:django_http_requests_latency_seconds:quantile_rate30s
|
||||
expr: histogram_quantile(0.50, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le))
|
||||
labels:
|
||||
quantile: "50"
|
||||
- record: job:django_http_requests_latency_seconds:quantile_rate30s
|
||||
expr: histogram_quantile(0.95, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le))
|
||||
labels:
|
||||
quantile: "95"
|
||||
- record: job:django_http_requests_latency_seconds:quantile_rate30s
|
||||
expr: histogram_quantile(0.99, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le))
|
||||
labels:
|
||||
quantile: "99"
|
||||
- record: job:django_http_requests_latency_seconds:quantile_rate30s
|
||||
expr: histogram_quantile(0.999, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le))
|
||||
labels:
|
||||
quantile: "99.9"
|
||||
- name: Aggregate model operations
|
||||
rules:
|
||||
- record: job:django_model_inserts_total:sum_rate1m
|
||||
expr: sum(rate(django_model_inserts_total[1m])) by (job, model)
|
||||
- record: job:django_model_updates_total:sum_rate1m
|
||||
expr: sum(rate(django_model_updates_total[1m])) by (job, model)
|
||||
- record: job:django_model_deletes_total:sum_rate1m
|
||||
expr: sum(rate(django_model_deletes_total[1m])) by (job, model)
|
||||
- name: Aggregate database operations
|
||||
rules:
|
||||
- record: job:django_db_new_connections_total:sum_rate30s
|
||||
expr: sum(rate(django_db_new_connections_total[30s])) by (alias, vendor)
|
||||
- record: job:django_db_new_connection_errors_total:sum_rate30s
|
||||
expr: sum(rate(django_db_new_connection_errors_total[30s])) by (alias, vendor)
|
||||
- record: job:django_db_execute_total:sum_rate30s
|
||||
expr: sum(rate(django_db_execute_total[30s])) by (alias, vendor)
|
||||
- record: job:django_db_execute_many_total:sum_rate30s
|
||||
expr: sum(rate(django_db_execute_many_total[30s])) by (alias, vendor)
|
||||
- record: job:django_db_errors_total:sum_rate30s
|
||||
expr: sum(rate(django_db_errors_total[30s])) by (alias, vendor, type)
|
||||
- name: Aggregate migrations
|
||||
rules:
|
||||
- record: job:django_migrations_applied_total:max
|
||||
expr: max(django_migrations_applied_total) by (job, connection)
|
||||
- record: job:django_migrations_unapplied_total:max
|
||||
expr: max(django_migrations_unapplied_total) by (job, connection)
|
||||
- name: Alerts
|
||||
rules:
|
||||
- alert: UnappliedMigrations
|
||||
expr: job:django_migrations_unapplied_total:max > 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: testing
|
||||
{{- end }}
|
@ -7,23 +7,23 @@ metadata:
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
k8s.passbook.io/component: static
|
||||
k8s.passbook.beryju.org/component: static
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.io/component: static
|
||||
k8s.passbook.beryju.org/component: static
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.io/component: static
|
||||
k8s.passbook.beryju.org/component: static
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}-static
|
||||
image: "beryju/passbook-static:{{ .Values.image.tag }}"
|
||||
image: "{{ .Values.image.name_static }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
|
@ -7,7 +7,7 @@ metadata:
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
k8s.passbook.io/component: static
|
||||
k8s.passbook.beryju.org/component: static
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
@ -18,4 +18,4 @@ spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.io/component: static
|
||||
k8s.passbook.beryju.org/component: static
|
||||
|
@ -1,17 +0,0 @@
|
||||
{{- if .Values.monitoring.enabled -}}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
name: {{ include "passbook.fullname" . }}-static-monitoring
|
||||
spec:
|
||||
endpoints:
|
||||
- port: http
|
||||
selector:
|
||||
matchLabels:
|
||||
k8s.passbook.io/component: static
|
||||
{{- end }}
|
@ -7,35 +7,43 @@ metadata:
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
k8s.passbook.io/component: web
|
||||
k8s.passbook.beryju.org/component: web
|
||||
spec:
|
||||
replicas: 2
|
||||
replicas: {{ .Values.serverReplicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.io/component: web
|
||||
k8s.passbook.beryju.org/component: web
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.io/component: web
|
||||
k8s.passbook.beryju.org/component: web
|
||||
spec:
|
||||
volumes:
|
||||
- name: config-volume
|
||||
configMap:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/name
|
||||
operator: In
|
||||
values:
|
||||
- {{ include "passbook.name" . }}
|
||||
- key: app.kubernetes.io/instance
|
||||
operator: In
|
||||
values:
|
||||
- {{ .Release.Name }}
|
||||
- key: k8s.passbook.beryju.org/component
|
||||
operator: In
|
||||
values:
|
||||
- web
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
initContainers:
|
||||
- name: passbook-database-migrations
|
||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- ./manage.py
|
||||
- migrate
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
name: config-volume
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
args: [migrate]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
@ -58,14 +66,8 @@ spec:
|
||||
key: postgresql-password
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: Always
|
||||
args:
|
||||
- uwsgi
|
||||
- uwsgi.ini
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
name: config-volume
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
args: [server]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
@ -74,18 +76,18 @@ spec:
|
||||
- name: PASSBOOK_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
key: secret_key
|
||||
name: "{{ include "passbook.fullname" . }}-secret-key"
|
||||
key: "secret_key"
|
||||
- name: PASSBOOK_REDIS__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-redis"
|
||||
key: redis-password
|
||||
key: "redis-password"
|
||||
- name: PASSBOOK_POSTGRESQL__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-postgresql"
|
||||
key: postgresql-password
|
||||
key: "postgresql-password"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8000
|
||||
|
@ -7,7 +7,7 @@ metadata:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
k8s.passbook.io/component: web
|
||||
k8s.passbook.beryju.org/component: web
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
@ -18,4 +18,4 @@ spec:
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.io/component: web
|
||||
k8s.passbook.beryju.org/component: web
|
||||
|
@ -1,26 +0,0 @@
|
||||
{{- if .Values.monitoring.enabled -}}
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
name: {{ include "passbook.fullname" . }}-web-monitoring
|
||||
spec:
|
||||
endpoints:
|
||||
- basicAuth:
|
||||
password:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
key: secret_key
|
||||
username:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
key: monitoring_username
|
||||
port: http
|
||||
path: /metrics/
|
||||
interval: 10s
|
||||
selector:
|
||||
matchLabels:
|
||||
k8s.passbook.io/component: web
|
||||
{{- end }}
|
@ -7,60 +7,64 @@ metadata:
|
||||
helm.sh/chart: {{ include "passbook.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
k8s.passbook.io/component: worker
|
||||
k8s.passbook.beryju.org/component: worker
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: {{ .Values.workerReplicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.io/component: worker
|
||||
k8s.passbook.beryju.org/component: worker
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "passbook.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
k8s.passbook.io/component: worker
|
||||
k8s.passbook.beryju.org/component: worker
|
||||
spec:
|
||||
volumes:
|
||||
- name: config-volume
|
||||
configMap:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchExpressions:
|
||||
- key: app.kubernetes.io/name
|
||||
operator: In
|
||||
values:
|
||||
- {{ include "passbook.name" . }}
|
||||
- key: app.kubernetes.io/instance
|
||||
operator: In
|
||||
values:
|
||||
- {{ .Release.Name }}
|
||||
- key: k8s.passbook.beryju.org/component
|
||||
operator: In
|
||||
values:
|
||||
- worker
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||
image: "{{ .Values.image.name }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- celery
|
||||
- worker
|
||||
- --autoscale=10,3
|
||||
- -E
|
||||
- -B
|
||||
- -A=passbook.root.celery
|
||||
- -s=/tmp/celerybeat-schedule
|
||||
volumeMounts:
|
||||
- mountPath: /etc/passbook
|
||||
name: config-volume
|
||||
args: [worker]
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
prefix: PASSBOOK_
|
||||
name: "{{ include "passbook.fullname" . }}-config"
|
||||
prefix: "PASSBOOK_"
|
||||
env:
|
||||
- name: PASSBOOK_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
name: "{{ include "passbook.fullname" . }}-secret-key"
|
||||
key: secret_key
|
||||
- name: PASSBOOK_REDIS__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-redis"
|
||||
key: redis-password
|
||||
key: "redis-password"
|
||||
- name: PASSBOOK_POSTGRESQL__PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: "{{ .Release.Name }}-postgresql"
|
||||
key: postgresql-password
|
||||
key: "postgresql-password"
|
||||
resources:
|
||||
requests:
|
||||
cpu: 150m
|
||||
|
@ -1,30 +1,27 @@
|
||||
# Default values for passbook.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
###################################
|
||||
# Values directly affecting passbook
|
||||
###################################
|
||||
image:
|
||||
tag: 0.9.0-rc1
|
||||
name: beryju/passbook
|
||||
name_static: beryju/passbook-static
|
||||
tag: 0.10.1-stable
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
serverReplicas: 1
|
||||
workerReplicas: 1
|
||||
|
||||
config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
error_reporting: false
|
||||
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
|
||||
# Optionally enable Elastic APM Support
|
||||
apm:
|
||||
enabled: false
|
||||
server_url: ""
|
||||
secret_token: ""
|
||||
verify_server_cert: true
|
||||
|
||||
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
|
||||
# This requires the CoreOS Prometheus Operator.
|
||||
monitoring:
|
||||
enabled: false
|
||||
|
||||
# Enable Database Backups to S3
|
||||
# backup:
|
||||
@ -33,20 +30,15 @@ monitoring:
|
||||
# bucket: s3-bucket
|
||||
# host: s3-host
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - passbook.k8s.local
|
||||
###################################
|
||||
# Values controlling dependencies
|
||||
###################################
|
||||
|
||||
# These settings configure the packaged PostgreSQL and Redis chart.
|
||||
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
|
||||
|
||||
@ -58,3 +50,15 @@ redis:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
|
||||
ingress:
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
path: /
|
||||
hosts:
|
||||
- passbook.k8s.local
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - passbook.k8s.local
|
||||
|
14
lifecycle/bootstrap.sh
Executable file
14
lifecycle/bootstrap.sh
Executable file
@ -0,0 +1,14 @@
|
||||
#!/bin/bash -e
|
||||
python -m lifecycle.wait_for_db
|
||||
printf '{"event": "Bootstrap completed", "level": "info", "logger": "bootstrap", "command": "%s"}\n' "$@"
|
||||
if [[ "$1" == "server" ]]; then
|
||||
gunicorn -c /lifecycle/gunicorn.conf.py passbook.root.asgi:application
|
||||
elif [[ "$1" == "worker" ]]; then
|
||||
celery worker --autoscale=10,3 -E -B -A=passbook.root.celery -s=/tmp/celerybeat-schedule
|
||||
elif [[ "$1" == "migrate" ]]; then
|
||||
# Run system migrations first, run normal migrations after
|
||||
python -m lifecycle.migrate
|
||||
python -m manage migrate
|
||||
else
|
||||
python -m manage "$@"
|
||||
fi
|
51
lifecycle/gunicorn.conf.py
Normal file
51
lifecycle/gunicorn.conf.py
Normal file
@ -0,0 +1,51 @@
|
||||
"""Gunicorn config"""
|
||||
from multiprocessing import cpu_count
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
|
||||
bind = "0.0.0.0:8000"
|
||||
|
||||
user = "passbook"
|
||||
group = "passbook"
|
||||
|
||||
worker_class = "uvicorn.workers.UvicornWorker"
|
||||
# Docker containers don't have /tmp as tmpfs
|
||||
worker_tmp_dir = "/dev/shm"
|
||||
|
||||
logconfig_dict = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"json_formatter": {
|
||||
"()": structlog.stdlib.ProcessorFormatter,
|
||||
"processor": structlog.processors.JSONRenderer(),
|
||||
"foreign_pre_chain": [
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.stdlib.add_logger_name,
|
||||
structlog.processors.TimeStamper(),
|
||||
structlog.processors.StackInfoRenderer(),
|
||||
structlog.processors.format_exc_info,
|
||||
],
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"error_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "json_formatter",
|
||||
},
|
||||
"console": {"class": "logging.StreamHandler", "formatter": "json_formatter"},
|
||||
},
|
||||
"loggers": {
|
||||
"uvicorn": {"handlers": ["console"], "level": "WARNING", "propagate": False},
|
||||
"gunicorn": {"handlers": ["console"], "level": "INFO", "propagate": False},
|
||||
},
|
||||
}
|
||||
|
||||
# if we're running in kubernetes, use fixed workers because we can scale with more pods
|
||||
# otherwise (assume docker-compose), use as much as we can
|
||||
if Path("/var/run/secrets/kubernetes.io").exists():
|
||||
workers = 2
|
||||
else:
|
||||
worker = cpu_count()
|
||||
threads = 4
|
55
lifecycle/migrate.py
Executable file
55
lifecycle/migrate.py
Executable file
@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python
|
||||
"""System Migration handler"""
|
||||
from importlib.util import module_from_spec, spec_from_file_location
|
||||
from inspect import getmembers, isclass
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from psycopg2 import connect
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class BaseMigration:
|
||||
"""Base System Migration"""
|
||||
|
||||
cur: Any
|
||||
con: Any
|
||||
|
||||
def __init__(self, cur: Any, con: Any):
|
||||
self.cur = cur
|
||||
self.con = con
|
||||
|
||||
def needs_migration(self) -> bool:
|
||||
"""Return true if Migration needs to be run"""
|
||||
return False
|
||||
|
||||
def run(self):
|
||||
"""Run the actual migration"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
conn = connect(
|
||||
dbname=CONFIG.y("postgresql.name"),
|
||||
user=CONFIG.y("postgresql.user"),
|
||||
password=CONFIG.y("postgresql.password"),
|
||||
host=CONFIG.y("postgresql.host"),
|
||||
)
|
||||
curr = conn.cursor()
|
||||
|
||||
for migration in Path(__file__).parent.absolute().glob("system_migrations/*.py"):
|
||||
spec = spec_from_file_location("lifecycle.system_migrations", migration)
|
||||
mod = module_from_spec(spec)
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
for _, sub in getmembers(mod, isclass):
|
||||
migration = sub(curr, conn)
|
||||
if migration.needs_migration():
|
||||
LOGGER.info("Migration needs to be applied", migration=sub)
|
||||
migration.run()
|
||||
LOGGER.info("Migration finished applying", migration=sub)
|
50
lifecycle/system_migrations/to_0_10.py
Normal file
50
lifecycle/system_migrations/to_0_10.py
Normal file
@ -0,0 +1,50 @@
|
||||
from os import system
|
||||
|
||||
from lifecycle.migrate import BaseMigration
|
||||
|
||||
SQL_STATEMENT = """delete from django_migrations where app = 'passbook_stages_prompt';
|
||||
drop table passbook_stages_prompt_prompt cascade;
|
||||
drop table passbook_stages_prompt_promptstage cascade;
|
||||
drop table passbook_stages_prompt_promptstage_fields;
|
||||
drop table corsheaders_corsmodel cascade;
|
||||
drop table oauth2_provider_accesstoken cascade;
|
||||
drop table oauth2_provider_grant cascade;
|
||||
drop table oauth2_provider_refreshtoken cascade;
|
||||
drop table oidc_provider_client cascade;
|
||||
drop table oidc_provider_client_response_types cascade;
|
||||
drop table oidc_provider_code cascade;
|
||||
drop table oidc_provider_responsetype cascade;
|
||||
drop table oidc_provider_rsakey cascade;
|
||||
drop table oidc_provider_token cascade;
|
||||
drop table oidc_provider_userconsent cascade;
|
||||
drop table passbook_providers_app_gw_applicationgatewayprovider cascade;
|
||||
delete from django_migrations where app = 'passbook_flows' and name = '0008_default_flows';
|
||||
delete from django_migrations where app = 'passbook_flows' and name = '0009_source_flows';
|
||||
delete from django_migrations where app = 'passbook_flows' and name = '0010_provider_flows';
|
||||
delete from django_migrations where app = 'passbook_stages_password' and
|
||||
name = '0002_passwordstage_change_flow';"""
|
||||
|
||||
|
||||
class To010Migration(BaseMigration):
|
||||
def needs_migration(self) -> bool:
|
||||
self.cur.execute(
|
||||
"select * from information_schema.tables where table_name='oidc_provider_client'"
|
||||
)
|
||||
return bool(self.cur.rowcount)
|
||||
|
||||
def system_crit(self, command):
|
||||
retval = system(command) # nosec
|
||||
if retval != 0:
|
||||
raise Exception("Migration error")
|
||||
|
||||
def run(self):
|
||||
self.cur.execute(SQL_STATEMENT)
|
||||
self.con.commit()
|
||||
self.system_crit("./manage.py migrate passbook_stages_prompt")
|
||||
self.system_crit("./manage.py migrate passbook_flows 0008_default_flows --fake")
|
||||
self.system_crit("./manage.py migrate passbook_flows 0009_source_flows --fake")
|
||||
self.system_crit(
|
||||
"./manage.py migrate passbook_flows 0010_provider_flows --fake"
|
||||
)
|
||||
self.system_crit("./manage.py migrate passbook_flows")
|
||||
self.system_crit("./manage.py migrate passbook_stages_password --fake")
|
18
mkdocs.yml
18
mkdocs.yml
@ -10,8 +10,7 @@ nav:
|
||||
- Kubernetes: installation/kubernetes.md
|
||||
- Flows:
|
||||
Overview: flow/flows.md
|
||||
Examples:
|
||||
- Login: flow/examples/login.md
|
||||
Examples: flow/examples/examples.md
|
||||
- Stages:
|
||||
- Captcha Stage: flow/stages/captcha/index.md
|
||||
- Dummy Stage: flow/stages/dummy/index.md
|
||||
@ -27,7 +26,14 @@ nav:
|
||||
- User Logout Stage: flow/stages/user_logout.md
|
||||
- User Write Stage: flow/stages/user_write.md
|
||||
- Sources: sources.md
|
||||
- Providers: providers.md
|
||||
- Providers:
|
||||
- OAuth2: providers/oauth2.md
|
||||
- SAML: providers/saml.md
|
||||
- Proxy: providers/proxy.md
|
||||
- Outposts:
|
||||
- Overview: outposts/outposts.md
|
||||
- Deploy on docker-compose: outposts/deploy-docker-compose.md
|
||||
- Deploy on Kubernetes: outposts/deploy-kubernetes.md
|
||||
- Expressions:
|
||||
- Overview: expressions/index.md
|
||||
- Reference:
|
||||
@ -46,7 +52,11 @@ nav:
|
||||
- Harbor: integrations/services/harbor/index.md
|
||||
- Sentry: integrations/services/sentry/index.md
|
||||
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
|
||||
- Upgrading from v0.8.x: upgrading-from-0.8.x.md
|
||||
- Upgrading:
|
||||
- to 0.9: upgrading/to-0.9.md
|
||||
- to 0.10: upgrading/to-0.10.md
|
||||
- Troubleshooting:
|
||||
- Access problems: troubleshooting/access.md
|
||||
|
||||
repo_name: "BeryJu/passbook"
|
||||
repo_url: https://github.com/BeryJu/passbook
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.9.0-rc1"
|
||||
__version__ = "0.10.1-stable"
|
||||
|
@ -33,7 +33,7 @@ class YAMLString(str):
|
||||
"""YAML String type"""
|
||||
|
||||
|
||||
class YAMLField(forms.CharField):
|
||||
class YAMLField(forms.JSONField):
|
||||
"""Django's JSON Field converted to YAML"""
|
||||
|
||||
default_error_messages = {
|
||||
|
@ -46,6 +46,12 @@
|
||||
{% trans 'Providers' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:outposts' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:outposts' 'passbook_admin:outpost-create' 'passbook_admin:outpost-update' 'passbook_admin:outpost-delete' %}">
|
||||
{% trans 'Outposts' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="pf-c-nav__item">
|
||||
<a href="{% url 'passbook_admin:property-mappings' %}"
|
||||
class="pf-c-nav__link {% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}">
|
||||
|
@ -1,36 +0,0 @@
|
||||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="pf-icon pf-icon-applications"></i>
|
||||
{% trans 'Request' %}
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col" style="min-width: 150px;">{% trans 'Key' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Value' %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for key, value in request_dict.items %}
|
||||
<tr role="row">
|
||||
<td role="cell">{{ key }}</td>
|
||||
<td role="cell">{{ value }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
13
passbook/admin/templates/administration/flow/import.html
Normal file
13
passbook/admin/templates/administration/flow/import.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends base_template|default:"generic/form.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block above_form %}
|
||||
<h1>
|
||||
{% trans 'Import Flow' %}
|
||||
</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block action %}
|
||||
{% trans 'Import Flow' %}
|
||||
{% endblock %}
|
@ -20,6 +20,7 @@
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-secondary" type="button">{% trans 'Import' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
@ -62,6 +63,7 @@
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-update' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:flow-delete' pk=flow.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-execute' pk=flow.pk %}?next={{ request.get_full_path }}">{% trans 'Execute' %}</a>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:flow-export' pk=flow.pk %}?next={{ request.get_full_path }}">{% trans 'Export' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -81,6 +83,7 @@
|
||||
{% trans 'Currently no flows exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
<a href="{% url 'passbook_admin:flow-import' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Import' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
97
passbook/admin/templates/administration/outpost/list.html
Normal file
97
passbook/admin/templates/administration/outpost/list.html
Normal file
@ -0,0 +1,97 @@
|
||||
{% extends "administration/base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load humanize %}
|
||||
{% load passbook_utils %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
.pf-m-success {
|
||||
color: var(--pf-global--success-color--100);
|
||||
}
|
||||
.pf-m-danger {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1>
|
||||
<i class="fas fa-map-marker"></i>
|
||||
{% trans 'Outposts' %}
|
||||
</h1>
|
||||
<p>{% trans "Outposts are deployments of passbook components to support different environments and protocols, like reverse proxies." %}</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<div class="pf-c-card">
|
||||
{% if object_list %}
|
||||
<div class="pf-c-toolbar">
|
||||
<div class="pf-c-toolbar__content">
|
||||
<div class="pf-c-toolbar__bulk-select">
|
||||
<a href="{% url 'passbook_admin:flow-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
|
||||
<thead>
|
||||
<tr role="row">
|
||||
<th role="columnheader" scope="col">{% trans 'Name' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Providers' %}</th>
|
||||
<th role="columnheader" scope="col">{% trans 'Health' %}</th>
|
||||
<th role="cell"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody role="rowgroup">
|
||||
{% for outpost in object_list %}
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<a href="{% url 'passbook_outposts:setup' outpost_pk=outpost.pk %}">{{ outpost.name }}</a>
|
||||
</th>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{{ outpost.providers.all.select_subclasses|join:", " }}
|
||||
</span>
|
||||
</td>
|
||||
<td role="cell">
|
||||
{% with health=outpost.health %}
|
||||
{% if health %}
|
||||
<i class="fas fa-check pf-m-success"></i> {{ health|naturaltime }}
|
||||
{% else %}
|
||||
<i class="fas fa-times pf-m-danger"></i> Unhealthy
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
<td>
|
||||
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:outpost-update' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:outpost-delete' pk=outpost.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
|
||||
<a href="https://passbook.beryju.org/outposts/outposts/#deploy">{% trans 'Deploy' %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pf-c-toolbar" id="page-layout-table-simple-toolbar-bottom">
|
||||
{% include 'partials/pagination.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="pf-c-empty-state">
|
||||
<div class="pf-c-empty-state__content">
|
||||
<i class="fas fa-cubes pf-c-empty-state__icon" aria-hidden="true"></i>
|
||||
<h1 class="pf-c-title pf-m-lg">
|
||||
{% trans 'No Outposts.' %}
|
||||
</h1>
|
||||
<div class="pf-c-empty-state__body">
|
||||
{% trans 'Currently no outposts exist. Click the button below to create one.' %}
|
||||
</div>
|
||||
<a href="{% url 'passbook_admin:outpost-create' %}?back={{ request.get_full_path }}" class="pf-c-button pf-m-primary" type="button">{% trans 'Create' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
@ -55,7 +55,7 @@
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ policy.name }}</div>
|
||||
{% if not policy.bindings.exists %}
|
||||
{% if not policy.bindings.exists and not policy.promptstage_set.exists %}
|
||||
<i class="pf-icon pf-icon-warning-triangle"></i>
|
||||
<small>{% trans 'Warning: Policy is not assigned.' %}</small>
|
||||
{% else %}
|
||||
|
@ -36,7 +36,7 @@
|
||||
<tr role="row">
|
||||
<th role="columnheader">
|
||||
<div>
|
||||
<div>{{ token.pk }}</div>
|
||||
<div>{{ token.pk.hex }}</div>
|
||||
</div>
|
||||
</th>
|
||||
<td role="cell">
|
||||
@ -51,7 +51,11 @@
|
||||
</td>
|
||||
<td role="cell">
|
||||
<span>
|
||||
{% if not token.expiring %}
|
||||
-
|
||||
{% else %}
|
||||
{{ token.expires }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -30,7 +30,7 @@
|
||||
<div class="pf-l-stack__item">
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<form action="" method="post" class="pf-c-form pf-m-horizontal">
|
||||
<form action="" method="post" class="pf-c-form pf-m-horizontal" enctype="multipart/form-data">
|
||||
{% include 'partials/form_horizontal.html' with form=form %}
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
|
@ -4,9 +4,9 @@ from django.urls import path
|
||||
from passbook.admin.views import (
|
||||
applications,
|
||||
certificate_key_pair,
|
||||
debug,
|
||||
flows,
|
||||
groups,
|
||||
outposts,
|
||||
overview,
|
||||
policies,
|
||||
policies_bindings,
|
||||
@ -192,6 +192,7 @@ urlpatterns = [
|
||||
# Flows
|
||||
path("flows/", flows.FlowListView.as_view(), name="flows"),
|
||||
path("flows/create/", flows.FlowCreateView.as_view(), name="flow-create",),
|
||||
path("flows/import/", flows.FlowImportView.as_view(), name="flow-import",),
|
||||
path(
|
||||
"flows/<uuid:pk>/update/", flows.FlowUpdateView.as_view(), name="flow-update",
|
||||
),
|
||||
@ -200,6 +201,9 @@ urlpatterns = [
|
||||
flows.FlowDebugExecuteView.as_view(),
|
||||
name="flow-execute",
|
||||
),
|
||||
path(
|
||||
"flows/<uuid:pk>/export/", flows.FlowExportView.as_view(), name="flow-export",
|
||||
),
|
||||
path(
|
||||
"flows/<uuid:pk>/delete/", flows.FlowDeleteView.as_view(), name="flow-delete",
|
||||
),
|
||||
@ -235,13 +239,17 @@ urlpatterns = [
|
||||
name="user-password-reset",
|
||||
),
|
||||
# Groups
|
||||
path("group/", groups.GroupListView.as_view(), name="group"),
|
||||
path("group/create/", groups.GroupCreateView.as_view(), name="group-create"),
|
||||
path("groups/", groups.GroupListView.as_view(), name="groups"),
|
||||
path("groups/create/", groups.GroupCreateView.as_view(), name="group-create"),
|
||||
path(
|
||||
"group/<uuid:pk>/update/", groups.GroupUpdateView.as_view(), name="group-update"
|
||||
"groups/<uuid:pk>/update/",
|
||||
groups.GroupUpdateView.as_view(),
|
||||
name="group-update",
|
||||
),
|
||||
path(
|
||||
"group/<uuid:pk>/delete/", groups.GroupDeleteView.as_view(), name="group-delete"
|
||||
"groups/<uuid:pk>/delete/",
|
||||
groups.GroupDeleteView.as_view(),
|
||||
name="group-delete",
|
||||
),
|
||||
# Certificate-Key Pairs
|
||||
path(
|
||||
@ -264,8 +272,19 @@ urlpatterns = [
|
||||
certificate_key_pair.CertificateKeyPairDeleteView.as_view(),
|
||||
name="certificatekeypair-delete",
|
||||
),
|
||||
# Groups
|
||||
path("groups/", groups.GroupListView.as_view(), name="groups"),
|
||||
# Debug
|
||||
path("debug/request/", debug.DebugRequestView.as_view(), name="debug-request"),
|
||||
# Outposts
|
||||
path("outposts/", outposts.OutpostListView.as_view(), name="outposts",),
|
||||
path(
|
||||
"outposts/create/", outposts.OutpostCreateView.as_view(), name="outpost-create",
|
||||
),
|
||||
path(
|
||||
"outposts/<uuid:pk>/update/",
|
||||
outposts.OutpostUpdateView.as_view(),
|
||||
name="outpost-update",
|
||||
),
|
||||
path(
|
||||
"outposts/<uuid:pk>/delete/",
|
||||
outposts.OutpostDeleteView.as_view(),
|
||||
name="outpost-delete",
|
||||
),
|
||||
]
|
||||
|
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
|
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
|
@ -1,15 +0,0 @@
|
||||
"""passbook administration debug views"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
|
||||
class DebugRequestView(LoginRequiredMixin, TemplateView):
|
||||
"""Show debug info about request"""
|
||||
|
||||
template_name = "administration/debug/request.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["request_dict"] = {}
|
||||
for key in dir(self.request):
|
||||
kwargs["request_dict"][key] = getattr(self.request, key)
|
||||
return super().get_context_data(**kwargs)
|
@ -1,19 +1,23 @@
|
||||
"""passbook Flow administration"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http import HttpRequest, HttpResponse, JsonResponse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views.generic import DetailView, ListView, UpdateView
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import DetailView, FormView, ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.flows.forms import FlowForm
|
||||
from passbook.flows.forms import FlowForm, FlowImportForm
|
||||
from passbook.flows.models import Flow
|
||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||
from passbook.flows.transfer.common import DataclassEncoder
|
||||
from passbook.flows.transfer.exporter import FlowExporter
|
||||
from passbook.flows.transfer.importer import FlowImporter
|
||||
from passbook.flows.views import SESSION_KEY_PLAN, FlowPlanner
|
||||
from passbook.lib.utils.urls import redirect_with_qs
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
@ -88,3 +92,42 @@ class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
|
||||
return redirect_with_qs(
|
||||
"passbook_flows:flow-executor-shell", self.request.GET, flow_slug=flow.slug,
|
||||
)
|
||||
|
||||
|
||||
class FlowImportView(LoginRequiredMixin, FormView):
|
||||
"""Import flow from JSON Export; only allowed for superusers
|
||||
as these flows can contain python code"""
|
||||
|
||||
form_class = FlowImportForm
|
||||
template_name = "administration/flow/import.html"
|
||||
success_url = reverse_lazy("passbook_admin:flows")
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not request.user.is_superuser:
|
||||
return self.handle_no_permission()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form: FlowImportForm) -> HttpResponse:
|
||||
importer = FlowImporter(form.cleaned_data["flow"].read().decode())
|
||||
successful = importer.apply()
|
||||
if not successful:
|
||||
messages.error(self.request, _("Failed to import flow."))
|
||||
else:
|
||||
messages.success(self.request, _("Successfully imported flow."))
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class FlowExportView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
|
||||
"""Export Flow"""
|
||||
|
||||
model = Flow
|
||||
permission_required = "passbook_flows.export_flow"
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request: HttpRequest, pk: str) -> HttpResponse:
|
||||
"""Debug exectue flow, setting the current user as pending user"""
|
||||
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"'
|
||||
return response
|
||||
|
@ -5,7 +5,7 @@ from django.contrib.auth.mixins import (
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
|
67
passbook/admin/views/outposts.py
Normal file
67
passbook/admin/views/outposts.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""passbook Outpost administration"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import ListView, UpdateView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
||||
from passbook.admin.views.utils import DeleteMessageView
|
||||
from passbook.lib.views import CreateAssignPermView
|
||||
from passbook.outposts.forms import OutpostForm
|
||||
from passbook.outposts.models import Outpost
|
||||
|
||||
|
||||
class OutpostListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all outposts"""
|
||||
|
||||
model = Outpost
|
||||
permission_required = "passbook_outposts.view_outpost"
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
template_name = "administration/outpost/list.html"
|
||||
|
||||
|
||||
class OutpostCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Outpost"""
|
||||
|
||||
model = Outpost
|
||||
form_class = OutpostForm
|
||||
permission_required = "passbook_outposts.add_outpost"
|
||||
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:outposts")
|
||||
success_message = _("Successfully created Outpost")
|
||||
|
||||
|
||||
class OutpostUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update outpost"""
|
||||
|
||||
model = Outpost
|
||||
form_class = OutpostForm
|
||||
permission_required = "passbook_outposts.change_outpost"
|
||||
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:outposts")
|
||||
success_message = _("Successfully updated Certificate-Key Pair")
|
||||
|
||||
|
||||
class OutpostDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
|
||||
"""Delete outpost"""
|
||||
|
||||
model = Outpost
|
||||
permission_required = "passbook_outposts.delete_outpost"
|
||||
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:outposts")
|
||||
success_message = _("Successfully deleted Certificate-Key Pair")
|
@ -58,7 +58,7 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
application=None
|
||||
)
|
||||
kwargs["policies_without_binding"] = len(
|
||||
Policy.objects.filter(bindings__isnull=True)
|
||||
Policy.objects.filter(bindings__isnull=True, promptstage__isnull=True)
|
||||
)
|
||||
kwargs["cached_policies"] = len(cache.keys("policy_*"))
|
||||
kwargs["cached_flows"] = len(cache.keys("flow_*"))
|
||||
|
@ -10,7 +10,7 @@ from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.db.models import QuerySet
|
||||
from django.http import HttpResponse
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import FormView
|
||||
from django.views.generic.detail import DetailView
|
||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user