Compare commits
153 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
3e2375f970 | |||
38ad8e5fd3 | |||
c481558a46 | |||
e27a05a7fc | |||
e4886f0c6f | |||
8b2ce5476a | |||
1b82283a20 | |||
7f3d0113c2 | |||
0f6dd33a6b | |||
5b79b3fd22 | |||
d68c72f1fa | |||
9267d0c1dd | |||
865abc005a | |||
a2725d5b82 | |||
4a05bc6e02 | |||
4e8238603a | |||
ff25c1c057 | |||
78cddca0d7 | |||
4742ee1d93 | |||
0c2dc309e7 | |||
144935d10f | |||
74ad1b6759 | |||
591d2f89a1 | |||
7c353f9297 | |||
cd1af15c56 | |||
878169ea2e | |||
38dfb03668 | |||
e2631cec0e | |||
5dad853f8a | |||
9f00843441 | |||
f31cd7dec6 | |||
1c1afca31f | |||
fbd4bdef33 | |||
5b22f9b6c3 | |||
083e317028 | |||
95416623b3 | |||
813b2676de | |||
aeca66a288 | |||
04a5428148 | |||
73b173b92a | |||
7cbf20a71c | |||
7a98e6d92b | |||
49e915f98b | |||
3aa2f1e892 | |||
bc4b7ef44d | |||
9400b01a55 | |||
e57da71dcf | |||
7268afaaf9 | |||
205183445c | |||
a08bdfdbcd | |||
e6c47fee26 | |||
a5629c5155 | |||
41689fe3ce | |||
8e84208e2c | |||
32a48fa07a | |||
773a9c0692 | |||
8808e3afe0 | |||
ecea85f8ca | |||
5dfa141e35 | |||
447e81d0b8 | |||
e138076e1d | |||
721d133dc3 | |||
75b687ecbe | |||
bdd1863177 | |||
e5b85e8e6a | |||
d7481c9de7 | |||
571373866e | |||
e36d7928e4 | |||
2be026dd44 | |||
d5b9de3569 | |||
e22620b0ec | |||
ba74a3213d | |||
d9ecb7070d | |||
fc4a46bd9c | |||
78301b7bab | |||
7bf7bde856 | |||
9bdff14403 | |||
f124314eab | |||
684e4ffdcf | |||
d9ff5c69c8 | |||
8142e3df45 | |||
73920899de | |||
13666965a7 | |||
86f16e2781 | |||
2ed8e72c62 | |||
edeed18ae8 | |||
d24133d8a2 | |||
b9733e56aa | |||
cd34413914 | |||
c3a4a76d43 | |||
a59a29b256 | |||
dce1edbe53 | |||
264d43827a | |||
6207226bdf | |||
ebf33f39c9 | |||
696cd1f247 | |||
b7b3abc462 | |||
575739d07c | |||
2d7e70eebf | |||
387f3c981f | |||
865435fb25 | |||
b10c5306b9 | |||
7c706369cd | |||
20dd6355c1 | |||
ba8d5d6e27 | |||
c448f87027 | |||
2b8c70a61f | |||
9d7ed9a0ed | |||
ff69b4affe | |||
d77afd1ded | |||
c3909f9196 | |||
fa55ba5ef0 | |||
766518ee0e | |||
74b2b26a20 | |||
4ebbc6f065 | |||
3bd1eadd51 | |||
8eb3f0f708 | |||
31ea2e7139 | |||
323b4b4a5d | |||
7b8e1bea92 | |||
f986dc89ad | |||
b21fd10093 | |||
6f9c19b142 | |||
f45643ca87 | |||
85f8bea784 | |||
b428ec5237 | |||
92428529ad | |||
f6761b5b0b | |||
307b04f4ca | |||
6a520a5697 | |||
f22dbba931 | |||
82cf482fba | |||
a6afb99edd | |||
ac5f8465b9 | |||
218acb9e38 | |||
927c718fdd | |||
b7a6d6e739 | |||
0946d6a25d | |||
c1e98e2f0c | |||
807cbbeaaf | |||
6c358c4e0a | |||
74cd0bc08f | |||
b08ec0477e | |||
328c999cb9 | |||
c37e382c15 | |||
784dd0fdd6 | |||
e6256cb9c8 | |||
4520e3f8b8 | |||
23146de2bf | |||
e24f4fe3a8 | |||
8e6b69f96f | |||
979bea17ed | |||
30dba285d9 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.7.3-beta
|
||||
current_version = 0.8.1-beta
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
@ -15,11 +15,11 @@ values =
|
||||
beta
|
||||
stable
|
||||
|
||||
[bumpversion:file:helm/passbook/values.yaml]
|
||||
[bumpversion:file:helm/values.yaml]
|
||||
|
||||
[bumpversion:file:helm/passbook/Chart.yaml]
|
||||
[bumpversion:file:helm/Chart.yaml]
|
||||
|
||||
[bumpversion:file:.gitlab-ci.yml]
|
||||
[bumpversion:file:.github/workflows/release.yml]
|
||||
|
||||
[bumpversion:file:passbook/__init__.py]
|
||||
|
||||
|
147
.github/workflows/ci.yml
vendored
Normal file
147
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,147 @@
|
||||
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'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip 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'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip 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'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip 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'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip pipenv && pipenv install --dev
|
||||
- name: Lint with bandit
|
||||
run: pipenv run bandit -r passbook
|
||||
# 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'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip 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/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
key: ${{ runner.os }}-pipenv-${{ hashFiles('Pipfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pipenv-
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip pipenv && pipenv install --dev
|
||||
- name: Run coverage
|
||||
run: pipenv run ./scripts/coverage.sh
|
89
.github/workflows/release.yml
vendored
Normal file
89
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
name: passbook-release
|
||||
on:
|
||||
release
|
||||
|
||||
jobs:
|
||||
# Build
|
||||
build-server:
|
||||
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:0.8.1-beta
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.8.1-beta
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook:latest
|
||||
build-gatekeeper:
|
||||
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:0.8.1-beta \
|
||||
-t beryju/passbook-gatekeeper:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-gatekeeper:0.8.1-beta
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-gatekeeper:latest
|
||||
build-static:
|
||||
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:0.8.1-beta
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.8.1-beta
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-static:latest
|
||||
test-release:
|
||||
needs:
|
||||
- build-server
|
||||
- build-static
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
export PASSBOOK_DOMAIN=localhost
|
||||
docker-compose pull
|
||||
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"
|
60
.github/workflows/tag.yml
vendored
Normal file
60
.github/workflows/tag.yml
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'version/*'
|
||||
|
||||
name: passbook-version-tag
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Create Release from Tag
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Pre-release test
|
||||
run: |
|
||||
export PASSBOOK_DOMAIN=localhost
|
||||
docker-compose pull
|
||||
docker build \
|
||||
--no-cache \
|
||||
-t beryju/passbook:latest \
|
||||
-f Dockerfile .
|
||||
docker-compose up --no-start
|
||||
docker-compose start postgresql redis
|
||||
docker-compose run -u root server bash -c "pip install --no-cache -r requirements-dev.txt && ./manage.py test"
|
||||
- name: Install Helm
|
||||
run: |
|
||||
apt update && apt install -y curl
|
||||
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
|
||||
- name: Helm package
|
||||
run: |
|
||||
helm dependency update helm/
|
||||
helm package helm/
|
||||
mv passbook-*.tgz passbook-chart.tgz
|
||||
- name: Extract verison number
|
||||
id: get_version
|
||||
uses: actions/github-script@0.2.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
return context.payload.ref.replace(/\/refs\/tags\/version\//, '');
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1.0.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ steps.get_version.outputs.result }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
- name: Upload packaged Helm Chart
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./passbook-chart.tgz
|
||||
asset_name: passbook-chart.tgz
|
||||
asset_content_type: application/gzip
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -63,6 +63,7 @@ coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
unittest.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
@ -184,7 +185,6 @@ dmypy.json
|
||||
[Ii]nclude
|
||||
[Ll]ib64
|
||||
[Ll]ocal
|
||||
[Ss]cripts
|
||||
pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
|
||||
|
164
.gitlab-ci.yml
164
.gitlab-ci.yml
@ -1,164 +0,0 @@
|
||||
# Global Variables
|
||||
stages:
|
||||
- build-base-image
|
||||
- build-dev-image
|
||||
- test
|
||||
- build
|
||||
- package
|
||||
- post-release
|
||||
image: docker.beryju.org/passbook/dev:latest
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: passbook
|
||||
POSTGRES_USER: passbook
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
|
||||
before_script:
|
||||
- pip install pipenv
|
||||
# Ensure all dependencies are installed, even those not included in passbook/dev
|
||||
# According to pipenv docs, -d outputs all packages, however it actually does not
|
||||
- pipenv lock -r > requirements-all.txt
|
||||
- pipenv lock -rd >> requirements-all.txt
|
||||
- pip install -r requirements-all.txt
|
||||
|
||||
create-base-image:
|
||||
image:
|
||||
name: gcr.io/kaniko-project/executor:debug
|
||||
entrypoint: [""]
|
||||
before_script:
|
||||
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
||||
script:
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/base.Dockerfile --destination docker.beryju.org/passbook/base:latest
|
||||
stage: build-base-image
|
||||
only:
|
||||
refs:
|
||||
- tags
|
||||
- /^version/.*$/
|
||||
|
||||
build-dev-image:
|
||||
image:
|
||||
name: gcr.io/kaniko-project/executor:debug
|
||||
entrypoint: [""]
|
||||
before_script:
|
||||
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
||||
script:
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dev.Dockerfile --destination docker.beryju.org/passbook/dev:latest
|
||||
stage: build-dev-image
|
||||
only:
|
||||
refs:
|
||||
- tags
|
||||
- /^version/.*$/
|
||||
|
||||
|
||||
isort:
|
||||
script:
|
||||
- isort -c -sg env
|
||||
stage: test
|
||||
services:
|
||||
- postgres:latest
|
||||
- redis:latest
|
||||
migrations:
|
||||
script:
|
||||
- python manage.py migrate
|
||||
stage: test
|
||||
services:
|
||||
- postgres:latest
|
||||
- redis:latest
|
||||
# prospector:
|
||||
# script:
|
||||
# - prospector
|
||||
# stage: test
|
||||
# services:
|
||||
# - postgres:latest
|
||||
# - redis:latest
|
||||
pylint:
|
||||
script:
|
||||
- pylint passbook
|
||||
stage: test
|
||||
services:
|
||||
- postgres:latest
|
||||
- redis:latest
|
||||
coverage:
|
||||
script:
|
||||
- coverage run --concurrency=multiprocessing manage.py test
|
||||
- coverage combine
|
||||
- coverage report
|
||||
stage: test
|
||||
services:
|
||||
- postgres:latest
|
||||
- redis:latest
|
||||
|
||||
build-passbook-server:
|
||||
stage: build
|
||||
image:
|
||||
name: gcr.io/kaniko-project/executor:debug
|
||||
entrypoint: [""]
|
||||
before_script:
|
||||
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
||||
script:
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.7.3-beta
|
||||
only:
|
||||
- tags
|
||||
- /^version/.*$/
|
||||
build-passbook-static:
|
||||
stage: build
|
||||
image:
|
||||
name: gcr.io/kaniko-project/executor:debug
|
||||
entrypoint: [""]
|
||||
before_script:
|
||||
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
||||
script:
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/static.Dockerfile --destination docker.beryju.org/passbook/static:latest --destination docker.beryju.org/passbook/static:0.7.3-beta
|
||||
only:
|
||||
- tags
|
||||
- /^version/.*$/
|
||||
# running collectstatic fully initialises django, hence we need that databases
|
||||
services:
|
||||
- postgres:latest
|
||||
- redis:latest
|
||||
build-passbook-gatekeeper:
|
||||
stage: build
|
||||
image:
|
||||
name: gcr.io/kaniko-project/executor:debug
|
||||
entrypoint: [""]
|
||||
before_script:
|
||||
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
||||
script:
|
||||
- /kaniko/executor --context $CI_PROJECT_DIR/gatekeeper --dockerfile $CI_PROJECT_DIR/gatekeeper/Dockerfile --destination docker.beryju.org/passbook/gatekeeper:latest --destination docker.beryju.org/passbook/gatekeeper:0.7.3-beta
|
||||
only:
|
||||
- tags
|
||||
- /^version/.*$/
|
||||
|
||||
package-helm:
|
||||
image: debian:stretch-slim
|
||||
stage: package
|
||||
before_script:
|
||||
- apt update && apt install -y curl
|
||||
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
|
||||
script:
|
||||
- helm init --client-only
|
||||
- helm dependency update helm/passbook
|
||||
- helm package helm/passbook
|
||||
artifacts:
|
||||
paths:
|
||||
- passbook-*.tgz
|
||||
expire_in: 1 week
|
||||
only:
|
||||
- tags
|
||||
- /^version/.*$/
|
||||
|
||||
notify-sentry:
|
||||
image: getsentry/sentry-cli
|
||||
stage: post-release
|
||||
variables:
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
SENTRY_ORG: beryjuorg
|
||||
SENTRY_PROJECT: passbook
|
||||
before_script:
|
||||
- apk add curl
|
||||
script:
|
||||
- sentry-cli releases new passbook@0.7.3-beta
|
||||
- sentry-cli releases set-commits --auto passbook@0.7.3-beta
|
||||
only:
|
||||
- tags
|
||||
- /^version/.*$/
|
@ -1,6 +1,6 @@
|
||||
[MASTER]
|
||||
|
||||
disable=redefined-outer-name,arguments-differ,no-self-use,cyclic-import,fixme,locally-disabled,unpacking-non-sequence,too-many-ancestors,too-many-branches,too-few-public-methods
|
||||
disable=redefined-outer-name,arguments-differ,no-self-use,cyclic-import,fixme,locally-disabled,unpacking-non-sequence,too-many-ancestors,too-many-branches,too-few-public-methods,import-outside-toplevel,bad-continuation
|
||||
load-plugins=pylint_django,pylint.extensions.bad_builtin
|
||||
extension-pkg-whitelist=lxml
|
||||
const-rgx=[a-zA-Z0-9_]{1,40}$
|
||||
|
24
Dockerfile
24
Dockerfile
@ -1,4 +1,26 @@
|
||||
FROM docker.beryju.org/passbook/base:latest
|
||||
FROM python:3.8-slim-buster as locker
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
COPY ./Pipfile.lock /app/
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -rd > requirements-dev.txt
|
||||
|
||||
FROM python:3.8-slim-buster
|
||||
|
||||
COPY --from=locker /app/requirements.txt /app/
|
||||
COPY --from=locker /app/requirements-dev.txt /app/
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-11 && \
|
||||
rm -rf /var/lib/apt/ && \
|
||||
pip install -r requirements.txt --no-cache-dir && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /app passbook
|
||||
|
||||
COPY ./passbook/ /app/passbook
|
||||
COPY ./manage.py /app/
|
||||
|
14
Pipfile
14
Pipfile
@ -12,7 +12,6 @@ django-cors-middleware = "*"
|
||||
django-dbbackup = "*"
|
||||
django-filter = "*"
|
||||
django-guardian = "*"
|
||||
django-ipware = "*"
|
||||
django-model-utils = "*"
|
||||
django-oauth-toolkit = "*"
|
||||
django-oidc-provider = "*"
|
||||
@ -24,7 +23,7 @@ django-rest-framework = "*"
|
||||
django-storages = "*"
|
||||
djangorestframework-guardian = "*"
|
||||
drf-yasg = "*"
|
||||
kombu = "==4.5.0"
|
||||
kombu = "*"
|
||||
ldap3 = "*"
|
||||
lxml = "*"
|
||||
oauthlib = "*"
|
||||
@ -41,9 +40,10 @@ signxml = "*"
|
||||
structlog = "*"
|
||||
swagger-spec-validator = "*"
|
||||
urllib3 = {extras = ["secure"],version = "*"}
|
||||
jinja2 = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
python_version = "3.8"
|
||||
|
||||
[dev-packages]
|
||||
autopep8 = "*"
|
||||
@ -52,8 +52,10 @@ bumpversion = "*"
|
||||
colorama = "*"
|
||||
coverage = "*"
|
||||
django-debug-toolbar = "*"
|
||||
isort = "*"
|
||||
prospector = "*"
|
||||
pylint = "==2.3.1"
|
||||
pylint = "*"
|
||||
pylint-django = "*"
|
||||
unittest-xml-reporting = "*"
|
||||
black = "*"
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
|
902
Pipfile.lock
generated
902
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,13 @@
|
||||
# passbook
|
||||
|
||||

|
||||
|
||||
## Quick instance
|
||||
|
||||
```
|
||||
export PASSBOOK_DOMAIN=domain.tld
|
||||
# Optionally enable Error-reporting
|
||||
# export PASSBOOK_ERROR_REPORTING=true
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker-compose exec server ./manage.py migrate
|
||||
|
@ -1,23 +0,0 @@
|
||||
FROM python:3.7-slim-buster as locker
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
COPY ./Pipfile.lock /app/
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -rd > requirements-dev.txt
|
||||
|
||||
FROM python:3.7-slim-buster
|
||||
|
||||
COPY --from=locker /app/requirements.txt /app/
|
||||
COPY --from=locker /app/requirements-dev.txt /app/
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends postgresql-client-11 && \
|
||||
rm -rf /var/lib/apt/ && \
|
||||
pip install -r requirements.txt --no-cache-dir && \
|
||||
adduser --system --no-create-home --uid 1000 --group --home /app passbook
|
@ -1,3 +0,0 @@
|
||||
FROM docker.beryju.org/passbook/base:latest
|
||||
|
||||
RUN pip install -r /app/requirements-dev.txt --no-cache-dir
|
@ -21,15 +21,14 @@ services:
|
||||
labels:
|
||||
- traefik.enable=false
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest}
|
||||
image: beryju/passbook:${SERVER_TAG:-latest}
|
||||
command:
|
||||
- uwsgi
|
||||
- uwsgi.ini
|
||||
environment:
|
||||
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
|
||||
- PASSBOOK_REDIS__HOST=redis
|
||||
- PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false}
|
||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||
ports:
|
||||
@ -41,7 +40,7 @@ services:
|
||||
- traefik.docker.network=internal
|
||||
- traefik.frontend.rule=PathPrefix:/
|
||||
worker:
|
||||
image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest}
|
||||
image: beryju/passbook:${SERVER_TAG:-latest}
|
||||
command:
|
||||
- celery
|
||||
- worker
|
||||
@ -57,13 +56,11 @@ services:
|
||||
environment:
|
||||
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
|
||||
- PASSBOOK_REDIS__HOST=redis
|
||||
- PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false}
|
||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||
static:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: static.Dockerfile
|
||||
image: docker.beryju.org/passbook/static:latest
|
||||
image: beryju/passbook-static:latest
|
||||
networks:
|
||||
- internal
|
||||
labels:
|
||||
|
@ -7,4 +7,4 @@ threads = 2
|
||||
enable-threads = true
|
||||
uid = passbook
|
||||
gid = passbook
|
||||
disable-logging=True
|
||||
disable-logging = True
|
||||
|
14
docs/Dockerfile
Normal file
14
docs/Dockerfile
Normal file
@ -0,0 +1,14 @@
|
||||
FROM python:3.8-slim-buster as builder
|
||||
|
||||
WORKDIR /mkdocs
|
||||
|
||||
RUN pip install mkdocs mkdocs-material
|
||||
|
||||
COPY docs/ docs
|
||||
COPY mkdocs.yml .
|
||||
|
||||
RUN mkdocs build
|
||||
|
||||
FROM nginx
|
||||
|
||||
COPY --from=builder /mkdocs/site /usr/share/nginx/html
|
23
docs/factors.md
Normal file
23
docs/factors.md
Normal file
@ -0,0 +1,23 @@
|
||||
# Factors
|
||||
|
||||
A factor represents a single authenticating factor for a user. Common examples of this would be a password or an OTP. These factors can be combined in any order, and can be dynamically enabled using policies.
|
||||
|
||||
## Password Factor
|
||||
|
||||
This is the standard Password Factor. It allows you to select which Backend the password is checked with. here you can also specify which Policies are used to check the password. You can also specify which Factors a User has to pass to recover their account.
|
||||
|
||||
## Dummy Factor
|
||||
|
||||
This factor waits a random amount of time. Mostly used for debugging.
|
||||
|
||||
## E-Mail Factor
|
||||
|
||||
This factor is mostly for recovery, and used in conjunction with the Password Factor.
|
||||
|
||||
## OTP Factor
|
||||
|
||||
This is your typical One-Time Password implementation, compatible with Authy and Google Authenticator. You can enfore this Factor so that every user has to configure it, or leave it optional.
|
||||
|
||||
## Captcha Factor
|
||||
|
||||
While this factor doesn't really authenticate a user, it is part of the Authentication Flow. passbook uses Google's reCaptcha implementation.
|
55
docs/images/logo.svg
Normal file
55
docs/images/logo.svg
Normal file
@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path style="fill:#57565C;" d="M407,512H105C47.103,512,0,464.897,0,407V105C0,47.103,47.103,0,105,0h302
|
||||
c57.897,0,105,47.103,105,105v302C512,464.897,464.897,512,407,512z"/>
|
||||
<path style="fill:#3E3D42;" d="M407,0H256v512h151c57.897,0,105-47.103,105-105V105C512,47.103,464.897,0,407,0z"/>
|
||||
<rect x="91" y="141" style="fill:#00C3FF;" width="330" height="44"/>
|
||||
<rect x="256" y="141" style="fill:#00AAF0;" width="165" height="44"/>
|
||||
<rect x="91" y="176" style="fill:#FFDC40;" width="330" height="44"/>
|
||||
<rect x="256" y="176" style="fill:#FFAB15;" width="165" height="44"/>
|
||||
<rect x="91" y="206" style="fill:#87E694;" width="330" height="44"/>
|
||||
<rect x="256" y="206" style="fill:#66CC70;" width="165" height="44"/>
|
||||
<path style="fill:#F2F2F2;" d="M421,381c0,8.284-6.716,15-15,15H106c-8.284,0-15-6.716-15-15v-85h89.997
|
||||
c9.31,0,17.688,4.938,21.868,12.888C213.277,328.695,233.638,341,256,341s42.723-12.305,53.135-32.111
|
||||
c4.18-7.95,12.559-12.889,21.868-12.889H421V381z"/>
|
||||
<path style="fill:#FF6849;" d="M421,266h-89.997c-20.487,0-39.041,11.085-48.423,28.929C277.369,304.842,267.185,311,256,311
|
||||
s-21.369-6.158-26.58-16.071C220.038,277.085,201.484,266,180.997,266H91v-30h330V266z"/>
|
||||
<path style="fill:#F2F2F2;" d="M421,146H91v-15c0-8.284,6.716-15,15-15h300c8.284,0,15,6.716,15,15V146z"/>
|
||||
<path style="fill:#E5E5E5;" d="M331.003,296c-9.31,0-17.688,4.938-21.868,12.889C298.723,328.695,278.362,341,256,341v55h150
|
||||
c8.284,0,15-6.716,15-15v-85H331.003z"/>
|
||||
<path style="fill:#FD4B2D;" d="M256,236v75c11.185,0,21.369-6.158,26.58-16.071C291.962,277.085,310.516,266,331.003,266H421v-30
|
||||
H256z"/>
|
||||
<path style="fill:#E5E5E5;" d="M406,116H256v30h165v-15C421,122.716,414.284,116,406,116z"/>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
31
docs/index.md
Executable file
31
docs/index.md
Executable file
@ -0,0 +1,31 @@
|
||||
# Welcome
|
||||
|
||||
Welcome to the passbook Documentation. passbook is an open-source Identity Provider and Usermanagement software. It can be used as a central directory for users or customers and it can integrate with your existing Directory.
|
||||
|
||||
passbook can also be used as part of an Application to facilitate User Enrollment, Password recovery and Social Login.
|
||||
|
||||
passbook uses the following Terminology:
|
||||
|
||||
### Policy
|
||||
|
||||
A Policy is at a base level a yes/no gate. It will either evaluate to True or False depending on the Policy Kind and settings. For example, a "Group Membership Policy" evaluates to True if the User is member of the specified Group and False if not. This can be used to conditionally apply Factors and grant/deny access.
|
||||
|
||||
### Provider
|
||||
|
||||
A Provider is a way for other Applications to authenticate against passbook. Common Providers are OpenID Connect (OIDC) and SAML.
|
||||
|
||||
### Source
|
||||
|
||||
Sources are ways to get users into passbook. This might be an LDAP Connection to import Users from Active Directory, or an OAuth2 Connection to allow Social Logins.
|
||||
|
||||
### Application
|
||||
|
||||
An application links together Policies with a Provider, allowing you to control access. It also holds Information like UI Name, Icon and more.
|
||||
|
||||
### Factors
|
||||
|
||||
Factors represent Authentication Factors, like a Password or OTP. These Factors can be dynamically enabled using policies. This allows you to, for example, force users from a certain IP ranges to complete a Captcha to authenticate.
|
||||
|
||||
### Property Mappings
|
||||
|
||||
Property Mappings allow you to make Information available for external Applications. For example, if you want to login to AWS with passbook, you'd use Property Mappings to set the User's Roles based on their Groups.
|
26
docs/installation/docker-compose.md
Normal file
26
docs/installation/docker-compose.md
Normal file
@ -0,0 +1,26 @@
|
||||
# docker-compose
|
||||
|
||||
This installation Method is for test-setups and small-scale productive setups.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- docker
|
||||
- docker-compose
|
||||
|
||||
## Install
|
||||
|
||||
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/BeryJu/passbook/master/docker-compose.yml). Place it in a directory of your choice.
|
||||
|
||||
passbook needs to know it's primary URL to create links in E-Mails and set cookies, so you have to run the following command:
|
||||
|
||||
```
|
||||
export PASSBOOK_DOMAIN=domain.tld # this can be any domain or IP, it just needs to point to passbook.
|
||||
```
|
||||
|
||||
The compose file references the current latest version, which can be overridden with the `SERVER_TAG` Environment variable.
|
||||
|
||||
If you plan to use this setup for production, it is also advised to change the PostgreSQL Password by setting `PG_PASS` to a password of your choice.
|
||||
|
||||
Now you can pull the Docker images needed by running `docker-compose pull`. After this has finished, run `docker-compose up -d` to start passbook.
|
||||
|
||||
passbook will then be reachable on Port 80. You can optionally configure the packaged traefik to use Let's Encrypt for TLS Encryption.
|
6
docs/installation/install.md
Executable file
6
docs/installation/install.md
Executable file
@ -0,0 +1,6 @@
|
||||
# Installation
|
||||
|
||||
There are two supported ways to install passbook:
|
||||
|
||||
- [docker-compose](docker-compose.md) for test- or small productive setups
|
||||
- [Kubernetes](./kubernetes.md) for larger Productive setups
|
3
docs/installation/kubernetes.md
Normal file
3
docs/installation/kubernetes.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Kubernetes
|
||||
|
||||
For a mid to high-load Installation, Kubernetes is recommended. passbook is installed using a helm-chart.
|
32
docs/integrations/services/aws/index.md
Normal file
32
docs/integrations/services/aws/index.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Amazon Web Services Integration
|
||||
|
||||
## What is AWS
|
||||
|
||||
!!! note ""
|
||||
Amazon Web Services (AWS) is the world’s most comprehensive and broadly adopted cloud platform, offering over 175 fully featured services from data centers globally. Millions of customers—including the fastest-growing startups, largest enterprises, and leading government agencies—are using AWS to lower costs, become more agile, and innovate faster.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `passbook.company` is the FQDN of the passbook Install
|
||||
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
|
||||
|
||||
- ACS URL: `https://signin.aws.amazon.com/saml`
|
||||
- Audience: `urn:amazon:webservices`
|
||||
- Issuer: `passbook`
|
||||
|
||||
You can of course use a custom Signing Certificate, and adjust durations.
|
||||
|
||||
## AWS
|
||||
|
||||
Create a Role with the Permissions you desire, and note the ARN.
|
||||
|
||||
AWS requires two custom PropertyMappings; `Role` and `RoleSessionName`. Create them as following:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Afterwards export the Metadata from passbook, and create an Identity Provider [here](https://console.aws.amazon.com/iam/home#/providers).
|
Binary file not shown.
After Width: | Height: | Size: 65 KiB |
BIN
docs/integrations/services/aws/property-mapping-role.png
Normal file
BIN
docs/integrations/services/aws/property-mapping-role.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
58
docs/integrations/services/gitlab/index.md
Normal file
58
docs/integrations/services/gitlab/index.md
Normal file
@ -0,0 +1,58 @@
|
||||
# GitLab Integration
|
||||
|
||||
## What is GitLab
|
||||
|
||||
From https://about.gitlab.com/what-is-gitlab/
|
||||
|
||||
!!! note ""
|
||||
GitLab is a complete DevOps platform, delivered as a single application. This makes GitLab unique and makes Concurrent DevOps possible, unlocking your organization from the constraints of a pieced together toolchain. Join us for a live Q&A to learn how GitLab can give you unmatched visibility and higher levels of efficiency in a single application across the DevOps lifecycle.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `gitlab.company` is the FQDN of the GitLab Install
|
||||
- `passbook.company` is the FQDN of the passbook Install
|
||||
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
|
||||
|
||||
- ACS URL: `https://gitlab.company/users/auth/saml/callback`
|
||||
- Audience: `https://gitlab.company`
|
||||
- Issuer: `https://gitlab.company`
|
||||
|
||||
You can of course use a custom Signing Certificate, and adjust durations. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
|
||||
|
||||
## GitLab Configuration
|
||||
|
||||
Paste the following block in your `gitlab.rb` file, after replacing the placeholder values from above. The file is located in `/etc/gitlab`.
|
||||
|
||||
```ruby
|
||||
gitlab_rails['omniauth_enabled'] = true
|
||||
gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
|
||||
gitlab_rails['omniauth_sync_email_from_provider'] = 'saml'
|
||||
gitlab_rails['omniauth_sync_profile_from_provider'] = ['saml']
|
||||
gitlab_rails['omniauth_sync_profile_attributes'] = ['email']
|
||||
gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'saml'
|
||||
gitlab_rails['omniauth_block_auto_created_users'] = false
|
||||
gitlab_rails['omniauth_auto_link_saml_user'] = true
|
||||
gitlab_rails['omniauth_providers'] = [
|
||||
{
|
||||
name: 'saml',
|
||||
args: {
|
||||
assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback',
|
||||
idp_cert_fingerprint: '4E:1E:CD:67:4A:67:5A:E9:6A:D0:3C:E6:DD:7A:F2:44:2E:76:00:6A',
|
||||
idp_sso_target_url: 'https://passbook.company/application/saml/<passbook application slug>/login/',
|
||||
issuer: 'https://gitlab.company',
|
||||
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
|
||||
attribute_statements: {
|
||||
email: ['urn:oid:1.3.6.1.4.1.5923.1.1.1.6'],
|
||||
first_name: ['urn:oid:2.5.4.3'],
|
||||
nickname: ['urn:oid:2.16.840.1.113730.3.1.241']
|
||||
}
|
||||
},
|
||||
label: 'passbook'
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Afterwards, either run `gitlab-ctl reconfigure` if you're running GitLab Omnibus, or restart the container if you're using the container.
|
BIN
docs/integrations/services/harbor/harbor.png
Normal file
BIN
docs/integrations/services/harbor/harbor.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 348 KiB |
27
docs/integrations/services/harbor/index.md
Normal file
27
docs/integrations/services/harbor/index.md
Normal file
@ -0,0 +1,27 @@
|
||||
# Harbor Integration
|
||||
|
||||
## What is Harbor
|
||||
|
||||
From https://goharbor.io
|
||||
|
||||
!!! note ""
|
||||
Harbor is an open source container image registry that secures images with role-based access control, scans images for vulnerabilities, and signs images as trusted. A CNCF Incubating project, Harbor delivers compliance, performance, and interoperability to help you consistently and securely manage images across cloud native compute platforms like Kubernetes and Docker.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `harbor.company` is the FQDN of the Harbor Install
|
||||
- `passbook.company` is the FQDN of the passbook Install
|
||||
|
||||
Create an application in passbook. Create an OpenID Provider with the following Parameters:
|
||||
|
||||
- Client Type: `Confidential`
|
||||
- Response types: `code (Authorization Code Flow)`
|
||||
- JWT Algorithm: `RS256`
|
||||
- Redirect URIs: `https://harbor.company/c/oidc/callback`
|
||||
- Scopes: `openid`
|
||||
|
||||
## Harbor
|
||||
|
||||

|
28
docs/integrations/services/rancher/index.md
Normal file
28
docs/integrations/services/rancher/index.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Rancher Integration
|
||||
|
||||
## What is Rancher
|
||||
|
||||
From https://rancher.com/products/rancher
|
||||
|
||||
!!! note ""
|
||||
An Enterprise Platform for Managing Kubernetes Everywhere
|
||||
Rancher is a platform built to address the needs of the DevOps teams deploying applications with Kubernetes, and the IT staff responsible for delivering an enterprise-critical service.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `rancher.company` is the FQDN of the Rancher Install
|
||||
- `passbook.company` is the FQDN of the passbook Install
|
||||
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
|
||||
|
||||
- ACS URL: `https://rancher.company/v1-saml/adfs/saml/acs`
|
||||
- Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
|
||||
- Issuer: `passbook`
|
||||
|
||||
You can of course use a custom Signing Certificate, and adjust durations.
|
||||
|
||||
## Rancher
|
||||
|
||||

|
BIN
docs/integrations/services/rancher/rancher.png
Normal file
BIN
docs/integrations/services/rancher/rancher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 525 KiB |
41
docs/integrations/services/sentry/index.md
Normal file
41
docs/integrations/services/sentry/index.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Sentry Integration
|
||||
|
||||
## What is Sentry
|
||||
|
||||
From https://sentry.io
|
||||
|
||||
!!! note ""
|
||||
Sentry provides self-hosted and cloud-based error monitoring that helps all software
|
||||
teams discover, triage, and prioritize errors in real-time.
|
||||
|
||||
One million developers at over fifty thousand companies already ship
|
||||
better software faster with Sentry. Won’t you join them?
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `sentry.company` is the FQDN of the Sentry Install
|
||||
- `passbook.company` is the FQDN of the passbook Install
|
||||
|
||||
Create an application in passbook. Create an OpenID Provider with the following Parameters:
|
||||
|
||||
- Client Type: `Confidential`
|
||||
- Response types: `code (Authorization Code Flow)`
|
||||
- JWT Algorithm: `RS256`
|
||||
- Redirect URIs: `https://sentry.company/auth/sso/`
|
||||
- Scopes: `openid email`
|
||||
|
||||
## Sentry
|
||||
|
||||
**This guide assumes you've installed Sentry using [getsentry/onpremise](https://github.com/getsentry/onpremise)**
|
||||
|
||||
- Add `sentry-auth-oidc` to `onpremise/sentry/requirements.txt` (Create the file if it doesn't exist yet)
|
||||
- Add the following block to your `onpremise/sentry/sentry.conf.py`:
|
||||
```
|
||||
OIDC_ISSUER = "passbook"
|
||||
OIDC_CLIENT_ID = "<Client ID from passbook>"
|
||||
OIDC_CLIENT_SECRET = "<Client Secret from passbook>"
|
||||
OIDC_SCOPE = "openid email"
|
||||
OIDC_DOMAIN = "https://passbook.company/application/oidc/"
|
||||
```
|
74
docs/integrations/services/tower-awx/index.md
Normal file
74
docs/integrations/services/tower-awx/index.md
Normal file
@ -0,0 +1,74 @@
|
||||
# Ansible Tower / AWX Integration
|
||||
|
||||
## What is Tower
|
||||
|
||||
From https://docs.ansible.com/ansible/2.5/reference_appendices/tower.html
|
||||
|
||||
!!! note ""
|
||||
Ansible Tower (formerly ‘AWX’) is a web-based solution that makes Ansible even more easy to use for IT teams of all kinds. It’s designed to be the hub for all of your automation tasks.
|
||||
|
||||
Tower allows you to control access to who can access what, even allowing sharing of SSH credentials without someone being able to transfer those credentials. Inventory can be graphically managed or synced with a wide variety of cloud sources. It logs all of your jobs, integrates well with LDAP, and has an amazing browsable REST API. Command line tools are available for easy integration with Jenkins as well. Provisioning callbacks provide great support for autoscaling topologies.
|
||||
|
||||
!!! note
|
||||
AWX is the Open-Source version of Tower, and AWX will be used interchangeably throughout this document.
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `awx.company` is the FQDN of the AWX/Tower Install
|
||||
- `passbook.company` is the FQDN of the passbook Install
|
||||
|
||||
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
|
||||
|
||||
- ACS URL: `https://awx.company/sso/complete/saml/`
|
||||
- Audience: `awx`
|
||||
- Issuer: `https://awx.company/sso/metadata/saml/`
|
||||
|
||||
You can of course use a custom Signing Certificate, and adjust durations.
|
||||
|
||||
## AWX Configuration
|
||||
|
||||
Navigate to `https://awx.company/#/settings/auth` to configure SAML. Set the Field `SAML SERVICE PROVIDER ENTITY ID` to `awx`.
|
||||
|
||||
For the fields `SAML SERVICE PROVIDER PUBLIC CERTIFICATE` and `SAML SERVICE PROVIDER PRIVATE KEY`, you can either use custom Certificates, or use the self-signed Pair generated by Passbook.
|
||||
|
||||
Provide Metadata in the `SAML Service Provider Organization Info` Field:
|
||||
|
||||
```json
|
||||
{
|
||||
"en-US": {
|
||||
"name": "passbook",
|
||||
"url": "https://passbook.company",
|
||||
"displayname": "passbook"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Provide Metadata in the `SAML Service Provider Technical Contact` and `SAML Service Provider Technical Contact` Fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"givenName": "Admin Name",
|
||||
"emailAddress": "admin@company"
|
||||
}
|
||||
```
|
||||
|
||||
In the `SAML Enabled Identity Providers` paste the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"passbook": {
|
||||
"attr_username": "urn:oid:2.16.840.1.113730.3.1.241",
|
||||
"attr_user_permanent_id": "urn:oid:0.9.2342.19200300.100.1.1",
|
||||
"x509cert": "MIIDEjCCAfqgAwIBAgIRAJZ9pOZ1g0xjiHtQAAejsMEwDQYJKoZIhvcNAQELBQAwMDEuMCwGA1UEAwwlcGFzc2Jvb2sgU2VsZi1zaWduZWQgU0FNTCBDZXJ0aWZpY2F0ZTAeFw0xOTEyMjYyMDEwNDFaFw0yMDEyMjYyMDEwNDFaMFkxLjAsBgNVBAMMJXBhc3Nib29rIFNlbGYtc2lnbmVkIFNBTUwgQ2VydGlmaWNhdGUxETAPBgNVBAoMCHBhc3Nib29rMRQwEgYDVQQLDAtTZWxmLXNpZ25lZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO/ktBYZkY9xAijF4acvzX6Q1K8KoIZeyde8fVgcWBz4L5FgDQ4/dni4k2YAcPdwteGL4nKVzetUzjbRCBUNuO6lqU4J4WNNX4Xg4Ir7XLRoAQeo+omTPBdpJ1p02HjtN5jT01umN3bK2yto1e37CJhK6WJiaXqRewPxh4lI4aqdj3BhFkJ3I3r2qxaWOAXQ6X7fg3w/ny7QP53//ouZo7hSLY3GIcRKgvdjjVM3OW5C3WLpOq5Dez5GWVJ17aeFCfGQ8bwFKde6qfYqyGcU9xHB36TtVHB9hSFP/tUFhkiSOxtsrYwCgCyXm4UTSpP+wiNyjKfFw7qGLBvA2hGTNw8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAh9PeAqPRQk1/SSygIFADZBi08O/DPCshFwEHvJATIcTzcDD8UGAjXh+H5OlkDyX7KyrcaNvYaafCUo63A+WprdtdY5Ty6SBEwTYyiQyQfwM9BfK+imCoif1Ai7xAelD7p9lNazWq7JU+H/Ep7U7Q7LvpxAbK0JArt+IWTb2NcMb3OWE1r0gFbs44O1l6W9UbJTbyLMzbGbe5i+NHlgnwPwuhtRMh0NUYabGHKcHbhwyFhfGAQv2dAp5KF1E5gu6ZzCiFePzc0FrqXQyb2zpFYcJHXquiqaOeG7cZxRHYcjrl10Vxzki64XVA9BpdELgKSnupDGUEJsRUt3WVOmvZuA==",
|
||||
"url": "https://passbook.company/application/saml/awx/login/",
|
||||
"attr_last_name": "User.LastName",
|
||||
"entity_id": "https://awx.company/sso/metadata/saml/",
|
||||
"attr_email": "urn:oid:0.9.2342.19200300.100.1.3",
|
||||
"attr_first_name": "urn:oid:2.5.4.3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`x509cert` is the Certificate configured in passbook. Remove the --BEGIN CERTIFICATE-- and --END CERTIFICATE-- headers, then enter the cert as one non-breaking string.
|
33
docs/k8s/deployment.yml
Normal file
33
docs/k8s/deployment.yml
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
apiVersion: apps/v1beta2
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: passbook-docs
|
||||
namespace: prod-passbook-docs
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
app.kubernetes.io/managed-by: passbook-docs
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
spec:
|
||||
containers:
|
||||
- name: passbook-docs
|
||||
image: "beryju/passbook-docs:latest"
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
resources:
|
||||
limits:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 20Mi
|
21
docs/k8s/ingress.yml
Normal file
21
docs/k8s/ingress.yml
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
name: passbook-docs
|
||||
namespace: prod-passbook-docs
|
||||
spec:
|
||||
rules:
|
||||
- host: docs.passbook.beryju.org
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: passbook-docs-http
|
||||
servicePort: http
|
||||
path: /
|
||||
tls:
|
||||
- hosts:
|
||||
- docs.passbook.beryju.org
|
||||
secretName: passbook-docs-acme
|
17
docs/k8s/service.yml
Normal file
17
docs/k8s/service.yml
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: passbook-docs-http
|
||||
namespace: prod-passbook-docs
|
||||
labels:
|
||||
app.kubernetes.io/name: passbook-docs
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app.kubernetes.io/name: passbook-docs
|
19
docs/policies/expression/index.md
Normal file
19
docs/policies/expression/index.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Expression Policy
|
||||
|
||||
Expression Policies allows you to write custom Policy Logic using Jinja2 Templating language.
|
||||
|
||||
For a language reference, see [here](https://jinja.palletsprojects.com/en/2.11.x/templates/).
|
||||
|
||||
The following objects are passed into the variable:
|
||||
|
||||
- `request`: A PolicyRequest object, which has the following properties:
|
||||
- `request.user`: The current User, which the Policy is applied against. ([ref](../../property-mappings/reference/user-object.md))
|
||||
- `request.http_request`: The Django HTTP Request, as documented [here](https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects).
|
||||
- `request.obj`: A Django Model instance. This is only set if the Policy is ran against an object.
|
||||
- `pb_is_sso_flow`: Boolean which is true if request was initiated by authenticating through an external Provider.
|
||||
- `pb_is_group_member(user, group_name)`: Function which checks if `user` is member of a Group with Name `gorup_name`.
|
||||
|
||||
There are also the following custom filters available:
|
||||
|
||||
- `regex_match(regex)`: Return True if value matches `regex`
|
||||
- `regex_replace(regex, repl)`: Replace string matched by `regex` with `repl`
|
50
docs/policies/index.md
Normal file
50
docs/policies/index.md
Normal file
@ -0,0 +1,50 @@
|
||||
# Policies
|
||||
|
||||
## Kinds
|
||||
|
||||
There are two different Kind of policies, a Standard Policy and a Password Policy. Normal Policies just evaluate to True or False, and can be used everywhere. Password Policies apply when a Password is set (during User enrollment, Recovery or anywhere else). These policies can be used to apply Password Rules like length, etc. The can also be used to expire passwords after a certain amount of time.
|
||||
|
||||
## Standard Policies
|
||||
|
||||
---
|
||||
|
||||
### Group-Membership Policy
|
||||
|
||||
This policy evaluates to True if the current user is a Member of the selected group.
|
||||
|
||||
### Reputation Policy
|
||||
|
||||
passbook keeps track of failed login attempts by Source IP and Attempted Username. These values are saved as scores. Each failed login decreases the Score for the Client IP as well as the targeted Username by one.
|
||||
|
||||
This policy can be used to for example prompt Clients with a low score to pass a Captcha before they can continue.
|
||||
|
||||
## Expression Policy
|
||||
|
||||
See [Expression Policy](expression/index.md).
|
||||
|
||||
### Webhook Policy
|
||||
|
||||
This policy allows you to send an arbitrary HTTP Request to any URL. You can then use JSONPath to extract the result you need.
|
||||
|
||||
## Password Policies
|
||||
|
||||
---
|
||||
|
||||
### Password Policy
|
||||
|
||||
This Policy allows you to specify Password rules, like Length and required Characters.
|
||||
The following rules can be set:
|
||||
|
||||
- Minimum amount of Uppercase Characters
|
||||
- Minimum amount of Lowercase Characters
|
||||
- Minimum amount of Symbols Characters
|
||||
- Minimum Length
|
||||
- Symbol charset (define which characters are counted as symbols)
|
||||
|
||||
### Have I Been Pwned Policy
|
||||
|
||||
This Policy checks the hashed Password against the [Have I Been Pwned](https://haveibeenpwned.com/) API. This only sends the first 5 characters of the hashed password. The remaining comparison is done within passbook.
|
||||
|
||||
### Password-Expiry Policy
|
||||
|
||||
This policy can enforce regular password rotation by expiring set Passwords after a finite amount of time. This forces users to set a new password.
|
21
docs/property-mappings/index.md
Normal file
21
docs/property-mappings/index.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Property Mappings
|
||||
|
||||
Property Mappings allow you to pass information to external Applications. For example, pass the current user's Groups as a SAML Parameter. Property Mappings are also used to map Source fields to passbook fields, for example when using LDAP.
|
||||
|
||||
## SAML Property Mapping
|
||||
|
||||
SAML Property Mappings allow you embed Information into the SAML AuthN Request. THis Information can then be used by the Application to assign permissions for example.
|
||||
|
||||
You can find examples [here](integrations/)
|
||||
|
||||
## LDAP Property Mapping
|
||||
|
||||
LDAP Property Mappings are used when you define a LDAP Source. These Mappings define which LDAP Property maps to which passbook Property. By default, these mappings are created:
|
||||
|
||||
- Autogenerated LDAP Mapping: givenName -> first_name
|
||||
- Autogenerated LDAP Mapping: mail -> email
|
||||
- Autogenerated LDAP Mapping: name -> name
|
||||
- Autogenerated LDAP Mapping: sAMAccountName -> username
|
||||
- Autogenerated LDAP Mapping: sn -> last_name
|
||||
|
||||
These are configured for the most common LDAP Setups.
|
20
docs/property-mappings/reference/user-object.md
Normal file
20
docs/property-mappings/reference/user-object.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Passbook User Object
|
||||
|
||||
The User object has the following attributes:
|
||||
|
||||
- `username`: User's Username
|
||||
- `email` User's E-Mail
|
||||
- `name` User's Display Name
|
||||
- `is_staff` Boolean field if user is staff
|
||||
- `is_active` Boolean field if user is active
|
||||
- `date_joined` Date User joined/was created
|
||||
- `password_change_date` Date Password was last changed
|
||||
- `attributes` Dynamic Attributes
|
||||
|
||||
## Examples
|
||||
|
||||
List all the User's Group Names
|
||||
|
||||
```jinja2
|
||||
[{% for group in user.groups.all() %}'{{ group.name }}',{% endfor %}]
|
||||
```
|
17
docs/providers.md
Normal file
17
docs/providers.md
Normal file
@ -0,0 +1,17 @@
|
||||
# Providers
|
||||
|
||||
Providers allow external Applications to authenticate against passbook and use its User Information.
|
||||
|
||||
## OpenID Provider
|
||||
|
||||
This provider uses the commonly used OpenID Connect variation of OAuth2.
|
||||
|
||||
## OAuth2 Provider
|
||||
|
||||
This provider is slightly different than the OpenID Provider. While it uses the same basic OAuth2 Protocol, it provides a GitHub-compatible Endpoint. This allows you to integrate Applications, which don't support Custom OpenID Providers.
|
||||
The API exposes Username, E-Mail, Name and Groups in a GitHub-compatible format.
|
||||
|
||||
## SAML Provider
|
||||
|
||||
This provider allows you to integrate Enterprise Software using the SAML2 Protocol. It supports signed Requests. This Provider uses [Property Mappings](property-mappings/index.md#saml-property-mapping) to determine which fields are exposed and what values they return. This makes it possible to expose Vendor-specific Fields.
|
||||
Default fields are exposed through Auto-generated Property Mappings, which are prefixed with "Autogenerated..."
|
39
docs/sources.md
Normal file
39
docs/sources.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Sources
|
||||
|
||||
Sources allow you to connect passbook to an existing User directory. They can also be used for Social-Login, using external Providers like Facebook, Twitter, etc.
|
||||
|
||||
## Generic OAuth Source
|
||||
|
||||
**All Integration-specific Sources are documented in the Integrations Section**
|
||||
|
||||
This source allows users to enroll themselves with an External OAuth-based Identity Provider. The Generic Provider expects the Endpoint to return OpenID-Connect compatible Information. Vendor specific Implementations have their own OAuth Source.
|
||||
|
||||
- Policies: Allow/Forbid Users from linking their Accounts with this Provider
|
||||
- Request Token URL: This field is used for OAuth v1 Implementations and will be provided by the Provider.
|
||||
- Authorization URL: This value will be provided by the Provider.
|
||||
- Access Token URL: This value will be provided by the Provider.
|
||||
- Profile URL: This URL is called by passbook to retrieve User information upon successful authentication.
|
||||
- Consumer key/Consumer secret: These values will be provided by the Provider.
|
||||
|
||||
## SAML Source
|
||||
|
||||
This source allows passbook to act as a SAML Service Provider. Just like the SAML Provider, it supports signed Requests. Vendor specific documentation can be found in the Integrations Section
|
||||
|
||||
## LDAP Source
|
||||
|
||||
This source allows you to import Users and Groups from an LDAP Server
|
||||
|
||||
- Server URI: URI to your LDAP Server/Domain Controller
|
||||
- Bind CN: CN to bind as, this can also be a UPN in the format of `user@domain.tld`
|
||||
- Bind password: Password used during the bind process
|
||||
- Enable Start TLS: Enables StartTLS functionality. To use SSL instead, use port `636`
|
||||
- Base DN: Base DN used for all LDAP queries
|
||||
- Addition User DN: Prepended to Base DN for User-queries.
|
||||
- Addition Group DN: Prepended to Base DN for Group-queries.
|
||||
- User object filter: Consider Objects matching this filter to be Users.
|
||||
- Group object filter: Consider Objects matching this filter to be Groups.
|
||||
- User group membership field: Field which contains Groups of user.
|
||||
- Object uniqueness field: Field which contains a unique Identifier.
|
||||
- Sync groups: Enable/disable Group synchronization. Groups are synced in the background every 5 minutes.
|
||||
- Sync parent group: Optionally set this Group as parent Group for all synced Groups (allows you to, for example, import AD Groups under a root `imported-from-ad` group.)
|
||||
- Property mappings: Define which LDAP Properties map to which passbook Properties. The default set of Property Mappings is generated for Active Directory. See also [LDAP Property Mappings](property-mappings/index.md#ldap-property-mapping)
|
@ -6,4 +6,4 @@ dependencies:
|
||||
repository: https://kubernetes-charts.storage.googleapis.com/
|
||||
version: 9.5.1
|
||||
digest: sha256:f18b5dc8d0be13d584407405c60d10b6b84d25f7fa8aaa3dd0e5385c38f5c516
|
||||
generated: "2019-11-07T10:23:07.259176+01:00"
|
||||
generated: "2019-12-14T13:33:48.4341939Z"
|
@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
appVersion: "0.7.3-beta"
|
||||
appVersion: "0.8.1-beta"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.7.3-beta"
|
||||
version: "0.8.1-beta"
|
||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
@ -12,5 +12,5 @@ data:
|
||||
host: "{{ .Release.Name }}-redis-master"
|
||||
cache_db: 0
|
||||
message_queue_db: 1
|
||||
error_report_enabled: {{ .Values.config.error_reporting }}
|
||||
error_reporting: {{ .Values.config.error_reporting }}
|
||||
domain: ".{{ index .Values.ingress.hosts 0 }}"
|
@ -21,7 +21,7 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}-static
|
||||
image: "docker.beryju.org/passbook/static:{{ .Values.image.tag }}"
|
||||
image: "beryju/passbook-static:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
@ -31,13 +31,13 @@ spec:
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 5
|
||||
httpGet:
|
||||
path: /_/healthz
|
||||
path: /-/ping
|
||||
port: http
|
||||
readinessProbe:
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 5
|
||||
httpGet:
|
||||
path: /_/healthz
|
||||
path: /-/ping
|
||||
port: http
|
||||
resources:
|
||||
requests:
|
@ -26,7 +26,7 @@ spec:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
initContainers:
|
||||
- name: passbook-database-migrations
|
||||
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
|
||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||
command:
|
||||
- ./manage.py
|
||||
args:
|
||||
@ -56,7 +56,7 @@ spec:
|
||||
key: postgresql-password
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
|
||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- uwsgi
|
@ -18,6 +18,7 @@ spec:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
key: monitoring_username
|
||||
port: http
|
||||
path: /metrics/
|
||||
interval: 10s
|
||||
selector:
|
||||
matchLabels:
|
@ -26,7 +26,7 @@ spec:
|
||||
name: {{ include "passbook.fullname" . }}-config
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
|
||||
image: "beryju/passbook:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- celery
|
@ -2,7 +2,7 @@
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
image:
|
||||
tag: 0.7.3-beta
|
||||
tag: 0.8.1-beta
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
@ -10,7 +10,7 @@ config:
|
||||
# Optionally specify fixed secret_key, otherwise generated automatically
|
||||
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
|
||||
# Enable error reporting
|
||||
error_reporting: true
|
||||
error_reporting: false
|
||||
email:
|
||||
host: localhost
|
||||
|
||||
@ -42,3 +42,5 @@ redis:
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
39
mkdocs.yml
Normal file
39
mkdocs.yml
Normal file
@ -0,0 +1,39 @@
|
||||
site_name: passbook Docs
|
||||
site_url: https://beryju.github.io/passbook
|
||||
copyright: "Copyright © 2019 - 2020 BeryJu.org"
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Installation:
|
||||
- Installation: installation/install.md
|
||||
- docker-compose: installation/docker-compose.md
|
||||
- Kubernetes: installation/kubernetes.md
|
||||
- Sources: sources.md
|
||||
- Providers: providers.md
|
||||
- Property Mappings:
|
||||
- Overview: property-mappings/index.md
|
||||
- Reference:
|
||||
- User Object: property-mappings/reference/user-object.md
|
||||
- Factors: factors.md
|
||||
- Policies:
|
||||
- Overview: policies/index.md
|
||||
- Expression: policies/expression/index.md
|
||||
- Integrations:
|
||||
- as Provider:
|
||||
- Amazon Web Services: integrations/services/aws/index.md
|
||||
- GitLab: integrations/services/gitlab/index.md
|
||||
- Rancher: integrations/services/rancher/index.md
|
||||
- Harbor: integrations/services/harbor/index.md
|
||||
- Sentry: integrations/services/sentry/index.md
|
||||
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
|
||||
|
||||
repo_name: "BeryJu.org/passbook"
|
||||
repo_url: https://github.com/BeryJu/passbook
|
||||
theme:
|
||||
name: "material"
|
||||
logo: "images/logo.svg"
|
||||
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
permalink: "¶"
|
||||
- admonition
|
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = '0.7.3-beta'
|
||||
__version__ = "0.8.1-beta"
|
||||
|
@ -5,7 +5,7 @@ from django.apps import AppConfig
|
||||
class PassbookAdminConfig(AppConfig):
|
||||
"""passbook admin app config"""
|
||||
|
||||
name = 'passbook.admin'
|
||||
label = 'passbook_admin'
|
||||
mountpoint = 'administration/'
|
||||
verbose_name = 'passbook Admin'
|
||||
name = "passbook.admin"
|
||||
label = "passbook_admin"
|
||||
mountpoint = "administration/"
|
||||
verbose_name = "passbook Admin"
|
||||
|
@ -16,7 +16,7 @@ class YAMLField(forms.CharField):
|
||||
"""Django's JSON Field converted to YAML"""
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _("'%(value)s' value must be valid YAML."),
|
||||
"invalid": _("'%(value)s' value must be valid YAML."),
|
||||
}
|
||||
widget = forms.Textarea
|
||||
|
||||
@ -31,9 +31,7 @@ class YAMLField(forms.CharField):
|
||||
converted = yaml.safe_load(value)
|
||||
except yaml.YAMLError:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid'],
|
||||
code='invalid',
|
||||
params={'value': value},
|
||||
self.error_messages["invalid"], code="invalid", params={"value": value},
|
||||
)
|
||||
if isinstance(converted, str):
|
||||
return YAMLString(converted)
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""p2 form helpers"""
|
||||
"""passbook form helpers"""
|
||||
from django import forms
|
||||
|
||||
from passbook.admin.fields import YAMLField
|
||||
@ -9,29 +9,32 @@ class TagModelForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# Check if we have an instance, load tags otherwise use an empty dict
|
||||
instance = kwargs.get('instance', None)
|
||||
instance = kwargs.get("instance", None)
|
||||
tags = instance.tags if instance else {}
|
||||
# Make sure all predefined tags exist in tags, and set default if they don't
|
||||
predefined_tags = self._meta.model().get_predefined_tags() # pylint: disable=no-member
|
||||
predefined_tags = (
|
||||
self._meta.model().get_predefined_tags() # pylint: disable=no-member
|
||||
)
|
||||
for key, value in predefined_tags.items():
|
||||
if key not in tags:
|
||||
tags[key] = value
|
||||
# Format JSON
|
||||
kwargs['initial']['tags'] = tags
|
||||
kwargs["initial"]["tags"] = tags
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_tags(self):
|
||||
"""Make sure all required tags are set"""
|
||||
if hasattr(self.instance, 'get_required_keys') and hasattr(self.instance, 'tags'):
|
||||
if hasattr(self.instance, "get_required_keys") and hasattr(
|
||||
self.instance, "tags"
|
||||
):
|
||||
for key in self.instance.get_required_keys():
|
||||
if key not in self.cleaned_data.get('tags'):
|
||||
if key not in self.cleaned_data.get("tags"):
|
||||
raise forms.ValidationError("Tag %s missing." % key)
|
||||
return self.cleaned_data.get('tags')
|
||||
return self.cleaned_data.get("tags")
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
class TagModelFormMeta:
|
||||
"""Base Meta class that uses the YAMLField"""
|
||||
|
||||
field_classes = {
|
||||
'tags': YAMLField
|
||||
}
|
||||
field_classes = {"tags": YAMLField}
|
||||
|
@ -1,7 +1,7 @@
|
||||
"""passbook core source form fields"""
|
||||
# from django import forms
|
||||
|
||||
SOURCE_FORM_FIELDS = ['name', 'slug', 'enabled', 'policies']
|
||||
SOURCE_SERIALIZER_FIELDS = ['pk', 'name', 'slug', 'enabled', 'policies']
|
||||
SOURCE_FORM_FIELDS = ["name", "slug", "enabled", "policies"]
|
||||
SOURCE_SERIALIZER_FIELDS = ["pk", "name", "slug", "enabled", "policies"]
|
||||
|
||||
# class SourceForm(forms.Form)
|
||||
|
@ -12,10 +12,10 @@ class UserForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
model = User
|
||||
fields = ['username', 'name', 'email', 'is_staff', 'is_active', 'attributes']
|
||||
fields = ["username", "name", "email", "is_staff", "is_active", "attributes"]
|
||||
widgets = {
|
||||
'name': forms.TextInput,
|
||||
"name": forms.TextInput,
|
||||
}
|
||||
field_classes = {
|
||||
'attributes': YAMLField,
|
||||
"attributes": YAMLField,
|
||||
}
|
||||
|
@ -11,15 +11,16 @@ def impersonate(get_response):
|
||||
|
||||
# User is superuser and has __impersonate ID set
|
||||
if request.user.is_superuser and "__impersonate" in request.GET:
|
||||
request.session['impersonate_id'] = request.GET["__impersonate"]
|
||||
request.session["impersonate_id"] = request.GET["__impersonate"]
|
||||
# user wants to stop impersonation
|
||||
elif "__unimpersonate" in request.GET and 'impersonate_id' in request.session:
|
||||
del request.session['impersonate_id']
|
||||
elif "__unimpersonate" in request.GET and "impersonate_id" in request.session:
|
||||
del request.session["impersonate_id"]
|
||||
|
||||
# Actually impersonate user
|
||||
if request.user.is_superuser and 'impersonate_id' in request.session:
|
||||
request.user = User.objects.get(pk=request.session['impersonate_id'])
|
||||
if request.user.is_superuser and "impersonate_id" in request.session:
|
||||
request.user = User.objects.get(pk=request.session["impersonate_id"])
|
||||
|
||||
response = get_response(request)
|
||||
return response
|
||||
|
||||
return middleware
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""passbook admin settings"""
|
||||
|
||||
MIDDLEWARE = [
|
||||
'passbook.admin.middleware.impersonate',
|
||||
"passbook.admin.middleware.impersonate",
|
||||
]
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends "generic/form.html" %}
|
||||
{% extends base_template|default:"generic/form.html" %}
|
||||
|
||||
{% load utils %}
|
||||
{% load i18n %}
|
||||
|
@ -20,6 +20,7 @@
|
||||
<link rel="stylesheet" href="{% static 'codemirror/lib/codemirror.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'codemirror/theme/monokai.css' %}">
|
||||
<script src="{% static 'codemirror/mode/yaml/yaml.js' %}"></script>
|
||||
<script src="{% static 'codemirror/mode/jinja2/jinja2.js' %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@ -29,21 +30,33 @@
|
||||
<div class="">
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% include 'partials/form.html' with form=form %}
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<a class="btn btn-default" href="{% back %}">{% trans "Cancel" %}</a>
|
||||
<input type="submit" class="btn btn-primary" value="{% block action %}{% endblock %}" />
|
||||
</form>
|
||||
</div>
|
||||
{% block beneath_form %}
|
||||
{% endblock %}
|
||||
<script>
|
||||
let attributes = document.getElementsByName('attributes');
|
||||
const attributes = document.getElementsByName('attributes');
|
||||
if (attributes.length > 0) {
|
||||
let myCodeMirror = CodeMirror.fromTextArea(attributes[0], {
|
||||
// https://github.com/codemirror/CodeMirror/issues/5092
|
||||
attributes[0].removeAttribute("required");
|
||||
const attributesCM = CodeMirror.fromTextArea(attributes[0], {
|
||||
mode: 'yaml',
|
||||
theme: 'monokai',
|
||||
lineNumbers: true,
|
||||
});
|
||||
}
|
||||
const expressions = document.getElementsByName('expression');
|
||||
if (expressions.length > 0) {
|
||||
// https://github.com/codemirror/CodeMirror/issues/5092
|
||||
expressions[0].removeAttribute("required");
|
||||
const expressionCM = CodeMirror.fromTextArea(expressions[0], {
|
||||
mode: 'jinja2',
|
||||
theme: 'monokai',
|
||||
lineNumbers: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% extends "generic/form.html" %}
|
||||
{% extends base_template|default:"generic/form.html" %}
|
||||
|
||||
{% load utils %}
|
||||
{% load i18n %}
|
||||
|
@ -10,20 +10,23 @@ from passbook.lib.utils.template import render_to_string
|
||||
register = template.Library()
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
@register.simple_tag()
|
||||
def get_links(model_instance):
|
||||
"""Find all link_ methods on an object instance, run them and return as dict"""
|
||||
prefix = 'link_'
|
||||
prefix = "link_"
|
||||
links = {}
|
||||
|
||||
if not isinstance(model_instance, Model):
|
||||
LOGGER.warning("Model %s is not instance of Model", model_instance)
|
||||
LOGGER.warning("Model is not instance of Model", model_instance=model_instance)
|
||||
return links
|
||||
|
||||
try:
|
||||
for name, method in inspect.getmembers(model_instance, predicate=inspect.ismethod):
|
||||
for name, method in inspect.getmembers(
|
||||
model_instance, predicate=inspect.ismethod
|
||||
):
|
||||
if name.startswith(prefix):
|
||||
human_name = name.replace(prefix, '').replace('_', ' ').capitalize()
|
||||
human_name = name.replace(prefix, "").replace("_", " ").capitalize()
|
||||
link = method()
|
||||
if link:
|
||||
links[human_name] = link
|
||||
@ -36,17 +39,19 @@ def get_links(model_instance):
|
||||
@register.simple_tag(takes_context=True)
|
||||
def get_htmls(context, model_instance):
|
||||
"""Find all html_ methods on an object instance, run them and return as dict"""
|
||||
prefix = 'html_'
|
||||
prefix = "html_"
|
||||
htmls = []
|
||||
|
||||
if not isinstance(model_instance, Model):
|
||||
LOGGER.warning("Model %s is not instance of Model", model_instance)
|
||||
LOGGER.warning("Model is not instance of Model", model_instance=model_instance)
|
||||
return htmls
|
||||
|
||||
try:
|
||||
for name, method in inspect.getmembers(model_instance, predicate=inspect.ismethod):
|
||||
for name, method in inspect.getmembers(
|
||||
model_instance, predicate=inspect.ismethod
|
||||
):
|
||||
if name.startswith(prefix):
|
||||
template, _context = method(context.get('request'))
|
||||
template, _context = method(context.get("request"))
|
||||
htmls.append(render_to_string(template, _context))
|
||||
except NotImplementedError:
|
||||
pass
|
||||
|
@ -1,82 +1,157 @@
|
||||
"""passbook URL Configuration"""
|
||||
from django.urls import path
|
||||
|
||||
from passbook.admin.views import (applications, audit, debug, factors, groups,
|
||||
invitations, overview, policy,
|
||||
property_mapping, providers, sources, users)
|
||||
from passbook.admin.views import (
|
||||
applications,
|
||||
audit,
|
||||
debug,
|
||||
factors,
|
||||
groups,
|
||||
invitations,
|
||||
overview,
|
||||
policy,
|
||||
property_mapping,
|
||||
providers,
|
||||
sources,
|
||||
users,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
path('', overview.AdministrationOverviewView.as_view(), name='overview'),
|
||||
path("", overview.AdministrationOverviewView.as_view(), name="overview"),
|
||||
# Applications
|
||||
path('applications/', applications.ApplicationListView.as_view(),
|
||||
name='applications'),
|
||||
path('applications/create/', applications.ApplicationCreateView.as_view(),
|
||||
name='application-create'),
|
||||
path('applications/<uuid:pk>/update/',
|
||||
applications.ApplicationUpdateView.as_view(), name='application-update'),
|
||||
path('applications/<uuid:pk>/delete/',
|
||||
applications.ApplicationDeleteView.as_view(), name='application-delete'),
|
||||
path(
|
||||
"applications/", applications.ApplicationListView.as_view(), name="applications"
|
||||
),
|
||||
path(
|
||||
"applications/create/",
|
||||
applications.ApplicationCreateView.as_view(),
|
||||
name="application-create",
|
||||
),
|
||||
path(
|
||||
"applications/<uuid:pk>/update/",
|
||||
applications.ApplicationUpdateView.as_view(),
|
||||
name="application-update",
|
||||
),
|
||||
path(
|
||||
"applications/<uuid:pk>/delete/",
|
||||
applications.ApplicationDeleteView.as_view(),
|
||||
name="application-delete",
|
||||
),
|
||||
# Sources
|
||||
path('sources/', sources.SourceListView.as_view(), name='sources'),
|
||||
path('sources/create/', sources.SourceCreateView.as_view(), name='source-create'),
|
||||
path('sources/<uuid:pk>/update/', sources.SourceUpdateView.as_view(), name='source-update'),
|
||||
path('sources/<uuid:pk>/delete/', sources.SourceDeleteView.as_view(), name='source-delete'),
|
||||
path("sources/", sources.SourceListView.as_view(), name="sources"),
|
||||
path("sources/create/", sources.SourceCreateView.as_view(), name="source-create"),
|
||||
path(
|
||||
"sources/<uuid:pk>/update/",
|
||||
sources.SourceUpdateView.as_view(),
|
||||
name="source-update",
|
||||
),
|
||||
path(
|
||||
"sources/<uuid:pk>/delete/",
|
||||
sources.SourceDeleteView.as_view(),
|
||||
name="source-delete",
|
||||
),
|
||||
# Policies
|
||||
path('policies/', policy.PolicyListView.as_view(), name='policies'),
|
||||
path('policies/create/', policy.PolicyCreateView.as_view(), name='policy-create'),
|
||||
path('policies/<uuid:pk>/update/', policy.PolicyUpdateView.as_view(), name='policy-update'),
|
||||
path('policies/<uuid:pk>/delete/', policy.PolicyDeleteView.as_view(), name='policy-delete'),
|
||||
path('policies/<uuid:pk>/test/', policy.PolicyTestView.as_view(), name='policy-test'),
|
||||
path("policies/", policy.PolicyListView.as_view(), name="policies"),
|
||||
path("policies/create/", policy.PolicyCreateView.as_view(), name="policy-create"),
|
||||
path(
|
||||
"policies/<uuid:pk>/update/",
|
||||
policy.PolicyUpdateView.as_view(),
|
||||
name="policy-update",
|
||||
),
|
||||
path(
|
||||
"policies/<uuid:pk>/delete/",
|
||||
policy.PolicyDeleteView.as_view(),
|
||||
name="policy-delete",
|
||||
),
|
||||
path(
|
||||
"policies/<uuid:pk>/test/", policy.PolicyTestView.as_view(), name="policy-test"
|
||||
),
|
||||
# Providers
|
||||
path('providers/', providers.ProviderListView.as_view(), name='providers'),
|
||||
path('providers/create/',
|
||||
providers.ProviderCreateView.as_view(), name='provider-create'),
|
||||
path('providers/<int:pk>/update/',
|
||||
providers.ProviderUpdateView.as_view(), name='provider-update'),
|
||||
path('providers/<int:pk>/delete/',
|
||||
providers.ProviderDeleteView.as_view(), name='provider-delete'),
|
||||
path("providers/", providers.ProviderListView.as_view(), name="providers"),
|
||||
path(
|
||||
"providers/create/",
|
||||
providers.ProviderCreateView.as_view(),
|
||||
name="provider-create",
|
||||
),
|
||||
path(
|
||||
"providers/<int:pk>/update/",
|
||||
providers.ProviderUpdateView.as_view(),
|
||||
name="provider-update",
|
||||
),
|
||||
path(
|
||||
"providers/<int:pk>/delete/",
|
||||
providers.ProviderDeleteView.as_view(),
|
||||
name="provider-delete",
|
||||
),
|
||||
# Factors
|
||||
path('factors/', factors.FactorListView.as_view(), name='factors'),
|
||||
path('factors/create/',
|
||||
factors.FactorCreateView.as_view(), name='factor-create'),
|
||||
path('factors/<uuid:pk>/update/',
|
||||
factors.FactorUpdateView.as_view(), name='factor-update'),
|
||||
path('factors/<uuid:pk>/delete/',
|
||||
factors.FactorDeleteView.as_view(), name='factor-delete'),
|
||||
path("factors/", factors.FactorListView.as_view(), name="factors"),
|
||||
path("factors/create/", factors.FactorCreateView.as_view(), name="factor-create"),
|
||||
path(
|
||||
"factors/<uuid:pk>/update/",
|
||||
factors.FactorUpdateView.as_view(),
|
||||
name="factor-update",
|
||||
),
|
||||
path(
|
||||
"factors/<uuid:pk>/delete/",
|
||||
factors.FactorDeleteView.as_view(),
|
||||
name="factor-delete",
|
||||
),
|
||||
# Factors
|
||||
path('property-mappings/', property_mapping.PropertyMappingListView.as_view(),
|
||||
name='property-mappings'),
|
||||
path('property-mappings/create/',
|
||||
property_mapping.PropertyMappingCreateView.as_view(), name='property-mapping-create'),
|
||||
path('property-mappings/<uuid:pk>/update/',
|
||||
property_mapping.PropertyMappingUpdateView.as_view(), name='property-mapping-update'),
|
||||
path('property-mappings/<uuid:pk>/delete/',
|
||||
property_mapping.PropertyMappingDeleteView.as_view(), name='property-mapping-delete'),
|
||||
path(
|
||||
"property-mappings/",
|
||||
property_mapping.PropertyMappingListView.as_view(),
|
||||
name="property-mappings",
|
||||
),
|
||||
path(
|
||||
"property-mappings/create/",
|
||||
property_mapping.PropertyMappingCreateView.as_view(),
|
||||
name="property-mapping-create",
|
||||
),
|
||||
path(
|
||||
"property-mappings/<uuid:pk>/update/",
|
||||
property_mapping.PropertyMappingUpdateView.as_view(),
|
||||
name="property-mapping-update",
|
||||
),
|
||||
path(
|
||||
"property-mappings/<uuid:pk>/delete/",
|
||||
property_mapping.PropertyMappingDeleteView.as_view(),
|
||||
name="property-mapping-delete",
|
||||
),
|
||||
# Invitations
|
||||
path('invitations/', invitations.InvitationListView.as_view(), name='invitations'),
|
||||
path('invitations/create/',
|
||||
invitations.InvitationCreateView.as_view(), name='invitation-create'),
|
||||
path('invitations/<uuid:pk>/delete/',
|
||||
invitations.InvitationDeleteView.as_view(), name='invitation-delete'),
|
||||
path("invitations/", invitations.InvitationListView.as_view(), name="invitations"),
|
||||
path(
|
||||
"invitations/create/",
|
||||
invitations.InvitationCreateView.as_view(),
|
||||
name="invitation-create",
|
||||
),
|
||||
path(
|
||||
"invitations/<uuid:pk>/delete/",
|
||||
invitations.InvitationDeleteView.as_view(),
|
||||
name="invitation-delete",
|
||||
),
|
||||
# Users
|
||||
path('users/', users.UserListView.as_view(),
|
||||
name='users'),
|
||||
path('users/create/', users.UserCreateView.as_view(), name='user-create'),
|
||||
path('users/<int:pk>/update/',
|
||||
users.UserUpdateView.as_view(), name='user-update'),
|
||||
path('users/<int:pk>/delete/',
|
||||
users.UserDeleteView.as_view(), name='user-delete'),
|
||||
path('users/<int:pk>/reset/',
|
||||
users.UserPasswordResetView.as_view(), name='user-password-reset'),
|
||||
path("users/", users.UserListView.as_view(), name="users"),
|
||||
path("users/create/", users.UserCreateView.as_view(), name="user-create"),
|
||||
path("users/<int:pk>/update/", users.UserUpdateView.as_view(), name="user-update"),
|
||||
path("users/<int:pk>/delete/", users.UserDeleteView.as_view(), name="user-delete"),
|
||||
path(
|
||||
"users/<int:pk>/reset/",
|
||||
users.UserPasswordResetView.as_view(),
|
||||
name="user-password-reset",
|
||||
),
|
||||
# Groups
|
||||
path('group/', groups.GroupListView.as_view(), name='group'),
|
||||
path('group/create/', groups.GroupCreateView.as_view(), name='group-create'),
|
||||
path('group/<uuid:pk>/update/', groups.GroupUpdateView.as_view(), name='group-update'),
|
||||
path('group/<uuid:pk>/delete/', groups.GroupDeleteView.as_view(), name='group-delete'),
|
||||
path("group/", groups.GroupListView.as_view(), name="group"),
|
||||
path("group/create/", groups.GroupCreateView.as_view(), name="group-create"),
|
||||
path(
|
||||
"group/<uuid:pk>/update/", groups.GroupUpdateView.as_view(), name="group-update"
|
||||
),
|
||||
path(
|
||||
"group/<uuid:pk>/delete/", groups.GroupDeleteView.as_view(), name="group-delete"
|
||||
),
|
||||
# Audit Log
|
||||
path('audit/', audit.EventListView.as_view(), name='audit-log'),
|
||||
path("audit/", audit.EventListView.as_view(), name="audit-log"),
|
||||
# Groups
|
||||
path('groups/', groups.GroupListView.as_view(), name='groups'),
|
||||
path("groups/", groups.GroupListView.as_view(), name="groups"),
|
||||
# Debug
|
||||
path('debug/request/', debug.DebugRequestView.as_view(), name='debug-request'),
|
||||
path("debug/request/", debug.DebugRequestView.as_view(), name="debug-request"),
|
||||
]
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""passbook Application 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.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
@ -18,55 +19,61 @@ class ApplicationListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all applications"""
|
||||
|
||||
model = Application
|
||||
permission_required = 'passbook_core.view_application'
|
||||
ordering = 'name'
|
||||
permission_required = "passbook_core.view_application"
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
template_name = 'administration/application/list.html'
|
||||
template_name = "administration/application/list.html"
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class ApplicationCreateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin, CreateAssignPermView):
|
||||
class ApplicationCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Application"""
|
||||
|
||||
model = Application
|
||||
form_class = ApplicationForm
|
||||
permission_required = 'passbook_core.add_application'
|
||||
permission_required = "passbook_core.add_application"
|
||||
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:applications')
|
||||
success_message = _('Successfully created Application')
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:applications")
|
||||
success_message = _("Successfully created Application")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['type'] = 'Application'
|
||||
kwargs["type"] = "Application"
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class ApplicationUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, UpdateView):
|
||||
class ApplicationUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update application"""
|
||||
|
||||
model = Application
|
||||
form_class = ApplicationForm
|
||||
permission_required = 'passbook_core.change_application'
|
||||
permission_required = "passbook_core.change_application"
|
||||
|
||||
template_name = 'generic/update.html'
|
||||
success_url = reverse_lazy('passbook_admin:applications')
|
||||
success_message = _('Successfully updated Application')
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:applications")
|
||||
success_message = _("Successfully updated Application")
|
||||
|
||||
|
||||
class ApplicationDeleteView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, DeleteView):
|
||||
class ApplicationDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
"""Delete application"""
|
||||
|
||||
model = Application
|
||||
permission_required = 'passbook_core.delete_application'
|
||||
permission_required = "passbook_core.delete_application"
|
||||
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:applications')
|
||||
success_message = _('Successfully deleted Application')
|
||||
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)
|
||||
|
@ -9,10 +9,10 @@ class EventListView(PermissionListMixin, ListView):
|
||||
"""Show list of all invitations"""
|
||||
|
||||
model = Event
|
||||
template_name = 'administration/audit/list.html'
|
||||
permission_required = 'passbook_audit.view_event'
|
||||
ordering = '-created'
|
||||
template_name = "administration/audit/list.html"
|
||||
permission_required = "passbook_audit.view_event"
|
||||
ordering = "-created"
|
||||
paginate_by = 10
|
||||
|
||||
def get_queryset(self):
|
||||
return Event.objects.all().order_by('-created')
|
||||
return Event.objects.all().order_by("-created")
|
||||
|
@ -6,10 +6,10 @@ from django.views.generic import TemplateView
|
||||
class DebugRequestView(LoginRequiredMixin, TemplateView):
|
||||
"""Show debug info about request"""
|
||||
|
||||
template_name = 'administration/debug/request.html'
|
||||
template_name = "administration/debug/request.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['request_dict'] = {}
|
||||
kwargs["request_dict"] = {}
|
||||
for key in dir(self.request):
|
||||
kwargs['request_dict'][key] = getattr(self.request, key)
|
||||
kwargs["request_dict"][key] = getattr(self.request, key)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""passbook Factor 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.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
@ -18,62 +19,69 @@ from passbook.lib.views import CreateAssignPermView
|
||||
def all_subclasses(cls):
|
||||
"""Recursively return all subclassess of cls"""
|
||||
return set(cls.__subclasses__()).union(
|
||||
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
|
||||
[s for c in cls.__subclasses__() for s in all_subclasses(c)]
|
||||
)
|
||||
|
||||
|
||||
class FactorListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all factors"""
|
||||
|
||||
model = Factor
|
||||
template_name = 'administration/factor/list.html'
|
||||
permission_required = 'passbook_core.view_factor'
|
||||
ordering = 'order'
|
||||
template_name = "administration/factor/list.html"
|
||||
permission_required = "passbook_core.view_factor"
|
||||
ordering = "order"
|
||||
paginate_by = 40
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['types'] = {
|
||||
x.__name__: x._meta.verbose_name for x in all_subclasses(Factor)}
|
||||
kwargs["types"] = {
|
||||
x.__name__: x._meta.verbose_name for x in all_subclasses(Factor)
|
||||
}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class FactorCreateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin, CreateAssignPermView):
|
||||
class FactorCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Factor"""
|
||||
|
||||
model = Factor
|
||||
template_name = 'generic/create.html'
|
||||
permission_required = 'passbook_core.add_factor'
|
||||
template_name = "generic/create.html"
|
||||
permission_required = "passbook_core.add_factor"
|
||||
|
||||
success_url = reverse_lazy('passbook_admin:factors')
|
||||
success_message = _('Successfully created Factor')
|
||||
success_url = reverse_lazy("passbook_admin:factors")
|
||||
success_message = _("Successfully created Factor")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
factor_type = self.request.GET.get('type')
|
||||
factor_type = self.request.GET.get("type")
|
||||
model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
|
||||
kwargs['type'] = model._meta.verbose_name
|
||||
kwargs["type"] = model._meta.verbose_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self):
|
||||
factor_type = self.request.GET.get('type')
|
||||
factor_type = self.request.GET.get("type")
|
||||
model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
|
||||
if not model:
|
||||
raise Http404
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class FactorUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, UpdateView):
|
||||
class FactorUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update factor"""
|
||||
|
||||
model = Factor
|
||||
permission_required = 'passbook_core.update_application'
|
||||
template_name = 'generic/update.html'
|
||||
success_url = reverse_lazy('passbook_admin:factors')
|
||||
success_message = _('Successfully updated Factor')
|
||||
permission_required = "passbook_core.update_application"
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:factors")
|
||||
success_message = _("Successfully updated Factor")
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
@ -81,21 +89,26 @@ class FactorUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
|
||||
return (
|
||||
Factor.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
|
||||
class FactorDeleteView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, DeleteView):
|
||||
class FactorDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
"""Delete factor"""
|
||||
|
||||
model = Factor
|
||||
template_name = 'generic/delete.html'
|
||||
permission_required = 'passbook_core.delete_factor'
|
||||
success_url = reverse_lazy('passbook_admin:factors')
|
||||
success_message = _('Successfully deleted Factor')
|
||||
template_name = "generic/delete.html"
|
||||
permission_required = "passbook_core.delete_factor"
|
||||
success_url = reverse_lazy("passbook_admin:factors")
|
||||
success_message = _("Successfully deleted Factor")
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
|
||||
return (
|
||||
Factor.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""passbook Group 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.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import ugettext as _
|
||||
@ -18,40 +19,45 @@ class GroupListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all groups"""
|
||||
|
||||
model = Group
|
||||
permission_required = 'passbook_core.view_group'
|
||||
ordering = 'name'
|
||||
permission_required = "passbook_core.view_group"
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
template_name = 'administration/group/list.html'
|
||||
template_name = "administration/group/list.html"
|
||||
|
||||
|
||||
class GroupCreateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin, CreateAssignPermView):
|
||||
class GroupCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Group"""
|
||||
|
||||
model = Group
|
||||
form_class = GroupForm
|
||||
permission_required = 'passbook_core.add_group'
|
||||
permission_required = "passbook_core.add_group"
|
||||
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:groups')
|
||||
success_message = _('Successfully created Group')
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:groups")
|
||||
success_message = _("Successfully created Group")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['type'] = 'Group'
|
||||
kwargs["type"] = "Group"
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
class GroupUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, UpdateView):
|
||||
class GroupUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update group"""
|
||||
|
||||
model = Group
|
||||
form_class = GroupForm
|
||||
permission_required = 'passbook_core.change_group'
|
||||
permission_required = "passbook_core.change_group"
|
||||
|
||||
template_name = 'generic/update.html'
|
||||
success_url = reverse_lazy('passbook_admin:groups')
|
||||
success_message = _('Successfully updated Group')
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:groups")
|
||||
success_message = _("Successfully updated Group")
|
||||
|
||||
|
||||
class GroupDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView):
|
||||
@ -59,9 +65,9 @@ class GroupDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView):
|
||||
|
||||
model = Group
|
||||
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:groups')
|
||||
success_message = _('Successfully deleted 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)
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""passbook Invitation 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.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import reverse_lazy
|
||||
@ -20,47 +21,49 @@ class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all invitations"""
|
||||
|
||||
model = Invitation
|
||||
permission_required = 'passbook_core.view_invitation'
|
||||
template_name = 'administration/invitation/list.html'
|
||||
permission_required = "passbook_core.view_invitation"
|
||||
template_name = "administration/invitation/list.html"
|
||||
|
||||
|
||||
class InvitationCreateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin, CreateAssignPermView):
|
||||
class InvitationCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Invitation"""
|
||||
|
||||
model = Invitation
|
||||
form_class = InvitationForm
|
||||
permission_required = 'passbook_core.add_invitation'
|
||||
permission_required = "passbook_core.add_invitation"
|
||||
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:invitations')
|
||||
success_message = _('Successfully created Invitation')
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:invitations")
|
||||
success_message = _("Successfully created Invitation")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['type'] = 'Invitation'
|
||||
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
|
||||
obj.save()
|
||||
invitation_created.send(
|
||||
sender=self,
|
||||
request=self.request,
|
||||
invitation=obj)
|
||||
invitation_created.send(sender=self, request=self.request, invitation=obj)
|
||||
return HttpResponseRedirect(self.success_url)
|
||||
|
||||
|
||||
class InvitationDeleteView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, DeleteView):
|
||||
class InvitationDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
"""Delete invitation"""
|
||||
|
||||
model = Invitation
|
||||
permission_required = 'passbook_core.delete_invitation'
|
||||
permission_required = "passbook_core.delete_invitation"
|
||||
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:invitations')
|
||||
success_message = _('Successfully deleted Invitation')
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:invitations")
|
||||
success_message = _("Successfully deleted Invitation")
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
|
@ -5,34 +5,45 @@ from django.views.generic import TemplateView
|
||||
|
||||
from passbook import __version__
|
||||
from passbook.admin.mixins import AdminRequiredMixin
|
||||
from passbook.core.models import (Application, Factor, Invitation, Policy,
|
||||
Provider, Source, User)
|
||||
from passbook.core.models import (
|
||||
Application,
|
||||
Factor,
|
||||
Invitation,
|
||||
Policy,
|
||||
Provider,
|
||||
Source,
|
||||
User,
|
||||
)
|
||||
from passbook.root.celery import CELERY_APP
|
||||
|
||||
|
||||
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
|
||||
"""Overview View"""
|
||||
|
||||
template_name = 'administration/overview.html'
|
||||
template_name = "administration/overview.html"
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
"""Handle post (clear cache from modal)"""
|
||||
if 'clear' in self.request.POST:
|
||||
if "clear" in self.request.POST:
|
||||
cache.clear()
|
||||
return redirect(reverse('passbook_core:auth-login'))
|
||||
return redirect(reverse("passbook_core:auth-login"))
|
||||
return self.get(*args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['application_count'] = len(Application.objects.all())
|
||||
kwargs['policy_count'] = len(Policy.objects.all())
|
||||
kwargs['user_count'] = len(User.objects.all())
|
||||
kwargs['provider_count'] = len(Provider.objects.all())
|
||||
kwargs['source_count'] = len(Source.objects.all())
|
||||
kwargs['factor_count'] = len(Factor.objects.all())
|
||||
kwargs['invitation_count'] = len(Invitation.objects.all())
|
||||
kwargs['version'] = __version__
|
||||
kwargs['worker_count'] = len(CELERY_APP.control.ping(timeout=0.5))
|
||||
kwargs['providers_without_application'] = Provider.objects.filter(application=None)
|
||||
kwargs['policies_without_attachment'] = len(Policy.objects.filter(policymodel__isnull=True))
|
||||
kwargs['cached_policies'] = len(cache.keys('policy_*'))
|
||||
kwargs["application_count"] = len(Application.objects.all())
|
||||
kwargs["policy_count"] = len(Policy.objects.all())
|
||||
kwargs["user_count"] = len(User.objects.all())
|
||||
kwargs["provider_count"] = len(Provider.objects.all())
|
||||
kwargs["source_count"] = len(Source.objects.all())
|
||||
kwargs["factor_count"] = len(Factor.objects.all())
|
||||
kwargs["invitation_count"] = len(Invitation.objects.all())
|
||||
kwargs["version"] = __version__
|
||||
kwargs["worker_count"] = len(CELERY_APP.control.ping(timeout=0.5))
|
||||
kwargs["providers_without_application"] = Provider.objects.filter(
|
||||
application=None
|
||||
)
|
||||
kwargs["policies_without_attachment"] = len(
|
||||
Policy.objects.filter(policymodel__isnull=True)
|
||||
)
|
||||
kwargs["cached_policies"] = len(cache.keys("policy_*"))
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""passbook Policy 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.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
@ -22,49 +23,68 @@ class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all policies"""
|
||||
|
||||
model = Policy
|
||||
permission_required = 'passbook_core.view_policy'
|
||||
permission_required = "passbook_core.view_policy"
|
||||
|
||||
template_name = 'administration/policy/list.html'
|
||||
template_name = "administration/policy/list.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['types'] = {
|
||||
x.__name__: x._meta.verbose_name for x in Policy.__subclasses__()}
|
||||
kwargs["types"] = {
|
||||
x.__name__: x._meta.verbose_name for x in Policy.__subclasses__()
|
||||
}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().order_by('order').select_subclasses()
|
||||
return super().get_queryset().order_by("order").select_subclasses()
|
||||
|
||||
|
||||
class PolicyCreateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin, CreateAssignPermView):
|
||||
class PolicyCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Policy"""
|
||||
|
||||
model = Policy
|
||||
permission_required = 'passbook_core.add_policy'
|
||||
permission_required = "passbook_core.add_policy"
|
||||
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:policies')
|
||||
success_message = _('Successfully created Policy')
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:policies")
|
||||
success_message = _("Successfully created Policy")
|
||||
|
||||
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):
|
||||
policy_type = self.request.GET.get('type')
|
||||
model = next(x for x in Policy.__subclasses__()
|
||||
if x.__name__ == policy_type)
|
||||
policy_type = self.request.GET.get("type")
|
||||
model = next(x for x in Policy.__subclasses__() if x.__name__ == policy_type)
|
||||
if not model:
|
||||
raise Http404
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class PolicyUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, UpdateView):
|
||||
class PolicyUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update policy"""
|
||||
|
||||
model = Policy
|
||||
permission_required = 'passbook_core.change_policy'
|
||||
permission_required = "passbook_core.change_policy"
|
||||
|
||||
template_name = 'generic/update.html'
|
||||
success_url = reverse_lazy('passbook_admin:policies')
|
||||
success_message = _('Successfully updated Policy')
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:policies")
|
||||
success_message = _("Successfully updated Policy")
|
||||
|
||||
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
|
||||
@ -72,22 +92,27 @@ class PolicyUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
|
||||
return (
|
||||
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
|
||||
class PolicyDeleteView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, DeleteView):
|
||||
class PolicyDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
"""Delete policy"""
|
||||
|
||||
model = Policy
|
||||
permission_required = 'passbook_core.delete_policy'
|
||||
permission_required = "passbook_core.delete_policy"
|
||||
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:policies')
|
||||
success_message = _('Successfully deleted Policy')
|
||||
template_name = "generic/delete.html"
|
||||
success_url = reverse_lazy("passbook_admin:policies")
|
||||
success_message = _("Successfully deleted Policy")
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
|
||||
return (
|
||||
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
messages.success(self.request, self.success_message)
|
||||
@ -99,15 +124,17 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo
|
||||
|
||||
model = Policy
|
||||
form_class = PolicyTestForm
|
||||
permission_required = 'passbook_core.view_policy'
|
||||
template_name = 'administration/policy/test.html'
|
||||
permission_required = "passbook_core.view_policy"
|
||||
template_name = "administration/policy/test.html"
|
||||
object = None
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
|
||||
return (
|
||||
Policy.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['policy'] = self.get_object()
|
||||
kwargs["policy"] = self.get_object()
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
@ -116,13 +143,13 @@ class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, Fo
|
||||
|
||||
def form_valid(self, form):
|
||||
policy = self.get_object()
|
||||
user = form.cleaned_data.get('user')
|
||||
user = form.cleaned_data.get("user")
|
||||
policy_engine = PolicyEngine([policy], user, self.request)
|
||||
policy_engine.use_cache = False
|
||||
policy_engine.build()
|
||||
result = policy_engine.passing
|
||||
if result:
|
||||
messages.success(self.request, _('User successfully passed policy.'))
|
||||
messages.success(self.request, _("User successfully passed policy."))
|
||||
else:
|
||||
messages.error(self.request, _("User didn't pass policy."))
|
||||
return self.render_to_response(self.get_context_data(form=form, result=result))
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""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.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
@ -18,65 +19,88 @@ from passbook.lib.views import CreateAssignPermView
|
||||
def all_subclasses(cls):
|
||||
"""Recursively return all subclassess of cls"""
|
||||
return set(cls.__subclasses__()).union(
|
||||
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
|
||||
[s for c in cls.__subclasses__() for s in all_subclasses(c)]
|
||||
)
|
||||
|
||||
|
||||
class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all property_mappings"""
|
||||
|
||||
model = PropertyMapping
|
||||
permission_required = 'passbook_core.view_propertymapping'
|
||||
template_name = 'administration/property_mapping/list.html'
|
||||
ordering = 'name'
|
||||
permission_required = "passbook_core.view_propertymapping"
|
||||
template_name = "administration/property_mapping/list.html"
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['types'] = {
|
||||
x.__name__: x._meta.verbose_name for x in all_subclasses(PropertyMapping)}
|
||||
kwargs["types"] = {
|
||||
x.__name__: x._meta.verbose_name 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):
|
||||
class PropertyMappingCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new PropertyMapping"""
|
||||
|
||||
model = PropertyMapping
|
||||
permission_required = 'passbook_core.add_propertymapping'
|
||||
permission_required = "passbook_core.add_propertymapping"
|
||||
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:property-mappings')
|
||||
success_message = _('Successfully created Property Mapping')
|
||||
template_name = "generic/create.html"
|
||||
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')
|
||||
model = next(x for x in all_subclasses(PropertyMapping)
|
||||
if x.__name__ == property_mapping_type)
|
||||
kwargs['type'] = model._meta.verbose_name
|
||||
property_mapping_type = self.request.GET.get("type")
|
||||
model = next(
|
||||
x
|
||||
for x in all_subclasses(PropertyMapping)
|
||||
if x.__name__ == property_mapping_type
|
||||
)
|
||||
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')
|
||||
model = next(x for x in all_subclasses(PropertyMapping)
|
||||
if x.__name__ == property_mapping_type)
|
||||
property_mapping_type = self.request.GET.get("type")
|
||||
model = next(
|
||||
x
|
||||
for x in all_subclasses(PropertyMapping)
|
||||
if x.__name__ == property_mapping_type
|
||||
)
|
||||
if not model:
|
||||
raise Http404
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class PropertyMappingUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, UpdateView):
|
||||
class PropertyMappingUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update property_mapping"""
|
||||
|
||||
model = PropertyMapping
|
||||
permission_required = 'passbook_core.change_propertymapping'
|
||||
permission_required = "passbook_core.change_propertymapping"
|
||||
|
||||
template_name = 'generic/update.html'
|
||||
success_url = reverse_lazy('passbook_admin:property-mappings')
|
||||
success_message = _('Successfully updated Property Mapping')
|
||||
template_name = "generic/update.html"
|
||||
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
|
||||
@ -84,22 +108,31 @@ class PropertyMappingUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return PropertyMapping.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
|
||||
return (
|
||||
PropertyMapping.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
class PropertyMappingDeleteView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, DeleteView):
|
||||
class PropertyMappingDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
"""Delete property_mapping"""
|
||||
|
||||
model = PropertyMapping
|
||||
permission_required = 'passbook_core.delete_propertymapping'
|
||||
permission_required = "passbook_core.delete_propertymapping"
|
||||
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:property-mappings')
|
||||
success_message = _('Successfully deleted Property Mapping')
|
||||
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()
|
||||
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)
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""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.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
@ -19,48 +20,55 @@ class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all providers"""
|
||||
|
||||
model = Provider
|
||||
permission_required = 'passbook_core.add_provider'
|
||||
template_name = 'administration/provider/list.html'
|
||||
permission_required = "passbook_core.add_provider"
|
||||
template_name = "administration/provider/list.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['types'] = {
|
||||
x.__name__: x._meta.verbose_name for x in Provider.__subclasses__()}
|
||||
kwargs["types"] = {
|
||||
x.__name__: x._meta.verbose_name for x in Provider.__subclasses__()
|
||||
}
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().select_subclasses()
|
||||
|
||||
|
||||
class ProviderCreateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin, CreateAssignPermView):
|
||||
class ProviderCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Provider"""
|
||||
|
||||
model = Provider
|
||||
permission_required = 'passbook_core.add_provider'
|
||||
permission_required = "passbook_core.add_provider"
|
||||
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:providers')
|
||||
success_message = _('Successfully created Provider')
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:providers")
|
||||
success_message = _("Successfully created Provider")
|
||||
|
||||
def get_form_class(self):
|
||||
provider_type = self.request.GET.get('type')
|
||||
model = next(x for x in Provider.__subclasses__()
|
||||
if x.__name__ == provider_type)
|
||||
provider_type = self.request.GET.get("type")
|
||||
model = next(
|
||||
x for x in Provider.__subclasses__() if x.__name__ == provider_type
|
||||
)
|
||||
if not model:
|
||||
raise Http404
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class ProviderUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, UpdateView):
|
||||
class ProviderUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update provider"""
|
||||
|
||||
model = Provider
|
||||
permission_required = 'passbook_core.change_provider'
|
||||
permission_required = "passbook_core.change_provider"
|
||||
|
||||
template_name = 'generic/update.html'
|
||||
success_url = reverse_lazy('passbook_admin:providers')
|
||||
success_message = _('Successfully updated Provider')
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:providers")
|
||||
success_message = _("Successfully updated Provider")
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
@ -68,22 +76,31 @@ class ProviderUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return Provider.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
|
||||
return (
|
||||
Provider.objects.filter(pk=self.kwargs.get("pk"))
|
||||
.select_subclasses()
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
class ProviderDeleteView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, DeleteView):
|
||||
class ProviderDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
"""Delete provider"""
|
||||
|
||||
model = Provider
|
||||
permission_required = 'passbook_core.delete_provider'
|
||||
permission_required = "passbook_core.delete_provider"
|
||||
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:providers')
|
||||
success_message = _('Successfully deleted Provider')
|
||||
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()
|
||||
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)
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""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.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy
|
||||
@ -18,55 +19,63 @@ from passbook.lib.views import CreateAssignPermView
|
||||
def all_subclasses(cls):
|
||||
"""Recursively return all subclassess of cls"""
|
||||
return set(cls.__subclasses__()).union(
|
||||
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
|
||||
[s for c in cls.__subclasses__() for s in all_subclasses(c)]
|
||||
)
|
||||
|
||||
|
||||
class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all sources"""
|
||||
|
||||
model = Source
|
||||
permission_required = 'passbook_core.view_source'
|
||||
ordering = 'name'
|
||||
permission_required = "passbook_core.view_source"
|
||||
ordering = "name"
|
||||
paginate_by = 40
|
||||
template_name = 'administration/source/list.html'
|
||||
template_name = "administration/source/list.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs['types'] = {
|
||||
x.__name__: x._meta.verbose_name for x in all_subclasses(Source)}
|
||||
kwargs["types"] = {
|
||||
x.__name__: x._meta.verbose_name 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):
|
||||
class SourceCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create new Source"""
|
||||
|
||||
model = Source
|
||||
permission_required = 'passbook_core.add_source'
|
||||
permission_required = "passbook_core.add_source"
|
||||
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:sources')
|
||||
success_message = _('Successfully created Source')
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:sources")
|
||||
success_message = _("Successfully created Source")
|
||||
|
||||
def get_form_class(self):
|
||||
source_type = self.request.GET.get('type')
|
||||
source_type = self.request.GET.get("type")
|
||||
model = next(x for x in all_subclasses(Source) if x.__name__ == source_type)
|
||||
if not model:
|
||||
raise Http404
|
||||
return path_to_class(model.form)
|
||||
|
||||
|
||||
class SourceUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, UpdateView):
|
||||
class SourceUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update source"""
|
||||
|
||||
model = Source
|
||||
permission_required = 'passbook_core.change_source'
|
||||
permission_required = "passbook_core.change_source"
|
||||
|
||||
template_name = 'generic/update.html'
|
||||
success_url = reverse_lazy('passbook_admin:sources')
|
||||
success_message = _('Successfully updated Source')
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:sources")
|
||||
success_message = _("Successfully updated Source")
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
@ -74,22 +83,27 @@ class SourceUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
return form_class
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return Source.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
|
||||
return (
|
||||
Source.objects.filter(pk=self.kwargs.get("pk")).select_subclasses().first()
|
||||
)
|
||||
|
||||
|
||||
class SourceDeleteView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, DeleteView):
|
||||
class SourceDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
"""Delete source"""
|
||||
|
||||
model = Source
|
||||
permission_required = 'passbook_core.delete_source'
|
||||
permission_required = "passbook_core.delete_source"
|
||||
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:sources')
|
||||
success_message = _('Successfully deleted Source')
|
||||
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()
|
||||
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)
|
||||
|
@ -1,8 +1,9 @@
|
||||
"""passbook User 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.auth.mixins import (
|
||||
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
|
||||
)
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
@ -19,50 +20,58 @@ class UserListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||
"""Show list of all users"""
|
||||
|
||||
model = User
|
||||
permission_required = 'passbook_core.view_user'
|
||||
ordering = 'username'
|
||||
permission_required = "passbook_core.view_user"
|
||||
ordering = "username"
|
||||
paginate_by = 40
|
||||
template_name = 'administration/user/list.html'
|
||||
template_name = "administration/user/list.html"
|
||||
|
||||
|
||||
class UserCreateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin, CreateAssignPermView):
|
||||
class UserCreateView(
|
||||
SuccessMessageMixin,
|
||||
LoginRequiredMixin,
|
||||
DjangoPermissionRequiredMixin,
|
||||
CreateAssignPermView,
|
||||
):
|
||||
"""Create user"""
|
||||
|
||||
model = User
|
||||
form_class = UserForm
|
||||
permission_required = 'passbook_core.add_user'
|
||||
permission_required = "passbook_core.add_user"
|
||||
|
||||
template_name = 'generic/create.html'
|
||||
success_url = reverse_lazy('passbook_admin:users')
|
||||
success_message = _('Successfully created User')
|
||||
template_name = "generic/create.html"
|
||||
success_url = reverse_lazy("passbook_admin:users")
|
||||
success_message = _("Successfully created User")
|
||||
|
||||
|
||||
class UserUpdateView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, UpdateView):
|
||||
class UserUpdateView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView
|
||||
):
|
||||
"""Update user"""
|
||||
|
||||
model = User
|
||||
form_class = UserForm
|
||||
permission_required = 'passbook_core.change_user'
|
||||
permission_required = "passbook_core.change_user"
|
||||
|
||||
context_object_name = 'object' # By default the object's name
|
||||
# is user which is used by other checks
|
||||
template_name = 'generic/update.html'
|
||||
success_url = reverse_lazy('passbook_admin:users')
|
||||
success_message = _('Successfully updated User')
|
||||
# By default the object's name is user which is used by other checks
|
||||
context_object_name = "object"
|
||||
template_name = "generic/update.html"
|
||||
success_url = reverse_lazy("passbook_admin:users")
|
||||
success_message = _("Successfully updated User")
|
||||
|
||||
|
||||
class UserDeleteView(SuccessMessageMixin, LoginRequiredMixin,
|
||||
PermissionRequiredMixin, DeleteView):
|
||||
class UserDeleteView(
|
||||
SuccessMessageMixin, LoginRequiredMixin, PermissionRequiredMixin, DeleteView
|
||||
):
|
||||
"""Delete user"""
|
||||
|
||||
model = User
|
||||
permission_required = 'passbook_core.delete_user'
|
||||
permission_required = "passbook_core.delete_user"
|
||||
|
||||
template_name = 'generic/delete.html'
|
||||
success_url = reverse_lazy('passbook_admin:users')
|
||||
success_message = _('Successfully deleted User')
|
||||
# By default the object's name is user which is used by other checks
|
||||
context_object_name = "object"
|
||||
template_name = "generic/delete.html"
|
||||
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)
|
||||
@ -73,13 +82,16 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
|
||||
"""Get Password reset link for user"""
|
||||
|
||||
model = User
|
||||
permission_required = 'passbook_core.reset_user_password'
|
||||
permission_required = "passbook_core.reset_user_password"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""Create nonce for user and return link"""
|
||||
super().get(request, *args, **kwargs)
|
||||
nonce = Nonce.objects.create(user=self.object)
|
||||
link = request.build_absolute_uri(reverse(
|
||||
'passbook_core:auth-password-reset', kwargs={'nonce': nonce.uuid}))
|
||||
messages.success(request, _('Password reset link: <pre>%(link)s</pre>' % {'link': link}))
|
||||
return redirect('passbook_admin:users')
|
||||
link = request.build_absolute_uri(
|
||||
reverse("passbook_core:auth-password-reset", kwargs={"nonce": nonce.uuid})
|
||||
)
|
||||
messages.success(
|
||||
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})
|
||||
)
|
||||
return redirect("passbook_admin:users")
|
||||
|
@ -6,7 +6,7 @@ from django.apps import AppConfig
|
||||
class PassbookAPIConfig(AppConfig):
|
||||
"""passbook API Config"""
|
||||
|
||||
name = 'passbook.api'
|
||||
label = 'passbook_api'
|
||||
mountpoint = 'api/'
|
||||
verbose_name = 'passbook API'
|
||||
name = "passbook.api"
|
||||
label = "passbook_api"
|
||||
mountpoint = "api/"
|
||||
verbose_name = "passbook API"
|
||||
|
@ -9,13 +9,13 @@ class CustomObjectPermissions(DjangoObjectPermissions):
|
||||
"""Similar to `DjangoObjectPermissions`, but adding 'view' permissions."""
|
||||
|
||||
perms_map = {
|
||||
'GET': ['%(app_label)s.view_%(model_name)s'],
|
||||
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
|
||||
'HEAD': ['%(app_label)s.view_%(model_name)s'],
|
||||
'POST': ['%(app_label)s.add_%(model_name)s'],
|
||||
'PUT': ['%(app_label)s.change_%(model_name)s'],
|
||||
'PATCH': ['%(app_label)s.change_%(model_name)s'],
|
||||
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
|
||||
"GET": ["%(app_label)s.view_%(model_name)s"],
|
||||
"OPTIONS": ["%(app_label)s.view_%(model_name)s"],
|
||||
"HEAD": ["%(app_label)s.view_%(model_name)s"],
|
||||
"POST": ["%(app_label)s.add_%(model_name)s"],
|
||||
"PUT": ["%(app_label)s.change_%(model_name)s"],
|
||||
"PATCH": ["%(app_label)s.change_%(model_name)s"],
|
||||
"DELETE": ["%(app_label)s.delete_%(model_name)s"],
|
||||
}
|
||||
|
||||
|
||||
|
@ -5,6 +5,6 @@ from passbook.api.v1.urls import urlpatterns as v1_urls
|
||||
from passbook.api.v2.urls import urlpatterns as v2_urls
|
||||
|
||||
urlpatterns = [
|
||||
path('v1/', include(v1_urls)),
|
||||
path('v2/', include(v2_urls)),
|
||||
path("v1/", include(v1_urls)),
|
||||
path("v2/", include(v2_urls)),
|
||||
]
|
||||
|
@ -7,16 +7,16 @@ from oauth2_provider.views.mixins import ScopedResourceMixin
|
||||
class OpenIDUserInfoView(ScopedResourceMixin, View):
|
||||
"""Passbook v1 OpenID API"""
|
||||
|
||||
required_scopes = ['openid:userinfo']
|
||||
required_scopes = ["openid:userinfo"]
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
def get(self, request, *_, **__):
|
||||
"""Passbook v1 OpenID API"""
|
||||
payload = {
|
||||
'sub': request.user.uuid.int,
|
||||
'name': request.user.get_full_name(),
|
||||
'given_name': request.user.name,
|
||||
'family_name': '',
|
||||
'preferred_username': request.user.username,
|
||||
'email': request.user.email,
|
||||
"sub": request.user.uuid.int,
|
||||
"name": request.user.get_full_name(),
|
||||
"given_name": request.user.name,
|
||||
"family_name": "",
|
||||
"preferred_username": request.user.username,
|
||||
"email": request.user.email,
|
||||
}
|
||||
return JsonResponse(payload)
|
||||
|
@ -3,6 +3,4 @@ from django.urls import path
|
||||
|
||||
from passbook.api.v1.openid import OpenIDUserInfoView
|
||||
|
||||
urlpatterns = [
|
||||
path('openid/', OpenIDUserInfoView.as_view(), name='openid')
|
||||
]
|
||||
urlpatterns = [path("openid/", OpenIDUserInfoView.as_view(), name="openid")]
|
||||
|
@ -24,80 +24,79 @@ from passbook.factors.otp.api import OTPFactorViewSet
|
||||
from passbook.factors.password.api import PasswordFactorViewSet
|
||||
from passbook.lib.utils.reflection import get_apps
|
||||
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
|
||||
from passbook.policies.group.api import GroupMembershipPolicyViewSet
|
||||
from passbook.policies.expression.api import ExpressionPolicyViewSet
|
||||
from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
|
||||
from passbook.policies.matcher.api import FieldMatcherPolicyViewSet
|
||||
from passbook.policies.password.api import PasswordPolicyViewSet
|
||||
from passbook.policies.reputation.api import ReputationPolicyViewSet
|
||||
from passbook.policies.sso.api import SSOLoginPolicyViewSet
|
||||
from passbook.policies.webhook.api import WebhookPolicyViewSet
|
||||
from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet
|
||||
from passbook.providers.oauth.api import OAuth2ProviderViewSet
|
||||
from passbook.providers.oidc.api import OpenIDProviderViewSet
|
||||
from passbook.providers.saml.api import (SAMLPropertyMappingViewSet,
|
||||
SAMLProviderViewSet)
|
||||
from passbook.sources.ldap.api import (LDAPPropertyMappingViewSet,
|
||||
LDAPSourceViewSet)
|
||||
from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProviderViewSet
|
||||
from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
||||
from passbook.sources.oauth.api import OAuthSourceViewSet
|
||||
|
||||
LOGGER = get_logger()
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
for _passbook_app in get_apps():
|
||||
if hasattr(_passbook_app, 'api_mountpoint'):
|
||||
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/invitations', InvitationViewSet)
|
||||
router.register('core/groups', GroupViewSet)
|
||||
router.register('core/users', UserViewSet)
|
||||
router.register('audit/events', EventViewSet)
|
||||
router.register('sources/all', SourceViewSet)
|
||||
router.register('sources/ldap', LDAPSourceViewSet)
|
||||
router.register('sources/oauth', OAuthSourceViewSet)
|
||||
router.register('policies/all', PolicyViewSet)
|
||||
router.register('policies/passwordexpiry', PasswordExpiryPolicyViewSet)
|
||||
router.register('policies/groupmembership', GroupMembershipPolicyViewSet)
|
||||
router.register('policies/haveibeenpwned', HaveIBeenPwendPolicyViewSet)
|
||||
router.register('policies/fieldmatcher', FieldMatcherPolicyViewSet)
|
||||
router.register('policies/password', PasswordPolicyViewSet)
|
||||
router.register('policies/reputation', ReputationPolicyViewSet)
|
||||
router.register('policies/ssologin', SSOLoginPolicyViewSet)
|
||||
router.register('policies/webhook', WebhookPolicyViewSet)
|
||||
router.register('providers/all', ProviderViewSet)
|
||||
router.register('providers/applicationgateway', ApplicationGatewayProviderViewSet)
|
||||
router.register('providers/oauth', OAuth2ProviderViewSet)
|
||||
router.register('providers/openid', OpenIDProviderViewSet)
|
||||
router.register('providers/saml', SAMLProviderViewSet)
|
||||
router.register('propertymappings/all', PropertyMappingViewSet)
|
||||
router.register('propertymappings/ldap', LDAPPropertyMappingViewSet)
|
||||
router.register('propertymappings/saml', SAMLPropertyMappingViewSet)
|
||||
router.register('factors/all', FactorViewSet)
|
||||
router.register('factors/captcha', CaptchaFactorViewSet)
|
||||
router.register('factors/dummy', DummyFactorViewSet)
|
||||
router.register('factors/email', EmailFactorViewSet)
|
||||
router.register('factors/otp', OTPFactorViewSet)
|
||||
router.register('factors/password', PasswordFactorViewSet)
|
||||
router.register("core/applications", ApplicationViewSet)
|
||||
router.register("core/invitations", InvitationViewSet)
|
||||
router.register("core/groups", GroupViewSet)
|
||||
router.register("core/users", UserViewSet)
|
||||
router.register("audit/events", EventViewSet)
|
||||
router.register("sources/all", SourceViewSet)
|
||||
router.register("sources/ldap", LDAPSourceViewSet)
|
||||
router.register("sources/oauth", OAuthSourceViewSet)
|
||||
router.register("policies/all", PolicyViewSet)
|
||||
router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
|
||||
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
|
||||
router.register("policies/password", PasswordPolicyViewSet)
|
||||
router.register("policies/reputation", ReputationPolicyViewSet)
|
||||
router.register("policies/webhook", WebhookPolicyViewSet)
|
||||
router.register("policies/expression", ExpressionPolicyViewSet)
|
||||
router.register("providers/all", ProviderViewSet)
|
||||
router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet)
|
||||
router.register("providers/oauth", OAuth2ProviderViewSet)
|
||||
router.register("providers/openid", OpenIDProviderViewSet)
|
||||
router.register("providers/saml", SAMLProviderViewSet)
|
||||
router.register("propertymappings/all", PropertyMappingViewSet)
|
||||
router.register("propertymappings/ldap", LDAPPropertyMappingViewSet)
|
||||
router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
|
||||
router.register("factors/all", FactorViewSet)
|
||||
router.register("factors/captcha", CaptchaFactorViewSet)
|
||||
router.register("factors/dummy", DummyFactorViewSet)
|
||||
router.register("factors/email", EmailFactorViewSet)
|
||||
router.register("factors/otp", OTPFactorViewSet)
|
||||
router.register("factors/password", PasswordFactorViewSet)
|
||||
|
||||
info = openapi.Info(
|
||||
title="passbook API",
|
||||
default_version='v2',
|
||||
default_version="v2",
|
||||
# description="Test description",
|
||||
# terms_of_service="https://www.google.com/policies/terms/",
|
||||
contact=openapi.Contact(email="hello@beryju.org"),
|
||||
license=openapi.License(name="MIT License"),
|
||||
)
|
||||
SchemaView = get_schema_view(
|
||||
info,
|
||||
public=True,
|
||||
permission_classes=(CustomObjectPermissions,),
|
||||
info, public=True, permission_classes=(CustomObjectPermissions,),
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^swagger(?P<format>\.json|\.yaml)$',
|
||||
SchemaView.without_ui(cache_timeout=0), name='schema-json'),
|
||||
path('swagger/', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
|
||||
path('redoc/', SchemaView.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
|
||||
url(
|
||||
r"^swagger(?P<format>\.json|\.yaml)$",
|
||||
SchemaView.without_ui(cache_timeout=0),
|
||||
name="schema-json",
|
||||
),
|
||||
path(
|
||||
"swagger/",
|
||||
SchemaView.with_ui("swagger", cache_timeout=0),
|
||||
name="schema-swagger-ui",
|
||||
),
|
||||
path("redoc/", SchemaView.with_ui("redoc", cache_timeout=0), name="schema-redoc"),
|
||||
] + router.urls
|
||||
|
@ -2,4 +2,4 @@
|
||||
|
||||
from passbook.lib.admin import admin_autoregister
|
||||
|
||||
admin_autoregister('passbook_audit')
|
||||
admin_autoregister("passbook_audit")
|
||||
|
@ -11,7 +11,16 @@ class EventSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
|
||||
model = Event
|
||||
fields = ['pk', 'user', 'action', 'date', 'app', 'context', 'request_ip', 'created', ]
|
||||
fields = [
|
||||
"pk",
|
||||
"user",
|
||||
"action",
|
||||
"date",
|
||||
"app",
|
||||
"context",
|
||||
"request_ip",
|
||||
"created",
|
||||
]
|
||||
|
||||
|
||||
class EventViewSet(ReadOnlyModelViewSet):
|
||||
|
@ -7,10 +7,10 @@ from django.apps import AppConfig
|
||||
class PassbookAuditConfig(AppConfig):
|
||||
"""passbook audit app"""
|
||||
|
||||
name = 'passbook.audit'
|
||||
label = 'passbook_audit'
|
||||
verbose_name = 'passbook Audit'
|
||||
mountpoint = 'audit/'
|
||||
name = "passbook.audit"
|
||||
label = "passbook_audit"
|
||||
verbose_name = "passbook Audit"
|
||||
mountpoint = "audit/"
|
||||
|
||||
def ready(self):
|
||||
import_module('passbook.audit.signals')
|
||||
import_module("passbook.audit.signals")
|
||||
|
@ -18,20 +18,55 @@ class Migration(migrations.Migration):
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AuditEntry',
|
||||
name="AuditEntry",
|
||||
fields=[
|
||||
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('action', models.TextField(choices=[('login', 'login'), ('login_failed', 'login_failed'), ('logout', 'logout'), ('authorize_application', 'authorize_application'), ('suspicious_request', 'suspicious_request'), ('sign_up', 'sign_up'), ('password_reset', 'password_reset'), ('invitation_created', 'invitation_created'), ('invitation_used', 'invitation_used')])),
|
||||
('date', models.DateTimeField(auto_now_add=True)),
|
||||
('app', models.TextField()),
|
||||
('context', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
|
||||
('request_ip', models.GenericIPAddressField()),
|
||||
('created', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
(
|
||||
"uuid",
|
||||
models.UUIDField(
|
||||
default=uuid.uuid4,
|
||||
editable=False,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
),
|
||||
),
|
||||
(
|
||||
"action",
|
||||
models.TextField(
|
||||
choices=[
|
||||
("login", "login"),
|
||||
("login_failed", "login_failed"),
|
||||
("logout", "logout"),
|
||||
("authorize_application", "authorize_application"),
|
||||
("suspicious_request", "suspicious_request"),
|
||||
("sign_up", "sign_up"),
|
||||
("password_reset", "password_reset"),
|
||||
("invitation_created", "invitation_created"),
|
||||
("invitation_used", "invitation_used"),
|
||||
]
|
||||
),
|
||||
),
|
||||
("date", models.DateTimeField(auto_now_add=True)),
|
||||
("app", models.TextField()),
|
||||
(
|
||||
"context",
|
||||
django.contrib.postgres.fields.jsonb.JSONField(
|
||||
blank=True, default=dict
|
||||
),
|
||||
),
|
||||
("request_ip", models.GenericIPAddressField()),
|
||||
("created", models.DateTimeField(auto_now_add=True)),
|
||||
(
|
||||
"user",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Audit Entry',
|
||||
'verbose_name_plural': 'Audit Entries',
|
||||
"verbose_name": "Audit Entry",
|
||||
"verbose_name_plural": "Audit Entries",
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -8,12 +8,9 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('passbook_audit', '0001_initial'),
|
||||
("passbook_audit", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='AuditEntry',
|
||||
new_name='Event',
|
||||
),
|
||||
migrations.RenameModel(old_name="AuditEntry", new_name="Event",),
|
||||
]
|
||||
|
40
passbook/audit/migrations/0003_auto_20191205_1407.py
Normal file
40
passbook/audit/migrations/0003_auto_20191205_1407.py
Normal file
@ -0,0 +1,40 @@
|
||||
# Generated by Django 2.2.8 on 2019-12-05 14:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import passbook.audit.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_audit", "0002_auto_20191028_0829"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="event",
|
||||
options={
|
||||
"verbose_name": "Audit Event",
|
||||
"verbose_name_plural": "Audit Events",
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="event",
|
||||
name="action",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("LOGIN", "login"),
|
||||
("LOGIN_FAILED", "login_failed"),
|
||||
("LOGOUT", "logout"),
|
||||
("AUTHORIZE_APPLICATION", "authorize_application"),
|
||||
("SUSPICIOUS_REQUEST", "suspicious_request"),
|
||||
("SIGN_UP", "sign_up"),
|
||||
("PASSWORD_RESET", "password_reset"),
|
||||
("INVITE_CREATED", "invitation_created"),
|
||||
("INVITE_USED", "invitation_used"),
|
||||
("CUSTOM", "custom"),
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
19
passbook/audit/migrations/0004_auto_20191205_1502.py
Normal file
19
passbook/audit/migrations/0004_auto_20191205_1502.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 2.2.8 on 2019-12-05 15:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_audit", "0003_auto_20191205_1407"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(model_name="event", name="request_ip",),
|
||||
migrations.AddField(
|
||||
model_name="event",
|
||||
name="client_ip",
|
||||
field=models.GenericIPAddressField(null=True),
|
||||
),
|
||||
]
|
@ -1,75 +1,142 @@
|
||||
"""passbook audit models"""
|
||||
from enum import Enum
|
||||
from inspect import getmodule, stack
|
||||
from typing import Any, Dict, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import JSONField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext as _
|
||||
from ipware import get_client_ip
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.lib.models import UUIDModel
|
||||
from passbook.lib.utils.http import get_client_ip
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
"""clean source of all Models that would interfere with the JSONField.
|
||||
Models are replaced with a dictionary of {
|
||||
app: str,
|
||||
name: str,
|
||||
pk: Any
|
||||
}"""
|
||||
for key, value in source.items():
|
||||
if isinstance(value, dict):
|
||||
source[key] = sanitize_dict(value)
|
||||
elif isinstance(value, models.Model):
|
||||
model_content_type = ContentType.objects.get_for_model(value)
|
||||
source[key] = sanitize_dict(
|
||||
{
|
||||
"app": model_content_type.app_label,
|
||||
"name": model_content_type.model,
|
||||
"pk": value.pk,
|
||||
}
|
||||
)
|
||||
elif isinstance(value, UUID):
|
||||
source[key] = value.hex
|
||||
return source
|
||||
|
||||
|
||||
class EventAction(Enum):
|
||||
"""All possible actions to save into the audit log"""
|
||||
|
||||
LOGIN = "login"
|
||||
LOGIN_FAILED = "login_failed"
|
||||
LOGOUT = "logout"
|
||||
AUTHORIZE_APPLICATION = "authorize_application"
|
||||
SUSPICIOUS_REQUEST = "suspicious_request"
|
||||
SIGN_UP = "sign_up"
|
||||
PASSWORD_RESET = "password_reset" # noqa # nosec
|
||||
INVITE_CREATED = "invitation_created"
|
||||
INVITE_USED = "invitation_used"
|
||||
CUSTOM = "custom"
|
||||
|
||||
@staticmethod
|
||||
def as_choices():
|
||||
"""Generate choices of actions used for database"""
|
||||
return tuple((x, y.value) for x, y in EventAction.__members__.items())
|
||||
|
||||
|
||||
class Event(UUIDModel):
|
||||
"""An individual audit log event"""
|
||||
|
||||
ACTION_LOGIN = 'login'
|
||||
ACTION_LOGIN_FAILED = 'login_failed'
|
||||
ACTION_LOGOUT = 'logout'
|
||||
ACTION_AUTHORIZE_APPLICATION = 'authorize_application'
|
||||
ACTION_SUSPICIOUS_REQUEST = 'suspicious_request'
|
||||
ACTION_SIGN_UP = 'sign_up'
|
||||
ACTION_PASSWORD_RESET = 'password_reset' # noqa # nosec
|
||||
ACTION_INVITE_CREATED = 'invitation_created'
|
||||
ACTION_INVITE_USED = 'invitation_used'
|
||||
ACTIONS = (
|
||||
(ACTION_LOGIN, ACTION_LOGIN),
|
||||
(ACTION_LOGIN_FAILED, ACTION_LOGIN_FAILED),
|
||||
(ACTION_LOGOUT, ACTION_LOGOUT),
|
||||
(ACTION_AUTHORIZE_APPLICATION, ACTION_AUTHORIZE_APPLICATION),
|
||||
(ACTION_SUSPICIOUS_REQUEST, ACTION_SUSPICIOUS_REQUEST),
|
||||
(ACTION_SIGN_UP, ACTION_SIGN_UP),
|
||||
(ACTION_PASSWORD_RESET, ACTION_PASSWORD_RESET),
|
||||
(ACTION_INVITE_CREATED, ACTION_INVITE_CREATED),
|
||||
(ACTION_INVITE_USED, ACTION_INVITE_USED),
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)
|
||||
action = models.TextField(choices=ACTIONS)
|
||||
action = models.TextField(choices=EventAction.as_choices())
|
||||
date = models.DateTimeField(auto_now_add=True)
|
||||
app = models.TextField()
|
||||
context = JSONField(default=dict, blank=True)
|
||||
request_ip = models.GenericIPAddressField()
|
||||
client_ip = models.GenericIPAddressField(null=True)
|
||||
created = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
@staticmethod
|
||||
def create(action, request, **kwargs):
|
||||
"""Create Event from arguments"""
|
||||
client_ip, _ = get_client_ip(request)
|
||||
if not hasattr(request, 'user'):
|
||||
user = None
|
||||
else:
|
||||
user = request.user
|
||||
if isinstance(user, AnonymousUser):
|
||||
user = kwargs.get('user', None)
|
||||
entry = Event.objects.create(
|
||||
action=action,
|
||||
user=user,
|
||||
# User 255.255.255.255 as fallback if IP cannot be determined
|
||||
request_ip=client_ip or '255.255.255.255',
|
||||
context=kwargs)
|
||||
LOGGER.debug("Created Audit entry", action=action,
|
||||
user=user, from_ip=client_ip, context=kwargs)
|
||||
return entry
|
||||
def _get_app_from_request(request: HttpRequest) -> str:
|
||||
if not isinstance(request, HttpRequest):
|
||||
return ""
|
||||
return request.resolver_match.app_name
|
||||
|
||||
@staticmethod
|
||||
def new(
|
||||
action: EventAction,
|
||||
app: Optional[str] = None,
|
||||
_inspect_offset: int = 1,
|
||||
**kwargs,
|
||||
) -> "Event":
|
||||
"""Create new Event instance from arguments. Instance is NOT saved."""
|
||||
if not isinstance(action, EventAction):
|
||||
raise ValueError(
|
||||
f"action must be EventAction instance but was {type(action)}"
|
||||
)
|
||||
if not app:
|
||||
app = getmodule(stack()[_inspect_offset][0]).__name__
|
||||
cleaned_kwargs = sanitize_dict(kwargs)
|
||||
event = Event(action=action.value, app=app, context=cleaned_kwargs)
|
||||
return event
|
||||
|
||||
def from_http(
|
||||
self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None
|
||||
) -> "Event":
|
||||
"""Add data from a Django-HttpRequest, allowing the creation of
|
||||
Events independently from requests.
|
||||
`user` arguments optionally overrides user from requests."""
|
||||
if hasattr(request, "user"):
|
||||
if isinstance(request.user, AnonymousUser):
|
||||
self.user = get_anonymous_user()
|
||||
else:
|
||||
self.user = request.user
|
||||
if user:
|
||||
self.user = user
|
||||
# User 255.255.255.255 as fallback if IP cannot be determined
|
||||
self.client_ip = get_client_ip(request) or "255.255.255.255"
|
||||
# If there's no app set, we get it from the requests too
|
||||
if not self.app:
|
||||
self.app = Event._get_app_from_request(request)
|
||||
self.save()
|
||||
return self
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self._state.adding:
|
||||
raise ValidationError("you may not edit an existing %s" % self._meta.model_name)
|
||||
super().save(*args, **kwargs)
|
||||
raise ValidationError(
|
||||
"you may not edit an existing %s" % self._meta.model_name
|
||||
)
|
||||
LOGGER.debug(
|
||||
"Created Audit event",
|
||||
action=self.action,
|
||||
context=self.context,
|
||||
client_ip=self.client_ip,
|
||||
)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Audit Entry')
|
||||
verbose_name_plural = _('Audit Entries')
|
||||
verbose_name = _("Audit Event")
|
||||
verbose_name_plural = _("Audit Events")
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user