Compare commits

...

94 Commits

Author SHA1 Message Date
c7f078ffcc new release: 0.9.0-pre7 2020-07-07 22:34:44 +02:00
571cb3d65f sources/oauth: disable twitter source while its broken 2020-07-07 22:25:50 +02:00
8c500c38b1 policies/reputation: only change score when credentials contain username 2020-07-07 22:25:37 +02:00
5644e57e6a sources/oauth: directly call AuthorizedServiceBackend instead of authenticate() 2020-07-07 22:23:45 +02:00
cfc181eed1 sources/oauth: fix wrong comparions
closes #118
2020-07-07 21:46:16 +02:00
91bea38b8e lib: ignore APM errors 2020-07-07 21:45:36 +02:00
d95c5aa739 root: allow changing of APM verify_server_cert setting 2020-07-07 19:59:32 +02:00
0b250b897e new release: 0.9.0-pre6 2020-07-07 19:14:29 +02:00
c6880a0f16 Merge pull request #117 from BeryJu/apm
Support for Elastic APM
2020-07-07 18:48:40 +02:00
beb5ffcbdd ci: fix gatekeeper dockerfile path 2020-07-07 18:48:24 +02:00
0715cac39b root: remove psutil as we have external monitoring for CPU 2020-07-07 18:24:24 +02:00
41117d873d ci: fix gatekeeper building the wrong image 2020-07-07 18:23:15 +02:00
231e448b1a lib/eval: fix import order 2020-07-07 18:05:38 +02:00
b3b8cd807d root: expose APM settings in helm chart 2020-07-07 17:54:07 +02:00
9021bbd5de root: implement APM support 2020-07-07 17:43:10 +02:00
169475ab39 crypto: add colon seperator for fingerprint 2020-07-07 17:05:31 +02:00
c00e01626e sources/ldap: adjust task schedule name 2020-07-07 17:04:07 +02:00
05d4a9ef62 policies/reputation: rewrite to save score into cache and save into DB via worker 2020-07-07 17:03:57 +02:00
17a2ac73e7 stages/user_write: add signals 2020-07-07 15:49:02 +02:00
6bc6f947dd stages/invitation: move invite signals from core to app 2020-07-07 15:46:13 +02:00
b048a1fb4f ci: notify sentry of new releases 2020-07-07 14:09:28 +02:00
363940ee8d root: fix API requests erroring 2020-07-07 14:02:20 +02:00
a64e53479c Merge pull request #115 from BeryJu/dependabot/pip/boto3-1.14.17
build(deps): bump boto3 from 1.14.16 to 1.14.17
2020-07-07 13:34:53 +02:00
14fdbe7720 Merge pull request #116 from BeryJu/dependabot/pip/coverage-5.2
build(deps-dev): bump coverage from 5.1 to 5.2
2020-07-07 13:34:41 +02:00
f56332c954 Merge branch 'master' into dependabot/pip/boto3-1.14.17 2020-07-07 13:14:07 +02:00
21c53c748f Merge branch 'master' into dependabot/pip/coverage-5.2 2020-07-07 13:13:55 +02:00
b12182c1d1 admin: improve overview layout 2020-07-07 13:13:15 +02:00
d8f27f595a admin: use django cache for admin version (expiry) 2020-07-07 13:12:54 +02:00
b25dc2aaa3 build(deps-dev): bump coverage from 5.1 to 5.2
Bumps [coverage](https://github.com/nedbat/coveragepy) from 5.1 to 5.2.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/coverage-5.1...coverage-5.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-07 05:24:51 +00:00
3ec3849e72 build(deps): bump boto3 from 1.14.16 to 1.14.17
Bumps [boto3](https://github.com/boto/boto3) from 1.14.16 to 1.14.17.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.16...1.14.17)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-07 05:23:18 +00:00
2dc1b65718 ui: fix modal layout 2020-07-06 20:50:14 +02:00
af22f507f4 sources/oauth: fix template for user settings 2020-07-06 17:48:53 +02:00
9958019bf3 core: fix user's sidebar links for sources 2020-07-06 17:46:41 +02:00
02d65972cb admin: fix submit button on update form 2020-07-06 17:46:30 +02:00
24ad893350 admin: fix token_list template 2020-07-06 17:43:20 +02:00
9c5792b1e1 docs: migrate TOTP and Static OTP devices 2020-07-06 17:42:46 +02:00
094d191bff new release: 0.9.0-pre5 2020-07-06 12:52:34 +02:00
49fb9f688b Merge pull request #114 from BeryJu/dependabot/pip/sentry-sdk-0.16.0
build(deps): bump sentry-sdk from 0.15.1 to 0.16.0
2020-07-06 11:51:21 +02:00
7d161e5aa1 build(deps): bump sentry-sdk from 0.15.1 to 0.16.0
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 0.15.1 to 0.16.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGES.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/0.15.1...0.16.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-06 05:18:21 +00:00
78e5d471e3 core: fix type annotation for user settings 2020-07-05 23:49:33 +02:00
2e2c9f5287 api: add token authentication 2020-07-05 23:37:58 +02:00
d5a3e09a98 core: add token Intents 2020-07-05 23:14:57 +02:00
2402cfe29d providers/* use name for __str__ 2020-07-05 23:00:40 +02:00
26613b6ea9 core: fix application overview 2020-07-05 22:58:52 +02:00
e5165abf04 stages/user_login: Allow changing of session duration 2020-07-04 15:20:45 +02:00
b26882a450 flows: FlowStageBinding group Stage by type 2020-07-04 15:02:21 +02:00
94281bee88 admin: improve policy binding listing by showing Target object type 2020-07-04 00:18:19 +02:00
16b966c16e policies: Show grouped Dropdown for Target 2020-07-04 00:16:16 +02:00
d3b0992456 flows: FlowStageBinding: rename .flow to .target to fix select_subclasses() 2020-07-04 00:14:21 +02:00
dd74b73b4f Merge pull request #40 from BeryJu/azure-pipelines
Set up CI with Azure Pipelines
2020-07-03 10:47:29 +02:00
0bdfccc1f3 ci: final cleanup 2020-07-03 10:17:24 +02:00
ceb0793bc9 ci: publish unittest results and coverage 2020-07-03 09:54:25 +02:00
abea85b635 ci: fix incorrect node version for pyright 2020-07-03 09:39:23 +02:00
01c83f6f4a Merge branch 'master' into azure-pipelines
# Conflicts:
#	.github/workflows/ci.yml
#	README.md
2020-07-03 09:33:04 +02:00
9167c9c3ba Merge pull request #112 from BeryJu/dependabot/pip/django-prometheus-2.1.0.dev46
build(deps): bump django-prometheus from 2.1.0.dev42 to 2.1.0.dev46
2020-07-03 09:09:16 +02:00
04add2e52d build(deps): bump django-prometheus from 2.1.0.dev42 to 2.1.0.dev46
Bumps [django-prometheus](https://github.com/korfuri/django-prometheus) from 2.1.0.dev42 to 2.1.0.dev46.
- [Release notes](https://github.com/korfuri/django-prometheus/releases)
- [Changelog](https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/korfuri/django-prometheus/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-03 06:53:47 +00:00
1e9241d45b Merge pull request #111 from BeryJu/dependabot/pip/boto3-1.14.16
build(deps): bump boto3 from 1.14.15 to 1.14.16
2020-07-03 08:49:41 +02:00
22ee198a31 build(deps): bump boto3 from 1.14.15 to 1.14.16
Bumps [boto3](https://github.com/boto/boto3) from 1.14.15 to 1.14.16.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.15...1.14.16)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-03 05:20:53 +00:00
1d9c92d548 admin: add generic form tests 2020-07-02 22:29:30 +02:00
b30b58924f e2e: Add denied tests for oauth and oidc provider 2020-07-02 21:55:02 +02:00
bead19c64c flows: cleanup denied view, use everywhere 2020-07-02 13:48:42 +02:00
76e2ba4764 e2e/provider/saml: add negative case 2020-07-02 13:48:21 +02:00
8d095d7436 Merge pull request #109 from BeryJu/dependabot/pip/django-prometheus-2.1.0.dev42
build(deps): bump django-prometheus from 2.1.0.dev40 to 2.1.0.dev42
2020-07-02 11:59:11 +02:00
d3a7fd5818 build(deps): bump django-prometheus from 2.1.0.dev40 to 2.1.0.dev42
Bumps [django-prometheus](https://github.com/korfuri/django-prometheus) from 2.1.0.dev40 to 2.1.0.dev42.
- [Release notes](https://github.com/korfuri/django-prometheus/releases)
- [Changelog](https://github.com/korfuri/django-prometheus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/korfuri/django-prometheus/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-02 09:56:06 +00:00
247a8dbc8f Merge pull request #110 from BeryJu/dependabot/pip/boto3-1.14.15
build(deps): bump boto3 from 1.14.14 to 1.14.15
2020-07-02 11:52:16 +02:00
9241adfc68 build(deps): bump boto3 from 1.14.14 to 1.14.15
Bumps [boto3](https://github.com/boto/boto3) from 1.14.14 to 1.14.15.
- [Release notes](https://github.com/boto/boto3/releases)
- [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
- [Commits](https://github.com/boto/boto3/compare/1.14.14...1.14.15)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-07-02 05:21:07 +00:00
ae83ee6d31 providers/saml: fix access result not being checked properly 2020-07-02 00:23:52 +02:00
4701374021 admin: remove duplicate code into new base classes 2020-07-02 00:13:33 +02:00
bd40585247 providers/samlv2: remove SAMLv2 from master 2020-07-01 23:21:58 +02:00
cc0b8164b0 providers/*: use PolicyAccessMixin to simplify 2020-07-01 23:18:10 +02:00
310b31a8b7 core: fix linting 2020-07-01 22:35:38 +02:00
13900bc603 lib: cleanup unused widgets 2020-07-01 22:27:58 +02:00
6634cc2edf root: add group_membership policy 2020-07-01 21:18:05 +02:00
3478a2cf6d admin: add filter to hide classes with __debug_only__ when Debug is disabled 2020-07-01 18:53:13 +02:00
3b70d12a5f *: rephrase strings 2020-07-01 18:40:52 +02:00
219acf76d5 core: fix forms for radio buttons 2020-07-01 12:47:27 +02:00
ec6f467fa2 ui: Make Checkbox label click trigger checkbox toggle 2020-07-01 12:37:13 +02:00
0e6561987e admin: fix user and group create not triggering sidebar 2020-07-01 12:36:44 +02:00
62c20b6e67 admin: add list of all tokens 2020-07-01 12:27:30 +02:00
13084562c5 admin: fix Password Recovery function not working 2020-07-01 12:10:12 +02:00
02c1c434a2 core: update styling of impersonate banner 2020-07-01 12:01:58 +02:00
9882342ed1 Merge branch 'master' into azure-pipelines
# Conflicts:
#	.github/workflows/ci.yml
2020-06-02 20:40:04 +02:00
1c906b12be ci: set static network for static build 2020-05-29 10:04:23 +02:00
4d835b18cc ci: fix network for static build 2020-05-29 09:43:00 +02:00
e02ff7ec30 ci: fix codecov token not being set correctly 2020-05-29 09:18:17 +02:00
2e67b0194b Update azure-pipelines.yml for Azure Pipelines 2020-05-29 09:15:57 +02:00
02f0712934 ci: fix static being built on wrong docker image 2020-05-28 21:19:06 +02:00
7e7ea47f39 ci: fix level of stages on build jobs 2020-05-28 21:00:30 +02:00
7e52711e3a ci: fix names of build jobs 2020-05-28 19:46:10 +02:00
40fd1c9c1f ci: fix duplicate key 2020-05-28 19:45:25 +02:00
4037a444eb ci: migrate building 2020-05-28 19:44:25 +02:00
1ed7e900f2 ci: migrate unittests and coverage 2020-05-28 19:29:28 +02:00
cfc8d0a0f7 ci: migrate lint to az 2020-05-28 19:15:18 +02:00
df33616544 Set up CI with Azure Pipelines
[skip ci]
2020-05-28 18:57:48 +02:00
193 changed files with 2284 additions and 1509 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.9.0-pre4
current_version = 0.9.0-pre7
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)

View File

@ -1,230 +0,0 @@
name: passbook-ci
on:
- push
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
jobs:
# Linting
pylint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev
- name: Lint with pylint
run: pipenv run pylint passbook
black:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev
- name: Lint with black
run: pipenv run black --check passbook
prospector:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev && pipenv install --dev prospector --skip-lock
- name: Lint with prospector
run: pipenv run prospector
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev
- name: Lint with bandit
run: pipenv run bandit -r passbook
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/python@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
pyright:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-node@v1
with:
node-version: '12'
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install pyright
run: npm install -g pyright
- name: Show pyright version
run: pyright --version
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev
- name: Lint with pyright
run: pipenv run pyright
# Actual CI tests
migrations:
needs:
- pylint
- black
- prospector
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
ports:
- 5432:5432
redis:
image: redis:latest
ports:
- 6379:6379
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- name: Install dependencies
run: sudo pip install -U wheel pipenv && pipenv install --dev
- name: Run migrations
run: pipenv run ./manage.py migrate
coverage:
needs:
- pylint
- black
- prospector
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
ports:
- 5432:5432
redis:
image: redis:latest
ports:
- 6379:6379
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/setup-python@v1
with:
python-version: '3.8'
- uses: actions/setup-node@v1
with:
node-version: '12'
- name: Install dependencies
run: |
sudo pip install -U wheel pipenv
pipenv install --dev
- name: Prepare Chrome node
run: |
cd e2e
docker-compose pull -q chrome
docker-compose up -d chrome
- name: Build static files for e2e test
run: |
cd passbook/static/static
yarn
- name: Run coverage
run: pipenv run coverage run ./manage.py test --failfast
- uses: actions/upload-artifact@v2
if: failure()
with:
path: out/
- name: Create XML Report
run: pipenv run coverage xml
- uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
# Build
build-server:
needs:
- migrations
- coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/passbook:gh-${GITHUB_REF##*/}
-f Dockerfile .
- name: Push Docker Container to Registry
run: docker push beryju/passbook:gh-${GITHUB_REF##*/}
build-gatekeeper:
needs:
- migrations
- coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
run: |
cd gatekeeper
docker build \
--no-cache \
-t beryju/passbook-gatekeeper:gh-${GITHUB_REF##*/} \
-f Dockerfile .
- name: Push Docker Container to Registry
run: docker push beryju/passbook-gatekeeper:gh-${GITHUB_REF##*/}
build-static:
needs:
- migrations
- coverage
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
redis:
image: redis:latest
steps:
- uses: actions/checkout@v1
- name: Docker Login Registry
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
run: docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
- name: Building Docker Image
run: docker build
--no-cache
--network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:gh-${GITHUB_REF##*/}
-f static.Dockerfile .
- name: Push Docker Container to Registry
run: docker push beryju/passbook-static:gh-${GITHUB_REF##*/}

View File

@ -16,11 +16,11 @@ jobs:
- name: Building Docker Image
run: docker build
--no-cache
-t beryju/passbook:0.9.0-pre4
-t beryju/passbook:0.9.0-pre7
-t beryju/passbook:latest
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook:0.9.0-pre4
run: docker push beryju/passbook:0.9.0-pre7
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook:latest
build-gatekeeper:
@ -37,11 +37,11 @@ jobs:
cd gatekeeper
docker build \
--no-cache \
-t beryju/passbook-gatekeeper:0.9.0-pre4 \
-t beryju/passbook-gatekeeper:0.9.0-pre7 \
-t beryju/passbook-gatekeeper:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-gatekeeper:0.9.0-pre4
run: docker push beryju/passbook-gatekeeper:0.9.0-pre7
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-gatekeeper:latest
build-static:
@ -66,11 +66,11 @@ jobs:
run: docker build
--no-cache
--network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:0.9.0-pre4
-t beryju/passbook-static:0.9.0-pre7
-t beryju/passbook-static:latest
-f static.Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-static:0.9.0-pre4
run: docker push beryju/passbook-static:0.9.0-pre7
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-static:latest
test-release:
@ -86,3 +86,19 @@ jobs:
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"
sentry-release:
needs:
- test-release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Create a Sentry.io release
uses: tclindner/sentry-releases-action@v1.2.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: beryjuorg
SENTRY_PROJECT: passbook
SENTRY_URL: https://sentry.beryju.org
with:
tagName: 0.9.0-pre7
environment: production

3
.gitignore vendored
View File

@ -196,3 +196,6 @@ local.env.yml
### Helm ###
# Chart dependencies
**/charts/*.tgz
# Selenium Screenshots
selenium_screenshots/**

View File

@ -41,6 +41,7 @@ structlog = "*"
swagger-spec-validator = "*"
urllib3 = {extras = ["secure"],version = "*"}
facebook-sdk = "*"
elastic-apm = "*"
[requires]
python_version = "3.8"

125
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "fd0192b73c01aaffb90716ce7b6d4e5be9adb8788d3ebd58e54ccd6f85d9b71b"
"sha256": "f90d79a67bbd689ca4e7ccbfd528e4ed45078e848c36d84e53ff9c6b2a1e92ed"
},
"pipfile-spec": 6,
"requires": {
@ -46,18 +46,18 @@
},
"boto3": {
"hashes": [
"sha256:4c2f5f9f28930e236845e2cddbe01cb093ca96dc1f5c6e2b2b254722018a2268",
"sha256:87beffba2360b8077413f2d473cb828d0a5bda513bd1d6fb7b137c57b686aeb6"
"sha256:ae57df1fbad7e29954a160d77cbf650d6562eb0d304c1206afa71d914e771a66",
"sha256:cbe618d61cb8f75cd9495ea36e69bad7c8984eb11f02ad247be4c9a2eb7eb647"
],
"index": "pypi",
"version": "==1.14.14"
"version": "==1.14.17"
},
"botocore": {
"hashes": [
"sha256:6a2e9768dad8ae9771302d5922b977dca6bb9693f9b6a5f6ed0e7ac375e2ca40",
"sha256:96d668ae5246d236ea83e4586349552d6584e8b1551ae2fccc0bd4ed528a746f"
"sha256:5528c04c360019c24f2706ce82872c9ab767a8c581beffdfdaf006cce7499cac",
"sha256:d65b5574dad8c221344496352245828d9ffecaa0868199eb04ccd2eb2ff09133"
],
"version": "==1.17.14"
"version": "==1.17.17"
},
"celery": {
"hashes": [
@ -232,11 +232,11 @@
},
"django-prometheus": {
"hashes": [
"sha256:7b44f45b18f5cc4322b206887646c1848aab42a842218875c5400333fa5d17ff",
"sha256:7b7a2a09bde96ca8e66bcf9de040a239d28f52e55f51884da9380e2d4b1c7550"
"sha256:054924b6aedd41f3f76940578127e7fac852a696805f956da39b7941c78bba91",
"sha256:208e55e3a11ac9edd53a3b342b033e140ffe27914af1202f0b064edf99e83fc3"
],
"index": "pypi",
"version": "==2.1.0.dev40"
"version": "==2.1.0.dev46"
},
"django-recaptcha": {
"hashes": [
@ -306,6 +306,38 @@
],
"version": "==1.0.0"
},
"elastic-apm": {
"hashes": [
"sha256:0ffd86d8449d7b63c6053c5032e09abf398c753214a55de8a4e15cb9b56108d1",
"sha256:18006ade25a91a8030b5fcfa825d5c364b13bc5e902b818725341f8c9a00895a",
"sha256:1d8335f94660c246d5475ec3b15452cd0f5b51affb2e1d16eb2fbc36380308a8",
"sha256:25fea9cb6c99efc229b1449d7fbdda76260404cc74abefcc0cc86b3a5102d99d",
"sha256:2841bee5650b736d5ebb199d728a18b415dfed22cb367cd913619a691dfe39e8",
"sha256:301d159933f19115b21f92bc1ff7f0073bfea13ca24c6ff34023c23077a08e44",
"sha256:4eaaebd088315d7ba2726b21fea06279598cad128be073b28c0462049f093d5a",
"sha256:515d027d380df818ec304d4d28121c39069a0d919cb2eb7f8e29019a14d62c2a",
"sha256:5ad2b431298567f642d44826be2c557d9aca5761c0240be23f9a52b66833cc93",
"sha256:5dbf19570bdf97e169b5901913e9a3e271ff5e10d298a608e214802dca8c9065",
"sha256:724ded78cc24d2c7d8bc81642a938d9bfc2dcb8b5bdad1b1da242300f9f4ec73",
"sha256:7e859162f4c187defe26fb00c974a128eee2bd8988cd30ccffdcaaeb56cb2248",
"sha256:9180ed12b9c12cc794f3b57069d1e7b2a04352af02f8d0bc89c9251231f8660e",
"sha256:92f885cc67a9d78e72b174feaa979ddb5188d7cef2b5a7739be740955e07c5ed",
"sha256:976eaaf3825df760946f31b5426544fecc4c32fd66e124565ede7151f8152689",
"sha256:abafeff08ff285cc03c33e822633c6e25a9434174413f72a5032393e9f95a1e0",
"sha256:ad21169ebee7ae35d6c42cd6ac9e7658d6e07bc6a3f34dcc4f0a32e03d736fdc",
"sha256:b2b4ff079a20d620d7f87a345d37cf9b7f2bc1c8cc8c9317fb0c3979371f0d41",
"sha256:b3b72d26104de89124cca965b234b6b67be4604518e168aedcd52c7229c923e9",
"sha256:c4a144ecb0b1570c1f6a285cd6f28f2eda89c0696ed494892e3250bb6fed7909",
"sha256:dc368bbac6401fa0c9d7a35429257190759f4f33099783d9e0557ce12d64ca6c",
"sha256:e28a81802784ea80d21c294a4ab4e47f658a4031caa5c320147925ab62c6a0d4",
"sha256:e7832a5ad503d6cd4a7eaa4cee782ccdf113afa99708e3d005fe9aef539a8222",
"sha256:f2674a3aee0c38df82dedade353c944a2f55b215c7d5b0776e1bb89ce87de57c",
"sha256:f7b37f65c0ca971038f6b69c7581ea762fbc89d9631107babc04c646898686d2",
"sha256:fbd1a68b4cf32298e09652958ec3cf13462a5269408522211cfd3e02b451c3b2"
],
"index": "pypi",
"version": "==5.8.0"
},
"facebook-sdk": {
"hashes": [
"sha256:2e987b3e0f466a6f4ee77b935eb023dba1384134f004a2af21f1cfff7fe0806e",
@ -735,11 +767,11 @@
},
"sentry-sdk": {
"hashes": [
"sha256:06825c15a78934e78941ea25910db71314c891608a46492fc32c15902c6b2119",
"sha256:3ac0c430761b3cb7682ce612151d829f8644bb3830d4e530c75b02ceb745ff49"
"sha256:da06bc3641e81ec2c942f87a0676cd9180044fa3d1697524a0005345997542e2",
"sha256:e80d61af85d99a1222c1a3e2a24023618374cd50a99673aa7fa3cf920e7d813b"
],
"index": "pypi",
"version": "==0.15.1"
"version": "==0.16.0"
},
"service-identity": {
"hashes": [
@ -945,40 +977,43 @@
},
"coverage": {
"hashes": [
"sha256:00f1d23f4336efc3b311ed0d807feb45098fc86dee1ca13b3d6768cdab187c8a",
"sha256:01333e1bd22c59713ba8a79f088b3955946e293114479bbfc2e37d522be03355",
"sha256:0cb4be7e784dcdc050fc58ef05b71aa8e89b7e6636b99967fadbdba694cf2b65",
"sha256:0e61d9803d5851849c24f78227939c701ced6704f337cad0a91e0972c51c1ee7",
"sha256:1601e480b9b99697a570cea7ef749e88123c04b92d84cedaa01e117436b4a0a9",
"sha256:2742c7515b9eb368718cd091bad1a1b44135cc72468c731302b3d641895b83d1",
"sha256:2d27a3f742c98e5c6b461ee6ef7287400a1956c11421eb574d843d9ec1f772f0",
"sha256:402e1744733df483b93abbf209283898e9f0d67470707e3c7516d84f48524f55",
"sha256:5c542d1e62eece33c306d66fe0a5c4f7f7b3c08fecc46ead86d7916684b36d6c",
"sha256:5f2294dbf7875b991c381e3d5af2bcc3494d836affa52b809c91697449d0eda6",
"sha256:6402bd2fdedabbdb63a316308142597534ea8e1895f4e7d8bf7476c5e8751fef",
"sha256:66460ab1599d3cf894bb6baee8c684788819b71a5dc1e8fa2ecc152e5d752019",
"sha256:782caea581a6e9ff75eccda79287daefd1d2631cc09d642b6ee2d6da21fc0a4e",
"sha256:79a3cfd6346ce6c13145731d39db47b7a7b859c0272f02cdb89a3bdcbae233a0",
"sha256:7a5bdad4edec57b5fb8dae7d3ee58622d626fd3a0be0dfceda162a7035885ecf",
"sha256:8fa0cbc7ecad630e5b0f4f35b0f6ad419246b02bc750de7ac66db92667996d24",
"sha256:a027ef0492ede1e03a8054e3c37b8def89a1e3c471482e9f046906ba4f2aafd2",
"sha256:a3f3654d5734a3ece152636aad89f58afc9213c6520062db3978239db122f03c",
"sha256:a82b92b04a23d3c8a581fc049228bafde988abacba397d57ce95fe95e0338ab4",
"sha256:acf3763ed01af8410fc36afea23707d4ea58ba7e86a8ee915dfb9ceff9ef69d0",
"sha256:adeb4c5b608574a3d647011af36f7586811a2c1197c861aedb548dd2453b41cd",
"sha256:b83835506dfc185a319031cf853fa4bb1b3974b1f913f5bb1a0f3d98bdcded04",
"sha256:bb28a7245de68bf29f6fb199545d072d1036a1917dca17a1e75bbb919e14ee8e",
"sha256:bf9cb9a9fd8891e7efd2d44deb24b86d647394b9705b744ff6f8261e6f29a730",
"sha256:c317eaf5ff46a34305b202e73404f55f7389ef834b8dbf4da09b9b9b37f76dd2",
"sha256:dbe8c6ae7534b5b024296464f387d57c13caa942f6d8e6e0346f27e509f0f768",
"sha256:de807ae933cfb7f0c7d9d981a053772452217df2bf38e7e6267c9cbf9545a796",
"sha256:dead2ddede4c7ba6cb3a721870f5141c97dc7d85a079edb4bd8d88c3ad5b20c7",
"sha256:dec5202bfe6f672d4511086e125db035a52b00f1648d6407cc8e526912c0353a",
"sha256:e1ea316102ea1e1770724db01998d1603ed921c54a86a2efcb03428d5417e489",
"sha256:f90bfc4ad18450c80b024036eaf91e4a246ae287701aaa88eaebebf150868052"
"sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d",
"sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2",
"sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703",
"sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404",
"sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7",
"sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405",
"sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d",
"sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c",
"sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6",
"sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70",
"sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40",
"sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4",
"sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613",
"sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10",
"sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b",
"sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0",
"sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec",
"sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1",
"sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d",
"sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913",
"sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e",
"sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62",
"sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e",
"sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a",
"sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d",
"sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f",
"sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e",
"sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b",
"sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c",
"sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032",
"sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a",
"sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee",
"sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c",
"sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b"
],
"index": "pypi",
"version": "==5.1"
"version": "==5.2"
},
"cryptography": {
"hashes": [

View File

@ -1,6 +1,7 @@
<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">
![CI Build status](https://img.shields.io/github/workflow/status/beryju/passbook/passbook-ci?style=flat-square)
=======
![CI Build status](https://img.shields.io/azure-devops/build/beryjuorg/5d94b893-6dea-4f68-a8fe-10f1674fc3a9/1?style=flat-square)
![Docker pulls](https://img.shields.io/docker/pulls/beryju/passbook.svg?style=flat-square)
![Docker pulls (gatekeeper)](https://img.shields.io/docker/pulls/beryju/passbook-gatekeeper.svg?style=flat-square)
![Latest version](https://img.shields.io/docker/v/beryju/passbook?sort=semver&style=flat-square)
@ -50,31 +51,7 @@ pipenv sync -d
```
Since passbook uses PostgreSQL-specific fields, you also need a local PostgreSQL instance to develop. passbook also uses redis for caching and message queueing.
For these databases you can use [Postgres.app](https://postgresapp.com/) and [Redis.app](https://jpadilla.github.io/redisapp/) on macOS or use it via docker-comppose:
```yaml
version: '3.7'
services:
postgresql:
container_name: postgres
image: postgres:11
volumes:
- db-data:/var/lib/postgresql/data
ports:
- 127.0.0.1:5432:5432
restart: always
redis:
container_name: redis
image: redis
ports:
- 127.0.0.1:6379:6379
restart: always
volumes:
db-data:
driver: local
```
For these databases you can use [Postgres.app](https://postgresapp.com/) and [Redis.app](https://jpadilla.github.io/redisapp/) on macOS or use it the docker-compose file in `scripts/docker-compose.yml`.
To tell passbook about these databases, create a file in the project root called `local.env.yml` with the following contents:

228
azure-pipelines.yml Normal file
View File

@ -0,0 +1,228 @@
trigger:
- master
resources:
- repo: self
variables:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
stages:
- stage: Lint
jobs:
- job: pylint
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run pylint passbook
- job: black
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run black --check passbook
- job: prospector
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
pipenv install --dev prospector --skip-lock
- task: CmdLine@2
inputs:
script: pipenv run prospector passbook
- job: bandit
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run bandit -r passbook
- job: pyright
pool:
vmImage: ubuntu-latest
steps:
- task: UseNode@1
inputs:
version: '12.x'
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: CmdLine@2
inputs:
script: npm install -g pyright
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run pyright
- stage: Test
jobs:
- job: migrations
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: CmdLine@2
inputs:
script: pipenv run ./manage.py migrate
- job: coverage
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.8'
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/docker-compose.yml'
action: 'Run services'
buildImages: false
- task: CmdLine@2
inputs:
script: |
sudo pip install -U wheel pipenv
pipenv install --dev
- task: DockerCompose@0
displayName: Run ChromeDriver
inputs:
dockerComposeFile: 'e2e/docker-compose.yml'
action: 'Run a specific service'
serviceName: 'chrome'
- task: CmdLine@2
displayName: Build static files for e2e
inputs:
script: |
cd passbook/static/static
yarn
- task: CmdLine@2
displayName: Run full test suite
inputs:
script: pipenv run coverage run ./manage.py test --failfast
- task: PublishBuildArtifacts@1
condition: failed()
displayName: Upload screenshots if selenium tests fail
inputs:
PathtoPublish: 'selenium_screenshots/'
ArtifactName: 'drop'
publishLocation: 'Container'
- task: CmdLine@2
inputs:
script: |
pipenv run coverage xml
pipenv run coverage html
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: 'coverage.xml'
- task: PublishTestResults@2
condition: succeededOrFailed()
inputs:
testRunTitle: 'Publish test results for Python $(python.version)'
testResultsFiles: 'unittest.xml'
- task: CmdLine@2
env:
CODECOV_TOKEN: $(CODECOV_TOKEN)
inputs:
script: bash <(curl -s https://codecov.io/bash)
- stage: Build
jobs:
- job: build_server
pool:
vmImage: 'ubuntu-latest'
steps:
- task: Docker@2
inputs:
containerRegistry: 'dockerhub'
repository: 'beryju/passbook'
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'
steps:
- task: DockerCompose@0
displayName: Run services
inputs:
dockerComposeFile: 'scripts/docker-compose.yml'
action: 'Run services'
buildImages: false
- task: Docker@2
inputs:
containerRegistry: 'dockerhub'
repository: 'beryju/passbook-static'
command: 'build'
Dockerfile: 'static.Dockerfile'
tags: 'gh-$(Build.SourceBranchName)'
arguments: "--network=beryjupassbook_default"
- task: Docker@2
inputs:
containerRegistry: 'dockerhub'
repository: 'beryju/passbook-static'
command: 'push'
tags: 'gh-$(Build.SourceBranchName)'

View File

@ -22,6 +22,12 @@ config:
# 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.

View File

@ -6,13 +6,13 @@ To export data from your old instance, run this command:
- docker-compose
```
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
docker-compose exec server ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event otp_totp.totpdevice otp_static.staticdevice otp_static.statictoken
docker cp passbook_server_1:/tmp/passbook_dump.json passbook_dump.json
```
- kubernetes
```
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event
kubectl exec -it passbook-web-... -- ./manage.py dumpdata -o /tmp/passbook_dump.json passbook_core.User passbook_core.Group passbook_crypto.CertificateKeyPair passbook_audit.Event otp_totp.totpdevice otp_static.staticdevice otp_static.statictoken
kubectl cp passbook-web-...:/tmp/passbook_dump.json passbook_dump.json
```

View File

@ -105,10 +105,10 @@ class TestFlowsEnroll(SeleniumTestCase):
ident_stage.enrollment_flow = flow
ident_stage.save()
FlowStageBinding.objects.create(flow=flow, stage=first_stage, order=0)
FlowStageBinding.objects.create(flow=flow, stage=second_stage, order=1)
FlowStageBinding.objects.create(flow=flow, stage=user_write, order=2)
FlowStageBinding.objects.create(flow=flow, stage=user_login, order=3)
FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0)
FlowStageBinding.objects.create(target=flow, stage=second_stage, order=1)
FlowStageBinding.objects.create(target=flow, stage=user_write, order=2)
FlowStageBinding.objects.create(target=flow, stage=user_login, order=3)
self.driver.get(self.live_server_url)
self.wait.until(
@ -206,11 +206,11 @@ class TestFlowsEnroll(SeleniumTestCase):
ident_stage.enrollment_flow = flow
ident_stage.save()
FlowStageBinding.objects.create(flow=flow, stage=first_stage, order=0)
FlowStageBinding.objects.create(flow=flow, stage=second_stage, order=1)
FlowStageBinding.objects.create(flow=flow, stage=user_write, order=2)
FlowStageBinding.objects.create(flow=flow, stage=email_stage, order=3)
FlowStageBinding.objects.create(flow=flow, stage=user_login, order=4)
FlowStageBinding.objects.create(target=flow, stage=first_stage, order=0)
FlowStageBinding.objects.create(target=flow, stage=second_stage, order=1)
FlowStageBinding.objects.create(target=flow, stage=user_write, order=2)
FlowStageBinding.objects.create(target=flow, stage=email_stage, order=3)
FlowStageBinding.objects.create(target=flow, stage=user_login, order=4)
self.driver.get(self.live_server_url)
self.driver.find_element(By.CSS_SELECTOR, "[role=enroll]").click()

View File

@ -11,6 +11,8 @@ 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
@ -192,3 +194,42 @@ class TestProviderOAuth(SeleniumTestCase):
).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,
redirect_uris="http://localhost:3000/login/github",
skip_authorization=True,
authorization_flow=authorization_flow,
)
app = Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
)
negative_policy = ExpressionPolicy.objects.create(
name="negative-static", expression="return False"
)
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--github").click()
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url(self.url("passbook_flows:denied"))
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "#flow-body > header > h1").text,
"Permission denied",
)

View File

@ -14,6 +14,8 @@ from docker.types import Healthcheck
from e2e.utils import USER, SeleniumTestCase, ensure_rsa_key
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.oidc.models import OpenIDProvider
@ -252,3 +254,50 @@ class TestProviderOIDC(SeleniumTestCase):
).get_attribute("value"),
USER().email,
)
def test_authorization_denied(self):
"""test OpenID Provider flow (default authorization with access deny)"""
sleep(1)
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-explicit-consent"
)
client = Client.objects.create(
name="grafana",
client_type="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,
)
# 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,
)
app = Application.objects.create(
name="Grafana", slug="grafana", provider=provider,
)
negative_policy = ExpressionPolicy.objects.create(
name="negative-static", expression="return False"
)
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
self.driver.get("http://localhost:3000")
self.driver.find_element(By.CLASS_NAME, "btn-service--oauth").click()
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url(self.url("passbook_flows:denied"))
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "#flow-body > header > h1").text,
"Permission denied",
)

View File

@ -12,6 +12,8 @@ from passbook.core.models import Application
from passbook.crypto.models import CertificateKeyPair
from passbook.flows.models import Flow
from passbook.lib.utils.reflection import class_to_path
from passbook.policies.expression.models import ExpressionPolicy
from passbook.policies.models import PolicyBinding
from passbook.providers.saml.models import (
SAMLBindings,
SAMLPropertyMapping,
@ -174,3 +176,41 @@ class TestProviderSAML(SeleniumTestCase):
self.driver.find_element(By.XPATH, "/html/body/pre").text,
f"Hello, {USER().name}!",
)
def test_sp_initiated_denied(self):
"""test SAML Provider flow SP-initiated flow (Policy denies access)"""
# Bootstrap all needed objects
authorization_flow = Flow.objects.get(
slug="default-provider-authorization-implicit-consent"
)
negative_policy = ExpressionPolicy.objects.create(
name="negative-static", expression="return False"
)
provider: SAMLProvider = SAMLProvider.objects.create(
name="saml-test",
processor_path=class_to_path(GenericProcessor),
acs_url="http://localhost:9009/saml/acs",
audience="passbook-e2e",
issuer="passbook-e2e",
sp_binding=SAMLBindings.POST,
authorization_flow=authorization_flow,
signing_kp=CertificateKeyPair.objects.first(),
)
provider.property_mappings.set(SAMLPropertyMapping.objects.all())
provider.save()
app = Application.objects.create(
name="SAML", slug="passbook-saml", provider=provider,
)
PolicyBinding.objects.create(target=app, policy=negative_policy, order=0)
self.container = self.setup_client(provider)
self.driver.get("http://localhost:9009/")
self.driver.find_element(By.ID, "id_uid_field").click()
self.driver.find_element(By.ID, "id_uid_field").send_keys(USER().username)
self.driver.find_element(By.ID, "id_uid_field").send_keys(Keys.ENTER)
self.driver.find_element(By.ID, "id_password").send_keys(USER().username)
self.driver.find_element(By.ID, "id_password").send_keys(Keys.ENTER)
self.wait_for_url(self.url("passbook_flows:denied"))
self.assertEqual(
self.driver.find_element(By.CSS_SELECTOR, "#flow-body > header > h1").text,
"Permission denied",
)

View File

@ -43,7 +43,7 @@ class SeleniumTestCase(StaticLiveServerTestCase):
def setUp(self):
super().setUp()
makedirs("out", exist_ok=True)
makedirs("selenium_screenshots/", exist_ok=True)
self.driver = self._get_driver()
self.driver.maximize_window()
self.driver.implicitly_wait(300)
@ -58,7 +58,9 @@ class SeleniumTestCase(StaticLiveServerTestCase):
)
def tearDown(self):
self.driver.save_screenshot(f"out/{self.__class__.__name__}_{time()}.png")
self.driver.save_screenshot(
f"selenium_screenshots/{self.__class__.__name__}_{time()}.png"
)
for line in self.driver.get_log("browser"):
self.logger.warning(
line["message"], source=line["source"], level=line["level"]

View File

@ -1,6 +1,6 @@
apiVersion: v1
appVersion: "0.9.0-pre4"
appVersion: "0.9.0-pre7"
description: A Helm chart for passbook.
name: passbook
version: "0.9.0-pre4"
version: "0.9.0-pre7"
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png

View File

@ -21,3 +21,8 @@ data:
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 }}

View File

@ -2,7 +2,7 @@
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
image:
tag: 0.9.0-pre4
tag: 0.9.0-pre7
nameOverride: ""
@ -14,6 +14,12 @@ config:
# 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.

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = "0.9.0-pre4"
__version__ = "0.9.0-pre7"

View File

@ -122,15 +122,21 @@
{% trans 'Certificates' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:tokens' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:tokens' 'passbook_admin:token-delete' %}">
{% trans 'Tokens' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:users' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
class="pf-c-nav__link {% is_active 'passbook_admin:users' 'passbook_admin:user-create' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
{% trans 'Users' %}
</a>
</li>
<li class="pf-c-nav__item">
<a href="{% url 'passbook_admin:groups' %}"
class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
class="pf-c-nav__link {% is_active 'passbook_admin:groups' 'passbook_admin:group-create' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
{% trans 'Groups' %}
</a>
</li>

View File

@ -10,29 +10,33 @@
</section>
<section class="pf-c-page__main-section">
<div class="pf-l-gallery pf-m-gutter">
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:applications' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-applications"></i> {% trans 'Applications' %}
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ application_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ application_count }}
</p>
</div>
</a>
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:sources' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-middleware"></i> {% trans 'Sources' %}
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ source_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ source_count }}
</p>
</div>
</a>
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:providers' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Providers' %}
@ -40,15 +44,19 @@
</div>
<div class="pf-c-card__body">
{% if providers_without_application.exists %}
<i class="pf-icon pf-icon-warning-triangle"></i> {{ provider_count }}
<p class="aggregate-status">
<i class="fa fa-exclamation-triangle"></i> {{ provider_count }}
</p>
<p>{% trans 'Warning: At least one Provider has no application assigned.' %}</p>
{% else %}
<i class="pf-icon pf-icon-ok"></i> {{ provider_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ provider_count }}
</p>
{% endif %}
</div>
</a>
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-plugged"></i> {% trans 'Stages' %}
@ -56,26 +64,32 @@
</div>
<div class="pf-c-card__body">
{% if stage_count < 1 %}
<i class="pficon-error-circle-o"></i> {{ stage_count }}
<p class="aggregate-status">
<i class="pficon-error-circle-o"></i> {{ stage_count }}
</p>
<p>{% trans 'No Stages configured. No Users will be able to login.' %}"></p>
{% else %}
<i class="pf-icon pf-icon-ok"></i> {{ stage_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ stage_count }}
</p>
{% endif %}
</div>
</a>
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:stages' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-topology"></i> {% trans 'Flows' %}
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ flow_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ flow_count }}
</p>
</div>
</a>
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:policies' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-infrastructure"></i> {% trans 'Policies' %}
@ -83,58 +97,71 @@
</div>
<div class="pf-c-card__body">
{% if policies_without_binding %}
<i class="pf-icon pf-icon-warning-triangle"></i> {{ policy_count }}
<p class="aggregate-status">
<i class="fa fa-exclamation-triangle"></i> {{ policy_count }}
</p>
<p>{% trans 'Policies without binding exist.' %}</p>
{% else %}
<i class="pf-icon pf-icon-ok"></i> {{ policy_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ policy_count }}
</p>
{% endif %}
</div>
</a>
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:stage-invitations' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-migration"></i> {% trans 'Invitation' %}
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ invitation_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ invitation_count }}
</p>
</div>
</a>
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-m-hoverable pf-m-compact">
<a href="{% url 'passbook_admin:users' %}" class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-user"></i> {% trans 'Users' %}
</div>
</div>
<div class="pf-c-card__body">
<i class="pf-icon pf-icon-ok"></i> {{ user_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ user_count }}
</p>
</div>
</a>
<div class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-bundle"></i> {% trans 'Version' %}
</div>
</div>
<div class="pf-c-card__body">
<p class="aggregate-status">
{% if version >= version_latest %}
<i class="fa fa-check-circle"></i> {{ version }}
{% else %}
<i class="fa fa-exclamation-triangle"></i> {{ version }}
{% endif %}
</p>
{% if version >= version_latest %}
<i class="pf-icon pf-icon-ok"></i>
{% blocktrans with version=version %}
{{ version }} (Up-to-date!)
{% blocktrans %}
Up-to-date!
{% endblocktrans %}
{% else %}
<i class="pf-icon pf-icon-warning-triangle"></i>
{% blocktrans with version=version latest=version_latest %}
{{ version }} ({{ latest }} is available!)
{% blocktrans with latest=version_latest %}
{{ latest }} is available!
{% endblocktrans %}
{% endif %}
</div>
</div>
<div class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Workers' %}
@ -142,15 +169,19 @@
</div>
<div class="pf-c-card__body">
{% if worker_count < 1 %}
<i class="pf-icon pf-icon-warning-triangle"></i> {{ worker_count }}
<p class="aggregate-status">
<i class="fa fa-exclamation-triangle"></i> {{ worker_count }}
</p>
<p>{% trans 'No workers connected.' %}</p>
{% else %}
<i class="pf-icon pf-icon-ok"></i> {{ worker_count }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ worker_count }}
</p>
{% endif %}
</div>
</div>
<a class="pf-c-card pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
<a class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact" data-target="modal" data-modal="clearCacheModalRoot">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Policies' %}
@ -158,13 +189,37 @@
</div>
<div class="pf-c-card__body">
{% if cached_policies < 1 %}
<i class="pf-icon pf-icon-warning-triangle"></i> {{ cached_policies }}
<p class="aggregate-status">
<i class="fa fa-exclamation-triangle"></i> {{ cached_policies }}
</p>
<p>{% trans 'No policies cached. Users may experience slow response times.' %}</p>
{% else %}
<i class="pf-icon pf-icon-ok"></i> {{ cached_policies }}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ cached_policies }}
</p>
{% endif %}
</div>
</a>
<div class="pf-c-card pf-c-card-aggregate pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="pf-icon pf-icon-server"></i> {% trans 'Cached Flows' %}
</div>
</div>
<div class="pf-c-card__body">
{% if cached_flows < 1 %}
<p class="aggregate-status">
<span class="fa fa-exclamation-triangle"></span> {{ cached_flows }}
</p>
<p>{% trans 'No flows cached.' %}</p>
{% else %}
<p class="aggregate-status">
<i class="fa fa-check-circle"></i> {{ cached_flows }}
</p>
{% endif %}
</div>
</div>
</section>
</div>
<div class="pf-c-backdrop" id="clearCacheModalRoot" hidden>
@ -173,7 +228,9 @@
<button data-modal-close class="pf-c-button pf-m-plain" type="button" aria-label="Close dialog">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1>
<div class="pf-c-modal-box__header">
<h1 class="pf-c-title pf-m-2xl" id="modal-title">{% trans 'Clear Cache' %}?</h1>
</div>
<div class="pf-c-modal-box__body" id="modal-description">
<form method="post" id="clearForm">
{% csrf_token %}

View File

@ -28,29 +28,50 @@
<table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid">
<thead>
<tr role="row">
<th role="columnheader" scope="col">{% trans 'Enabled' %}</th>
<th role="columnheader" scope="col">{% trans 'Policy' %}</th>
<th role="columnheader" scope="col">{% trans 'Target' %}</th>
<th role="columnheader" scope="col">{% trans 'Enabled' %}</th>
<th role="columnheader" scope="col">{% trans 'Order' %}</th>
<th role="columnheader" scope="col">{% trans 'Timeout' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup" class="pf-m-expanded">
{% for binding in object_list %}
<tr role="row pf-c-table__expandable-row pf-m-expanded">
<th role="cell">
<div>{{ binding.enabled }}</div>
</th>
<th role="cell">
<div>{{ binding.policy }}</div>
</th>
<th role="cell">
<div>{{ binding.target|verbose_name }}</div>
</th>
<td>
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:policy-binding-update' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:policy-binding-delete' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
<tbody role="rowgroup">
{% for pbm in object_list %}
<tr role="role">
<td>
{{ pbm }}
<small>
{{ pbm|fieldtype }}
</small>
</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
{% for binding in pbm.bindings %}
<tr class="row pf-c-table__expandable-row pf-m-expanded">
<th role="cell">
<div>{{ binding.policy }}</div>
<small>
{{ binding.policy|fieldtype }}
</small>
</th>
<th role="cell">
<div>{{ binding.enabled }}</div>
</th>
<th role="cell">
<div>{{ binding.order }}</div>
</th>
<th role="cell">
<div>{{ binding.timeout }}</div>
</th>
<td class="pb-table-action" role="cell">
<a class="pf-c-button pf-m-secondary" href="{% url 'passbook_admin:policy-binding-update' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:policy-binding-delete' pk=binding.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>

View File

@ -35,7 +35,7 @@
</tr>
</thead>
<tbody role="rowgroup">
{% regroup object_list by flow as grouped_bindings %}
{% regroup object_list by target as grouped_bindings %}
{% for flow in grouped_bindings %}
<tr role="role">
<td>
@ -56,9 +56,9 @@
</td>
<th role="columnheader">
<div>
<div>{{ binding.flow.slug }}</div>
<div>{{ binding.target.slug }}</div>
<small>
{{ binding.flow.name }}
{{ binding.target.name }}
</small>
</div>
</th>

View File

@ -0,0 +1,82 @@
{% 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="fas fa-key"></i>
{% trans 'Tokens' %}
</h1>
<p>{% trans "Tokens are used throughout passbook for Email validation stages, Recovery keys and API access." %}</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">
{% 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 'Token' %}</th>
<th role="columnheader" scope="col">{% trans 'User' %}</th>
<th role="columnheader" scope="col">{% trans 'Expires?' %}</th>
<th role="columnheader" scope="col">{% trans 'Expiry Date' %}</th>
<th role="cell"></th>
</tr>
</thead>
<tbody role="rowgroup">
{% for token in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<div>{{ token.pk }}</div>
</div>
</th>
<td role="cell">
<span>
{{ token.user }}
</span>
</td>
<td role="cell">
<span>
{{ token.expiring|yesno:"Yes,No" }}
</span>
</td>
<td role="cell">
<span>
{{ token.expires }}
</span>
</td>
<td>
<a class="pf-c-button pf-m-danger" href="{% url 'passbook_admin:token-delete' pk=token.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</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 Tokens.' %}
</h1>
<div class="pf-c-empty-state__body">
{% trans 'Currently no tokens exist.' %}
</div>
</div>
</div>
{% endif %}
</div>
</section>
{% endblock %}

View File

@ -12,7 +12,7 @@
{% endblock %}
{% block action %}
{% blocktrans with type=form|form_verbose_name|title %}
{% blocktrans with type=form|form_verbose_name %}
Update {{ type }}
{% endblocktrans %}
{% endblock %}

View File

@ -1,12 +1,15 @@
"""admin tests"""
from importlib import import_module
from typing import Callable
from django.forms import ModelForm
from django.shortcuts import reverse
from django.test import Client, TestCase
from django.urls.exceptions import NoReverseMatch
from passbook.admin.urls import urlpatterns
from passbook.core.models import User
from passbook.lib.utils.reflection import get_apps
class TestAdmin(TestCase):
@ -34,4 +37,28 @@ def generic_view_tester(view_name: str) -> Callable:
for url in urlpatterns:
method_name = url.name.replace("-", "_")
setattr(TestAdmin, f"test_{method_name}", generic_view_tester(url.name))
setattr(TestAdmin, f"test_view_{method_name}", generic_view_tester(url.name))
def generic_form_tester(form: ModelForm) -> Callable:
"""Test a form"""
def tester(self: TestAdmin):
form_inst = form()
self.assertFalse(form_inst.is_valid())
return tester
# Load the forms module from every app, so we have all forms loaded
for app in get_apps():
module = app.__module__.replace(".apps", ".forms")
try:
import_module(module)
except ImportError:
pass
for form_class in ModelForm.__subclasses__():
setattr(
TestAdmin, f"test_form_{form_class.__name__}", generic_form_tester(form_class)
)

View File

@ -17,6 +17,7 @@ from passbook.admin.views import (
stages_bindings,
stages_invitations,
stages_prompts,
tokens,
users,
)
@ -41,6 +42,13 @@ urlpatterns = [
applications.ApplicationDeleteView.as_view(),
name="application-delete",
),
# Tokens
path("tokens/", tokens.TokenListView.as_view(), name="tokens"),
path(
"tokens/<uuid:pk>/delete/",
tokens.TokenDeleteView.as_view(),
name="token-delete",
),
# Sources
path("sources/", sources.SourceListView.as_view(), name="sources"),
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),

View File

@ -1,5 +1,4 @@
"""passbook Application administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -7,9 +6,10 @@ 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.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.core.forms.applications import ApplicationForm
from passbook.core.models import Application
from passbook.lib.views import CreateAssignPermView
@ -24,9 +24,6 @@ class ApplicationListView(LoginRequiredMixin, PermissionListMixin, ListView):
paginate_by = 40
template_name = "administration/application/list.html"
def get_queryset(self):
return super().get_queryset().select_subclasses()
class ApplicationCreateView(
SuccessMessageMixin,
@ -44,10 +41,6 @@ class ApplicationCreateView(
success_url = reverse_lazy("passbook_admin:applications")
success_message = _("Successfully created Application")
def get_context_data(self, **kwargs):
kwargs["type"] = "Application"
return super().get_context_data(**kwargs)
class ApplicationUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
@ -64,7 +57,7 @@ class ApplicationUpdateView(
class ApplicationDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete application"""
@ -74,7 +67,3 @@ class ApplicationDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:applications")
success_message = _("Successfully deleted Application")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,5 +1,4 @@
"""passbook CertificateKeyPair administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -7,9 +6,10 @@ 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.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.crypto.forms import CertificateKeyPairForm
from passbook.crypto.models import CertificateKeyPair
from passbook.lib.views import CreateAssignPermView
@ -41,10 +41,6 @@ class CertificateKeyPairCreateView(
success_url = reverse_lazy("passbook_admin:certificate_key_pair")
success_message = _("Successfully created CertificateKeyPair")
def get_context_data(self, **kwargs):
kwargs["type"] = "Certificate-Key Pair"
return super().get_context_data(**kwargs)
class CertificateKeyPairUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
@ -61,7 +57,7 @@ class CertificateKeyPairUpdateView(
class CertificateKeyPairDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete certificatekeypair"""
@ -71,7 +67,3 @@ class CertificateKeyPairDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:certificate_key_pair")
success_message = _("Successfully deleted Certificate-Key Pair")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,5 +1,4 @@
"""passbook Flow administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -8,9 +7,10 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpRequest, HttpResponse
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, DetailView, ListView, UpdateView
from django.views.generic import DetailView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.flows.forms import FlowForm
from passbook.flows.models import Flow
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
@ -45,9 +45,30 @@ class FlowCreateView(
success_url = reverse_lazy("passbook_admin:flows")
success_message = _("Successfully created Flow")
def get_context_data(self, **kwargs):
kwargs["type"] = "Flow"
return super().get_context_data(**kwargs)
class FlowUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update flow"""
model = Flow
form_class = FlowForm
permission_required = "passbook_flows.change_flow"
template_name = "generic/update.html"
success_url = reverse_lazy("passbook_admin:flows")
success_message = _("Successfully updated Flow")
class FlowDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete flow"""
model = Flow
permission_required = "passbook_flows.delete_flow"
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:flows")
success_message = _("Successfully deleted Flow")
class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
@ -67,34 +88,3 @@ class FlowDebugExecuteView(LoginRequiredMixin, PermissionRequiredMixin, DetailVi
return redirect_with_qs(
"passbook_flows:flow-executor-shell", self.request.GET, flow_slug=flow.slug,
)
class FlowUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
):
"""Update flow"""
model = Flow
form_class = FlowForm
permission_required = "passbook_flows.change_flow"
template_name = "generic/update.html"
success_url = reverse_lazy("passbook_admin:flows")
success_message = _("Successfully updated Flow")
class FlowDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
"""Delete flow"""
model = Flow
permission_required = "passbook_flows.delete_flow"
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:flows")
success_message = _("Successfully deleted Flow")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,5 +1,4 @@
"""passbook Group administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -7,9 +6,10 @@ 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.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.core.forms.groups import GroupForm
from passbook.core.models import Group
from passbook.lib.views import CreateAssignPermView
@ -41,10 +41,6 @@ class GroupCreateView(
success_url = reverse_lazy("passbook_admin:groups")
success_message = _("Successfully created Group")
def get_context_data(self, **kwargs):
kwargs["type"] = "Group"
return super().get_context_data(**kwargs)
class GroupUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
@ -60,15 +56,12 @@ class GroupUpdateView(
success_message = _("Successfully updated Group")
class GroupDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView):
class GroupDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete group"""
model = Group
permission_required = "passbook_flows.delete_group"
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:groups")
success_message = _("Successfully deleted Group")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,6 +1,4 @@
"""passbook administration overview"""
from functools import lru_cache
from django.core.cache import cache
from django.shortcuts import redirect, reverse
from django.views.generic import TemplateView
@ -15,18 +13,21 @@ from passbook.policies.models import Policy
from passbook.root.celery import CELERY_APP
from passbook.stages.invitation.models import Invitation
VERSION_CACHE_KEY = "passbook_latest_version"
@lru_cache
def latest_version() -> Version:
"""Get latest release from GitHub, cached"""
try:
data = get(
"https://api.github.com/repos/beryju/passbook/releases/latest"
).json()
tag_name = data.get("tag_name")
return parse(tag_name.split("/")[1])
except RequestException:
return parse("0.0.0")
if not cache.get(VERSION_CACHE_KEY):
try:
data = get(
"https://api.github.com/repos/beryju/passbook/releases/latest"
).json()
tag_name = data.get("tag_name")
cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], 30)
except (RequestException, IndexError):
cache.set(VERSION_CACHE_KEY, "0.0.0", 30)
return parse(cache.get(VERSION_CACHE_KEY))
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
@ -60,4 +61,5 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
Policy.objects.filter(bindings__isnull=True)
)
kwargs["cached_policies"] = len(cache.keys("policy_*"))
kwargs["cached_flows"] = len(cache.keys("flow_*"))
return super().get_context_data(**kwargs)

View File

@ -8,22 +8,25 @@ from django.contrib.auth.mixins import (
)
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import QuerySet
from django.forms import Form
from django.http import Http404, HttpRequest, HttpResponse
from django.http import HttpResponse
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, FormView, ListView, UpdateView
from django.views.generic import FormView
from django.views.generic.detail import DetailView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.forms.policies import PolicyTestForm
from passbook.lib.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView
from passbook.admin.views.utils import (
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
)
from passbook.policies.models import Policy, PolicyBinding
from passbook.policies.process import PolicyProcess, PolicyRequest
class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
class PolicyListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
"""Show list of all policies"""
model = Policy
@ -32,19 +35,12 @@ class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
ordering = "name"
template_name = "administration/policy/list.html"
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs["types"] = {x.__name__: x for x in all_subclasses(Policy)}
return super().get_context_data(**kwargs)
def get_queryset(self) -> QuerySet:
return super().get_queryset().select_subclasses()
class PolicyCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
InheritanceCreateView,
):
"""Create new Policy"""
@ -55,24 +51,12 @@ class PolicyCreateView(
success_url = reverse_lazy("passbook_admin:policies")
success_message = _("Successfully created Policy")
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self) -> Form:
policy_type = self.request.GET.get("type")
try:
model = next(x for x in all_subclasses(Policy) if x.__name__ == policy_type)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
class PolicyUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update policy"""
@ -83,27 +67,8 @@ class PolicyUpdateView(
success_url = reverse_lazy("passbook_admin:policies")
success_message = _("Successfully updated Policy")
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self) -> Form:
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None) -> Policy:
return (
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
class PolicyDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
class PolicyDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete policy"""
model = Policy
@ -113,15 +78,6 @@ class PolicyDeleteView(
success_url = reverse_lazy("passbook_admin:policies")
success_message = _("Successfully deleted Policy")
def get_object(self, queryset=None) -> Policy:
return (
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
def delete(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, FormView):
"""View to test policy(s)"""

View File

@ -1,18 +1,19 @@
"""passbook PolicyBinding 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.db.models import QuerySet
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
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.policies.forms import PolicyBindingForm
from passbook.policies.models import PolicyBinding
from passbook.policies.models import PolicyBinding, PolicyBindingModel
class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
@ -22,7 +23,20 @@ class PolicyBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
permission_required = "passbook_policies.view_policybinding"
paginate_by = 10
ordering = ["order", "target"]
template_name = "administration/policybinding/list.html"
template_name = "administration/policy_binding/list.html"
def get_queryset(self) -> QuerySet:
# Since `select_subclasses` does not work with a foreign key, we have to do two queries here
# First, get all pbm objects that have bindings attached
objects = (
PolicyBindingModel.objects.filter(policies__isnull=False)
.select_subclasses()
.select_related()
.order_by("pk")
)
for pbm in objects:
pbm.bindings = PolicyBinding.objects.filter(target__pk=pbm.pbm_uuid)
return objects
class PolicyBindingCreateView(
@ -55,16 +69,9 @@ class PolicyBindingUpdateView(
success_url = reverse_lazy("passbook_admin:policies-bindings")
success_message = _("Successfully updated PolicyBinding")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
class PolicyBindingDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete policybinding"""
@ -74,7 +81,3 @@ class PolicyBindingDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:policies-bindings")
success_message = _("Successfully deleted PolicyBinding")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,22 +1,25 @@
"""passbook PropertyMapping 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 Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
)
from passbook.core.models import PropertyMapping
from passbook.lib.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView
class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView):
class PropertyMappingListView(
LoginRequiredMixin, PermissionListMixin, InheritanceListView
):
"""Show list of all property_mappings"""
model = PropertyMapping
@ -25,19 +28,12 @@ class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView)
ordering = "name"
paginate_by = 40
def get_context_data(self, **kwargs):
kwargs["types"] = {x.__name__: x for x in all_subclasses(PropertyMapping)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class PropertyMappingCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
InheritanceCreateView,
):
"""Create new PropertyMapping"""
@ -48,38 +44,12 @@ class PropertyMappingCreateView(
success_url = reverse_lazy("passbook_admin:property-mappings")
success_message = _("Successfully created Property Mapping")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
property_mapping_type = self.request.GET.get("type")
try:
model = next(
x
for x in all_subclasses(PropertyMapping)
if x.__name__ == property_mapping_type
)
except StopIteration as exc:
raise Http404 from exc
kwargs["type"] = model._meta.verbose_name
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self):
property_mapping_type = self.request.GET.get("type")
try:
model = next(
x
for x in all_subclasses(PropertyMapping)
if x.__name__ == property_mapping_type
)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
class PropertyMappingUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update property_mapping"""
@ -90,28 +60,9 @@ class PropertyMappingUpdateView(
success_url = reverse_lazy("passbook_admin:property-mappings")
success_message = _("Successfully updated Property Mapping")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return (
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
class PropertyMappingDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete property_mapping"""
@ -121,14 +72,3 @@ class PropertyMappingDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:property-mappings")
success_message = _("Successfully deleted Property Mapping")
def get_object(self, queryset=None):
return (
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,22 +1,23 @@
"""passbook Provider 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 Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
)
from passbook.core.models import Provider
from passbook.lib.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView
class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
class ProviderListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
"""Show list of all providers"""
model = Provider
@ -25,19 +26,12 @@ class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
paginate_by = 10
ordering = "id"
def get_context_data(self, **kwargs):
kwargs["types"] = {x.__name__: x for x in all_subclasses(Provider)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class ProviderCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
InheritanceCreateView,
):
"""Create new Provider"""
@ -48,19 +42,12 @@ class ProviderCreateView(
success_url = reverse_lazy("passbook_admin:providers")
success_message = _("Successfully created Provider")
def get_form_class(self):
provider_type = self.request.GET.get("type")
try:
model = next(
x for x in all_subclasses(Provider) if x.__name__ == provider_type
)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
class ProviderUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update provider"""
@ -71,21 +58,9 @@ class ProviderUpdateView(
success_url = reverse_lazy("passbook_admin:providers")
success_message = _("Successfully updated Provider")
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return (
Provider.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
class ProviderDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete provider"""
@ -95,14 +70,3 @@ class ProviderDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:providers")
success_message = _("Successfully deleted Provider")
def get_object(self, queryset=None):
return (
Provider.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,22 +1,23 @@
"""passbook Source 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 Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
)
from passbook.core.models import Source
from passbook.lib.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView
class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
class SourceListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
"""Show list of all sources"""
model = Source
@ -25,19 +26,12 @@ class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
paginate_by = 40
template_name = "administration/source/list.html"
def get_context_data(self, **kwargs):
kwargs["types"] = {x.__name__: x for x in all_subclasses(Source)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class SourceCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
InheritanceCreateView,
):
"""Create new Source"""
@ -48,17 +42,12 @@ class SourceCreateView(
success_url = reverse_lazy("passbook_admin:sources")
success_message = _("Successfully created Source")
def get_form_class(self):
source_type = self.request.GET.get("type")
try:
model = next(x for x in all_subclasses(Source) if x.__name__ == source_type)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
class SourceUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update source"""
@ -69,20 +58,8 @@ class SourceUpdateView(
success_url = reverse_lazy("passbook_admin:sources")
success_message = _("Successfully updated Source")
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return (
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
class SourceDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
class SourceDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete source"""
model = Source
@ -91,12 +68,3 @@ class SourceDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:sources")
success_message = _("Successfully deleted Source")
def get_object(self, queryset=None):
return (
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,22 +1,23 @@
"""passbook Stage 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 Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import (
DeleteMessageView,
InheritanceCreateView,
InheritanceListView,
InheritanceUpdateView,
)
from passbook.flows.models import Stage
from passbook.lib.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView
class StageListView(LoginRequiredMixin, PermissionListMixin, ListView):
class StageListView(LoginRequiredMixin, PermissionListMixin, InheritanceListView):
"""Show list of all stages"""
model = Stage
@ -25,19 +26,12 @@ class StageListView(LoginRequiredMixin, PermissionListMixin, ListView):
ordering = "name"
paginate_by = 40
def get_context_data(self, **kwargs):
kwargs["types"] = {x.__name__: x for x in all_subclasses(Stage)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class StageCreateView(
SuccessMessageMixin,
LoginRequiredMixin,
DjangoPermissionRequiredMixin,
CreateAssignPermView,
InheritanceCreateView,
):
"""Create new Stage"""
@ -48,24 +42,12 @@ class StageCreateView(
success_url = reverse_lazy("passbook_admin:stages")
success_message = _("Successfully created Stage")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
stage_type = self.request.GET.get("type")
model = next(x for x in all_subclasses(Stage) if x.__name__ == stage_type)
kwargs["type"] = model._meta.verbose_name
return kwargs
def get_form_class(self):
stage_type = self.request.GET.get("type")
try:
model = next(x for x in all_subclasses(Stage) if x.__name__ == stage_type)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
class StageUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
SuccessMessageMixin,
LoginRequiredMixin,
PermissionRequiredMixin,
InheritanceUpdateView,
):
"""Update stage"""
@ -75,20 +57,8 @@ class StageUpdateView(
success_url = reverse_lazy("passbook_admin:stages")
success_message = _("Successfully updated Stage")
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return (
Stage.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
class StageDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
class StageDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete stage"""
model = Stage
@ -96,12 +66,3 @@ class StageDeleteView(
permission_required = "passbook_flows.delete_stage"
success_url = reverse_lazy("passbook_admin:stages")
success_message = _("Successfully deleted Stage")
def get_object(self, queryset=None):
return (
Stage.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
)
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,5 +1,4 @@
"""passbook StageBinding administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -7,9 +6,10 @@ 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.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.flows.forms import FlowStageBindingForm
from passbook.flows.models import FlowStageBinding
from passbook.lib.views import CreateAssignPermView
@ -21,7 +21,7 @@ class StageBindingListView(LoginRequiredMixin, PermissionListMixin, ListView):
model = FlowStageBinding
permission_required = "passbook_flows.view_flowstagebinding"
paginate_by = 10
ordering = ["flow", "order"]
ordering = ["target", "order"]
template_name = "administration/stage_binding/list.html"
@ -41,13 +41,6 @@ class StageBindingCreateView(
success_url = reverse_lazy("passbook_admin:stage-bindings")
success_message = _("Successfully created StageBinding")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
class StageBindingUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
@ -62,16 +55,9 @@ class StageBindingUpdateView(
success_url = reverse_lazy("passbook_admin:stage-bindings")
success_message = _("Successfully updated StageBinding")
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
class StageBindingDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete FlowStageBinding"""
@ -81,7 +67,3 @@ class StageBindingDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:stage-bindings")
success_message = _("Successfully deleted FlowStageBinding")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,5 +1,4 @@
"""passbook Invitation administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -8,13 +7,14 @@ from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView
from django.views.generic import ListView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.core.signals import invitation_created
from passbook.admin.views.utils import DeleteMessageView
from passbook.lib.views import CreateAssignPermView
from passbook.stages.invitation.forms import InvitationForm
from passbook.stages.invitation.models import Invitation
from passbook.stages.invitation.signals import invitation_created
class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
@ -43,10 +43,6 @@ class InvitationCreateView(
success_url = reverse_lazy("passbook_admin:stage-invitations")
success_message = _("Successfully created Invitation")
def get_context_data(self, **kwargs):
kwargs["type"] = "Invitation"
return super().get_context_data(**kwargs)
def form_valid(self, form):
obj = form.save(commit=False)
obj.created_by = self.request.user
@ -56,7 +52,7 @@ class InvitationCreateView(
class InvitationDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView
):
"""Delete invitation"""
@ -66,7 +62,3 @@ class InvitationDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:stage-invitations")
success_message = _("Successfully deleted Invitation")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -1,5 +1,4 @@
"""passbook Prompt administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
@ -7,9 +6,10 @@ 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.views.generic import DeleteView, ListView, UpdateView
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.stages.prompt.forms import PromptAdminForm
from passbook.stages.prompt.models import Prompt
@ -41,10 +41,6 @@ class PromptCreateView(
success_url = reverse_lazy("passbook_admin:stage-prompts")
success_message = _("Successfully created Prompt")
def get_context_data(self, **kwargs):
kwargs["type"] = "Prompt"
return super().get_context_data(**kwargs)
class PromptUpdateView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
@ -60,9 +56,7 @@ class PromptUpdateView(
success_message = _("Successfully updated Prompt")
class PromptDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
class PromptDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete prompt"""
model = Prompt
@ -71,7 +65,3 @@ class PromptDeleteView(
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:stage-prompts")
success_message = _("Successfully deleted Prompt")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -0,0 +1,30 @@
"""passbook Token administration"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import ListView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.views.utils import DeleteMessageView
from passbook.core.models import Token
class TokenListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all tokens"""
model = Token
permission_required = "passbook_core.view_token"
ordering = "expires"
paginate_by = 40
template_name = "administration/token/list.html"
class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete token"""
model = Token
permission_required = "passbook_core.delete_token"
template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:tokens")
success_message = _("Successfully deleted Token")

View File

@ -5,10 +5,12 @@ from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.http import urlencode
from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, DetailView, ListView, UpdateView
from django.views.generic import DetailView, ListView, UpdateView
from guardian.mixins import (
PermissionListMixin,
PermissionRequiredMixin,
@ -16,6 +18,7 @@ from guardian.mixins import (
)
from passbook.admin.forms.users import UserForm
from passbook.admin.views.utils import DeleteMessageView
from passbook.core.models import Token, User
from passbook.lib.views import CreateAssignPermView
@ -66,9 +69,7 @@ class UserUpdateView(
success_message = _("Successfully updated User")
class UserDeleteView(
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
):
class UserDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
"""Delete user"""
model = User
@ -80,10 +81,6 @@ class UserDeleteView(
success_url = reverse_lazy("passbook_admin:users")
success_message = _("Successfully deleted User")
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""Get Password reset link for user"""
@ -91,13 +88,13 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
model = User
permission_required = "passbook_core.reset_user_password"
def get(self, request, *args, **kwargs):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Create token for user and return link"""
super().get(request, *args, **kwargs)
# TODO: create plan for user, get token
token = Token.objects.create(user=self.object)
querystring = urlencode({"token": token.token_uuid})
link = request.build_absolute_uri(
reverse("passbook_flows:default-recovery", kwargs={"token": token.uuid})
reverse("passbook_flows:default-recovery") + f"?{querystring}"
)
messages.success(
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})

View File

@ -0,0 +1,73 @@
"""passbook admin util views"""
from typing import Any, Dict
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.views.generic import DeleteView, ListView, UpdateView
from passbook.lib.utils.reflection import all_subclasses, path_to_class
from passbook.lib.views import CreateAssignPermView
class DeleteMessageView(SuccessMessageMixin, DeleteView):
"""DeleteView which shows `self.success_message` on successful deletion"""
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)
class InheritanceListView(ListView):
"""ListView for objects using InheritanceManager"""
def get_context_data(self, **kwargs):
kwargs["types"] = {x.__name__: x for x in all_subclasses(self.model)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class InheritanceCreateView(CreateAssignPermView):
"""CreateView for objects using InheritanceManager"""
def get_form_class(self):
provider_type = self.request.GET.get("type")
try:
model = next(
x for x in all_subclasses(self.model) if x.__name__ == provider_type
)
except StopIteration as exc:
raise Http404 from exc
return path_to_class(model.form)
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
class InheritanceUpdateView(UpdateView):
"""UpdateView for objects using InheritanceManager"""
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
form_cls = self.get_form_class()
if hasattr(form_cls, "template_name"):
kwargs["base_template"] = form_cls.template_name
return kwargs
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return (
self.model.objects.filter(pk=self.kwargs.get("pk"))
.select_subclasses()
.first()
)

43
passbook/api/auth.py Normal file
View File

@ -0,0 +1,43 @@
"""API Authentication"""
from base64 import b64decode
from typing import Any, Tuple, Union
from django.utils.translation import gettext as _
from rest_framework import HTTP_HEADER_ENCODING, exceptions
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.request import Request
from passbook.core.models import Token, TokenIntents, User
class PassbookTokenAuthentication(BaseAuthentication):
"""Token-based authentication using HTTP Basic authentication"""
def authenticate(self, request: Request) -> Union[Tuple[User, Any], None]:
"""Token-based authentication using HTTP Basic authentication"""
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b"basic":
return None
if len(auth) == 1:
msg = _("Invalid basic header. No credentials provided.")
raise exceptions.AuthenticationFailed(msg)
if len(auth) > 2:
msg = _(
"Invalid basic header. Credentials string should not contain spaces."
)
raise exceptions.AuthenticationFailed(msg)
header_data = b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(":")
tokens = Token.filter_not_expired(
token_uuid=header_data[2], intent=TokenIntents.INTENT_API
)
if not tokens.exists():
raise exceptions.AuthenticationFailed(_("Invalid token."))
return (tokens.first().user, None)
def authenticate_header(self, request: Request) -> str:
return 'Basic realm="passbook"'

View File

@ -6,5 +6,5 @@ from passbook.api.v2.urls import urlpatterns as v2_urls
urlpatterns = [
path("v1/", include(v1_urls)),
path("v2/", include(v2_urls)),
path("v2beta/", include(v2_urls)),
]

View File

@ -4,7 +4,6 @@ from django.urls import path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import routers
from structlog import get_logger
from passbook.api.permissions import CustomObjectPermissions
from passbook.audit.api import EventViewSet
@ -16,11 +15,11 @@ from passbook.core.api.providers import ProviderViewSet
from passbook.core.api.sources import SourceViewSet
from passbook.core.api.users import UserViewSet
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
from passbook.lib.utils.reflection import get_apps
from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet
from passbook.policies.dummy.api import DummyPolicyViewSet
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
from passbook.policies.expression.api import ExpressionPolicyViewSet
from passbook.policies.group_membership.api import GroupMembershipPolicyViewSet
from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
from passbook.policies.password.api import PasswordPolicyViewSet
from passbook.policies.reputation.api import ReputationPolicyViewSet
@ -47,15 +46,8 @@ from passbook.stages.user_login.api import UserLoginStageViewSet
from passbook.stages.user_logout.api import UserLogoutStageViewSet
from passbook.stages.user_write.api import UserWriteStageViewSet
LOGGER = get_logger()
router = routers.DefaultRouter()
for _passbook_app in get_apps():
if hasattr(_passbook_app, "api_mountpoint"):
for prefix, viewset in _passbook_app.api_mountpoint:
router.register(prefix, viewset)
LOGGER.debug("Mounted API URLs", app_name=_passbook_app.name)
router.register("core/applications", ApplicationViewSet)
router.register("core/groups", GroupViewSet)
router.register("core/users", UserViewSet)
@ -71,9 +63,10 @@ router.register("sources/oauth", OAuthSourceViewSet)
router.register("policies/all", PolicyViewSet)
router.register("policies/bindings", PolicyBindingViewSet)
router.register("policies/expression", ExpressionPolicyViewSet)
router.register("policies/group_membership", GroupMembershipPolicyViewSet)
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
router.register("policies/password_expiry", PasswordExpiryPolicyViewSet)
router.register("policies/password", PasswordPolicyViewSet)
router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
router.register("policies/reputation", ReputationPolicyViewSet)
router.register("providers/all", ProviderViewSet)

View File

@ -1,5 +1,6 @@
"""passbook audit signal listener"""
from typing import Dict
from threading import Thread
from typing import Any, Dict, Optional
from django.contrib.auth.signals import (
user_logged_in,
@ -11,21 +12,54 @@ from django.http import HttpRequest
from passbook.audit.models import Event, EventAction
from passbook.core.models import User
from passbook.core.signals import invitation_created, invitation_used, user_signed_up
from passbook.stages.invitation.models import Invitation
from passbook.stages.invitation.signals import invitation_created, invitation_used
from passbook.stages.user_write.signals import user_write
class EventNewThread(Thread):
"""Create Event in background thread"""
action: EventAction
request: HttpRequest
kwargs: Dict[str, Any]
user: Optional[User] = None
def __init__(self, action: EventAction, request: HttpRequest, **kwargs):
super().__init__()
self.action = action
self.request = request
self.kwargs = kwargs
def run(self):
Event.new(self.action, **self.kwargs).from_http(self.request, user=self.user)
@receiver(user_logged_in)
# pylint: disable=unused-argument
def on_user_logged_in(sender, request: HttpRequest, user: User, **_):
"""Log successful login"""
Event.new(EventAction.LOGIN).from_http(request)
thread = EventNewThread(EventAction.LOGIN, request)
thread.user = user
thread.run()
@receiver(user_logged_out)
# pylint: disable=unused-argument
def on_user_logged_out(sender, request: HttpRequest, user: User, **_):
"""Log successfully logout"""
Event.new(EventAction.LOGOUT).from_http(request)
thread = EventNewThread(EventAction.LOGOUT, request)
thread.user = user
thread.run()
@receiver(user_write)
# pylint: disable=unused-argument
def on_user_write(sender, request: HttpRequest, user: User, data: Dict[str, Any], **_):
"""Log User write"""
thread = EventNewThread(EventAction.CUSTOM, request, **data)
thread.user = user
thread.run()
@receiver(user_login_failed)
@ -34,29 +68,25 @@ def on_user_login_failed(
sender, credentials: Dict[str, str], request: HttpRequest, **_
):
"""Failed Login"""
Event.new(EventAction.LOGIN_FAILED, **credentials).from_http(request)
@receiver(user_signed_up)
# pylint: disable=unused-argument
def on_user_signed_up(sender, request: HttpRequest, user: User, **_):
"""Log successfully signed up"""
Event.new(EventAction.SIGN_UP).from_http(request)
thread = EventNewThread(EventAction.LOGIN_FAILED, request, **credentials)
thread.run()
@receiver(invitation_created)
# pylint: disable=unused-argument
def on_invitation_created(sender, request: HttpRequest, invitation, **_):
def on_invitation_created(sender, request: HttpRequest, invitation: Invitation, **_):
"""Log Invitation creation"""
Event.new(
EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex
).from_http(request)
thread = EventNewThread(
EventAction.INVITE_CREATED, request, invitation_uuid=invitation.invite_uuid.hex
)
thread.run()
@receiver(invitation_used)
# pylint: disable=unused-argument
def on_invitation_used(sender, request: HttpRequest, invitation, **_):
def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_):
"""Log Invitation usage"""
Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http(
request
thread = EventNewThread(
EventAction.INVITE_USED, request, invitation_uuid=invitation.invite_uuid.hex
)
thread.run()

View File

@ -3,12 +3,13 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from passbook.core.models import Application, Provider
from passbook.lib.widgets import GroupedModelChoiceField
class ApplicationForm(forms.ModelForm):
"""Application Form"""
provider = forms.ModelChoiceField(
provider = GroupedModelChoiceField(
queryset=Provider.objects.all().order_by("pk").select_subclasses(),
required=False,
)

View File

@ -2,6 +2,7 @@
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from passbook.admin.fields import CodeMirrorWidget, YAMLField
from passbook.core.models import Group, User
@ -34,4 +35,8 @@ class GroupForm(forms.ModelForm):
fields = ["name", "parent", "members", "attributes"]
widgets = {
"name": forms.TextInput(),
"attributes": CodeMirrorWidget,
}
field_classes = {
"attributes": YAMLField,
}

View File

@ -0,0 +1,28 @@
# Generated by Django 3.0.7 on 2020-07-03 22:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0003_default_user"),
]
operations = [
migrations.AlterModelOptions(
name="application",
options={
"verbose_name": "Application",
"verbose_name_plural": "Applications",
},
),
migrations.AlterModelOptions(
name="user",
options={
"permissions": (("reset_user_password", "Reset Password"),),
"verbose_name": "User",
"verbose_name_plural": "Users",
},
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 3.0.7 on 2020-07-05 21:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0004_auto_20200703_2213"),
]
operations = [
migrations.AddField(
model_name="token",
name="intent",
field=models.TextField(
choices=[
("verification", "Intent Verification"),
("api", "Intent Api"),
],
default="verification",
),
),
]

View File

@ -6,6 +6,7 @@ from uuid import uuid4
from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.db.models import Q, QuerySet
from django.http import HttpRequest
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@ -71,6 +72,8 @@ class User(GuardianUserMixin, AbstractUser):
class Meta:
permissions = (("reset_user_password", "Reset Password"),)
verbose_name = _("User")
verbose_name_plural = _("Users")
class Provider(models.Model):
@ -112,8 +115,6 @@ class Application(PolicyBindingModel):
meta_description = models.TextField(default="", blank=True)
meta_publisher = models.TextField(default="", blank=True)
objects = InheritanceManager()
def get_provider(self) -> Optional[Provider]:
"""Get casted provider instance"""
if not self.provider:
@ -123,6 +124,11 @@ class Application(PolicyBindingModel):
def __str__(self):
return self.name
class Meta:
verbose_name = _("Application")
verbose_name_plural = _("Applications")
class Source(PolicyBindingModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
@ -190,15 +196,39 @@ class UserSourceConnection(CreatedUpdatedModel):
unique_together = (("user", "source"),)
class TokenIntents(models.TextChoices):
"""Intents a Token can be created for."""
# Single user token
INTENT_VERIFICATION = "verification"
# Allow access to API
INTENT_API = "api"
class Token(models.Model):
"""One-time link for password resets/sign-up-confirmations"""
"""Token used to authenticate the User for API Access or confirm another Stage like Email."""
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
intent = models.TextField(
choices=TokenIntents.choices, default=TokenIntents.INTENT_VERIFICATION
)
expires = models.DateTimeField(default=default_token_duration)
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+")
expiring = models.BooleanField(default=True)
description = models.TextField(default="", blank=True)
@staticmethod
def filter_not_expired(**kwargs) -> QuerySet:
"""Filer for tokens which are not expired yet or are not expiring,
and match filters in `kwargs`"""
query = Q(**kwargs)
query_not_expired_yet = Q(expires__lt=now(), expiring=True)
query_not_expiring = Q(expiring=False)
return Token.objects.filter(
query & (query_not_expired_yet | query_not_expiring)
)
@property
def is_expired(self) -> bool:
"""Check if token is expired yet."""
@ -206,7 +236,7 @@ class Token(models.Model):
def __str__(self):
return (
f"Token f{self.token_uuid.hex} {self.description} (expires={self.expires})"
f"Token {self.token_uuid.hex} {self.description} (expires={self.expires})"
)
class Meta:

View File

@ -1,7 +1,4 @@
"""passbook core signals"""
from django.core.signals import Signal
user_signed_up = Signal(providing_args=["request", "user"])
invitation_created = Signal(providing_args=["request", "invitation"])
invitation_used = Signal(providing_args=["request", "invitation", "user"])
password_changed = Signal(providing_args=["user", "password"])

View File

@ -20,11 +20,15 @@
</head>
<body>
{% if 'impersonate_id' in request.session %}
<div class="experimental-pf-bar">
<span id="experimentalBar" class="experimental-pf-text">
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
<a href="?__unimpersonate=True" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
</span>
<div class="pf-c-banner pf-m-warning pf-c-alert pf-m-sticky">
<div class="pf-l-flex pf-m-justify-content-center pf-m-justify-content-space-between-on-lg pf-m-nowrap" style="height: 100%;">
<div class=""></div>
<div class="pf-u-display-none pf-u-display-block-on-lg">
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
<a href="?__unimpersonate=True" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
</div>
<div class=""></div>
</div>
</div>
{% endif %}
{% block body %}

View File

@ -22,8 +22,7 @@
<div class="pf-c-login__container">
<header class="pf-c-login__header">
<img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;" alt="passbook icon" />
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;"
alt="passbook branding" />
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;" alt="passbook branding" />
</header>
<main class="pf-c-login__main" id="flow-body">
<header class="pf-c-login__main-header">
@ -50,7 +49,7 @@
<li>
<a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
</li>
<!-- todo: load config.passbook.footer.links -->
<!-- TODO:load config.passbook.footer.links -->
</ul>
</footer>
</div>

View File

@ -1,21 +1,62 @@
{% extends 'login/base.html' %}
{% extends 'base/skeleton.html' %}
{% load static %}
{% load i18n %}
{% load passbook_utils %}
{% block card %}
<form method="POST" class="pf-c-form">
{% csrf_token %}
{% include 'partials/form.html' %}
<div class="pf-c-form__group">
<p>
<i class="pf-icon pf-icon-error-circle-o"></i>
{% trans 'Access denied' %}
</p>
{% block body %}
<div class="pf-c-background-image">
<svg xmlns="http://www.w3.org/2000/svg" class="pf-c-background-image__filter" width="0" height="0">
<filter id="image_overlay">
<feColorMatrix type="matrix" values="1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 1 0"></feColorMatrix>
<feComponentTransfer color-interpolation-filters="sRGB" result="duotone">
<feFuncR type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncR>
<feFuncG type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncG>
<feFuncB type="table" tableValues="0.086274509803922 0.43921568627451"></feFuncB>
<feFuncA type="table" tableValues="0 1"></feFuncA>
</feComponentTransfer>
</filter>
</svg>
</div>
<div class="pf-c-login">
<div class="pf-c-login__container">
<header class="pf-c-login__header">
<img class="pf-c-brand" src="{% static 'passbook/logo.svg' %}" style="height: 60px;" alt="passbook icon" />
<img class="pf-c-brand" src="{% static 'passbook/brand.svg' %}" style="height: 60px;" alt="passbook branding" />
</header>
<main class="pf-c-login__main" id="flow-body">
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">
{% trans 'Permission denied' %}
</h1>
</header>
<div class="pf-c-login__main-body">
{% block card %}
<form method="POST" class="pf-c-form">
{% csrf_token %}
{% include 'partials/form.html' %}
<div class="pf-c-form__group">
<p>
<i class="pf-icon pf-icon-error-circle-o"></i>
{% trans 'Access denied' %}
</p>
</div>
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
{% endblock %}
</div>
</main>
<footer class="pf-c-login__footer">
<p></p>
<ul class="pf-c-list pf-m-inline">
<li>
<a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
</li>
<!-- TODO: load config.passbook.footer.links -->
</ul>
</footer>
</div>
{% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %}
</form>
</div>
{% endblock %}

View File

@ -24,21 +24,23 @@
{% if applications %}
<div class="pf-l-gallery pf-m-gutter">
{% for app in applications %}
<a href="{{ app.meta_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact" id="card-1">
<div class="pf-c-card__head">
<a href="{{ app.meta_launch_url }}" class="pf-c-card pf-m-hoverable pf-m-compact">
<div class="pf-c-card__header">
{% if not app.meta_icon_url %}
<i class="pf-icon pf-icon-arrow"></i>
{% else %}
<img class="app-icon pf-c-avatar" src="{{ app.meta_icon_url }}" alt="{% trans 'Application Icon' %}">
{% endif %}
</div>
<div class="pf-c-card__header pf-c-title pf-m-md">
<div class="pf-c-card__title">
<p id="card-1-check-label">{{ app.name }}</p>
<div class="pf-c-content">
<small>{{ app.meta_publisher }}</small>
</div>
</div>
<div class="pf-c-card__body">{% trans app.meta_description %}</div>
<div class="pf-c-card__body">
{% trans app.meta_description|truncatewords:35 %}
</div>
</a>
{% endfor %}
</div>
@ -50,7 +52,7 @@
<div class="pf-c-empty-state__body">
{% trans "Either no applications are defined, or you don't have access to any." %}
</div>
{% if user.is_superuser %} {# todo: use guardian permissions instead #}
{% if user.is_superuser %} {# TODO:use guardian permissions instead #}
<a href="{% url 'passbook_admin:application-create' %}" class="pf-c-button pf-m-primary" type="button">
{% trans 'Create Application' %}
</a>

View File

@ -15,16 +15,18 @@
</div>
<div class="pf-c-form__group-control">
{% for c in field %}
<div class="radio col-sm-10">
<input type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}" value="{{ c.data.value }}"
{% if c.data.selected %} checked {% endif %}>
<label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
<div class="pf-c-radio">
<input class="pf-c-radio__input"
type="radio" id="{{ field.name }}-{{ forloop.counter0 }}"
name="{% if wizard %}{{ wizard.steps.current }}-{% endif %}{{ field.name }}"
value="{{ c.data.value }}"
{% if c.data.selected %} checked {% endif %}/>
<label class="pf-c-radio__label" for="{{ field.name }}-{{ forloop.counter0 }}">{{ c.choice_label }}</label>
</div>
{% endfor %}
{% if field.help_text %}
<p class="pf-c-form__helper-text">{{ field.help_text }}</p>
{% endif %}
{% endfor %}
</div>
{% elif field.field.widget|fieldtype == 'Select' %}
<div class="pf-c-form__group-label">

View File

@ -40,7 +40,7 @@
<ul class="pf-c-nav__list">
{% for source in user_sources_loc %}
<li class="pf-c-nav__item">
<a href="{{ source.view_name }}"
<a href="{{ source.url }}"
class="pf-c-nav__link {% if source.url == request.get_full_path %} pf-m-current {% endif %}">
{{ source.name }}
</a>

View File

@ -0,0 +1,18 @@
"""passbook user view tests"""
from django.test import TestCase
from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user
from passbook.core.models import Token
from passbook.core.tasks import clean_tokens
class TestTasks(TestCase):
"""Test Tasks"""
def test_token_cleanup(self):
"""Test Token cleanup task"""
Token.objects.create(expires=now(), user=get_anonymous_user())
self.assertEqual(Token.objects.all().count(), 1)
clean_tokens()
self.assertEqual(Token.objects.all().count(), 0)

View File

@ -1,40 +0,0 @@
"""passbook access helper classes"""
from django.contrib import messages
from django.http import HttpRequest
from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Application, Provider, User
from passbook.policies.engine import PolicyEngine
from passbook.policies.types import PolicyResult
LOGGER = get_logger()
class AccessMixin:
"""Mixin class for usage in Authorization views.
Provider functions to check application access, etc"""
# request is set by view but since this Mixin has no base class
request: HttpRequest = None
def provider_to_application(self, provider: Provider) -> Application:
"""Lookup application assigned to provider, throw error if no application assigned"""
try:
return provider.application
except Application.DoesNotExist as exc:
messages.error(
self.request,
_(
'Provider "%(name)s" has no application assigned'
% {"name": provider}
),
)
raise exc
def user_has_access(self, application: Application, user: User) -> PolicyResult:
"""Check if user has access to application."""
LOGGER.debug("Checking permissions", user=user, application=application)
policy_engine = PolicyEngine(application, user, self.request)
policy_engine.build()
return policy_engine.result

View File

@ -23,7 +23,7 @@ class UserSettingsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
def get_object(self):
return self.request.user
def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
kwargs = super().get_context_data(**kwargs)
unenrollment_flow = Flow.with_policy(
self.request, designation=FlowDesignation.UNRENOLLMENT

View File

@ -56,7 +56,9 @@ class CertificateKeyPair(CreatedUpdatedModel):
@property
def fingerprint(self) -> str:
"""Get SHA256 Fingerprint of certificate_data"""
return hexlify(self.certificate.fingerprint(hashes.SHA256())).decode("utf-8")
return hexlify(self.certificate.fingerprint(hashes.SHA256()), ":").decode(
"utf-8"
)
def __str__(self) -> str:
return f"Certificate-Key Pair {self.name} {self.fingerprint}"

View File

@ -27,7 +27,7 @@ class FlowStageBindingSerializer(ModelSerializer):
class Meta:
model = FlowStageBinding
fields = ["pk", "flow", "stage", "re_evaluate_policies", "order", "policies"]
fields = ["pk", "target", "stage", "re_evaluate_policies", "order", "policies"]
class FlowStageBindingViewSet(ModelViewSet):

View File

@ -3,7 +3,8 @@
from django import forms
from django.utils.translation import gettext_lazy as _
from passbook.flows.models import Flow, FlowStageBinding
from passbook.flows.models import Flow, FlowStageBinding, Stage
from passbook.lib.widgets import GroupedModelChoiceField
class FlowForm(forms.ModelForm):
@ -35,11 +36,13 @@ class FlowForm(forms.ModelForm):
class FlowStageBindingForm(forms.ModelForm):
"""FlowStageBinding Form"""
stage = GroupedModelChoiceField(queryset=Stage.objects.all().select_subclasses(),)
class Meta:
model = FlowStageBinding
fields = [
"flow",
"target",
"stage",
"re_evaluate_policies",
"order",

View File

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_flows", "0002_default_flows"),
("passbook_flows", "0001_initial"),
]
operations = [

View File

@ -6,7 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_flows", "0005_provider_flows"),
("passbook_flows", "0003_auto_20200523_1133"),
]
operations = [

View File

@ -0,0 +1,42 @@
# Generated by Django 3.0.7 on 2020-07-03 20:59
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_policies", "0002_auto_20200528_1647"),
("passbook_flows", "0006_auto_20200629_0857"),
]
operations = [
migrations.AlterModelOptions(
name="flowstagebinding",
options={
"ordering": ["order", "target"],
"verbose_name": "Flow Stage Binding",
"verbose_name_plural": "Flow Stage Bindings",
},
),
migrations.RenameField(
model_name="flowstagebinding", old_name="flow", new_name="target",
),
migrations.RenameField(
model_name="flow", old_name="pbm", new_name="policybindingmodel_ptr",
),
migrations.AlterUniqueTogether(
name="flowstagebinding", unique_together={("target", "stage", "order")},
),
migrations.AlterField(
model_name="flow",
name="policybindingmodel_ptr",
field=models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
to="passbook_policies.PolicyBindingModel",
),
),
]

View File

@ -45,13 +45,13 @@ def create_default_authentication_flow(
defaults={"name": "Welcome to passbook!",},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
flow=flow, stage=identification_stage, defaults={"order": 0,},
target=flow, stage=identification_stage, defaults={"order": 0,},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
flow=flow, stage=password_stage, defaults={"order": 1,},
target=flow, stage=password_stage, defaults={"order": 1,},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
flow=flow, stage=login_stage, defaults={"order": 2,},
target=flow, stage=login_stage, defaults={"order": 2,},
)
@ -73,7 +73,7 @@ def create_default_invalidation_flow(
defaults={"name": "Logout",},
)
FlowStageBinding.objects.using(db_alias).update_or_create(
flow=flow,
target=flow,
stage=UserLogoutStage.objects.using(db_alias).first(),
defaults={"order": 0,},
)
@ -82,7 +82,7 @@ def create_default_invalidation_flow(
class Migration(migrations.Migration):
dependencies = [
("passbook_flows", "0001_initial"),
("passbook_flows", "0007_auto_20200703_2059"),
("passbook_stages_user_login", "0001_initial"),
("passbook_stages_user_logout", "0001_initial"),
("passbook_stages_password", "0001_initial"),

View File

@ -80,17 +80,17 @@ def create_default_source_enrollment_flow(
)
binding, _ = FlowStageBinding.objects.using(db_alias).update_or_create(
flow=flow, stage=prompt_stage, defaults={"order": 0}
target=flow, stage=prompt_stage, defaults={"order": 0}
)
PolicyBinding.objects.using(db_alias).update_or_create(
policy=prompt_policy, target=binding, defaults={"order": 0}
)
FlowStageBinding.objects.using(db_alias).update_or_create(
flow=flow, stage=user_write, defaults={"order": 1}
target=flow, stage=user_write, defaults={"order": 1}
)
FlowStageBinding.objects.using(db_alias).update_or_create(
flow=flow, stage=user_login, defaults={"order": 2}
target=flow, stage=user_login, defaults={"order": 2}
)
@ -129,14 +129,14 @@ def create_default_source_authentication_flow(
name="default-source-authentication-login"
)
FlowStageBinding.objects.using(db_alias).update_or_create(
flow=flow, stage=user_login, defaults={"order": 0}
target=flow, stage=user_login, defaults={"order": 0}
)
class Migration(migrations.Migration):
dependencies = [
("passbook_flows", "0003_auto_20200523_1133"),
("passbook_flows", "0008_default_flows"),
("passbook_policies", "0001_initial"),
("passbook_policies_expression", "0001_initial"),
("passbook_stages_prompt", "0001_initial"),

View File

@ -34,14 +34,14 @@ def create_default_provider_authorization_flow(
name="default-provider-authorization-consent"
)
FlowStageBinding.objects.using(db_alias).update_or_create(
flow=flow, stage=stage, defaults={"order": 0}
target=flow, stage=stage, defaults={"order": 0}
)
class Migration(migrations.Migration):
dependencies = [
("passbook_flows", "0004_source_flows"),
("passbook_flows", "0009_source_flows"),
("passbook_stages_consent", "0001_initial"),
]

View File

@ -79,10 +79,6 @@ class Flow(PolicyBindingModel):
stages = models.ManyToManyField(Stage, through="FlowStageBinding", blank=True)
pbm = models.OneToOneField(
PolicyBindingModel, parent_link=True, on_delete=models.CASCADE, related_name="+"
)
@staticmethod
def with_policy(request: HttpRequest, **flow_filter) -> Optional["Flow"]:
"""Get a Flow by `**flow_filter` and check if the request from `request` can access it."""
@ -123,7 +119,7 @@ class FlowStageBinding(PolicyBindingModel):
fsb_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
flow = models.ForeignKey("Flow", on_delete=models.CASCADE)
target = models.ForeignKey("Flow", on_delete=models.CASCADE)
stage = models.ForeignKey(Stage, on_delete=models.CASCADE)
re_evaluate_policies = models.BooleanField(
@ -138,12 +134,12 @@ class FlowStageBinding(PolicyBindingModel):
objects = InheritanceManager()
def __str__(self) -> str:
return f"Flow Stage Binding #{self.order} {self.flow} -> {self.stage}"
return f"Flow Binding {self.target} -> {self.stage}"
class Meta:
ordering = ["order", "flow"]
ordering = ["order", "target"]
verbose_name = _("Flow Stage Binding")
verbose_name_plural = _("Flow Stage Bindings")
unique_together = (("flow", "stage", "order"),)
unique_together = (("target", "stage", "order"),)

View File

@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional
from django.core.cache import cache
from django.http import HttpRequest
from elasticapm import capture_span
from structlog import get_logger
from passbook.core.models import User
@ -88,6 +89,7 @@ class FlowPlanner:
self.allow_empty_flows = False
self.flow = flow
@capture_span(name="FlowPlanner", span_type="flow.planner.plan")
def plan(
self, request: HttpRequest, default_context: Optional[Dict[str, Any]] = None
) -> FlowPlan:
@ -127,6 +129,7 @@ class FlowPlanner:
raise EmptyFlowException()
return plan
@capture_span(name="FlowPlanner", span_type="flow.planner.build_plan")
def _build_plan(
self,
user: User,
@ -146,7 +149,7 @@ class FlowPlanner:
.select_related()
):
binding: FlowStageBinding = stage.flowstagebinding_set.get(
flow__pk=self.flow.pk
target__pk=self.flow.pk
)
engine = PolicyEngine(binding, user, request)
engine.request.context = plan.context

View File

@ -25,13 +25,13 @@ def invalidate_flow_cache(sender, instance, **_):
total = delete_cache_prefix(f"{cache_key(instance)}*")
LOGGER.debug("Invalidating Flow cache", flow=instance, len=total)
if isinstance(instance, FlowStageBinding):
total = delete_cache_prefix(f"{cache_key(instance.flow)}*")
total = delete_cache_prefix(f"{cache_key(instance.target)}*")
LOGGER.debug(
"Invalidating Flow cache from FlowStageBinding", binding=instance, len=total
)
if isinstance(instance, Stage):
total = 0
for binding in FlowStageBinding.objects.filter(stage=instance):
prefix = cache_key(binding.flow)
prefix = cache_key(binding.target)
total += delete_cache_prefix(f"{prefix}*")
LOGGER.debug("Invalidating Flow cache from Stage", stage=instance, len=total)

View File

@ -59,7 +59,7 @@
<li>
<a href="https://passbook.beryju.org/">{% trans 'Documentation' %}</a>
</li>
<!-- todo: load config.passbook.footer.links -->
<!-- TODO:load config.passbook.footer.links -->
</ul>
</footer>
</div>

View File

@ -73,7 +73,7 @@ class TestFlowPlanner(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy"), order=0
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
)
request = self.request_factory.get(
reverse("passbook_flows:flow-executor", kwargs={"flow_slug": flow.slug}),
@ -97,7 +97,7 @@ class TestFlowPlanner(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy"), order=0
target=flow, stage=DummyStage.objects.create(name="dummy"), order=0
)
user = User.objects.create(username="test-user")
@ -119,7 +119,7 @@ class TestFlowPlanner(TestCase):
)
FlowStageBinding.objects.create(
flow=flow,
target=flow,
stage=DummyStage.objects.create(name="dummy1"),
order=0,
re_evaluate_policies=True,
@ -145,10 +145,10 @@ class TestFlowPlanner(TestCase):
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
)
binding2 = FlowStageBinding.objects.create(
flow=flow,
target=flow,
stage=DummyStage.objects.create(name="dummy2"),
order=1,
re_evaluate_policies=True,

View File

@ -107,10 +107,10 @@ class TestFlowExecutor(TestCase):
designation=FlowDesignation.AUTHENTICATION,
)
FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
)
FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy2"), order=1
target=flow, stage=DummyStage.objects.create(name="dummy2"), order=1
)
exec_url = reverse(
@ -143,10 +143,10 @@ class TestFlowExecutor(TestCase):
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
)
binding2 = FlowStageBinding.objects.create(
flow=flow,
target=flow,
stage=DummyStage.objects.create(name="dummy2"),
order=1,
re_evaluate_policies=True,
@ -194,16 +194,16 @@ class TestFlowExecutor(TestCase):
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
)
binding2 = FlowStageBinding.objects.create(
flow=flow,
target=flow,
stage=DummyStage.objects.create(name="dummy2"),
order=1,
re_evaluate_policies=True,
)
binding3 = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy3"), order=2
target=flow, stage=DummyStage.objects.create(name="dummy3"), order=2
)
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)
@ -261,22 +261,22 @@ class TestFlowExecutor(TestCase):
false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2)
binding = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0
)
binding2 = FlowStageBinding.objects.create(
flow=flow,
target=flow,
stage=DummyStage.objects.create(name="dummy2"),
order=1,
re_evaluate_policies=True,
)
binding3 = FlowStageBinding.objects.create(
flow=flow,
target=flow,
stage=DummyStage.objects.create(name="dummy3"),
order=2,
re_evaluate_policies=True,
)
binding4 = FlowStageBinding.objects.create(
flow=flow, stage=DummyStage.objects.create(name="dummy4"), order=2
target=flow, stage=DummyStage.objects.create(name="dummy4"), order=2
)
PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0)

View File

@ -4,6 +4,7 @@ from textwrap import indent
from typing import Any, Dict, Iterable, Optional
from django.core.exceptions import ValidationError
from elasticapm import capture_span
from requests import Session
from structlog import get_logger
@ -68,6 +69,7 @@ class BaseEvaluator:
full_expression += f"\nresult = handler({handler_signature})"
return full_expression
@capture_span(name="BaseEvaluator", span_type="lib.evaluator.evaluate")
def evaluate(self, expression_source: str) -> Any:
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
If any exception is raised during execution, it is raised.

View File

@ -4,6 +4,7 @@ from botocore.client import ClientError
from django.core.exceptions import DisallowedHost, ValidationError
from django.db import InternalError, OperationalError, ProgrammingError
from django_redis.exceptions import ConnectionInterrupted
from elasticapm.transport.http import TransportException
from redis.exceptions import RedisError
from rest_framework.exceptions import APIException
from structlog import get_logger
@ -33,9 +34,10 @@ def before_send(event, hint):
OSError,
RedisError,
SentryIgnoredException,
TransportException,
)
if "exc_info" in hint:
_exc_type, exc_value, _ = hint["exc_info"]
_, exc_value, _ = hint["exc_info"]
if isinstance(exc_value, ignored_classes):
LOGGER.info("Supressing error %r", exc_value)
return None

View File

@ -5,7 +5,7 @@ from urllib.parse import urlencode
from django import template
from django.db.models import Model
from django.template import Context
from django.utils.html import escape
from django.utils.html import escape, mark_safe
from structlog import get_logger
from passbook.lib.config import CONFIG
@ -105,4 +105,4 @@ def debug(obj) -> str:
@register.filter
def doc(obj) -> str:
"""Return docstring of object"""
return obj.__doc__
return mark_safe(obj.__doc__.replace("\n", "<br>"))

View File

@ -23,4 +23,4 @@ def get_client_ip(request: Optional[HttpRequest]) -> Optional[str]:
Returns none if no IP Could be found"""
if request:
return _get_client_ip_from_meta(request.META)
return ""
return None

View File

@ -1,12 +1,19 @@
"""passbook lib reflection utilities"""
from importlib import import_module
from django.conf import settings
def all_subclasses(cls, sort=True):
"""Recursively return all subclassess of cls"""
classes = set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c, sort=sort)]
)
# Check if we're in debug mode, if not exclude classes which have `__debug_only__`
if not settings.DEBUG:
# Filter class out when __debug_only__ is not False
classes = [x for x in classes if not getattr(x, "__debug_only__", False)]
# classes = filter(lambda x: not getattr(x, "__debug_only__", False), classes)
if sort:
return sorted(classes, key=lambda x: x.__name__)
return classes
@ -34,10 +41,3 @@ def get_apps():
for _app in apps.get_app_configs():
if _app.name.startswith("passbook"):
yield _app
def app(name):
"""Return true if app with `name` is enabled"""
from django.conf import settings
return name in settings.INSTALLED_APPS

View File

@ -1,36 +1,26 @@
"""Dynamic array widget"""
from django import forms
"""Utility Widgets"""
from itertools import groupby
from django.forms.models import ModelChoiceField, ModelChoiceIterator
class DynamicArrayWidget(forms.TextInput):
"""Dynamic array widget"""
class GroupedModelChoiceIterator(ModelChoiceIterator):
"""ModelChoiceField which groups objects by their verbose_name"""
template_name = "lib/arrayfield.html"
def __iter__(self):
if self.field.empty_label is not None:
yield ("", self.field.empty_label)
queryset = self.queryset
# Can't use iterator() when queryset uses prefetch_related()
if not queryset._prefetch_related_lookups:
queryset = queryset.iterator()
# We can't use DB-level sorting as we sort by subclass
queryset = sorted(queryset, key=lambda x: x._meta.verbose_name)
for group, objs in groupby(queryset, key=lambda x: x._meta.verbose_name):
yield (group, [self.choice(obj) for obj in objs])
def get_context(self, name, value, attrs):
value = value or [""]
context = super().get_context(name, value, attrs)
final_attrs = context["widget"]["attrs"]
id_ = context["widget"]["attrs"].get("id")
subwidgets = []
for index, item in enumerate(context["widget"]["value"]):
widget_attrs = final_attrs.copy()
if id_:
widget_attrs["id"] = "{id_}_{index}".format(id_=id_, index=index)
widget = forms.TextInput()
widget.is_required = self.is_required
subwidgets.append(widget.get_context(name, item, widget_attrs)["widget"])
class GroupedModelChoiceField(ModelChoiceField):
"""ModelChoiceField which groups objects by their verbose_name"""
context["widget"]["subwidgets"] = subwidgets
return context
def value_from_datadict(self, data, files, name):
try:
getter = data.getlist
return [value for value in getter(name) if value]
except AttributeError:
return data.get(name)
def format_value(self, value):
return value or []
iterator = GroupedModelChoiceIterator

View File

@ -16,6 +16,8 @@ class DummyPolicy(Policy):
"""Policy used for debugging the PolicyEngine. Returns a fixed result,
but takes a random time to process."""
__debug_only__ = True
result = models.BooleanField(default=False)
wait_min = models.IntegerField(default=5)
wait_max = models.IntegerField(default=30)

View File

@ -5,6 +5,7 @@ from typing import List, Optional
from django.core.cache import cache
from django.http import HttpRequest
from elasticapm import capture_span
from structlog import get_logger
from passbook.core.models import User
@ -69,6 +70,7 @@ class PolicyEngine:
if policy.__class__ == Policy:
raise TypeError(f"Policy '{policy}' is root type")
@capture_span(name="PolicyEngine", span_type="policy.engine.build")
def build(self) -> "PolicyEngine":
"""Build task group"""
for binding in self._iter_bindings():

View File

@ -8,7 +8,7 @@ from passbook.policies.types import PolicyRequest, PolicyResult
class ExpressionPolicy(Policy):
"""Implement custom logic using python."""
"""Execute arbitrary Python code to implement custom checks and validation."""
expression = models.TextField()

View File

@ -1,7 +1,9 @@
"""General fields"""
from django import forms
from passbook.policies.models import PolicyBinding, PolicyBindingModel
from passbook.lib.widgets import GroupedModelChoiceField
from passbook.policies.models import Policy, PolicyBinding, PolicyBindingModel
GENERAL_FIELDS = ["name"]
GENERAL_SERIALIZER_FIELDS = ["pk", "name"]
@ -10,10 +12,11 @@ GENERAL_SERIALIZER_FIELDS = ["pk", "name"]
class PolicyBindingForm(forms.ModelForm):
"""Form to edit Policy to PolicyBindingModel Binding"""
target = forms.ModelChoiceField(
target = GroupedModelChoiceField(
queryset=PolicyBindingModel.objects.all().select_subclasses(),
to_field_name="pbm_uuid",
)
policy = GroupedModelChoiceField(queryset=Policy.objects.all().select_subclasses(),)
class Meta:

View File

@ -0,0 +1,23 @@
"""Group Membership Policy API"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS
from passbook.policies.group_membership.models import GroupMembershipPolicy
class GroupMembershipPolicySerializer(ModelSerializer):
"""Group Membership Policy Serializer"""
class Meta:
model = GroupMembershipPolicy
fields = GENERAL_SERIALIZER_FIELDS + [
"group",
]
class GroupMembershipPolicyViewSet(ModelViewSet):
"""Group Membership Policy Viewset"""
queryset = GroupMembershipPolicy.objects.all()
serializer_class = GroupMembershipPolicySerializer

View File

@ -0,0 +1,11 @@
"""passbook Group Membership policy app config"""
from django.apps import AppConfig
class PassbookPoliciesGroupMembershipConfig(AppConfig):
"""passbook Group Membership policy app config"""
name = "passbook.policies.group_membership"
label = "passbook_policies_group_membership"
verbose_name = "passbook Policies.Group Membership"

View File

@ -0,0 +1,20 @@
"""passbook Group Membership Policy forms"""
from django import forms
from passbook.policies.forms import GENERAL_FIELDS
from passbook.policies.group_membership.models import GroupMembershipPolicy
class GroupMembershipPolicyForm(forms.ModelForm):
"""GroupMembershipPolicy Form"""
class Meta:
model = GroupMembershipPolicy
fields = GENERAL_FIELDS + [
"group",
]
widgets = {
"name": forms.TextInput(),
}

View File

@ -0,0 +1,47 @@
# Generated by Django 3.0.7 on 2020-07-01 19:01
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("passbook_policies", "0002_auto_20200528_1647"),
("passbook_core", "0003_default_user"),
]
operations = [
migrations.CreateModel(
name="GroupMembershipPolicy",
fields=[
(
"policy_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="passbook_policies.Policy",
),
),
(
"group",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="passbook_core.Group",
),
),
],
options={
"verbose_name": "Group Membership Policy",
"verbose_name_plural": "Group Membership Policies",
},
bases=("passbook_policies.policy",),
),
]

View File

@ -0,0 +1,23 @@
"""user field matcher models"""
from django.db import models
from django.utils.translation import gettext as _
from passbook.core.models import Group
from passbook.policies.models import Policy
from passbook.policies.types import PolicyRequest, PolicyResult
class GroupMembershipPolicy(Policy):
"""Check that the user is member of the selected group."""
group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL)
form = "passbook.policies.group_membership.forms.GroupMembershipPolicyForm"
def passes(self, request: PolicyRequest) -> PolicyResult:
return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists())
class Meta:
verbose_name = _("Group Membership Policy")
verbose_name_plural = _("Group Membership Policies")

View File

@ -0,0 +1,32 @@
"""evaluator tests"""
from django.test import TestCase
from guardian.shortcuts import get_anonymous_user
from passbook.core.models import Group
from passbook.policies.group_membership.models import GroupMembershipPolicy
from passbook.policies.types import PolicyRequest
class TestGroupMembershipPolicy(TestCase):
"""GroupMembershipPolicy tests"""
def setUp(self):
self.request = PolicyRequest(user=get_anonymous_user())
def test_invalid(self):
"""user not in group"""
group = Group.objects.create(name="test")
policy: GroupMembershipPolicy = GroupMembershipPolicy.objects.create(
group=group
)
self.assertFalse(policy.passes(self.request).passing)
def test_valid(self):
"""user in group"""
group = Group.objects.create(name="test")
group.user_set.add(get_anonymous_user())
group.save()
policy: GroupMembershipPolicy = GroupMembershipPolicy.objects.create(
group=group
)
self.assertTrue(policy.passes(self.request).passing)

View File

@ -13,7 +13,7 @@ LOGGER = get_logger()
class HaveIBeenPwendPolicy(Policy):
"""Check if password is on HaveIBeenPwned's list by upload the first
"""Check if password is on HaveIBeenPwned's list by uploading the first
5 characters of the SHA1 Hash."""
allowed_count = models.IntegerField(default=0)

View File

@ -0,0 +1,62 @@
"""passbook access helper classes"""
from typing import Optional
from django.contrib import messages
from django.contrib.auth.mixins import AccessMixin
from django.http import HttpRequest, HttpResponse
from django.shortcuts import redirect
from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Application, Provider, User
from passbook.policies.engine import PolicyEngine
from passbook.policies.types import PolicyResult
LOGGER = get_logger()
class BaseMixin:
"""Base Mixin class, used to annotate View Member variables"""
request: HttpRequest
class PolicyAccessMixin(BaseMixin, AccessMixin):
"""Mixin class for usage in Authorization views.
Provider functions to check application access, etc"""
def handle_no_permission_authorized(self) -> HttpResponse:
"""Function called when user has no permissions but is authorized"""
return redirect("passbook_flows:denied")
def provider_to_application(self, provider: Provider) -> Application:
"""Lookup application assigned to provider, throw error if no application assigned"""
try:
return provider.application
except Application.DoesNotExist as exc:
messages.error(
self.request,
_(
'Provider "%(name)s" has no application assigned'
% {"name": provider}
),
)
raise exc
def user_has_access(
self, application: Application, user: Optional[User] = None
) -> PolicyResult:
"""Check if user has access to application."""
user = user or self.request.user
policy_engine = PolicyEngine(
application, user or self.request.user, self.request
)
policy_engine.build()
result = policy_engine.result
LOGGER.debug(
"AccessMixin user_has_access", user=user, app=application, result=result,
)
if not result.passing:
for message in result.messages:
messages.error(self.request, _(message))
return result

View File

@ -1,4 +1,4 @@
"""Source API Views"""
"""Password Policy API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
@ -22,7 +22,7 @@ class PasswordPolicySerializer(ModelSerializer):
class PasswordPolicyViewSet(ModelViewSet):
"""Source Viewset"""
"""Password Policy Viewset"""
queryset = PasswordPolicy.objects.all()
serializer_class = PasswordPolicySerializer

Some files were not shown because too many files have changed in this diff Show More