Compare commits
120 Commits
version/0.
...
version/0.
Author | SHA1 | Date | |
---|---|---|---|
b290bbf6d7 | |||
8d875cb01d | |||
36b1f8ba36 | |||
6c889eff27 | |||
9d8675e54b | |||
22ae986c0b | |||
2bef5f3911 | |||
3c2b8e5ee1 | |||
c96571bdba | |||
2dfd93afb1 | |||
1d22e30c70 | |||
07b7951390 | |||
995615d0a0 | |||
ac273aab75 | |||
44cd03654d | |||
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 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.7.6-beta
|
||||
current_version = 0.8.4-beta
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||
|
41
.github/workflows/ci.yml
vendored
41
.github/workflows/ci.yml
vendored
@ -9,12 +9,12 @@ env:
|
||||
jobs:
|
||||
# Linting
|
||||
pylint:
|
||||
runs-on: [ubuntu-latest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
python-version: '3.8'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
@ -26,12 +26,12 @@ jobs:
|
||||
- name: Lint with pylint
|
||||
run: pipenv run pylint passbook
|
||||
black:
|
||||
runs-on: [ubuntu-latest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
python-version: '3.8'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
@ -43,12 +43,29 @@ jobs:
|
||||
- name: Lint with black
|
||||
run: pipenv run black --check passbook
|
||||
prospector:
|
||||
runs-on: [ubuntu-latest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
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/
|
||||
@ -57,8 +74,8 @@ jobs:
|
||||
${{ runner.os }}-pipenv-
|
||||
- name: Install dependencies
|
||||
run: pip install -U pip pipenv && pipenv install --dev
|
||||
- name: Lint with prospector
|
||||
run: pipenv run prospector
|
||||
- name: Lint with bandit
|
||||
run: pipenv run bandit -r passbook
|
||||
# Actual CI tests
|
||||
migrations:
|
||||
needs:
|
||||
@ -78,12 +95,12 @@ jobs:
|
||||
image: redis:latest
|
||||
ports:
|
||||
- 6379:6379
|
||||
runs-on: [ubuntu-latest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
python-version: '3.8'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
@ -112,12 +129,12 @@ jobs:
|
||||
image: redis:latest
|
||||
ports:
|
||||
- 6379:6379
|
||||
runs-on: [ubuntu-latest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: '3.7'
|
||||
python-version: '3.8'
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/virtualenvs/
|
||||
|
69
.github/workflows/release.yml
vendored
69
.github/workflows/release.yml
vendored
@ -1,13 +1,11 @@
|
||||
name: passbook-release
|
||||
on:
|
||||
release:
|
||||
types: # This configuration does not affect the page_build event above
|
||||
- created
|
||||
release
|
||||
|
||||
jobs:
|
||||
# Build
|
||||
build-server:
|
||||
runs-on: [ubuntu-latest]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Docker Login Registry
|
||||
@ -16,11 +14,38 @@ jobs:
|
||||
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.7.6-beta -f Dockerfile .
|
||||
- name: Push Docker Container to Registry
|
||||
run: docker push beryju/passbook:0.7.6-beta
|
||||
run: docker build
|
||||
--no-cache
|
||||
-t beryju/passbook:0.8.4-beta
|
||||
-t beryju/passbook:latest
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook:0.8.4-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.4-beta \
|
||||
-t beryju/passbook-gatekeeper:latest \
|
||||
-f Dockerfile .
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-gatekeeper:0.8.4-beta
|
||||
- name: Push Docker Container to Registry (latest)
|
||||
run: docker push beryju/passbook-gatekeeper:latest
|
||||
build-static:
|
||||
runs-on: [ubuntu-latest]
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
@ -41,24 +66,24 @@ jobs:
|
||||
run: docker build
|
||||
--no-cache
|
||||
--network=$(docker network ls | grep github | awk '{print $1}')
|
||||
-t beryju/passbook-static:0.7.6-beta
|
||||
-t beryju/passbook-static:0.8.4-beta
|
||||
-t beryju/passbook-static:latest
|
||||
-f static.Dockerfile .
|
||||
- name: Push Docker Container to Registry
|
||||
run: docker push beryju/passbook-static:0.7.6-beta
|
||||
package-helm:
|
||||
- name: Push Docker Container to Registry (versioned)
|
||||
run: docker push beryju/passbook-static:0.8.4-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]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Install Helm
|
||||
- name: Run test suite in final docker images
|
||||
run: |
|
||||
apt update && apt install -y curl
|
||||
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
|
||||
helm init
|
||||
- name: Helm package
|
||||
run: |
|
||||
helm dependency update helm/passbook
|
||||
helm package helm/passbook
|
||||
|
||||
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
|
@ -1,4 +1,4 @@
|
||||
FROM python:3.7-slim-buster as locker
|
||||
FROM python:3.8-slim-buster as locker
|
||||
|
||||
COPY ./Pipfile /app/
|
||||
COPY ./Pipfile.lock /app/
|
||||
@ -9,7 +9,7 @@ RUN pip install pipenv && \
|
||||
pipenv lock -r > requirements.txt && \
|
||||
pipenv lock -rd > requirements-dev.txt
|
||||
|
||||
FROM python:3.7-slim-buster
|
||||
FROM python:3.8-slim-buster
|
||||
|
||||
COPY --from=locker /app/requirements.txt /app/
|
||||
COPY --from=locker /app/requirements-dev.txt /app/
|
||||
|
4
Pipfile
4
Pipfile
@ -40,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 = "*"
|
||||
@ -51,7 +52,6 @@ bumpversion = "*"
|
||||
colorama = "*"
|
||||
coverage = "*"
|
||||
django-debug-toolbar = "*"
|
||||
prospector = "*"
|
||||
pylint = "*"
|
||||
pylint-django = "*"
|
||||
unittest-xml-reporting = "*"
|
||||
|
742
Pipfile.lock
generated
742
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,7 @@
|
||||
# passbook
|
||||
|
||||

|
||||
|
||||
## Quick instance
|
||||
|
||||
```
|
||||
|
@ -7,4 +7,4 @@ threads = 2
|
||||
enable-threads = true
|
||||
uid = passbook
|
||||
gid = passbook
|
||||
disable-logging=True
|
||||
disable-logging = True
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM python:3.7-slim-buster as builder
|
||||
FROM python:3.8-slim-buster as builder
|
||||
|
||||
WORKDIR /mkdocs
|
||||
|
||||
|
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 |
@ -4,9 +4,8 @@
|
||||
|
||||
From https://about.gitlab.com/what-is-gitlab/
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
!!! 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
|
||||
|
||||
@ -21,7 +20,7 @@ Create an application in passbook and note the slug, as this will be used later.
|
||||
- Audience: `https://gitlab.company`
|
||||
- Issuer: `https://gitlab.company`
|
||||
|
||||
You can of course use a custom Signing Certificate, and adjust the Assertion Length. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
|
||||
You can of course use a custom Signing Certificate, and adjust durations. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
|
||||
|
||||
## GitLab Configuration
|
||||
|
||||
|
@ -4,9 +4,8 @@
|
||||
|
||||
From https://goharbor.io
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
!!! 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
|
||||
|
||||
|
@ -4,10 +4,9 @@
|
||||
|
||||
From https://rancher.com/products/rancher
|
||||
|
||||
```
|
||||
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.
|
||||
```
|
||||
!!! 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
|
||||
|
||||
@ -22,7 +21,7 @@ Create an application in passbook and note the slug, as this will be used later.
|
||||
- Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
|
||||
- Issuer: `passbook`
|
||||
|
||||
You can of course use a custom Signing Certificate, and adjust the Assertion Length.
|
||||
You can of course use a custom Signing Certificate, and adjust durations.
|
||||
|
||||
## Rancher
|
||||
|
||||
|
@ -4,13 +4,12 @@
|
||||
|
||||
From https://sentry.io
|
||||
|
||||
```
|
||||
Sentry provides self-hosted and cloud-based error monitoring that helps all software
|
||||
teams discover, triage, and prioritize errors in real-time.
|
||||
!!! 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?
|
||||
```
|
||||
One million developers at over fifty thousand companies already ship
|
||||
better software faster with Sentry. Won’t you join them?
|
||||
|
||||
## Preparation
|
||||
|
||||
|
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.
|
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`
|
@ -18,27 +18,9 @@ passbook keeps track of failed login attempts by Source IP and Attempted Usernam
|
||||
|
||||
This policy can be used to for example prompt Clients with a low score to pass a Captcha before they can continue.
|
||||
|
||||
### Field matcher Policy
|
||||
## Expression Policy
|
||||
|
||||
This policy allows you to evaluate arbitrary comparisons against the User instance. Currently supported fields are:
|
||||
|
||||
- Username
|
||||
- E-Mail
|
||||
- Name
|
||||
- Is_active
|
||||
- Date joined
|
||||
|
||||
Any of the following operations are supported:
|
||||
|
||||
- Starts with
|
||||
- Ends with
|
||||
- Contains
|
||||
- Regexp (standard Python engine)
|
||||
- Exact
|
||||
|
||||
### SSO Policy
|
||||
|
||||
This policy evaluates to True if the current Authentication Flow has been initiated through an external Source, like OAuth and SAML.
|
||||
See [Expression Policy](expression/index.md).
|
||||
|
||||
### Webhook Policy
|
||||
|
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 %}]
|
||||
```
|
@ -13,11 +13,5 @@ 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 also has [Property Mappings](property-mappings.md#saml-property-mapping), which allows you to expose Vendor-specific Fields.
|
||||
Default fields are:
|
||||
|
||||
- `eduPersonPrincipalName`: User's E-Mail
|
||||
- `cn`: User's Full Name
|
||||
- `mail`: User's E-Mail
|
||||
- `displayName`: User's Username
|
||||
- `uid`: User Unique Identifier
|
||||
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..."
|
||||
|
@ -36,4 +36,4 @@ This source allows you to import Users and Groups from an LDAP Server
|
||||
- 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.md#ldap-property-mapping)
|
||||
- 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)
|
||||
|
@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
appVersion: "0.7.6-beta"
|
||||
appVersion: "0.8.4-beta"
|
||||
description: A Helm chart for passbook.
|
||||
name: passbook
|
||||
version: "0.7.6-beta"
|
||||
version: "0.8.4-beta"
|
||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
||||
|
@ -18,6 +18,7 @@ spec:
|
||||
name: {{ include "passbook.fullname" . }}-secret-key
|
||||
key: monitoring_username
|
||||
port: http
|
||||
path: /metrics/
|
||||
interval: 10s
|
||||
selector:
|
||||
matchLabels:
|
||||
|
@ -2,7 +2,7 @@
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
image:
|
||||
tag: 0.7.6-beta
|
||||
tag: 0.8.4-beta
|
||||
|
||||
nameOverride: ""
|
||||
|
||||
@ -42,3 +42,5 @@ redis:
|
||||
master:
|
||||
persistence:
|
||||
enabled: false
|
||||
# https://stackoverflow.com/a/59189742
|
||||
disableCommands: []
|
||||
|
14
mkdocs.yml
14
mkdocs.yml
@ -1,5 +1,5 @@
|
||||
site_name: passbook Docs
|
||||
site_url: https://docs.passbook.beryju.org
|
||||
site_url: https://beryju.github.io/passbook
|
||||
copyright: "Copyright © 2019 - 2020 BeryJu.org"
|
||||
|
||||
nav:
|
||||
@ -10,15 +10,22 @@ nav:
|
||||
- Kubernetes: installation/kubernetes.md
|
||||
- Sources: sources.md
|
||||
- Providers: providers.md
|
||||
- Property Mappings: property-mappings.md
|
||||
- Property Mappings:
|
||||
- Overview: property-mappings/index.md
|
||||
- Reference:
|
||||
- User Object: property-mappings/reference/user-object.md
|
||||
- Factors: factors.md
|
||||
- Policies: policies.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
|
||||
@ -29,3 +36,4 @@ theme:
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
permalink: "¶"
|
||||
- admonition
|
||||
|
@ -1,2 +1,2 @@
|
||||
"""passbook"""
|
||||
__version__ = "0.7.6-beta"
|
||||
__version__ = "0.8.4-beta"
|
||||
|
@ -36,7 +36,7 @@
|
||||
<tr>
|
||||
<td>{{ source.name }}</td>
|
||||
<td>{{ source|fieldtype }}</td>
|
||||
<td>{{ source.additional_info|safe }}</td>
|
||||
<td>{{ source.ui_additional_info|safe|default:"" }}</td>
|
||||
<td>
|
||||
<a class="btn btn-default btn-sm"
|
||||
href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
|
||||
|
@ -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 %}
|
||||
|
@ -18,7 +18,7 @@ def get_links(model_instance):
|
||||
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:
|
||||
@ -43,7 +43,7 @@ def get_htmls(context, model_instance):
|
||||
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:
|
||||
|
@ -52,6 +52,13 @@ class PolicyCreateView(
|
||||
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)
|
||||
@ -72,6 +79,13 @@ class PolicyUpdateView(
|
||||
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
|
||||
form_class = path_to_class(form_class_path)
|
||||
|
@ -66,6 +66,9 @@ class PropertyMappingCreateView(
|
||||
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):
|
||||
@ -92,6 +95,13 @@ class PropertyMappingUpdateView(
|
||||
success_url = reverse_lazy("passbook_admin:property-mappings")
|
||||
success_message = _("Successfully updated Property Mapping")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
form_cls = self.get_form_class()
|
||||
if hasattr(form_cls, "template_name"):
|
||||
kwargs["base_template"] = form_cls.template_name
|
||||
return kwargs
|
||||
|
||||
def get_form_class(self):
|
||||
form_class_path = self.get_object().form
|
||||
form_class = path_to_class(form_class_path)
|
||||
|
@ -67,6 +67,8 @@ class UserDeleteView(
|
||||
model = User
|
||||
permission_required = "passbook_core.delete_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")
|
||||
|
@ -24,12 +24,10 @@ 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
|
||||
@ -57,13 +55,11 @@ 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("policies/expression", ExpressionPolicyViewSet)
|
||||
router.register("providers/all", ProviderViewSet)
|
||||
router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet)
|
||||
router.register("providers/oauth", OAuth2ProviderViewSet)
|
||||
|
@ -1,13 +1,14 @@
|
||||
"""passbook audit models"""
|
||||
from enum import Enum
|
||||
from inspect import getmodule, stack
|
||||
from typing import Optional, Dict, Any
|
||||
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.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.http import HttpRequest
|
||||
from django.utils.translation import gettext as _
|
||||
@ -32,11 +33,15 @@ def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||
source[key] = sanitize_dict(value)
|
||||
elif isinstance(value, models.Model):
|
||||
model_content_type = ContentType.objects.get_for_model(value)
|
||||
source[key] = {
|
||||
"app": model_content_type.app_label,
|
||||
"name": model_content_type.model,
|
||||
"pk": value.pk,
|
||||
}
|
||||
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
|
||||
|
||||
|
||||
@ -95,7 +100,6 @@ class Event(UUIDModel):
|
||||
app = getmodule(stack()[_inspect_offset][0]).__name__
|
||||
cleaned_kwargs = sanitize_dict(kwargs)
|
||||
event = Event(action=action.value, app=app, context=cleaned_kwargs)
|
||||
LOGGER.debug("Created Audit event", action=action, context=cleaned_kwargs)
|
||||
return event
|
||||
|
||||
def from_http(
|
||||
@ -124,6 +128,12 @@ class Event(UUIDModel):
|
||||
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:
|
||||
|
@ -1,9 +1,11 @@
|
||||
"""audit event tests"""
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from passbook.audit.models import Event, EventAction
|
||||
from passbook.core.models import Policy
|
||||
|
||||
|
||||
class TestAuditEvent(TestCase):
|
||||
@ -11,6 +13,21 @@ class TestAuditEvent(TestCase):
|
||||
|
||||
def test_new_with_model(self):
|
||||
"""Create a new Event passing a model as kwarg"""
|
||||
event = Event.new(EventAction.CUSTOM, model=get_anonymous_user())
|
||||
event.save()
|
||||
self.assertIsNotNone(event.pk)
|
||||
event = Event.new(EventAction.CUSTOM, test={"model": get_anonymous_user()})
|
||||
event.save() # We save to ensure nothing is un-saveable
|
||||
model_content_type = ContentType.objects.get_for_model(get_anonymous_user())
|
||||
self.assertEqual(
|
||||
event.context.get("test").get("model").get("app"),
|
||||
model_content_type.app_label,
|
||||
)
|
||||
|
||||
def test_new_with_uuid_model(self):
|
||||
"""Create a new Event passing a model (with UUID PK) as kwarg"""
|
||||
temp_model = Policy.objects.create()
|
||||
event = Event.new(EventAction.CUSTOM, model=temp_model)
|
||||
event.save() # We save to ensure nothing is un-saveable
|
||||
model_content_type = ContentType.objects.get_for_model(temp_model)
|
||||
self.assertEqual(
|
||||
event.context.get("model").get("app"), model_content_type.app_label
|
||||
)
|
||||
self.assertEqual(event.context.get("model").get("pk"), temp_model.pk.hex)
|
||||
|
@ -15,11 +15,13 @@ class ApplicationSerializer(ModelSerializer):
|
||||
"pk",
|
||||
"name",
|
||||
"slug",
|
||||
"launch_url",
|
||||
"icon_url",
|
||||
"provider",
|
||||
"policies",
|
||||
"skip_authorization",
|
||||
"provider",
|
||||
"meta_launch_url",
|
||||
"meta_icon_url",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
"policies",
|
||||
]
|
||||
|
||||
|
||||
|
5
passbook/core/exceptions.py
Normal file
5
passbook/core/exceptions.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""passbook core exceptions"""
|
||||
|
||||
|
||||
class PropertyMappingExpressionException(Exception):
|
||||
"""Error when a PropertyMapping Exception expression could not be parsed or evaluated."""
|
@ -19,19 +19,25 @@ class ApplicationForm(forms.ModelForm):
|
||||
fields = [
|
||||
"name",
|
||||
"slug",
|
||||
"launch_url",
|
||||
"icon_url",
|
||||
"provider",
|
||||
"policies",
|
||||
"skip_authorization",
|
||||
"provider",
|
||||
"meta_launch_url",
|
||||
"meta_icon_url",
|
||||
"meta_description",
|
||||
"meta_publisher",
|
||||
"policies",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"launch_url": forms.TextInput(),
|
||||
"icon_url": forms.TextInput(),
|
||||
"meta_launch_url": forms.TextInput(),
|
||||
"meta_icon_url": forms.TextInput(),
|
||||
"meta_publisher": forms.TextInput(),
|
||||
"policies": FilteredSelectMultiple(_("policies"), False),
|
||||
}
|
||||
labels = {
|
||||
"launch_url": _("Launch URL"),
|
||||
"icon_url": _("Icon URL"),
|
||||
"meta_launch_url": _("Launch URL"),
|
||||
"meta_icon_url": _("Icon URL"),
|
||||
"meta_description": _("Description"),
|
||||
"meta_publisher": _("Publisher"),
|
||||
}
|
||||
help_texts = {"policies": _("Policies required to access this Application.")}
|
||||
|
@ -70,7 +70,7 @@ class SignUpForm(forms.Form):
|
||||
"""Check if username is used already"""
|
||||
username = self.cleaned_data.get("username")
|
||||
if User.objects.filter(username=username).exists():
|
||||
LOGGER.warning("Username %s already exists", username)
|
||||
LOGGER.warning("username already exists", username=username)
|
||||
raise ValidationError(_("Username already exists"))
|
||||
return username
|
||||
|
||||
@ -79,7 +79,7 @@ class SignUpForm(forms.Form):
|
||||
email = self.cleaned_data.get("email")
|
||||
# Check if user exists already, error early
|
||||
if User.objects.filter(email=email).exists():
|
||||
LOGGER.debug("email %s exists in django", email)
|
||||
LOGGER.debug("email already exists", email=email)
|
||||
raise ValidationError(_("Email already exists"))
|
||||
return email
|
||||
|
||||
|
19
passbook/core/migrations/0006_propertymapping_template.py
Normal file
19
passbook/core/migrations/0006_propertymapping_template.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.0.3 on 2020-02-17 16:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0005_merge_20191025_2022"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="propertymapping",
|
||||
name="template",
|
||||
field=models.TextField(default=""),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
16
passbook/core/migrations/0007_auto_20200217_1934.py
Normal file
16
passbook/core/migrations/0007_auto_20200217_1934.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Generated by Django 3.0.3 on 2020-02-17 19:34
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0006_propertymapping_template"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="propertymapping", old_name="template", new_name="expression",
|
||||
),
|
||||
]
|
29
passbook/core/migrations/0008_auto_20200220_1242.py
Normal file
29
passbook/core/migrations/0008_auto_20200220_1242.py
Normal file
@ -0,0 +1,29 @@
|
||||
# Generated by Django 3.0.3 on 2020-02-20 12:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0007_auto_20200217_1934"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="application", old_name="icon_url", new_name="meta_icon_url",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="application", old_name="launch_url", new_name="meta_launch_url",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="application",
|
||||
name="meta_description",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="application",
|
||||
name="meta_publisher",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
@ -2,25 +2,34 @@
|
||||
from datetime import timedelta
|
||||
from random import SystemRandom
|
||||
from time import sleep
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
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.urls import reverse_lazy
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_prometheus.models import ExportModelOperationsMixin
|
||||
from guardian.mixins import GuardianUserMixin
|
||||
from jinja2 import Undefined
|
||||
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
|
||||
from jinja2.nativetypes import NativeEnvironment
|
||||
from model_utils.managers import InheritanceManager
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.types import UIUserSettings, UILoginButton
|
||||
from passbook.core.exceptions import PropertyMappingExpressionException
|
||||
from passbook.core.signals import password_changed
|
||||
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
|
||||
from passbook.policies.exceptions import PolicyException
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
NATIVE_ENVIRONMENT = NativeEnvironment()
|
||||
|
||||
|
||||
def default_nonce_duration():
|
||||
@ -28,7 +37,7 @@ def default_nonce_duration():
|
||||
return now() + timedelta(hours=4)
|
||||
|
||||
|
||||
class Group(UUIDModel):
|
||||
class Group(ExportModelOperationsMixin("group"), UUIDModel):
|
||||
"""Custom Group model which supports a basic hierarchy"""
|
||||
|
||||
name = models.CharField(_("name"), max_length=80)
|
||||
@ -49,7 +58,7 @@ class Group(UUIDModel):
|
||||
unique_together = (("name", "parent",),)
|
||||
|
||||
|
||||
class User(GuardianUserMixin, AbstractUser):
|
||||
class User(ExportModelOperationsMixin("user"), GuardianUserMixin, AbstractUser):
|
||||
"""Custom User model to allow easier adding o f user-based settings"""
|
||||
|
||||
uuid = models.UUIDField(default=uuid4, editable=False)
|
||||
@ -72,7 +81,7 @@ class User(GuardianUserMixin, AbstractUser):
|
||||
permissions = (("reset_user_password", "Reset Password"),)
|
||||
|
||||
|
||||
class Provider(models.Model):
|
||||
class Provider(ExportModelOperationsMixin("provider"), models.Model):
|
||||
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
|
||||
|
||||
property_mappings = models.ManyToManyField(
|
||||
@ -94,20 +103,7 @@ class PolicyModel(UUIDModel, CreatedUpdatedModel):
|
||||
policies = models.ManyToManyField("Policy", blank=True)
|
||||
|
||||
|
||||
class UserSettings:
|
||||
"""Dataclass for Factor and Source's user_settings"""
|
||||
|
||||
name: str
|
||||
icon: str
|
||||
view_name: str
|
||||
|
||||
def __init__(self, name: str, icon: str, view_name: str):
|
||||
self.name = name
|
||||
self.icon = icon
|
||||
self.view_name = view_name
|
||||
|
||||
|
||||
class Factor(PolicyModel):
|
||||
class Factor(ExportModelOperationsMixin("factor"), PolicyModel):
|
||||
"""Authentication factor, multiple instances of the same Factor can be used"""
|
||||
|
||||
name = models.TextField()
|
||||
@ -119,32 +115,36 @@ class Factor(PolicyModel):
|
||||
type = ""
|
||||
form = ""
|
||||
|
||||
def user_settings(self) -> Optional[UserSettings]:
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[UIUserSettings]:
|
||||
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||
user settings are available, or an instanace of UserSettings."""
|
||||
user settings are available, or an instanace of UIUserSettings."""
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
return f"Factor {self.slug}"
|
||||
|
||||
|
||||
class Application(PolicyModel):
|
||||
class Application(ExportModelOperationsMixin("application"), PolicyModel):
|
||||
"""Every Application which uses passbook for authentication/identification/authorization
|
||||
needs an Application record. Other authentication types can subclass this Model to
|
||||
add custom fields and other properties"""
|
||||
|
||||
name = models.TextField()
|
||||
slug = models.SlugField()
|
||||
launch_url = models.URLField(null=True, blank=True)
|
||||
icon_url = models.TextField(null=True, blank=True)
|
||||
skip_authorization = models.BooleanField(default=False)
|
||||
provider = models.OneToOneField(
|
||||
"Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT
|
||||
)
|
||||
skip_authorization = models.BooleanField(default=False)
|
||||
|
||||
meta_launch_url = models.URLField(null=True, blank=True)
|
||||
meta_icon_url = models.TextField(null=True, blank=True)
|
||||
meta_description = models.TextField(null=True, blank=True)
|
||||
meta_publisher = models.TextField(null=True, blank=True)
|
||||
|
||||
objects = InheritanceManager()
|
||||
|
||||
def get_provider(self):
|
||||
def get_provider(self) -> Optional[Provider]:
|
||||
"""Get casted provider instance"""
|
||||
if not self.provider:
|
||||
return None
|
||||
@ -154,11 +154,12 @@ class Application(PolicyModel):
|
||||
return self.name
|
||||
|
||||
|
||||
class Source(PolicyModel):
|
||||
class Source(ExportModelOperationsMixin("source"), PolicyModel):
|
||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||
|
||||
name = models.TextField()
|
||||
slug = models.SlugField()
|
||||
|
||||
enabled = models.BooleanField(default=True)
|
||||
property_mappings = models.ManyToManyField(
|
||||
"PropertyMapping", default=None, blank=True
|
||||
@ -169,19 +170,20 @@ class Source(PolicyModel):
|
||||
objects = InheritanceManager()
|
||||
|
||||
@property
|
||||
def login_button(self):
|
||||
"""Return a tuple of URL, Icon name and Name
|
||||
if Source should get a link on the login page"""
|
||||
def ui_login_button(self) -> Optional[UILoginButton]:
|
||||
"""If source uses a http-based flow, return UI Information about the login
|
||||
button. If source doesn't use http-based flow, return None."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def additional_info(self):
|
||||
def ui_additional_info(self) -> Optional[str]:
|
||||
"""Return additional Info, such as a callback URL. Show in the administration interface."""
|
||||
return None
|
||||
|
||||
def user_settings(self) -> Optional[UserSettings]:
|
||||
@property
|
||||
def ui_user_settings(self) -> Optional[UIUserSettings]:
|
||||
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||
user settings are available, or an instanace of UserSettings."""
|
||||
user settings are available, or an instanace of UIUserSettings."""
|
||||
return None
|
||||
|
||||
def __str__(self):
|
||||
@ -199,7 +201,7 @@ class UserSourceConnection(CreatedUpdatedModel):
|
||||
unique_together = (("user", "source"),)
|
||||
|
||||
|
||||
class Policy(UUIDModel, CreatedUpdatedModel):
|
||||
class Policy(ExportModelOperationsMixin("policy"), UUIDModel, CreatedUpdatedModel):
|
||||
"""Policies which specify if a user is authorized to use an Application. Can be overridden by
|
||||
other types to add other fields, more logic, etc."""
|
||||
|
||||
@ -241,7 +243,7 @@ class DebugPolicy(Policy):
|
||||
verbose_name_plural = _("Debug Policies")
|
||||
|
||||
|
||||
class Invitation(UUIDModel):
|
||||
class Invitation(ExportModelOperationsMixin("invitation"), UUIDModel):
|
||||
"""Single-use invitation link"""
|
||||
|
||||
created_by = models.ForeignKey("User", on_delete=models.CASCADE)
|
||||
@ -266,7 +268,7 @@ class Invitation(UUIDModel):
|
||||
verbose_name_plural = _("Invitations")
|
||||
|
||||
|
||||
class Nonce(UUIDModel):
|
||||
class Nonce(ExportModelOperationsMixin("nonce"), UUIDModel):
|
||||
"""One-time link for password resets/sign-up-confirmations"""
|
||||
|
||||
expires = models.DateTimeField(default=default_nonce_duration)
|
||||
@ -292,10 +294,34 @@ class PropertyMapping(UUIDModel):
|
||||
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
|
||||
|
||||
name = models.TextField()
|
||||
expression = models.TextField()
|
||||
|
||||
form = ""
|
||||
objects = InheritanceManager()
|
||||
|
||||
def evaluate(
|
||||
self, user: Optional[User], request: Optional[HttpRequest], **kwargs
|
||||
) -> Any:
|
||||
"""Evaluate `self.expression` using `**kwargs` as Context."""
|
||||
try:
|
||||
expression = NATIVE_ENVIRONMENT.from_string(self.expression)
|
||||
except TemplateSyntaxError as exc:
|
||||
raise PropertyMappingExpressionException from exc
|
||||
try:
|
||||
response = expression.render(user=user, request=request, **kwargs)
|
||||
if isinstance(response, Undefined):
|
||||
raise PropertyMappingExpressionException("Response was 'Undefined'")
|
||||
return response
|
||||
except UndefinedError as exc:
|
||||
raise PropertyMappingExpressionException from exc
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
try:
|
||||
NATIVE_ENVIRONMENT.from_string(self.expression)
|
||||
except TemplateSyntaxError as exc:
|
||||
raise ValidationError("Expression Syntax Error") from exc
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"Property Mapping {self.name}"
|
||||
|
||||
|
@ -1 +1 @@
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#FFFFFF;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><path d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
@ -7,7 +7,7 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<title>
|
||||
{% block title %}
|
||||
{% title %}
|
||||
|
@ -19,6 +19,9 @@
|
||||
<h1>{% trans 'Bad Request' %}</h1>
|
||||
</header>
|
||||
<form>
|
||||
{% if message %}
|
||||
<h3>{% trans message %}</h3>
|
||||
{% endif %}
|
||||
{% if 'back' in request.GET %}
|
||||
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
|
||||
{% endif %}
|
||||
|
@ -48,10 +48,16 @@
|
||||
<!--login-pf-section-->
|
||||
<section class="login-pf-social-section" role="contentinfo" aria-label="Log in with third party account">
|
||||
<ul class="login-pf-social login-pf-social-double-col list-unstyled">
|
||||
{% for url, icon, name in sources %}
|
||||
{% for source in sources %}
|
||||
<li class="login-pf-social-link">
|
||||
<a href="{{ url }}">
|
||||
<img src="{% static 'img/logos/' %}{{ icon }}.svg" alt="{{ name }}"> {{ name }}
|
||||
<a href="{{ source.url }}">
|
||||
{% if source.icon_path %}
|
||||
<img src="{% static source.icon_path %}" alt="{{ source.name }}">
|
||||
{% endif %}
|
||||
{% if source.icon_url %}
|
||||
<img src="icon_url" alt="{{ source.name }}">
|
||||
{% endif %}
|
||||
{{ source.name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
@ -2,42 +2,44 @@
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block head %}
|
||||
{{ block.super }}
|
||||
<style>
|
||||
img.app-icon {
|
||||
max-height: 72px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row row-cards-pf">
|
||||
{% for app in applications %}
|
||||
<div class="col-xs-12 col-sm-6 col-md-3">
|
||||
<div class="card-pf card-pf-accented card-pf-aggregate-status">
|
||||
<h2 class="card-pf-title">
|
||||
<span class="fa fa-shield"></span> {{ app.name }}
|
||||
</h2>
|
||||
<div class="card-pf-body">
|
||||
<p class="card-pf-aggregate-status-notifications">
|
||||
<span class="card-pf-aggregate-status-notification">
|
||||
<a href="{{ app.launch_url }}" class="add" data-toggle="tooltip" data-placement="top" title="{% trans 'Open App...' %}">
|
||||
{% if not app.icon_url %}
|
||||
<span class="pficon pficon-arrow"></span>
|
||||
{% else %}
|
||||
<img class="app-icon" src="{{ app.icon_url }}" alt="{% trans 'Application Icon' %}">
|
||||
{% endif %}
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
<div class="row row-cards-pf">
|
||||
{% for app in applications %}
|
||||
<div class="col-xs-12 col-sm-6 col-md-3">
|
||||
<div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
|
||||
<div class="card-pf-body">
|
||||
<div class="card-pf-top-element">
|
||||
<a href="{{ app.meta_launch_url }}">
|
||||
{% if not app.meta_icon_url %}
|
||||
<span class="pficon pficon-arrow card-pf-icon-circle"></span>
|
||||
{% else %}
|
||||
<img class="app-icon card-pf-icon-circle" src="{{ app.meta_icon_url }}" alt="{% trans 'Application Icon' %}">
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
<h2 class="card-pf-title text-center">
|
||||
<a href="{{ app.meta_launch_url }}">
|
||||
{{ app.name }}
|
||||
</a>
|
||||
</h2>
|
||||
{% if app.meta_publisher %}
|
||||
<div class="card-pf-items text-center">
|
||||
<small>{{ app.meta_publisher }}</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if app.meta_description %}
|
||||
<div class="card-pf-items text-center">
|
||||
<p>{{ app.meta_description }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-pf-view-checkbox">
|
||||
<a href="{{ app.meta_launch_url }}"></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<h1>{% trans 'No Applications available.' %}</h1>
|
||||
{% endfor %}
|
||||
</div><!-- /row -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,46 +1,53 @@
|
||||
"""passbook user settings template tags"""
|
||||
from typing import List
|
||||
from typing import List, Iterable
|
||||
|
||||
from django import template
|
||||
from django.template.context import RequestContext
|
||||
|
||||
from passbook.core.models import Factor, Source, UserSettings
|
||||
from passbook.core.types import UIUserSettings
|
||||
from passbook.core.models import Factor, Source
|
||||
from passbook.policies.engine import PolicyEngine
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def user_factors(context: RequestContext) -> List[UserSettings]:
|
||||
def user_factors(context: RequestContext) -> List[UIUserSettings]:
|
||||
"""Return list of all factors which apply to user"""
|
||||
user = context.get("request").user
|
||||
_all_factors = (
|
||||
_all_factors: Iterable[Factor] = (
|
||||
Factor.objects.filter(enabled=True).order_by("order").select_subclasses()
|
||||
)
|
||||
matching_factors: List[UserSettings] = []
|
||||
matching_factors: List[UIUserSettings] = []
|
||||
for factor in _all_factors:
|
||||
user_settings = factor.user_settings()
|
||||
user_settings = factor.ui_user_settings
|
||||
if not user_settings:
|
||||
continue
|
||||
policy_engine = PolicyEngine(
|
||||
factor.policies.all(), user, context.get("request")
|
||||
)
|
||||
policy_engine.build()
|
||||
if policy_engine.passing and user_settings:
|
||||
if policy_engine.passing:
|
||||
matching_factors.append(user_settings)
|
||||
return matching_factors
|
||||
|
||||
|
||||
@register.simple_tag(takes_context=True)
|
||||
def user_sources(context: RequestContext) -> List[UserSettings]:
|
||||
def user_sources(context: RequestContext) -> List[UIUserSettings]:
|
||||
"""Return a list of all sources which are enabled for the user"""
|
||||
user = context.get("request").user
|
||||
_all_sources = Source.objects.filter(enabled=True).select_subclasses()
|
||||
matching_sources: List[UserSettings] = []
|
||||
_all_sources: Iterable[Source] = (
|
||||
Source.objects.filter(enabled=True).select_subclasses()
|
||||
)
|
||||
matching_sources: List[UIUserSettings] = []
|
||||
for factor in _all_sources:
|
||||
user_settings = factor.user_settings()
|
||||
user_settings = factor.ui_user_settings
|
||||
if not user_settings:
|
||||
continue
|
||||
policy_engine = PolicyEngine(
|
||||
factor.policies.all(), user, context.get("request")
|
||||
)
|
||||
policy_engine.build()
|
||||
if policy_engine.passing and user_settings:
|
||||
if policy_engine.passing:
|
||||
matching_sources.append(user_settings)
|
||||
return matching_sources
|
||||
|
29
passbook/core/types.py
Normal file
29
passbook/core/types.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""passbook core dataclasses"""
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class UIUserSettings:
|
||||
"""Dataclass for Factor and Source's user_settings"""
|
||||
|
||||
name: str
|
||||
icon: str
|
||||
view_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class UILoginButton:
|
||||
"""Dataclass for Source's ui_ui_login_button"""
|
||||
|
||||
# Name, ran through i18n
|
||||
name: str
|
||||
|
||||
# URL Which Button points to
|
||||
url: str
|
||||
|
||||
# Icon name, ran through django's static
|
||||
icon_path: Optional[str] = None
|
||||
|
||||
# Icon URL, used as-is
|
||||
icon_url: Optional[str] = None
|
@ -47,9 +47,9 @@ class LoginView(UserPassesTestMixin, FormView):
|
||||
kwargs["sources"] = []
|
||||
sources = Source.objects.filter(enabled=True).select_subclasses()
|
||||
for source in sources:
|
||||
login_button = source.login_button
|
||||
if login_button:
|
||||
kwargs["sources"].append(login_button)
|
||||
ui_login_button = source.ui_login_button
|
||||
if ui_login_button:
|
||||
kwargs["sources"].append(ui_login_button)
|
||||
if kwargs["sources"]:
|
||||
self.template_name = "login/with_sources.html"
|
||||
return super().get_context_data(**kwargs)
|
||||
@ -72,7 +72,6 @@ class LoginView(UserPassesTestMixin, FormView):
|
||||
if not pre_user:
|
||||
# No user found
|
||||
return self.invalid_login(self.request)
|
||||
# self.request.session.flush()
|
||||
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
|
||||
return _redirect_with_qs("passbook_core:auth-process", self.request.GET)
|
||||
|
||||
@ -156,26 +155,9 @@ class SignUpView(UserPassesTestMixin, FormView):
|
||||
for error in exc.messages:
|
||||
errors.append(error)
|
||||
return self.form_invalid(form)
|
||||
# needs_confirmation = True
|
||||
# if self._invitation and not self._invitation.needs_confirmation:
|
||||
# needs_confirmation = False
|
||||
# if needs_confirmation:
|
||||
# nonce = Nonce.objects.create(user=self._user)
|
||||
# LOGGER.debug(str(nonce.uuid))
|
||||
# # Send email to user
|
||||
# send_email.delay(self._user.email, _('Confirm your account.'),
|
||||
# 'email/account_confirm.html', {
|
||||
# 'url': self.request.build_absolute_uri(
|
||||
# reverse('passbook_core:auth-sign-up-confirm', kwargs={
|
||||
# 'nonce': nonce.uuid
|
||||
# })
|
||||
# )
|
||||
# })
|
||||
# self._user.is_active = False
|
||||
# self._user.save()
|
||||
self.consume_invitation()
|
||||
messages.success(self.request, _("Successfully signed up!"))
|
||||
LOGGER.debug("Successfully signed up %s", form.cleaned_data.get("email"))
|
||||
LOGGER.debug("Successfully signed up", email=form.cleaned_data.get("email"))
|
||||
return redirect(reverse("passbook_core:auth-login"))
|
||||
|
||||
def consume_invitation(self):
|
||||
|
@ -15,7 +15,7 @@ class OverviewView(LoginRequiredMixin, TemplateView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs["applications"] = []
|
||||
for application in Application.objects.all():
|
||||
for application in Application.objects.all().order_by("name"):
|
||||
engine = PolicyEngine(
|
||||
application.policies.all(), self.request.user, self.request
|
||||
)
|
||||
|
@ -1,9 +1,9 @@
|
||||
"""OTP Factor"""
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Factor, UserSettings
|
||||
from passbook.core.types import UIUserSettings
|
||||
from passbook.core.models import Factor
|
||||
|
||||
|
||||
class OTPFactor(Factor):
|
||||
@ -17,9 +17,12 @@ class OTPFactor(Factor):
|
||||
type = "passbook.factors.otp.factors.OTPFactor"
|
||||
form = "passbook.factors.otp.forms.OTPFactorForm"
|
||||
|
||||
def user_settings(self) -> UserSettings:
|
||||
return UserSettings(
|
||||
_("OTP"), "pficon-locked", "passbook_factors_otp:otp-user-settings"
|
||||
@property
|
||||
def ui_user_settings(self) -> UIUserSettings:
|
||||
return UIUserSettings(
|
||||
name="OTP",
|
||||
icon="pficon-locked",
|
||||
view_name="passbook_factors_otp:otp-user-settings",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -7,8 +7,10 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import Http404, HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.views import View
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import FormView, TemplateView
|
||||
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
@ -19,7 +21,6 @@ from structlog import get_logger
|
||||
from passbook.audit.models import Event, EventAction
|
||||
from passbook.factors.otp.forms import OTPSetupForm
|
||||
from passbook.factors.otp.utils import otpauth_url
|
||||
from passbook.lib.boilerplate import NeverCacheMixin
|
||||
from passbook.lib.config import CONFIG
|
||||
|
||||
OTP_SESSION_KEY = "passbook_factors_otp_key"
|
||||
@ -146,7 +147,8 @@ class EnableView(LoginRequiredMixin, FormView):
|
||||
return redirect("passbook_factors_otp:otp-user-settings")
|
||||
|
||||
|
||||
class QRView(NeverCacheMixin, View):
|
||||
@method_decorator(never_cache, name="dispatch")
|
||||
class QRView(View):
|
||||
"""View returns an SVG image with the OTP token information"""
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
|
@ -3,7 +3,8 @@ from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from passbook.core.models import Factor, Policy, User, UserSettings
|
||||
from passbook.core.types import UIUserSettings
|
||||
from passbook.core.models import Factor, Policy, User
|
||||
|
||||
|
||||
class PasswordFactor(Factor):
|
||||
@ -18,9 +19,12 @@ class PasswordFactor(Factor):
|
||||
type = "passbook.factors.password.factor.PasswordFactor"
|
||||
form = "passbook.factors.password.forms.PasswordFactorForm"
|
||||
|
||||
def user_settings(self):
|
||||
return UserSettings(
|
||||
_("Change Password"), "pficon-key", "passbook_core:user-change-password"
|
||||
@property
|
||||
def ui_user_settings(self) -> UIUserSettings:
|
||||
return UIUserSettings(
|
||||
name="Change Password",
|
||||
icon="pficon-key",
|
||||
view_name="passbook_core:user-change-password",
|
||||
)
|
||||
|
||||
def password_passes(self, user: User) -> bool:
|
||||
|
@ -38,9 +38,8 @@ class TestFactorAuthentication(TestCase):
|
||||
def test_unauthenticated_raw(self):
|
||||
"""test direct call to AuthenticationView"""
|
||||
response = self.client.get(reverse("passbook_core:auth-process"))
|
||||
# Response should be 302 since no pending user is set
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("passbook_core:auth-login"))
|
||||
# Response should be 400 since no pending user is set
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
def test_unauthenticated_prepared(self):
|
||||
"""test direct call but with pending_uesr in session"""
|
||||
@ -71,9 +70,8 @@ class TestFactorAuthentication(TestCase):
|
||||
"""Test with already logged in user"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("passbook_core:auth-process"))
|
||||
# Response should be 302 since no pending user is set
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("passbook_core:overview"))
|
||||
# Response should be 400 since no pending user is set
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.client.logout()
|
||||
|
||||
def test_unauthenticated_post(self):
|
||||
|
@ -1,15 +1,17 @@
|
||||
"""passbook multi-factor authentication engine"""
|
||||
from typing import List, Tuple
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.shortcuts import get_object_or_404, redirect, reverse
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render, reverse
|
||||
from django.utils.http import urlencode
|
||||
from django.views.generic import View
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Factor, User
|
||||
from passbook.core.views.utils import PermissionDeniedView
|
||||
from passbook.lib.config import CONFIG
|
||||
from passbook.lib.utils.reflection import class_to_path, path_to_class
|
||||
from passbook.lib.utils.urls import is_url_absolute
|
||||
from passbook.policies.engine import PolicyEngine
|
||||
@ -23,7 +25,7 @@ def _redirect_with_qs(view, get_query_set=None):
|
||||
"""Wrapper to redirect whilst keeping GET Parameters"""
|
||||
target = reverse(view)
|
||||
if get_query_set:
|
||||
target += "?" + urlencode(get_query_set)
|
||||
target += "?" + urlencode(get_query_set.items())
|
||||
return redirect(target)
|
||||
|
||||
|
||||
@ -44,10 +46,28 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
current_factor: Factor
|
||||
|
||||
# Allow only not authenticated users to login
|
||||
def test_func(self):
|
||||
def test_func(self) -> bool:
|
||||
return AuthenticationView.SESSION_PENDING_USER in self.request.session
|
||||
|
||||
def handle_no_permission(self):
|
||||
def _check_config_domain(self) -> Optional[HttpResponse]:
|
||||
"""Checks if current request's domain matches configured Domain, and
|
||||
adds a warning if not."""
|
||||
current_domain = self.request.get_host()
|
||||
if ":" in current_domain:
|
||||
current_domain, _ = current_domain.split(":")
|
||||
config_domain = CONFIG.y("domain")
|
||||
if current_domain != config_domain:
|
||||
message = (
|
||||
f"Current domain of '{current_domain}' doesn't "
|
||||
f"match configured domain of '{config_domain}'."
|
||||
)
|
||||
LOGGER.warning(message)
|
||||
return render(
|
||||
self.request, "error/400.html", context={"message": message}, status=400
|
||||
)
|
||||
return None
|
||||
|
||||
def handle_no_permission(self) -> HttpResponse:
|
||||
# Function from UserPassesTestMixin
|
||||
if NEXT_ARG_NAME in self.request.GET:
|
||||
return redirect(self.request.GET.get(NEXT_ARG_NAME))
|
||||
@ -55,7 +75,7 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
return _redirect_with_qs("passbook_core:overview", self.request.GET)
|
||||
return _redirect_with_qs("passbook_core:auth-login", self.request.GET)
|
||||
|
||||
def get_pending_factors(self):
|
||||
def get_pending_factors(self) -> List[Tuple[str, str]]:
|
||||
"""Loading pending factors from Database or load from session variable"""
|
||||
# Write pending factors to session
|
||||
if AuthenticationView.SESSION_PENDING_FACTORS in self.request.session:
|
||||
@ -67,6 +87,7 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
)
|
||||
pending_factors = []
|
||||
for factor in _all_factors:
|
||||
factor: Factor
|
||||
LOGGER.debug(
|
||||
"Checking if factor applies to user",
|
||||
factor=factor,
|
||||
@ -81,10 +102,13 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
LOGGER.debug("Factor applies", factor=factor, user=self.pending_user)
|
||||
return pending_factors
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
# Check if user passes test (i.e. SESSION_PENDING_USER is set)
|
||||
user_test_result = self.get_test_func()()
|
||||
if not user_test_result:
|
||||
incorrect_domain_message = self._check_config_domain()
|
||||
if incorrect_domain_message:
|
||||
return incorrect_domain_message
|
||||
return self.handle_no_permission()
|
||||
# Extract pending user from session (only remember uid)
|
||||
self.pending_user = get_object_or_404(
|
||||
@ -117,7 +141,7 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
self._current_factor_class.request = request
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""pass get request to current factor"""
|
||||
LOGGER.debug(
|
||||
"Passing GET",
|
||||
@ -125,7 +149,7 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
)
|
||||
return self._current_factor_class.get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""pass post request to current factor"""
|
||||
LOGGER.debug(
|
||||
"Passing POST",
|
||||
@ -133,7 +157,7 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
)
|
||||
return self._current_factor_class.post(request, *args, **kwargs)
|
||||
|
||||
def user_ok(self):
|
||||
def user_ok(self) -> HttpResponse:
|
||||
"""Redirect to next Factor"""
|
||||
LOGGER.debug(
|
||||
"Factor passed",
|
||||
@ -160,14 +184,14 @@ class AuthenticationView(UserPassesTestMixin, View):
|
||||
LOGGER.debug("User passed all factors, logging in", user=self.pending_user)
|
||||
return self._user_passed()
|
||||
|
||||
def user_invalid(self):
|
||||
def user_invalid(self) -> HttpResponse:
|
||||
"""Show error message, user cannot login.
|
||||
This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
|
||||
LOGGER.debug("User invalid")
|
||||
self.cleanup()
|
||||
return _redirect_with_qs("passbook_core:auth-denied", self.request.GET)
|
||||
|
||||
def _user_passed(self):
|
||||
def _user_passed(self) -> HttpResponse:
|
||||
"""User Successfully passed all factors"""
|
||||
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
|
||||
login(self.request, self.pending_user, backend=backend)
|
||||
|
@ -1,12 +0,0 @@
|
||||
"""passbook django boilerplate code"""
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
|
||||
class NeverCacheMixin:
|
||||
"""Use never_cache as mixin for CBV"""
|
||||
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""Use never_cache as mixin for CBV"""
|
||||
return super().dispatch(*args, **kwargs)
|
@ -8,7 +8,6 @@ from urllib.parse import urlparse
|
||||
|
||||
import yaml
|
||||
from django.conf import ImproperlyConfigured
|
||||
from django.utils.autoreload import autoreload_started
|
||||
from structlog import get_logger
|
||||
|
||||
SEARCH_PATHS = ["passbook/lib/default.yml", "/etc/passbook/config.yml", "",] + glob(
|
||||
@ -142,12 +141,3 @@ class ConfigLoader:
|
||||
|
||||
|
||||
CONFIG = ConfigLoader()
|
||||
|
||||
|
||||
def signal_handler(sender, **_):
|
||||
"""Add all loaded config files to autoreload watcher"""
|
||||
for path in CONFIG.loaded_file:
|
||||
sender.watch_file(path)
|
||||
|
||||
|
||||
autoreload_started.connect(signal_handler)
|
||||
|
@ -1,12 +0,0 @@
|
||||
"""passbook util mixins"""
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
|
||||
class CSRFExemptMixin:
|
||||
"""wrapper to apply @csrf_exempt to CBV"""
|
||||
|
||||
@method_decorator(csrf_exempt)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
"""wrapper to apply @csrf_exempt to CBV"""
|
||||
return super().dispatch(*args, **kwargs)
|
@ -8,10 +8,12 @@ def before_send(event, hint):
|
||||
"""Check if error is database error, and ignore if so"""
|
||||
from django_redis.exceptions import ConnectionInterrupted
|
||||
from django.db import OperationalError, InternalError
|
||||
from django.core.exceptions import ValidationError
|
||||
from rest_framework.exceptions import APIException
|
||||
from billiard.exceptions import WorkerLostError
|
||||
from django.core.exceptions import DisallowedHost
|
||||
from botocore.client import ClientError
|
||||
from redis.exceptions import RedisError
|
||||
|
||||
ignored_classes = (
|
||||
OperationalError,
|
||||
@ -24,6 +26,9 @@ def before_send(event, hint):
|
||||
ConnectionResetError,
|
||||
KeyboardInterrupt,
|
||||
ClientError,
|
||||
ValidationError,
|
||||
OSError,
|
||||
RedisError,
|
||||
)
|
||||
if "exc_info" in hint:
|
||||
_exc_type, exc_value, _ = hint["exc_info"]
|
||||
|
@ -3,9 +3,9 @@ from hashlib import md5
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django import template
|
||||
from django.template import Context
|
||||
from django.apps import apps
|
||||
from django.db.models import Model
|
||||
from django.template import Context
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
@ -100,8 +100,8 @@ def gravatar(email, size=None, rating=None):
|
||||
# gravatar uses md5 for their URLs, so md5 can't be avoided
|
||||
gravatar_url = "%savatar/%s" % (
|
||||
"https://secure.gravatar.com/",
|
||||
md5(email.encode("utf-8")).hexdigest(),
|
||||
) # nosec
|
||||
md5(email.encode("utf-8")).hexdigest(), # nosec
|
||||
)
|
||||
|
||||
parameters = [p for p in (("s", size or "158"), ("r", rating or "g"),) if p[1]]
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""passbook helper views"""
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import CreateView
|
||||
from guardian.shortcuts import assign_perm
|
||||
|
||||
@ -20,6 +21,10 @@ class CreateAssignPermView(CreateView):
|
||||
self.object._meta.app_label,
|
||||
self.object._meta.model_name,
|
||||
)
|
||||
print(full_permission)
|
||||
assign_perm(full_permission, self.request.user, self.object)
|
||||
return response
|
||||
|
||||
|
||||
def bad_request_message(request: HttpRequest, message: str) -> HttpResponse:
|
||||
"""Return generic error page with message, with status code set to 400"""
|
||||
return render(request, "error/400.html", {"message": message}, status=400)
|
||||
|
@ -1,5 +1,5 @@
|
||||
"""passbook policy engine"""
|
||||
from multiprocessing import Pipe
|
||||
from multiprocessing import Pipe, set_start_method
|
||||
from multiprocessing.connection import Connection
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
@ -9,9 +9,12 @@ from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Policy, User
|
||||
from passbook.policies.process import PolicyProcess, cache_key
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
# This is only really needed for macOS, because Python 3.8 changed the default to spawn
|
||||
# spawn causes issues with objects that aren't picklable, and also the django setup
|
||||
set_start_method("fork")
|
||||
|
||||
|
||||
class PolicyProcessInfo:
|
||||
@ -36,13 +39,15 @@ class PolicyEngine:
|
||||
policies: List[Policy] = []
|
||||
request: PolicyRequest
|
||||
|
||||
__processes: List[PolicyProcessInfo] = []
|
||||
__cached_policies: List[PolicyResult]
|
||||
__processes: List[PolicyProcessInfo]
|
||||
|
||||
def __init__(self, policies, user: User, request: HttpRequest = None):
|
||||
self.policies = policies
|
||||
self.request = PolicyRequest(user)
|
||||
if request:
|
||||
self.request.http_request = request
|
||||
self.__cached_policies = []
|
||||
self.__processes = []
|
||||
|
||||
def _select_subclasses(self) -> List[Policy]:
|
||||
@ -55,21 +60,20 @@ class PolicyEngine:
|
||||
|
||||
def build(self) -> "PolicyEngine":
|
||||
"""Build task group"""
|
||||
cached_policies = []
|
||||
for policy in self._select_subclasses():
|
||||
cached_policy = cache.get(cache_key(policy, self.request.user), None)
|
||||
if cached_policy and self.use_cache:
|
||||
LOGGER.debug("Taking result from cache", policy=policy)
|
||||
cached_policies.append(cached_policy)
|
||||
else:
|
||||
LOGGER.debug("Evaluating policy", policy=policy)
|
||||
our_end, task_end = Pipe(False)
|
||||
task = PolicyProcess(policy, self.request, task_end)
|
||||
LOGGER.debug("Starting Process", policy=policy)
|
||||
task.start()
|
||||
self.__processes.append(
|
||||
PolicyProcessInfo(process=task, connection=our_end, policy=policy)
|
||||
)
|
||||
LOGGER.debug("P_ENG: Taking result from cache", policy=policy)
|
||||
self.__cached_policies.append(cached_policy)
|
||||
continue
|
||||
LOGGER.debug("P_ENG: Evaluating policy", policy=policy)
|
||||
our_end, task_end = Pipe(False)
|
||||
task = PolicyProcess(policy, self.request, task_end)
|
||||
LOGGER.debug("P_ENG: Starting Process", policy=policy)
|
||||
task.start()
|
||||
self.__processes.append(
|
||||
PolicyProcessInfo(process=task, connection=our_end, policy=policy)
|
||||
)
|
||||
# If all policies are cached, we have an empty list here.
|
||||
for proc_info in self.__processes:
|
||||
proc_info.process.join(proc_info.policy.timeout)
|
||||
@ -82,13 +86,14 @@ class PolicyEngine:
|
||||
def result(self) -> Tuple[bool, List[str]]:
|
||||
"""Get policy-checking result"""
|
||||
messages: List[str] = []
|
||||
for proc_info in self.__processes:
|
||||
LOGGER.debug(
|
||||
"Result", policy=proc_info.policy, passing=proc_info.result.passing
|
||||
)
|
||||
if proc_info.result.messages:
|
||||
messages += proc_info.result.messages
|
||||
if not proc_info.result.passing:
|
||||
process_results: List[PolicyResult] = [
|
||||
x.result for x in self.__processes if x.result
|
||||
]
|
||||
for result in process_results + self.__cached_policies:
|
||||
LOGGER.debug("P_ENG: result", passing=result.passing)
|
||||
if result.messages:
|
||||
messages += result.messages
|
||||
if not result.passing:
|
||||
return False, messages
|
||||
return True, messages
|
||||
|
||||
|
@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Policy
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
5
passbook/policies/expression/admin.py
Normal file
5
passbook/policies/expression/admin.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""Passbook passbook expression policy Admin"""
|
||||
|
||||
from passbook.lib.admin import admin_autoregister
|
||||
|
||||
admin_autoregister("passbook_policies_expression")
|
21
passbook/policies/expression/api.py
Normal file
21
passbook/policies/expression/api.py
Normal file
@ -0,0 +1,21 @@
|
||||
"""Expression Policy API"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS
|
||||
|
||||
|
||||
class ExpressionPolicySerializer(ModelSerializer):
|
||||
"""Group Membership Policy Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = ExpressionPolicy
|
||||
fields = GENERAL_SERIALIZER_FIELDS + ["expression"]
|
||||
|
||||
|
||||
class ExpressionPolicyViewSet(ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = ExpressionPolicy.objects.all()
|
||||
serializer_class = ExpressionPolicySerializer
|
11
passbook/policies/expression/apps.py
Normal file
11
passbook/policies/expression/apps.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""Passbook policy_expression app config"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PassbookPolicyExpressionConfig(AppConfig):
|
||||
"""Passbook policy_expression app config"""
|
||||
|
||||
name = "passbook.policies.expression"
|
||||
label = "passbook_policies_expression"
|
||||
verbose_name = "passbook Policies.Expression"
|
94
passbook/policies/expression/evaluator.py
Normal file
94
passbook/policies/expression/evaluator.py
Normal file
@ -0,0 +1,94 @@
|
||||
"""passbook expression policy evaluator"""
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from jinja2 import Undefined
|
||||
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
|
||||
from jinja2.nativetypes import NativeEnvironment
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.factors.view import AuthenticationView
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from passbook.core.models import User
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class Evaluator:
|
||||
"""Validate and evaulate jinja2-based expressions"""
|
||||
|
||||
_env: NativeEnvironment
|
||||
|
||||
def __init__(self):
|
||||
self._env = NativeEnvironment()
|
||||
# update passbook/policies/expression/templates/policy/expression/form.html
|
||||
# update docs/policies/expression/index.md
|
||||
self._env.filters["regex_match"] = Evaluator.jinja2_filter_regex_match
|
||||
self._env.filters["regex_replace"] = Evaluator.jinja2_filter_regex_replace
|
||||
|
||||
@staticmethod
|
||||
def jinja2_filter_regex_match(value: Any, regex: str) -> bool:
|
||||
"""Jinja2 Filter to run re.search"""
|
||||
return re.search(regex, value) is None
|
||||
|
||||
@staticmethod
|
||||
def jinja2_filter_regex_replace(value: Any, regex: str, repl: str) -> str:
|
||||
"""Jinja2 Filter to run re.sub"""
|
||||
return re.sub(regex, repl, value)
|
||||
|
||||
@staticmethod
|
||||
def jinja2_func_is_group_member(user: "User", group_name: str) -> bool:
|
||||
"""Check if `user` is member of group with name `group_name`"""
|
||||
return user.groups.filter(name=group_name).exists()
|
||||
|
||||
def _get_expression_context(
|
||||
self, request: PolicyRequest, **kwargs
|
||||
) -> Dict[str, Any]:
|
||||
"""Return dictionary with additional global variables passed to expression"""
|
||||
# update passbook/policies/expression/templates/policy/expression/form.html
|
||||
# update docs/policies/expression/index.md
|
||||
kwargs["pb_is_sso_flow"] = request.http_request.session.get(
|
||||
AuthenticationView.SESSION_IS_SSO_LOGIN, False
|
||||
)
|
||||
kwargs["pb_is_group_member"] = Evaluator.jinja2_func_is_group_member
|
||||
kwargs["pb_logger"] = get_logger()
|
||||
return kwargs
|
||||
|
||||
def evaluate(self, expression_source: str, request: PolicyRequest) -> PolicyResult:
|
||||
"""Parse and evaluate expression.
|
||||
If the Expression evaluates to a list with 2 items, the first is used as passing bool and
|
||||
the second as messages.
|
||||
If the Expression evaluates to a truthy-object, it is used as passing bool."""
|
||||
try:
|
||||
expression = self._env.from_string(expression_source)
|
||||
except TemplateSyntaxError as exc:
|
||||
return PolicyResult(False, str(exc))
|
||||
try:
|
||||
result = expression.render(
|
||||
request=request, **self._get_expression_context(request)
|
||||
)
|
||||
if isinstance(result, Undefined):
|
||||
LOGGER.warning(
|
||||
"Expression policy returned undefined",
|
||||
src=expression_source,
|
||||
req=request,
|
||||
)
|
||||
return PolicyResult(False)
|
||||
if isinstance(result, list) and len(result) == 2:
|
||||
return PolicyResult(*result)
|
||||
if result:
|
||||
return PolicyResult(result)
|
||||
return PolicyResult(False)
|
||||
except UndefinedError as exc:
|
||||
return PolicyResult(False, str(exc))
|
||||
|
||||
def validate(self, expression: str):
|
||||
"""Validate expression's syntax, raise ValidationError if Syntax is invalid"""
|
||||
try:
|
||||
self._env.from_string(expression)
|
||||
return True
|
||||
except TemplateSyntaxError as exc:
|
||||
raise ValidationError("Expression Syntax Error") from exc
|
22
passbook/policies/expression/forms.py
Normal file
22
passbook/policies/expression/forms.py
Normal file
@ -0,0 +1,22 @@
|
||||
"""passbook Expression Policy forms"""
|
||||
|
||||
from django import forms
|
||||
|
||||
from passbook.policies.expression.models import ExpressionPolicy
|
||||
from passbook.policies.forms import GENERAL_FIELDS
|
||||
|
||||
|
||||
class ExpressionPolicyForm(forms.ModelForm):
|
||||
"""ExpressionPolicy Form"""
|
||||
|
||||
template_name = "policy/expression/form.html"
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ExpressionPolicy
|
||||
fields = GENERAL_FIELDS + [
|
||||
"expression",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-07 14:07
|
||||
# Generated by Django 3.0.3 on 2020-02-18 14:00
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
@ -9,12 +9,12 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0001_initial"),
|
||||
("passbook_core", "0007_auto_20200217_1934"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SSOLoginPolicy",
|
||||
name="ExpressionPolicy",
|
||||
fields=[
|
||||
(
|
||||
"policy_ptr",
|
||||
@ -27,10 +27,11 @@ class Migration(migrations.Migration):
|
||||
to="passbook_core.Policy",
|
||||
),
|
||||
),
|
||||
("expression", models.TextField()),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "SSO Login Policy",
|
||||
"verbose_name_plural": "SSO Login Policies",
|
||||
"verbose_name": "Expression Policy",
|
||||
"verbose_name_plural": "Expression Policies",
|
||||
},
|
||||
bases=("passbook_core.policy",),
|
||||
),
|
28
passbook/policies/expression/models.py
Normal file
28
passbook/policies/expression/models.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""passbook expression Policy Models"""
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Policy
|
||||
from passbook.policies.expression.evaluator import Evaluator
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
|
||||
class ExpressionPolicy(Policy):
|
||||
"""Jinja2-based Expression policy that allows Admins to write their own logic"""
|
||||
|
||||
expression = models.TextField()
|
||||
|
||||
form = "passbook.policies.expression.forms.ExpressionPolicyForm"
|
||||
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
"""Evaluate and render expression. Returns PolicyResult(false) on error."""
|
||||
return Evaluator().evaluate(self.expression, request)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
Evaluator().validate(self.expression)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Expression Policy")
|
||||
verbose_name_plural = _("Expression Policies")
|
@ -0,0 +1,27 @@
|
||||
{% extends "generic/form.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block beneath_form %}
|
||||
<div class="form-group ">
|
||||
<label class="col-sm-2 control-label" for="friendly_name-2">
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<p>
|
||||
Expression using <a href="https://jinja.palletsprojects.com/en/2.11.x/templates/">Jinja</a>. Following variables are available:
|
||||
</p>
|
||||
<ul>
|
||||
<li><code>request.user</code>: Passbook User Object (<a href="https://beryju.github.io/passbook/property-mappings/reference/user-object/">Reference</a>)</li>
|
||||
<li><code>request.http_request</code>: Django HTTP Request Object (<a href="https://docs.djangoproject.com/en/3.0/ref/request-response/#httprequest-objects">Reference</a>) </li>
|
||||
<li><code>request.obj</code>: Model the Policy is run against. </li>
|
||||
<li><code>pb_is_sso_flow</code>: Boolean which is true if request was initiated by authenticating through an external Provider.</li>
|
||||
<li><code>pb_is_group_member(user, group_name)</code>: Function which checks if <code>user</code> is member of a Group with Name <code>group_name</code>.</li>
|
||||
</ul>
|
||||
<p>Custom Filters:</p>
|
||||
<ul>
|
||||
<li><code>regex_match(regex)</code>: Checks if value matches <code>regex</code></li>
|
||||
<li><code>regex_replace(regex, repl)</code>: Replace string matched by <code>regex</code> with <code>repl</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,4 +0,0 @@
|
||||
"""autodiscover admin"""
|
||||
from passbook.lib.admin import admin_autoregister
|
||||
|
||||
admin_autoregister("passbook_policies_group")
|
@ -1,21 +0,0 @@
|
||||
"""Source API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS
|
||||
from passbook.policies.group.models import GroupMembershipPolicy
|
||||
|
||||
|
||||
class GroupMembershipPolicySerializer(ModelSerializer):
|
||||
"""Group Membership Policy Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = GroupMembershipPolicy
|
||||
fields = GENERAL_SERIALIZER_FIELDS + ["group"]
|
||||
|
||||
|
||||
class GroupMembershipPolicyViewSet(ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = GroupMembershipPolicy.objects.all()
|
||||
serializer_class = GroupMembershipPolicySerializer
|
@ -1,11 +0,0 @@
|
||||
"""passbook Group policy app config"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PassbookPoliciesGroupConfig(AppConfig):
|
||||
"""passbook Group policy app config"""
|
||||
|
||||
name = "passbook.policies.group"
|
||||
label = "passbook_policies_group"
|
||||
verbose_name = "passbook Policies.Group"
|
@ -1,21 +0,0 @@
|
||||
"""passbook Policy forms"""
|
||||
|
||||
from django import forms
|
||||
|
||||
from passbook.policies.forms import GENERAL_FIELDS
|
||||
from passbook.policies.group.models import GroupMembershipPolicy
|
||||
|
||||
|
||||
class GroupMembershipPolicyForm(forms.ModelForm):
|
||||
"""GroupMembershipPolicy Form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = GroupMembershipPolicy
|
||||
fields = GENERAL_FIELDS + [
|
||||
"group",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"order": forms.NumberInput(),
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-07 14:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="GroupMembershipPolicy",
|
||||
fields=[
|
||||
(
|
||||
"policy_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="passbook_core.Policy",
|
||||
),
|
||||
),
|
||||
(
|
||||
"group",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="passbook_core.Group",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Group Membership Policy",
|
||||
"verbose_name_plural": "Group Membership Policies",
|
||||
},
|
||||
bases=("passbook_core.policy",),
|
||||
),
|
||||
]
|
@ -1,22 +0,0 @@
|
||||
"""passbook group models models"""
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Group, Policy
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
|
||||
|
||||
class GroupMembershipPolicy(Policy):
|
||||
"""Policy to check if the user is member in a certain group"""
|
||||
|
||||
group = models.ForeignKey(Group, on_delete=models.CASCADE)
|
||||
|
||||
form = "passbook.policies.group.forms.GroupMembershipPolicyForm"
|
||||
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists())
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Group Membership Policy")
|
||||
verbose_name_plural = _("Group Membership Policies")
|
@ -35,7 +35,7 @@ class HaveIBeenPwendPolicy(Policy):
|
||||
full_hash, count = line.split(":")
|
||||
if pw_hash[5:] == full_hash.lower():
|
||||
final_count = int(count)
|
||||
LOGGER.debug("Got count %d for hash %s", final_count, pw_hash[:5])
|
||||
LOGGER.debug("got hibp result", count=final_count, hash=pw_hash[:5])
|
||||
if final_count > self.allowed_count:
|
||||
message = _(
|
||||
"Password exists on %(count)d online lists." % {"count": final_count}
|
||||
|
@ -1,4 +0,0 @@
|
||||
"""autodiscover admin"""
|
||||
from passbook.lib.admin import admin_autoregister
|
||||
|
||||
admin_autoregister("passbook_policies_matcher")
|
@ -1,25 +0,0 @@
|
||||
"""Source API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS
|
||||
from passbook.policies.matcher.models import FieldMatcherPolicy
|
||||
|
||||
|
||||
class FieldMatcherPolicySerializer(ModelSerializer):
|
||||
"""Field Matcher Policy Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = FieldMatcherPolicy
|
||||
fields = GENERAL_SERIALIZER_FIELDS + [
|
||||
"user_field",
|
||||
"match_action",
|
||||
"value",
|
||||
]
|
||||
|
||||
|
||||
class FieldMatcherPolicyViewSet(ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = FieldMatcherPolicy.objects.all()
|
||||
serializer_class = FieldMatcherPolicySerializer
|
@ -1,11 +0,0 @@
|
||||
"""passbook Matcher policy app config"""
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PassbookPoliciesMatcherConfig(AppConfig):
|
||||
"""passbook Matcher policy app config"""
|
||||
|
||||
name = "passbook.policies.matcher"
|
||||
label = "passbook_policies_matcher"
|
||||
verbose_name = "passbook Policies.Matcher"
|
@ -1,23 +0,0 @@
|
||||
"""passbook Policy forms"""
|
||||
|
||||
from django import forms
|
||||
|
||||
from passbook.policies.forms import GENERAL_FIELDS
|
||||
from passbook.policies.matcher.models import FieldMatcherPolicy
|
||||
|
||||
|
||||
class FieldMatcherPolicyForm(forms.ModelForm):
|
||||
"""FieldMatcherPolicy Form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = FieldMatcherPolicy
|
||||
fields = GENERAL_FIELDS + [
|
||||
"user_field",
|
||||
"match_action",
|
||||
"value",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
"value": forms.TextInput(),
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-07 14:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("passbook_core", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="FieldMatcherPolicy",
|
||||
fields=[
|
||||
(
|
||||
"policy_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="passbook_core.Policy",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_field",
|
||||
models.TextField(
|
||||
choices=[
|
||||
("username", "Username"),
|
||||
("name", "Name"),
|
||||
("email", "E-Mail"),
|
||||
("is_staff", "Is staff"),
|
||||
("is_active", "Is active"),
|
||||
("data_joined", "Date joined"),
|
||||
]
|
||||
),
|
||||
),
|
||||
(
|
||||
"match_action",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("startswith", "Starts with"),
|
||||
("endswith", "Ends with"),
|
||||
("contains", "Contains"),
|
||||
("regexp", "Regexp"),
|
||||
("exact", "Exact"),
|
||||
],
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
("value", models.TextField()),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Field matcher Policy",
|
||||
"verbose_name_plural": "Field matcher Policies",
|
||||
},
|
||||
bases=("passbook_core.policy",),
|
||||
),
|
||||
]
|
@ -1,83 +0,0 @@
|
||||
"""user field matcher models"""
|
||||
import re
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Policy
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class FieldMatcherPolicy(Policy):
|
||||
"""Policy which checks if a field of the User model matches/doesn't match a
|
||||
certain pattern"""
|
||||
|
||||
MATCH_STARTSWITH = "startswith"
|
||||
MATCH_ENDSWITH = "endswith"
|
||||
MATCH_CONTAINS = "contains"
|
||||
MATCH_REGEXP = "regexp"
|
||||
MATCH_EXACT = "exact"
|
||||
|
||||
MATCHES = (
|
||||
(MATCH_STARTSWITH, _("Starts with")),
|
||||
(MATCH_ENDSWITH, _("Ends with")),
|
||||
(MATCH_CONTAINS, _("Contains")),
|
||||
(MATCH_REGEXP, _("Regexp")),
|
||||
(MATCH_EXACT, _("Exact")),
|
||||
)
|
||||
|
||||
USER_FIELDS = (
|
||||
("username", _("Username"),),
|
||||
("name", _("Name"),),
|
||||
("email", _("E-Mail"),),
|
||||
("is_staff", _("Is staff"),),
|
||||
("is_active", _("Is active"),),
|
||||
("data_joined", _("Date joined"),),
|
||||
)
|
||||
|
||||
user_field = models.TextField(choices=USER_FIELDS)
|
||||
match_action = models.CharField(max_length=50, choices=MATCHES)
|
||||
value = models.TextField()
|
||||
|
||||
form = "passbook.policies.matcher.forms.FieldMatcherPolicyForm"
|
||||
|
||||
def __str__(self):
|
||||
description = (
|
||||
f"{self.name}, user.{self.user_field} {self.match_action} '{self.value}'"
|
||||
)
|
||||
if self.name:
|
||||
description = f"{self.name}: {description}"
|
||||
return description
|
||||
|
||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||
"""Check if user instance passes this role"""
|
||||
if not hasattr(request.user, self.user_field):
|
||||
raise ValueError("Field does not exist")
|
||||
user_field_value = getattr(request.user, self.user_field, None)
|
||||
LOGGER.debug(
|
||||
"Checking field",
|
||||
value=user_field_value,
|
||||
action=self.match_action,
|
||||
should_be=self.value,
|
||||
)
|
||||
passes = False
|
||||
if self.match_action == FieldMatcherPolicy.MATCH_STARTSWITH:
|
||||
passes = user_field_value.startswith(self.value)
|
||||
if self.match_action == FieldMatcherPolicy.MATCH_ENDSWITH:
|
||||
passes = user_field_value.endswith(self.value)
|
||||
if self.match_action == FieldMatcherPolicy.MATCH_CONTAINS:
|
||||
passes = self.value in user_field_value
|
||||
if self.match_action == FieldMatcherPolicy.MATCH_REGEXP:
|
||||
pattern = re.compile(self.value)
|
||||
passes = bool(pattern.match(user_field_value))
|
||||
if self.match_action == FieldMatcherPolicy.MATCH_EXACT:
|
||||
passes = user_field_value == self.value
|
||||
return PolicyResult(passes)
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Field matcher Policy")
|
||||
verbose_name_plural = _("Field matcher Policies")
|
@ -6,7 +6,7 @@ from django.utils.translation import gettext as _
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Policy
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
@ -7,7 +7,7 @@ from structlog import get_logger
|
||||
|
||||
from passbook.core.models import Policy
|
||||
from passbook.policies.exceptions import PolicyException
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Policy, User
|
||||
from passbook.lib.utils.http import get_client_ip
|
||||
from passbook.policies.struct import PolicyRequest, PolicyResult
|
||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||
|
||||
|
||||
class ReputationPolicy(Policy):
|
||||
|
@ -1,4 +0,0 @@
|
||||
"""autodiscover admin"""
|
||||
from passbook.lib.admin import admin_autoregister
|
||||
|
||||
admin_autoregister("passbook_policies_sso")
|
@ -1,21 +0,0 @@
|
||||
"""Source API Views"""
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS
|
||||
from passbook.policies.sso.models import SSOLoginPolicy
|
||||
|
||||
|
||||
class SSOLoginPolicySerializer(ModelSerializer):
|
||||
"""SSO Login Policy Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = SSOLoginPolicy
|
||||
fields = GENERAL_SERIALIZER_FIELDS
|
||||
|
||||
|
||||
class SSOLoginPolicyViewSet(ModelViewSet):
|
||||
"""Source Viewset"""
|
||||
|
||||
queryset = SSOLoginPolicy.objects.all()
|
||||
serializer_class = SSOLoginPolicySerializer
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user