Compare commits

..

121 Commits

Author SHA1 Message Date
187780dab2 new release: 0.8.5-beta 2020-02-20 21:39:13 +01:00
d988f37afc lib: add SentryIgnoredException, to easily ignore exceptions from sentry 2020-02-20 21:38:53 +01:00
295c0bae3f sources/saml: validate SAMLResponse signature 2020-02-20 21:34:25 +01:00
38a22ddf13 providers/saml: cleanup encoding 2020-02-20 21:33:10 +01:00
d06f1abb89 providers/saml: add POST binding support to Metadata 2020-02-20 17:38:42 +01:00
027a64fad2 providers/saml: change default NameID Format to emailAddress 2020-02-20 17:37:09 +01:00
84fc54ddaa sources/saml: entity_id -> issuer 2020-02-20 17:23:27 +01:00
0b5caa85f5 all: sort imports and cleanup 2020-02-20 17:23:05 +01:00
14e0a17dbc ui: don't remove dashes when auto generating slug 2020-02-20 17:13:50 +01:00
3c04afa31f root: use different cookie names for dev instance 2020-02-20 17:08:11 +01:00
40a2a26904 sources/saml: fix Metadata cert including PEM header 2020-02-20 17:05:11 +01:00
c8b3c6e51a sources/saml: fix build_full_url using incorrect URL parameter 2020-02-20 17:04:54 +01:00
e0272a6422 providers/saml: Show error message when trying to get metadata without assigning application 2020-02-20 17:04:20 +01:00
b290bbf6d7 new release: 0.8.4-beta 2020-02-20 16:17:23 +01:00
8d875cb01d providers/saml: fix /login/ pointing to wrong view 2020-02-20 16:13:55 +01:00
36b1f8ba36 new release: 0.8.3-beta 2020-02-20 15:14:49 +01:00
6c889eff27 core: fix application icons not loading, fix with_sources being broken 2020-02-20 14:30:06 +01:00
9d8675e54b new release: 0.8.2-beta 2020-02-20 13:57:46 +01:00
22ae986c0b root: add logger name to log output 2020-02-20 13:52:14 +01:00
2bef5f3911 policies: struct -> types to match core 2020-02-20 13:52:05 +01:00
3c2b8e5ee1 all: prefix all UI related methods with ui_, switch to property and return dataclass 2020-02-20 13:51:41 +01:00
c96571bdba core: fix discord logo being hard to see 2020-02-20 13:50:05 +01:00
2dfd93afb1 core: add more fields for metadata of applications 2020-02-20 13:45:22 +01:00
1d22e30c70 lib: sentry ignore Redis and OSError 2020-02-19 17:13:44 +01:00
07b7951390 sources/ldap: handle user_sync errors better, show warning when user exists already 2020-02-19 16:20:33 +01:00
995615d0a0 policies/expression: Return False if Policy returns Undefined and log warning 2020-02-19 16:19:02 +01:00
ac273aab75 core: raise PropertyMappingExpressionException when PropertyMapping returns Undefined 2020-02-19 16:18:31 +01:00
44cd03654d core: base set maximum-scale to 1 2020-02-19 15:11:25 +01:00
3e2375f970 new release: 0.8.1-beta 2020-02-19 11:31:05 +01:00
38ad8e5fd3 policies/expression: fix pb_is_sso_flow 2020-02-19 11:01:20 +01:00
c481558a46 helm: fix error that FLUSHDB Command is not available 2020-02-19 10:57:57 +01:00
e27a05a7fc lib/sentry: ignore django validation error 2020-02-19 10:54:29 +01:00
e4886f0c6f new release: 0.8.0-beta 2020-02-19 10:29:52 +01:00
8b2ce5476a policies/expression: add annotation to update docs, name jinja filters/funcs more clearly 2020-02-19 10:23:42 +01:00
1b82283a20 docs: update policy types, add docs for expression policies 2020-02-19 10:21:28 +01:00
7f3d0113c2 policies: remove redundant policies which can be easily implemented with expressions 2020-02-19 09:51:15 +01:00
0f6dd33a6b api: add expression policy to API URLs 2020-02-19 09:49:57 +01:00
5b79b3fd22 policies/expression: move evaluation code into separate class 2020-02-19 09:49:38 +01:00
d68c72f1fa lib: remove method_decorator Mixins 2020-02-18 22:28:47 +01:00
9267d0c1dd all: general maintenance, prepare for pyright 2020-02-18 22:12:51 +01:00
865abc005a sources/oauth: remove leading spaces in default URLs 2020-02-18 21:49:53 +01:00
a2725d5b82 sources/oauth: remove redundant OAuth2Clients 2020-02-18 21:49:40 +01:00
4a05bc6e02 sources/oauth: improve default OAuth2 Client, send access_token as Bearer Authz 2020-02-18 21:49:23 +01:00
4e8238603a all: cleanup logging to be structured 2020-02-18 21:35:58 +01:00
ff25c1c057 admin: load custom policy templates 2020-02-18 21:35:21 +01:00
78cddca0d7 admin: fix user object being overwritten when deleting a user 2020-02-18 21:35:06 +01:00
4742ee1d93 docs: add aws integration 2020-02-18 20:14:54 +01:00
0c2dc309e7 providers/saml: fix metadata URLs using incorrect params 2020-02-18 20:14:28 +01:00
144935d10f docs: add ansible tower/awx integration guide 2020-02-18 17:33:31 +01:00
74ad1b6759 factors: strip port for domain check 2020-02-18 17:05:30 +01:00
591d2f89a1 audit: log event creation on save 2020-02-18 17:05:11 +01:00
7c353f9297 sources/oauth: remove supervisr 2020-02-18 17:01:08 +01:00
cd1af15c56 core: sort applications by name 2020-02-18 17:00:56 +01:00
878169ea2e core: only show icon on login page if defined 2020-02-18 17:00:26 +01:00
38dfb03668 new release: 0.7.17-beta 2020-02-18 16:29:23 +01:00
e2631cec0e factors/view: show concise error message when domain is mis-configured 2020-02-18 16:29:04 +01:00
5dad853f8a docs: use note blocks instead of code blocks for product description 2020-02-18 15:34:41 +01:00
9f00843441 policies/expression: add Expression based policy 2020-02-18 15:12:50 +01:00
f31cd7dec6 core: check PropertyMapping's expression syntax before save 2020-02-18 15:12:05 +01:00
1c1afca31f providers/saml: fix linting error 2020-02-18 11:34:04 +01:00
fbd4bdef33 providers/saml: add modal to show metadata without download 2020-02-18 10:57:43 +01:00
5b22f9b6c3 providers/saml: transition to dataclass from dict, cleanup unused templates, add missing autosubmit_form 2020-02-18 10:57:30 +01:00
083e317028 lib: add helper method for 400 response with message 2020-02-18 10:13:53 +01:00
95416623b3 sources/ldap: better handle property mapping evaluation errors 2020-02-18 10:13:05 +01:00
813b2676de providers/saml: better handle PropertyMapping evaluation errors 2020-02-18 10:12:42 +01:00
aeca66a288 providers/saml: change assertion_valid_not_before default to -5 minutes 2020-02-17 21:32:23 +01:00
04a5428148 new release: 0.7.16-beta 2020-02-17 21:02:54 +01:00
73b173b92a admin: fix form missing on update pages 2020-02-17 21:02:47 +01:00
7cbf20a71c admin: fix CodeMirror field not loading correctly 2020-02-17 21:02:35 +01:00
7a98e6d92b new release: 0.7.15-beta 2020-02-17 20:45:56 +01:00
49e915f98b Merge pull request #4 from BeryJu/propertymapping-jinja
PropertyMappings using Jinja
2020-02-17 20:45:04 +01:00
3aa2f1e892 *: propertymapping template -> expression 2020-02-17 20:38:14 +01:00
bc4b7ef44d providers/saml: add custom help text for templates, add docs for User Object reference 2020-02-17 20:30:14 +01:00
9400b01a55 admin: parameterise generic from's base template 2020-02-17 20:29:41 +01:00
e57da71dcf sources/ldap: update LDAP source to use new property mappings 2020-02-17 17:55:48 +01:00
7268afaaf9 providers/saml: update to new PropertyMappings 2020-02-17 17:50:11 +01:00
205183445c admin: add support for template field and Jinja2 highlighting 2020-02-17 17:48:53 +01:00
a08bdfdbcd root: remove prospector from Pipfile as it causes lock issues, install in CI 2020-02-17 17:48:18 +01:00
e6c47fee26 core: add template field to PropertyMapping 2020-02-17 17:47:51 +01:00
a5629c5155 providers/saml: add changeable signature and digest algorithm 2020-02-17 16:28:18 +01:00
41689fe3ce sources/* add missing migrations 2020-02-17 16:27:35 +01:00
8e84208e2c new release: 0.7.14-beta 2020-02-17 15:42:14 +01:00
32a48fa07a providers/saml: more typehints 2020-02-17 15:40:49 +01:00
773a9c0692 policies/engine: fix cached policy results being ignored 2020-02-17 15:37:51 +01:00
8808e3afe0 policies/engine: set mp start method to fork to fix issues under macOS 2020-02-17 15:20:30 +01:00
ecea85f8ca lib/config: remove autoreload handler as this API is gone in django 3 2020-02-17 15:20:11 +01:00
5dfa141e35 root/wsgi: log requests with event name of request 2020-02-16 14:36:31 +01:00
447e81d0b8 providers/saml: handle uncompressed SAML AuthNRequest 2020-02-16 14:08:35 +01:00
e138076e1d sources/saml: move labels from forms to models 2020-02-16 12:34:46 +01:00
721d133dc3 sources/oauth: move labels from form to models 2020-02-16 12:34:33 +01:00
75b687ecbe sources/ldap: move labels from form to models 2020-02-16 12:30:45 +01:00
bdd1863177 providers/saml: move field labels from Form into models 2020-02-16 12:30:26 +01:00
e5b85e8e6a providers/saml: move default saml properties to DB 2020-02-16 12:29:53 +01:00
d7481c9de7 new release: 0.7.13-beta 2020-02-14 15:35:05 +01:00
571373866e providers/saml: some more cleanup, fix get_time_string when called without argument 2020-02-14 15:34:24 +01:00
e36d7928e4 providers/saml: big cleanup, simplify base processor
add New fields for
 - assertion_valid_not_before
 - assertion_valid_not_on_or_after
 - session_valid_not_on_or_after
allow flexible time durations for these fields
fall back to Provider's ACS if none is specified in AuthNRequest
2020-02-14 15:19:48 +01:00
2be026dd44 global: fix import order 2020-02-14 15:17:40 +01:00
d5b9de3569 Merge pull request #3 from BeryJu/dependabot/pip/django-2.2.10
build(deps): bump django from 2.2.9 to 2.2.10
2020-02-12 09:31:13 +01:00
e22620b0ec build(deps): bump django from 2.2.9 to 2.2.10
Bumps [django](https://github.com/django/django) from 2.2.9 to 2.2.10.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/2.2.9...2.2.10)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-12 03:20:09 +00:00
ba74a3213d *: upgrade python 3.7 to 3.8 2020-01-19 21:03:01 +01:00
d9ecb7070d core: add more prometheus metrics 2020-01-19 21:01:26 +01:00
fc4a46bd9c root: fix credential variables overwriting each other 2020-01-17 11:16:23 +01:00
78301b7bab docs: fix site_url 2020-01-17 10:55:20 +01:00
7bf7bde856 root: fix prometheus path in ServiceMonitor, return WWW-Authenticate header so basic auth is sent 2020-01-17 10:55:11 +01:00
9bdff14403 providers/app_gw: fix wrong UPSTREAM parameter 2020-01-03 09:15:07 +01:00
f124314eab new release: 0.7.12-beta 2020-01-02 20:22:44 +01:00
684e4ffdcf providers/app_gw: fix formatting 2020-01-02 20:22:36 +01:00
d9ff5c69c8 providers/app_gw: fix assignment of response_types 2020-01-02 20:20:10 +01:00
8142e3df45 providers/oidc: fix application property of wrong object being used 2020-01-02 20:19:53 +01:00
73920899de static: use current pixie image 2020-01-02 20:09:30 +01:00
13666965a7 actions: fix build over gatekeeper 2020-01-02 16:55:30 +01:00
86f16e2781 providers/oidc: fix incorrectly sorted imports 2020-01-02 16:42:52 +01:00
2ed8e72c62 new release: 0.7.11-beta 2020-01-02 16:38:11 +01:00
edeed18ae8 providers/oidc: fix error when using with app_gw 2020-01-02 16:38:01 +01:00
d24133d8a2 core: fix _redirect_with_qs appending an array to the URL 2020-01-02 16:14:56 +01:00
b9733e56aa providers/app_gw: fix passbook domain being empty 2020-01-02 16:09:17 +01:00
cd34413914 providers/app_gw: separate host field into external_ and internal_ 2020-01-02 16:09:04 +01:00
c3a4a76d43 providers/app_gw: fix Client's response_type not being set 2020-01-02 16:06:32 +01:00
a59a29b256 actions: also build gatekeeper on release 2020-01-02 15:55:39 +01:00
dce1edbe53 new release: 0.7.10-beta 2020-01-02 14:54:52 +01:00
264d43827a actions: create release based on version number, not tag name 2020-01-02 14:46:44 +01:00
186 changed files with 2849 additions and 2238 deletions

View File

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

View File

@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: '3.7' python-version: '3.8'
- uses: actions/cache@v1 - uses: actions/cache@v1
with: with:
path: ~/.local/share/virtualenvs/ path: ~/.local/share/virtualenvs/
@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: '3.7' python-version: '3.8'
- uses: actions/cache@v1 - uses: actions/cache@v1
with: with:
path: ~/.local/share/virtualenvs/ path: ~/.local/share/virtualenvs/
@ -48,7 +48,7 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: '3.7' python-version: '3.8'
- uses: actions/cache@v1 - uses: actions/cache@v1
with: with:
path: ~/.local/share/virtualenvs/ path: ~/.local/share/virtualenvs/
@ -56,7 +56,7 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-pipenv- ${{ runner.os }}-pipenv-
- name: Install dependencies - name: Install dependencies
run: pip install -U pip pipenv && pipenv install --dev run: pip install -U pip pipenv && pipenv install --dev && pipenv install --dev prospector --skip-lock
- name: Lint with prospector - name: Lint with prospector
run: pipenv run prospector run: pipenv run prospector
bandit: bandit:
@ -65,7 +65,7 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: '3.7' python-version: '3.8'
- uses: actions/cache@v1 - uses: actions/cache@v1
with: with:
path: ~/.local/share/virtualenvs/ path: ~/.local/share/virtualenvs/
@ -100,7 +100,7 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: '3.7' python-version: '3.8'
- uses: actions/cache@v1 - uses: actions/cache@v1
with: with:
path: ~/.local/share/virtualenvs/ path: ~/.local/share/virtualenvs/
@ -134,7 +134,7 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-python@v1 - uses: actions/setup-python@v1
with: with:
python-version: '3.7' python-version: '3.8'
- uses: actions/cache@v1 - uses: actions/cache@v1
with: with:
path: ~/.local/share/virtualenvs/ path: ~/.local/share/virtualenvs/

View File

@ -1,8 +1,6 @@
name: passbook-release name: passbook-release
on: on:
release: release
types:
- created
jobs: jobs:
# Build # Build
@ -18,13 +16,34 @@ jobs:
- name: Building Docker Image - name: Building Docker Image
run: docker build run: docker build
--no-cache --no-cache
-t beryju/passbook:0.7.9-beta -t beryju/passbook:0.8.5-beta
-t beryju/passbook:latest -t beryju/passbook:latest
-f Dockerfile . -f Dockerfile .
- name: Push Docker Container to Registry (versioned) - name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook:0.7.9-beta run: docker push beryju/passbook:0.8.5-beta
- name: Push Docker Container to Registry (latest) - name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook: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.5-beta \
-t beryju/passbook-gatekeeper:latest \
-f Dockerfile .
- name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-gatekeeper:0.8.5-beta
- name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-gatekeeper:latest
build-static: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
@ -47,11 +66,11 @@ jobs:
run: docker build run: docker build
--no-cache --no-cache
--network=$(docker network ls | grep github | awk '{print $1}') --network=$(docker network ls | grep github | awk '{print $1}')
-t beryju/passbook-static:0.7.9-beta -t beryju/passbook-static:0.8.5-beta
-t beryju/passbook-static:latest -t beryju/passbook-static:latest
-f static.Dockerfile . -f static.Dockerfile .
- name: Push Docker Container to Registry (versioned) - name: Push Docker Container to Registry (versioned)
run: docker push beryju/passbook-static:0.7.9-beta run: docker push beryju/passbook-static:0.8.5-beta
- name: Push Docker Container to Registry (latest) - name: Push Docker Container to Registry (latest)
run: docker push beryju/passbook-static:latest run: docker push beryju/passbook-static:latest
test-release: test-release:

View File

@ -31,6 +31,13 @@ jobs:
helm dependency update helm/ helm dependency update helm/
helm package helm/ helm package helm/
mv passbook-*.tgz passbook-chart.tgz 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 - name: Create Release
id: create_release id: create_release
uses: actions/create-release@v1.0.0 uses: actions/create-release@v1.0.0
@ -38,10 +45,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
tag_name: ${{ github.ref }} tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }} release_name: Release ${{ steps.get_version.outputs.result }}
draft: false draft: false
prerelease: false prerelease: false
- name: Create Release from Tag - name: Upload packaged Helm Chart
id: upload-release-asset id: upload-release-asset
uses: actions/upload-release-asset@v1.0.1 uses: actions/upload-release-asset@v1.0.1
env: env:

View File

@ -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 /app/
COPY ./Pipfile.lock /app/ COPY ./Pipfile.lock /app/
@ -9,7 +9,7 @@ RUN pip install pipenv && \
pipenv lock -r > requirements.txt && \ pipenv lock -r > requirements.txt && \
pipenv lock -rd > requirements-dev.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.txt /app/
COPY --from=locker /app/requirements-dev.txt /app/ COPY --from=locker /app/requirements-dev.txt /app/

View File

@ -40,9 +40,10 @@ signxml = "*"
structlog = "*" structlog = "*"
swagger-spec-validator = "*" swagger-spec-validator = "*"
urllib3 = {extras = ["secure"],version = "*"} urllib3 = {extras = ["secure"],version = "*"}
jinja2 = "*"
[requires] [requires]
python_version = "3.7" python_version = "3.8"
[dev-packages] [dev-packages]
autopep8 = "*" autopep8 = "*"
@ -51,7 +52,6 @@ bumpversion = "*"
colorama = "*" colorama = "*"
coverage = "*" coverage = "*"
django-debug-toolbar = "*" django-debug-toolbar = "*"
prospector = "*"
pylint = "*" pylint = "*"
pylint-django = "*" pylint-django = "*"
unittest-xml-reporting = "*" unittest-xml-reporting = "*"

742
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
# passbook # passbook
![](https://github.com/BeryJu/passbook/workflows/passbook-ci/badge.svg)
## Quick instance ## Quick instance
``` ```

View File

@ -7,4 +7,4 @@ threads = 2
enable-threads = true enable-threads = true
uid = passbook uid = passbook
gid = passbook gid = passbook
disable-logging=True disable-logging = True

View File

@ -1,4 +1,4 @@
FROM python:3.7-slim-buster as builder FROM python:3.8-slim-buster as builder
WORKDIR /mkdocs WORKDIR /mkdocs

View File

@ -0,0 +1,32 @@
# Amazon Web Services Integration
## What is AWS
!!! note ""
Amazon Web Services (AWS) is the worlds 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:
![](./property-mapping-role.png)
![](./property-mapping-role-session-name.png)
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -4,9 +4,8 @@
From https://about.gitlab.com/what-is-gitlab/ From https://about.gitlab.com/what-is-gitlab/
``` !!! note ""
GitLab is a complete DevOps platform, delivered as a single application. This makes GitLab unique and makes Concurrent DevOps possible, unlocking your organization from the constraints of a pieced together toolchain. Join us for a live Q&A to learn how GitLab can give you unmatched visibility and higher levels of efficiency in a single application across the DevOps lifecycle. 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 ## Preparation
@ -21,7 +20,7 @@ Create an application in passbook and note the slug, as this will be used later.
- Audience: `https://gitlab.company` - Audience: `https://gitlab.company`
- Issuer: `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 ## GitLab Configuration

View File

@ -4,9 +4,8 @@
From https://goharbor.io From https://goharbor.io
``` !!! note ""
Harbor is an open source container image registry that secures images with role-based access control, scans images for vulnerabilities, and signs images as trusted. A CNCF Incubating project, Harbor delivers compliance, performance, and interoperability to help you consistently and securely manage images across cloud native compute platforms like Kubernetes and Docker. 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 ## Preparation

View File

@ -4,10 +4,9 @@
From https://rancher.com/products/rancher From https://rancher.com/products/rancher
``` !!! note ""
An Enterprise Platform for Managing Kubernetes Everywhere 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. 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 ## 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` - Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
- Issuer: `passbook` - 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 ## Rancher

View File

@ -4,13 +4,12 @@
From https://sentry.io From https://sentry.io
``` !!! note ""
Sentry provides self-hosted and cloud-based error monitoring that helps all software Sentry provides self-hosted and cloud-based error monitoring that helps all software
teams discover, triage, and prioritize errors in real-time. teams discover, triage, and prioritize errors in real-time.
One million developers at over fifty thousand companies already ship One million developers at over fifty thousand companies already ship
better software faster with Sentry. Wont you join them? better software faster with Sentry. Wont you join them?
```
## Preparation ## Preparation

View 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. Its 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.

View 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`

View File

@ -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. 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: See [Expression Policy](expression/index.md).
- 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.
### Webhook Policy ### Webhook Policy

View 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 %}]
```

View File

@ -13,11 +13,5 @@ The API exposes Username, E-Mail, Name and Groups in a GitHub-compatible format.
## SAML Provider ## 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. 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: Default fields are exposed through Auto-generated Property Mappings, which are prefixed with "Autogenerated..."
- `eduPersonPrincipalName`: User's E-Mail
- `cn`: User's Full Name
- `mail`: User's E-Mail
- `displayName`: User's Username
- `uid`: User Unique Identifier

View File

@ -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. - 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 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.) - 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)

View File

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

View File

@ -18,6 +18,7 @@ spec:
name: {{ include "passbook.fullname" . }}-secret-key name: {{ include "passbook.fullname" . }}-secret-key
key: monitoring_username key: monitoring_username
port: http port: http
path: /metrics/
interval: 10s interval: 10s
selector: selector:
matchLabels: matchLabels:

View File

@ -2,7 +2,7 @@
# This is a YAML-formatted file. # This is a YAML-formatted file.
# Declare variables to be passed into your templates. # Declare variables to be passed into your templates.
image: image:
tag: 0.7.9-beta tag: 0.8.5-beta
nameOverride: "" nameOverride: ""
@ -42,3 +42,5 @@ redis:
master: master:
persistence: persistence:
enabled: false enabled: false
# https://stackoverflow.com/a/59189742
disableCommands: []

View File

@ -1,5 +1,5 @@
site_name: passbook Docs site_name: passbook Docs
site_url: https://docs.passbook.beryju.org site_url: https://beryju.github.io/passbook
copyright: "Copyright &copy; 2019 - 2020 BeryJu.org" copyright: "Copyright &copy; 2019 - 2020 BeryJu.org"
nav: nav:
@ -10,15 +10,22 @@ nav:
- Kubernetes: installation/kubernetes.md - Kubernetes: installation/kubernetes.md
- Sources: sources.md - Sources: sources.md
- Providers: providers.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 - Factors: factors.md
- Policies: policies.md - Policies:
- Overview: policies/index.md
- Expression: policies/expression/index.md
- Integrations: - Integrations:
- as Provider: - as Provider:
- Amazon Web Services: integrations/services/aws/index.md
- GitLab: integrations/services/gitlab/index.md - GitLab: integrations/services/gitlab/index.md
- Rancher: integrations/services/rancher/index.md - Rancher: integrations/services/rancher/index.md
- Harbor: integrations/services/harbor/index.md - Harbor: integrations/services/harbor/index.md
- Sentry: integrations/services/sentry/index.md - Sentry: integrations/services/sentry/index.md
- Ansible Tower/AWX: integrations/services/tower-awx/index.md
repo_name: "BeryJu.org/passbook" repo_name: "BeryJu.org/passbook"
repo_url: https://github.com/BeryJu/passbook repo_url: https://github.com/BeryJu/passbook
@ -29,3 +36,4 @@ theme:
markdown_extensions: markdown_extensions:
- toc: - toc:
permalink: "¶" permalink: "¶"
- admonition

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = "0.7.9-beta" __version__ = "0.8.5-beta"

View File

@ -36,7 +36,7 @@
<tr> <tr>
<td>{{ source.name }}</td> <td>{{ source.name }}</td>
<td>{{ source|fieldtype }}</td> <td>{{ source|fieldtype }}</td>
<td>{{ source.additional_info|safe }}</td> <td>{{ source.ui_additional_info|safe|default:"" }}</td>
<td> <td>
<a class="btn btn-default btn-sm" <a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>

View File

@ -1,4 +1,4 @@
{% extends "generic/form.html" %} {% extends base_template|default:"generic/form.html" %}
{% load utils %} {% load utils %}
{% load i18n %} {% load i18n %}

View File

@ -20,6 +20,7 @@
<link rel="stylesheet" href="{% static 'codemirror/lib/codemirror.css' %}"> <link rel="stylesheet" href="{% static 'codemirror/lib/codemirror.css' %}">
<link rel="stylesheet" href="{% static 'codemirror/theme/monokai.css' %}"> <link rel="stylesheet" href="{% static 'codemirror/theme/monokai.css' %}">
<script src="{% static 'codemirror/mode/yaml/yaml.js' %}"></script> <script src="{% static 'codemirror/mode/yaml/yaml.js' %}"></script>
<script src="{% static 'codemirror/mode/jinja2/jinja2.js' %}"></script>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
@ -29,21 +30,33 @@
<div class=""> <div class="">
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% include 'partials/form.html' with form=form %} {% include 'partials/form.html' with form=form %}
{% block beneath_form %}
{% endblock %}
<a class="btn btn-default" href="{% back %}">{% trans "Cancel" %}</a> <a class="btn btn-default" href="{% back %}">{% trans "Cancel" %}</a>
<input type="submit" class="btn btn-primary" value="{% block action %}{% endblock %}" /> <input type="submit" class="btn btn-primary" value="{% block action %}{% endblock %}" />
</form> </form>
</div> </div>
{% block beneath_form %}
{% endblock %}
<script> <script>
let attributes = document.getElementsByName('attributes'); const attributes = document.getElementsByName('attributes');
if (attributes.length > 0) { 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', mode: 'yaml',
theme: 'monokai', theme: 'monokai',
lineNumbers: true, 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> </script>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,4 @@
{% extends "generic/form.html" %} {% extends base_template|default:"generic/form.html" %}
{% load utils %} {% load utils %}
{% load i18n %} {% load i18n %}

View File

@ -18,7 +18,7 @@ def get_links(model_instance):
links = {} links = {}
if not isinstance(model_instance, Model): 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 return links
try: try:
@ -43,7 +43,7 @@ def get_htmls(context, model_instance):
htmls = [] htmls = []
if not isinstance(model_instance, Model): 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 return htmls
try: try:

View File

@ -52,6 +52,13 @@ class PolicyCreateView(
success_url = reverse_lazy("passbook_admin:policies") success_url = reverse_lazy("passbook_admin:policies")
success_message = _("Successfully created Policy") 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): def get_form_class(self):
policy_type = self.request.GET.get("type") policy_type = self.request.GET.get("type")
model = next(x for x in Policy.__subclasses__() if x.__name__ == policy_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_url = reverse_lazy("passbook_admin:policies")
success_message = _("Successfully updated Policy") 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): def get_form_class(self):
form_class_path = self.get_object().form form_class_path = self.get_object().form
form_class = path_to_class(form_class_path) form_class = path_to_class(form_class_path)

View File

@ -66,6 +66,9 @@ class PropertyMappingCreateView(
if x.__name__ == property_mapping_type if x.__name__ == property_mapping_type
) )
kwargs["type"] = model._meta.verbose_name 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 return kwargs
def get_form_class(self): def get_form_class(self):
@ -92,6 +95,13 @@ class PropertyMappingUpdateView(
success_url = reverse_lazy("passbook_admin:property-mappings") success_url = reverse_lazy("passbook_admin:property-mappings")
success_message = _("Successfully updated Property Mapping") 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): def get_form_class(self):
form_class_path = self.get_object().form form_class_path = self.get_object().form
form_class = path_to_class(form_class_path) form_class = path_to_class(form_class_path)

View File

@ -67,6 +67,8 @@ class UserDeleteView(
model = User model = User
permission_required = "passbook_core.delete_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" template_name = "generic/delete.html"
success_url = reverse_lazy("passbook_admin:users") success_url = reverse_lazy("passbook_admin:users")
success_message = _("Successfully deleted User") success_message = _("Successfully deleted User")

View File

@ -24,12 +24,10 @@ from passbook.factors.otp.api import OTPFactorViewSet
from passbook.factors.password.api import PasswordFactorViewSet from passbook.factors.password.api import PasswordFactorViewSet
from passbook.lib.utils.reflection import get_apps from passbook.lib.utils.reflection import get_apps
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet 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.hibp.api import HaveIBeenPwendPolicyViewSet
from passbook.policies.matcher.api import FieldMatcherPolicyViewSet
from passbook.policies.password.api import PasswordPolicyViewSet from passbook.policies.password.api import PasswordPolicyViewSet
from passbook.policies.reputation.api import ReputationPolicyViewSet from passbook.policies.reputation.api import ReputationPolicyViewSet
from passbook.policies.sso.api import SSOLoginPolicyViewSet
from passbook.policies.webhook.api import WebhookPolicyViewSet from passbook.policies.webhook.api import WebhookPolicyViewSet
from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet
from passbook.providers.oauth.api import OAuth2ProviderViewSet from passbook.providers.oauth.api import OAuth2ProviderViewSet
@ -57,13 +55,11 @@ router.register("sources/ldap", LDAPSourceViewSet)
router.register("sources/oauth", OAuthSourceViewSet) router.register("sources/oauth", OAuthSourceViewSet)
router.register("policies/all", PolicyViewSet) router.register("policies/all", PolicyViewSet)
router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet) router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet)
router.register("policies/groupmembership", GroupMembershipPolicyViewSet)
router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet) router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet)
router.register("policies/fieldmatcher", FieldMatcherPolicyViewSet)
router.register("policies/password", PasswordPolicyViewSet) router.register("policies/password", PasswordPolicyViewSet)
router.register("policies/reputation", ReputationPolicyViewSet) router.register("policies/reputation", ReputationPolicyViewSet)
router.register("policies/ssologin", SSOLoginPolicyViewSet)
router.register("policies/webhook", WebhookPolicyViewSet) router.register("policies/webhook", WebhookPolicyViewSet)
router.register("policies/expression", ExpressionPolicyViewSet)
router.register("providers/all", ProviderViewSet) router.register("providers/all", ProviderViewSet)
router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet) router.register("providers/applicationgateway", ApplicationGatewayProviderViewSet)
router.register("providers/oauth", OAuth2ProviderViewSet) router.register("providers/oauth", OAuth2ProviderViewSet)

View File

@ -1,14 +1,14 @@
"""passbook audit models""" """passbook audit models"""
from enum import Enum from enum import Enum
from uuid import UUID
from inspect import getmodule, stack 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.conf import settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
@ -100,7 +100,6 @@ class Event(UUIDModel):
app = getmodule(stack()[_inspect_offset][0]).__name__ app = getmodule(stack()[_inspect_offset][0]).__name__
cleaned_kwargs = sanitize_dict(kwargs) cleaned_kwargs = sanitize_dict(kwargs)
event = Event(action=action.value, app=app, context=cleaned_kwargs) event = Event(action=action.value, app=app, context=cleaned_kwargs)
LOGGER.debug("Created Audit event", action=action, context=cleaned_kwargs)
return event return event
def from_http( def from_http(
@ -129,6 +128,12 @@ class Event(UUIDModel):
raise ValidationError( raise ValidationError(
"you may not edit an existing %s" % self._meta.model_name "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) return super().save(*args, **kwargs)
class Meta: class Meta:

View File

@ -15,11 +15,13 @@ class ApplicationSerializer(ModelSerializer):
"pk", "pk",
"name", "name",
"slug", "slug",
"launch_url",
"icon_url",
"provider",
"policies",
"skip_authorization", "skip_authorization",
"provider",
"meta_launch_url",
"meta_icon_url",
"meta_description",
"meta_publisher",
"policies",
] ]

View File

@ -0,0 +1,6 @@
"""passbook core exceptions"""
from passbook.lib.sentry import SentryIgnoredException
class PropertyMappingExpressionException(SentryIgnoredException):
"""Error when a PropertyMapping Exception expression could not be parsed or evaluated."""

View File

@ -19,19 +19,25 @@ class ApplicationForm(forms.ModelForm):
fields = [ fields = [
"name", "name",
"slug", "slug",
"launch_url",
"icon_url",
"provider",
"policies",
"skip_authorization", "skip_authorization",
"provider",
"meta_launch_url",
"meta_icon_url",
"meta_description",
"meta_publisher",
"policies",
] ]
widgets = { widgets = {
"name": forms.TextInput(), "name": forms.TextInput(),
"launch_url": forms.TextInput(), "meta_launch_url": forms.TextInput(),
"icon_url": forms.TextInput(), "meta_icon_url": forms.TextInput(),
"meta_publisher": forms.TextInput(),
"policies": FilteredSelectMultiple(_("policies"), False), "policies": FilteredSelectMultiple(_("policies"), False),
} }
labels = { labels = {
"launch_url": _("Launch URL"), "meta_launch_url": _("Launch URL"),
"icon_url": _("Icon URL"), "meta_icon_url": _("Icon URL"),
"meta_description": _("Description"),
"meta_publisher": _("Publisher"),
} }
help_texts = {"policies": _("Policies required to access this Application.")}

View File

@ -70,7 +70,7 @@ class SignUpForm(forms.Form):
"""Check if username is used already""" """Check if username is used already"""
username = self.cleaned_data.get("username") username = self.cleaned_data.get("username")
if User.objects.filter(username=username).exists(): 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")) raise ValidationError(_("Username already exists"))
return username return username
@ -79,7 +79,7 @@ class SignUpForm(forms.Form):
email = self.cleaned_data.get("email") email = self.cleaned_data.get("email")
# Check if user exists already, error early # Check if user exists already, error early
if User.objects.filter(email=email).exists(): 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")) raise ValidationError(_("Email already exists"))
return email return email

View 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,
),
]

View 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",
),
]

View 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),
),
]

View File

@ -2,25 +2,34 @@
from datetime import timedelta from datetime import timedelta
from random import SystemRandom from random import SystemRandom
from time import sleep from time import sleep
from typing import Optional from typing import Any, Optional
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.http import HttpRequest
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.timezone import now 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 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 model_utils.managers import InheritanceManager
from structlog import get_logger from structlog import get_logger
from passbook.core.exceptions import PropertyMappingExpressionException
from passbook.core.signals import password_changed from passbook.core.signals import password_changed
from passbook.core.types import UILoginButton, UIUserSettings
from passbook.lib.models import CreatedUpdatedModel, UUIDModel from passbook.lib.models import CreatedUpdatedModel, UUIDModel
from passbook.policies.exceptions import PolicyException from passbook.policies.exceptions import PolicyException
from passbook.policies.struct import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()
NATIVE_ENVIRONMENT = NativeEnvironment()
def default_nonce_duration(): def default_nonce_duration():
@ -28,7 +37,7 @@ def default_nonce_duration():
return now() + timedelta(hours=4) return now() + timedelta(hours=4)
class Group(UUIDModel): class Group(ExportModelOperationsMixin("group"), UUIDModel):
"""Custom Group model which supports a basic hierarchy""" """Custom Group model which supports a basic hierarchy"""
name = models.CharField(_("name"), max_length=80) name = models.CharField(_("name"), max_length=80)
@ -49,7 +58,7 @@ class Group(UUIDModel):
unique_together = (("name", "parent",),) 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""" """Custom User model to allow easier adding o f user-based settings"""
uuid = models.UUIDField(default=uuid4, editable=False) uuid = models.UUIDField(default=uuid4, editable=False)
@ -72,7 +81,7 @@ class User(GuardianUserMixin, AbstractUser):
permissions = (("reset_user_password", "Reset Password"),) 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""" """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
property_mappings = models.ManyToManyField( property_mappings = models.ManyToManyField(
@ -94,20 +103,7 @@ class PolicyModel(UUIDModel, CreatedUpdatedModel):
policies = models.ManyToManyField("Policy", blank=True) policies = models.ManyToManyField("Policy", blank=True)
class UserSettings: class Factor(ExportModelOperationsMixin("factor"), PolicyModel):
"""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):
"""Authentication factor, multiple instances of the same Factor can be used""" """Authentication factor, multiple instances of the same Factor can be used"""
name = models.TextField() name = models.TextField()
@ -119,32 +115,36 @@ class Factor(PolicyModel):
type = "" type = ""
form = "" 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 """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 return None
def __str__(self): def __str__(self):
return f"Factor {self.slug}" return f"Factor {self.slug}"
class Application(PolicyModel): class Application(ExportModelOperationsMixin("application"), PolicyModel):
"""Every Application which uses passbook for authentication/identification/authorization """Every Application which uses passbook for authentication/identification/authorization
needs an Application record. Other authentication types can subclass this Model to needs an Application record. Other authentication types can subclass this Model to
add custom fields and other properties""" add custom fields and other properties"""
name = models.TextField() name = models.TextField()
slug = models.SlugField() slug = models.SlugField()
launch_url = models.URLField(null=True, blank=True) skip_authorization = models.BooleanField(default=False)
icon_url = models.TextField(null=True, blank=True)
provider = models.OneToOneField( provider = models.OneToOneField(
"Provider", null=True, blank=True, default=None, on_delete=models.SET_DEFAULT "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() objects = InheritanceManager()
def get_provider(self): def get_provider(self) -> Optional[Provider]:
"""Get casted provider instance""" """Get casted provider instance"""
if not self.provider: if not self.provider:
return None return None
@ -154,11 +154,12 @@ class Application(PolicyModel):
return self.name return self.name
class Source(PolicyModel): class Source(ExportModelOperationsMixin("source"), PolicyModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
name = models.TextField() name = models.TextField()
slug = models.SlugField() slug = models.SlugField()
enabled = models.BooleanField(default=True) enabled = models.BooleanField(default=True)
property_mappings = models.ManyToManyField( property_mappings = models.ManyToManyField(
"PropertyMapping", default=None, blank=True "PropertyMapping", default=None, blank=True
@ -169,19 +170,20 @@ class Source(PolicyModel):
objects = InheritanceManager() objects = InheritanceManager()
@property @property
def login_button(self): def ui_login_button(self) -> Optional[UILoginButton]:
"""Return a tuple of URL, Icon name and Name """If source uses a http-based flow, return UI Information about the login
if Source should get a link on the login page""" button. If source doesn't use http-based flow, return None."""
return None return None
@property @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 additional Info, such as a callback URL. Show in the administration interface."""
return None 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 """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 return None
def __str__(self): def __str__(self):
@ -199,7 +201,7 @@ class UserSourceConnection(CreatedUpdatedModel):
unique_together = (("user", "source"),) 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 """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.""" other types to add other fields, more logic, etc."""
@ -241,7 +243,7 @@ class DebugPolicy(Policy):
verbose_name_plural = _("Debug Policies") verbose_name_plural = _("Debug Policies")
class Invitation(UUIDModel): class Invitation(ExportModelOperationsMixin("invitation"), UUIDModel):
"""Single-use invitation link""" """Single-use invitation link"""
created_by = models.ForeignKey("User", on_delete=models.CASCADE) created_by = models.ForeignKey("User", on_delete=models.CASCADE)
@ -266,7 +268,7 @@ class Invitation(UUIDModel):
verbose_name_plural = _("Invitations") verbose_name_plural = _("Invitations")
class Nonce(UUIDModel): class Nonce(ExportModelOperationsMixin("nonce"), UUIDModel):
"""One-time link for password resets/sign-up-confirmations""" """One-time link for password resets/sign-up-confirmations"""
expires = models.DateTimeField(default=default_nonce_duration) 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.""" """User-defined key -> x mapping which can be used by providers to expose extra data."""
name = models.TextField() name = models.TextField()
expression = models.TextField()
form = "" form = ""
objects = InheritanceManager() 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): def __str__(self):
return f"Property Mapping {self.name}" return f"Property Mapping {self.name}"

View File

@ -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

View File

@ -1,9 +1,8 @@
function convertToSlug(Text) { function convertToSlug(Text) {
return Text return Text
.toLowerCase() .toLowerCase()
.replace(/[^\w ]+/g, '') .replace(/[^\w ]+/g, '-')
.replace(/ +/g, '-') .replace(/ +/g, '-');
;
} }

View File

@ -7,7 +7,7 @@
<head> <head>
<meta charset="UTF-8"> <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> <title>
{% block title %} {% block title %}
{% title %} {% title %}

View File

@ -19,6 +19,9 @@
<h1>{% trans 'Bad Request' %}</h1> <h1>{% trans 'Bad Request' %}</h1>
</header> </header>
<form> <form>
{% if message %}
<h3>{% trans message %}</h3>
{% endif %}
{% if 'back' in request.GET %} {% if 'back' in request.GET %}
<a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a> <a href="{% back %}" class="btn btn-primary btn-block btn-lg">{% trans 'Back' %}</a>
{% endif %} {% endif %}

View File

@ -48,10 +48,16 @@
<!--login-pf-section--> <!--login-pf-section-->
<section class="login-pf-social-section" role="contentinfo" aria-label="Log in with third party account"> <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"> <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"> <li class="login-pf-social-link">
<a href="{{ url }}"> <a href="{{ source.url }}">
<img src="{% static 'img/logos/' %}{{ icon }}.svg" alt="{{ name }}"> {{ name }} {% 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> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

@ -2,42 +2,44 @@
{% load i18n %} {% load i18n %}
{% block head %}
{{ block.super }}
<style>
img.app-icon {
max-height: 72px;
}
</style>
{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">
<div class="row row-cards-pf"> <div class="row row-cards-pf">
{% for app in applications %} {% for app in applications %}
<div class="col-xs-12 col-sm-6 col-md-3"> <div class="col-xs-12 col-sm-6 col-md-3">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-view card-pf-view-select card-pf-view-single-select">
<h2 class="card-pf-title"> <div class="card-pf-body">
<span class="fa fa-shield"></span> {{ app.name }} <div class="card-pf-top-element">
</h2> <a href="{{ app.meta_launch_url }}">
<div class="card-pf-body"> {% if not app.meta_icon_url %}
<p class="card-pf-aggregate-status-notifications"> <span class="pficon pficon-arrow card-pf-icon-circle"></span>
<span class="card-pf-aggregate-status-notification"> {% else %}
<a href="{{ app.launch_url }}" class="add" data-toggle="tooltip" data-placement="top" title="{% trans 'Open App...' %}"> <img class="app-icon card-pf-icon-circle" src="{{ app.meta_icon_url }}" alt="{% trans 'Application Icon' %}">
{% if not app.icon_url %} {% endif %}
<span class="pficon pficon-arrow"></span> </a>
{% else %} </div>
<img class="app-icon" src="{{ app.icon_url }}" alt="{% trans 'Application Icon' %}"> <h2 class="card-pf-title text-center">
{% endif %} <a href="{{ app.meta_launch_url }}">
</a> {{ app.name }}
</span> </a>
</p> </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>
</div> {% endfor %}
</div> </div>
{% empty %}
<h1>{% trans 'No Applications available.' %}</h1>
{% endfor %}
</div><!-- /row -->
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,46 +1,53 @@
"""passbook user settings template tags""" """passbook user settings template tags"""
from typing import List from typing import Iterable, List
from django import template from django import template
from django.template.context import RequestContext from django.template.context import RequestContext
from passbook.core.models import Factor, Source, UserSettings from passbook.core.models import Factor, Source
from passbook.core.types import UIUserSettings
from passbook.policies.engine import PolicyEngine from passbook.policies.engine import PolicyEngine
register = template.Library() register = template.Library()
@register.simple_tag(takes_context=True) @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""" """Return list of all factors which apply to user"""
user = context.get("request").user user = context.get("request").user
_all_factors = ( _all_factors: Iterable[Factor] = (
Factor.objects.filter(enabled=True).order_by("order").select_subclasses() Factor.objects.filter(enabled=True).order_by("order").select_subclasses()
) )
matching_factors: List[UserSettings] = [] matching_factors: List[UIUserSettings] = []
for factor in _all_factors: for factor in _all_factors:
user_settings = factor.user_settings() user_settings = factor.ui_user_settings
if not user_settings:
continue
policy_engine = PolicyEngine( policy_engine = PolicyEngine(
factor.policies.all(), user, context.get("request") factor.policies.all(), user, context.get("request")
) )
policy_engine.build() policy_engine.build()
if policy_engine.passing and user_settings: if policy_engine.passing:
matching_factors.append(user_settings) matching_factors.append(user_settings)
return matching_factors return matching_factors
@register.simple_tag(takes_context=True) @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""" """Return a list of all sources which are enabled for the user"""
user = context.get("request").user user = context.get("request").user
_all_sources = Source.objects.filter(enabled=True).select_subclasses() _all_sources: Iterable[Source] = (
matching_sources: List[UserSettings] = [] Source.objects.filter(enabled=True).select_subclasses()
)
matching_sources: List[UIUserSettings] = []
for factor in _all_sources: for factor in _all_sources:
user_settings = factor.user_settings() user_settings = factor.ui_user_settings
if not user_settings:
continue
policy_engine = PolicyEngine( policy_engine = PolicyEngine(
factor.policies.all(), user, context.get("request") factor.policies.all(), user, context.get("request")
) )
policy_engine.build() policy_engine.build()
if policy_engine.passing and user_settings: if policy_engine.passing:
matching_sources.append(user_settings) matching_sources.append(user_settings)
return matching_sources return matching_sources

29
passbook/core/types.py Normal file
View File

@ -0,0 +1,29 @@
"""passbook core dataclasses"""
from dataclasses import dataclass
from typing import Optional
@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

View File

@ -47,9 +47,9 @@ class LoginView(UserPassesTestMixin, FormView):
kwargs["sources"] = [] kwargs["sources"] = []
sources = Source.objects.filter(enabled=True).select_subclasses() sources = Source.objects.filter(enabled=True).select_subclasses()
for source in sources: for source in sources:
login_button = source.login_button ui_login_button = source.ui_login_button
if login_button: if ui_login_button:
kwargs["sources"].append(login_button) kwargs["sources"].append(ui_login_button)
if kwargs["sources"]: if kwargs["sources"]:
self.template_name = "login/with_sources.html" self.template_name = "login/with_sources.html"
return super().get_context_data(**kwargs) return super().get_context_data(**kwargs)
@ -72,7 +72,6 @@ class LoginView(UserPassesTestMixin, FormView):
if not pre_user: if not pre_user:
# No user found # No user found
return self.invalid_login(self.request) return self.invalid_login(self.request)
# self.request.session.flush()
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
return _redirect_with_qs("passbook_core:auth-process", self.request.GET) return _redirect_with_qs("passbook_core:auth-process", self.request.GET)
@ -156,26 +155,9 @@ class SignUpView(UserPassesTestMixin, FormView):
for error in exc.messages: for error in exc.messages:
errors.append(error) errors.append(error)
return self.form_invalid(form) 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() self.consume_invitation()
messages.success(self.request, _("Successfully signed up!")) 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")) return redirect(reverse("passbook_core:auth-login"))
def consume_invitation(self): def consume_invitation(self):

View File

@ -15,7 +15,7 @@ class OverviewView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs["applications"] = [] kwargs["applications"] = []
for application in Application.objects.all(): for application in Application.objects.all().order_by("name"):
engine = PolicyEngine( engine = PolicyEngine(
application.policies.all(), self.request.user, self.request application.policies.all(), self.request.user, self.request
) )

View File

@ -1,9 +1,9 @@
"""OTP Factor""" """OTP Factor"""
from django.db import models from django.db import models
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import Factor, UserSettings from passbook.core.models import Factor
from passbook.core.types import UIUserSettings
class OTPFactor(Factor): class OTPFactor(Factor):
@ -17,9 +17,12 @@ class OTPFactor(Factor):
type = "passbook.factors.otp.factors.OTPFactor" type = "passbook.factors.otp.factors.OTPFactor"
form = "passbook.factors.otp.forms.OTPFactorForm" form = "passbook.factors.otp.forms.OTPFactorForm"
def user_settings(self) -> UserSettings: @property
return UserSettings( def ui_user_settings(self) -> UIUserSettings:
_("OTP"), "pficon-locked", "passbook_factors_otp:otp-user-settings" return UIUserSettings(
name="OTP",
icon="pficon-locked",
view_name="passbook_factors_otp:otp-user-settings",
) )
def __str__(self): def __str__(self):

View File

@ -7,8 +7,10 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import Http404, HttpRequest, HttpResponse from django.http import Http404, HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views import View from django.views import View
from django.views.decorators.cache import never_cache
from django.views.generic import FormView, TemplateView from django.views.generic import FormView, TemplateView
from django_otp.plugins.otp_static.models import StaticDevice, StaticToken from django_otp.plugins.otp_static.models import StaticDevice, StaticToken
from django_otp.plugins.otp_totp.models import TOTPDevice 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.audit.models import Event, EventAction
from passbook.factors.otp.forms import OTPSetupForm from passbook.factors.otp.forms import OTPSetupForm
from passbook.factors.otp.utils import otpauth_url from passbook.factors.otp.utils import otpauth_url
from passbook.lib.boilerplate import NeverCacheMixin
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
OTP_SESSION_KEY = "passbook_factors_otp_key" OTP_SESSION_KEY = "passbook_factors_otp_key"
@ -146,7 +147,8 @@ class EnableView(LoginRequiredMixin, FormView):
return redirect("passbook_factors_otp:otp-user-settings") 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""" """View returns an SVG image with the OTP token information"""
def get(self, request: HttpRequest) -> HttpResponse: def get(self, request: HttpRequest) -> HttpResponse:

View File

@ -1,7 +1,8 @@
"""passbook password policy exceptions""" """passbook password policy exceptions"""
from passbook.lib.sentry import SentryIgnoredException
class PasswordPolicyInvalid(Exception): class PasswordPolicyInvalid(SentryIgnoredException):
"""Exception raised when a Password Policy fails""" """Exception raised when a Password Policy fails"""
messages = [] messages = []

View File

@ -3,7 +3,8 @@ from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from passbook.core.models import Factor, Policy, User, UserSettings from passbook.core.models import Factor, Policy, User
from passbook.core.types import UIUserSettings
class PasswordFactor(Factor): class PasswordFactor(Factor):
@ -18,9 +19,12 @@ class PasswordFactor(Factor):
type = "passbook.factors.password.factor.PasswordFactor" type = "passbook.factors.password.factor.PasswordFactor"
form = "passbook.factors.password.forms.PasswordFactorForm" form = "passbook.factors.password.forms.PasswordFactorForm"
def user_settings(self): @property
return UserSettings( def ui_user_settings(self) -> UIUserSettings:
_("Change Password"), "pficon-key", "passbook_core:user-change-password" return UIUserSettings(
name="Change Password",
icon="pficon-key",
view_name="passbook_core:user-change-password",
) )
def password_passes(self, user: User) -> bool: def password_passes(self, user: User) -> bool:

View File

@ -38,9 +38,8 @@ class TestFactorAuthentication(TestCase):
def test_unauthenticated_raw(self): def test_unauthenticated_raw(self):
"""test direct call to AuthenticationView""" """test direct call to AuthenticationView"""
response = self.client.get(reverse("passbook_core:auth-process")) response = self.client.get(reverse("passbook_core:auth-process"))
# Response should be 302 since no pending user is set # Response should be 400 since no pending user is set
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 400)
self.assertEqual(response.url, reverse("passbook_core:auth-login"))
def test_unauthenticated_prepared(self): def test_unauthenticated_prepared(self):
"""test direct call but with pending_uesr in session""" """test direct call but with pending_uesr in session"""
@ -71,9 +70,8 @@ class TestFactorAuthentication(TestCase):
"""Test with already logged in user""" """Test with already logged in user"""
self.client.force_login(self.user) self.client.force_login(self.user)
response = self.client.get(reverse("passbook_core:auth-process")) response = self.client.get(reverse("passbook_core:auth-process"))
# Response should be 302 since no pending user is set # Response should be 400 since no pending user is set
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 400)
self.assertEqual(response.url, reverse("passbook_core:overview"))
self.client.logout() self.client.logout()
def test_unauthenticated_post(self): def test_unauthenticated_post(self):

View File

@ -1,15 +1,17 @@
"""passbook multi-factor authentication engine""" """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 import login
from django.contrib.auth.mixins import UserPassesTestMixin 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.utils.http import urlencode
from django.views.generic import View from django.views.generic import View
from structlog import get_logger from structlog import get_logger
from passbook.core.models import Factor, User from passbook.core.models import Factor, User
from passbook.core.views.utils import PermissionDeniedView 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.reflection import class_to_path, path_to_class
from passbook.lib.utils.urls import is_url_absolute from passbook.lib.utils.urls import is_url_absolute
from passbook.policies.engine import PolicyEngine 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""" """Wrapper to redirect whilst keeping GET Parameters"""
target = reverse(view) target = reverse(view)
if get_query_set: if get_query_set:
target += "?" + urlencode(get_query_set) target += "?" + urlencode(get_query_set.items())
return redirect(target) return redirect(target)
@ -44,10 +46,28 @@ class AuthenticationView(UserPassesTestMixin, View):
current_factor: Factor current_factor: Factor
# Allow only not authenticated users to login # 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 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 # Function from UserPassesTestMixin
if NEXT_ARG_NAME in self.request.GET: if NEXT_ARG_NAME in self.request.GET:
return redirect(self.request.GET.get(NEXT_ARG_NAME)) 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:overview", self.request.GET)
return _redirect_with_qs("passbook_core:auth-login", 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""" """Loading pending factors from Database or load from session variable"""
# Write pending factors to session # Write pending factors to session
if AuthenticationView.SESSION_PENDING_FACTORS in self.request.session: if AuthenticationView.SESSION_PENDING_FACTORS in self.request.session:
@ -67,6 +87,7 @@ class AuthenticationView(UserPassesTestMixin, View):
) )
pending_factors = [] pending_factors = []
for factor in _all_factors: for factor in _all_factors:
factor: Factor
LOGGER.debug( LOGGER.debug(
"Checking if factor applies to user", "Checking if factor applies to user",
factor=factor, factor=factor,
@ -81,10 +102,13 @@ class AuthenticationView(UserPassesTestMixin, View):
LOGGER.debug("Factor applies", factor=factor, user=self.pending_user) LOGGER.debug("Factor applies", factor=factor, user=self.pending_user)
return pending_factors 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) # Check if user passes test (i.e. SESSION_PENDING_USER is set)
user_test_result = self.get_test_func()() user_test_result = self.get_test_func()()
if not user_test_result: 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() return self.handle_no_permission()
# Extract pending user from session (only remember uid) # Extract pending user from session (only remember uid)
self.pending_user = get_object_or_404( self.pending_user = get_object_or_404(
@ -117,7 +141,7 @@ class AuthenticationView(UserPassesTestMixin, View):
self._current_factor_class.request = request self._current_factor_class.request = request
return super().dispatch(request, *args, **kwargs) 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""" """pass get request to current factor"""
LOGGER.debug( LOGGER.debug(
"Passing GET", "Passing GET",
@ -125,7 +149,7 @@ class AuthenticationView(UserPassesTestMixin, View):
) )
return self._current_factor_class.get(request, *args, **kwargs) 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""" """pass post request to current factor"""
LOGGER.debug( LOGGER.debug(
"Passing POST", "Passing POST",
@ -133,7 +157,7 @@ class AuthenticationView(UserPassesTestMixin, View):
) )
return self._current_factor_class.post(request, *args, **kwargs) return self._current_factor_class.post(request, *args, **kwargs)
def user_ok(self): def user_ok(self) -> HttpResponse:
"""Redirect to next Factor""" """Redirect to next Factor"""
LOGGER.debug( LOGGER.debug(
"Factor passed", "Factor passed",
@ -160,14 +184,14 @@ class AuthenticationView(UserPassesTestMixin, View):
LOGGER.debug("User passed all factors, logging in", user=self.pending_user) LOGGER.debug("User passed all factors, logging in", user=self.pending_user)
return self._user_passed() return self._user_passed()
def user_invalid(self): def user_invalid(self) -> HttpResponse:
"""Show error message, user cannot login. """Show error message, user cannot login.
This should only be shown if user authenticated successfully, but is disabled/locked/etc""" This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
LOGGER.debug("User invalid") LOGGER.debug("User invalid")
self.cleanup() self.cleanup()
return _redirect_with_qs("passbook_core:auth-denied", self.request.GET) 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""" """User Successfully passed all factors"""
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND] backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
login(self.request, self.pending_user, backend=backend) login(self.request, self.pending_user, backend=backend)

View File

@ -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)

View File

@ -8,7 +8,6 @@ from urllib.parse import urlparse
import yaml import yaml
from django.conf import ImproperlyConfigured from django.conf import ImproperlyConfigured
from django.utils.autoreload import autoreload_started
from structlog import get_logger from structlog import get_logger
SEARCH_PATHS = ["passbook/lib/default.yml", "/etc/passbook/config.yml", "",] + glob( SEARCH_PATHS = ["passbook/lib/default.yml", "/etc/passbook/config.yml", "",] + glob(
@ -142,12 +141,3 @@ class ConfigLoader:
CONFIG = 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)

View File

@ -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)

View File

@ -4,14 +4,20 @@ from structlog import get_logger
LOGGER = get_logger() LOGGER = get_logger()
class SentryIgnoredException(Exception):
"""Base Class for all errors that are supressed, and not sent to sentry."""
def before_send(event, hint): def before_send(event, hint):
"""Check if error is database error, and ignore if so""" """Check if error is database error, and ignore if so"""
from django_redis.exceptions import ConnectionInterrupted from django_redis.exceptions import ConnectionInterrupted
from django.db import OperationalError, InternalError from django.db import OperationalError, InternalError
from django.core.exceptions import ValidationError
from rest_framework.exceptions import APIException from rest_framework.exceptions import APIException
from billiard.exceptions import WorkerLostError from billiard.exceptions import WorkerLostError
from django.core.exceptions import DisallowedHost from django.core.exceptions import DisallowedHost
from botocore.client import ClientError from botocore.client import ClientError
from redis.exceptions import RedisError
ignored_classes = ( ignored_classes = (
OperationalError, OperationalError,
@ -24,6 +30,10 @@ def before_send(event, hint):
ConnectionResetError, ConnectionResetError,
KeyboardInterrupt, KeyboardInterrupt,
ClientError, ClientError,
ValidationError,
OSError,
RedisError,
SentryIgnoredException,
) )
if "exc_info" in hint: if "exc_info" in hint:
_exc_type, exc_value, _ = hint["exc_info"] _exc_type, exc_value, _ = hint["exc_info"]

View File

@ -3,9 +3,9 @@ from hashlib import md5
from urllib.parse import urlencode from urllib.parse import urlencode
from django import template from django import template
from django.template import Context
from django.apps import apps from django.apps import apps
from django.db.models import Model from django.db.models import Model
from django.template import Context
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _

View File

@ -1,5 +1,6 @@
"""passbook helper views""" """passbook helper views"""
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.views.generic import CreateView from django.views.generic import CreateView
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
@ -20,6 +21,10 @@ class CreateAssignPermView(CreateView):
self.object._meta.app_label, self.object._meta.app_label,
self.object._meta.model_name, self.object._meta.model_name,
) )
print(full_permission)
assign_perm(full_permission, self.request.user, self.object) assign_perm(full_permission, self.request.user, self.object)
return response 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)

View File

@ -1,5 +1,5 @@
"""passbook policy engine""" """passbook policy engine"""
from multiprocessing import Pipe from multiprocessing import Pipe, set_start_method
from multiprocessing.connection import Connection from multiprocessing.connection import Connection
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
@ -9,9 +9,12 @@ from structlog import get_logger
from passbook.core.models import Policy, User from passbook.core.models import Policy, User
from passbook.policies.process import PolicyProcess, cache_key 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() 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: class PolicyProcessInfo:
@ -36,13 +39,15 @@ class PolicyEngine:
policies: List[Policy] = [] policies: List[Policy] = []
request: PolicyRequest request: PolicyRequest
__processes: List[PolicyProcessInfo] = [] __cached_policies: List[PolicyResult]
__processes: List[PolicyProcessInfo]
def __init__(self, policies, user: User, request: HttpRequest = None): def __init__(self, policies, user: User, request: HttpRequest = None):
self.policies = policies self.policies = policies
self.request = PolicyRequest(user) self.request = PolicyRequest(user)
if request: if request:
self.request.http_request = request self.request.http_request = request
self.__cached_policies = []
self.__processes = [] self.__processes = []
def _select_subclasses(self) -> List[Policy]: def _select_subclasses(self) -> List[Policy]:
@ -55,21 +60,20 @@ class PolicyEngine:
def build(self) -> "PolicyEngine": def build(self) -> "PolicyEngine":
"""Build task group""" """Build task group"""
cached_policies = []
for policy in self._select_subclasses(): for policy in self._select_subclasses():
cached_policy = cache.get(cache_key(policy, self.request.user), None) cached_policy = cache.get(cache_key(policy, self.request.user), None)
if cached_policy and self.use_cache: if cached_policy and self.use_cache:
LOGGER.debug("Taking result from cache", policy=policy) LOGGER.debug("P_ENG: Taking result from cache", policy=policy)
cached_policies.append(cached_policy) self.__cached_policies.append(cached_policy)
else: continue
LOGGER.debug("Evaluating policy", policy=policy) LOGGER.debug("P_ENG: Evaluating policy", policy=policy)
our_end, task_end = Pipe(False) our_end, task_end = Pipe(False)
task = PolicyProcess(policy, self.request, task_end) task = PolicyProcess(policy, self.request, task_end)
LOGGER.debug("Starting Process", policy=policy) LOGGER.debug("P_ENG: Starting Process", policy=policy)
task.start() task.start()
self.__processes.append( self.__processes.append(
PolicyProcessInfo(process=task, connection=our_end, policy=policy) PolicyProcessInfo(process=task, connection=our_end, policy=policy)
) )
# If all policies are cached, we have an empty list here. # If all policies are cached, we have an empty list here.
for proc_info in self.__processes: for proc_info in self.__processes:
proc_info.process.join(proc_info.policy.timeout) proc_info.process.join(proc_info.policy.timeout)
@ -82,13 +86,14 @@ class PolicyEngine:
def result(self) -> Tuple[bool, List[str]]: def result(self) -> Tuple[bool, List[str]]:
"""Get policy-checking result""" """Get policy-checking result"""
messages: List[str] = [] messages: List[str] = []
for proc_info in self.__processes: process_results: List[PolicyResult] = [
LOGGER.debug( x.result for x in self.__processes if x.result
"Result", policy=proc_info.policy, passing=proc_info.result.passing ]
) for result in process_results + self.__cached_policies:
if proc_info.result.messages: LOGGER.debug("P_ENG: result", passing=result.passing)
messages += proc_info.result.messages if result.messages:
if not proc_info.result.passing: messages += result.messages
if not result.passing:
return False, messages return False, messages
return True, messages return True, messages

View File

@ -1,5 +1,6 @@
"""policy exceptions""" """policy exceptions"""
from passbook.lib.sentry import SentryIgnoredException
class PolicyException(Exception): class PolicyException(SentryIgnoredException):
"""Exception that should be raised during Policy Evaluation, and can be recovered from.""" """Exception that should be raised during Policy Evaluation, and can be recovered from."""

View File

@ -7,7 +7,7 @@ from django.utils.translation import gettext as _
from structlog import get_logger from structlog import get_logger
from passbook.core.models import Policy from passbook.core.models import Policy
from passbook.policies.struct import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -0,0 +1,5 @@
"""Passbook passbook expression policy Admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister("passbook_policies_expression")

View 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

View 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"

View 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

View 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(),
}

View File

@ -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 import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
@ -9,12 +9,12 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
("passbook_core", "0001_initial"), ("passbook_core", "0007_auto_20200217_1934"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="SSOLoginPolicy", name="ExpressionPolicy",
fields=[ fields=[
( (
"policy_ptr", "policy_ptr",
@ -27,10 +27,11 @@ class Migration(migrations.Migration):
to="passbook_core.Policy", to="passbook_core.Policy",
), ),
), ),
("expression", models.TextField()),
], ],
options={ options={
"verbose_name": "SSO Login Policy", "verbose_name": "Expression Policy",
"verbose_name_plural": "SSO Login Policies", "verbose_name_plural": "Expression Policies",
}, },
bases=("passbook_core.policy",), bases=("passbook_core.policy",),
), ),

View 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")

View File

@ -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 %}

View File

@ -1,4 +0,0 @@
"""autodiscover admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister("passbook_policies_group")

View File

@ -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

View File

@ -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"

View File

@ -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(),
}

View File

@ -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",),
),
]

View File

@ -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")

View File

@ -35,7 +35,7 @@ class HaveIBeenPwendPolicy(Policy):
full_hash, count = line.split(":") full_hash, count = line.split(":")
if pw_hash[5:] == full_hash.lower(): if pw_hash[5:] == full_hash.lower():
final_count = int(count) 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: if final_count > self.allowed_count:
message = _( message = _(
"Password exists on %(count)d online lists." % {"count": final_count} "Password exists on %(count)d online lists." % {"count": final_count}

View File

@ -1,4 +0,0 @@
"""autodiscover admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister("passbook_policies_matcher")

View File

@ -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

View File

@ -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"

View File

@ -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(),
}

View File

@ -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",),
),
]

View File

@ -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")

View File

@ -6,7 +6,7 @@ from django.utils.translation import gettext as _
from structlog import get_logger from structlog import get_logger
from passbook.core.models import Policy from passbook.core.models import Policy
from passbook.policies.struct import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -7,7 +7,7 @@ from structlog import get_logger
from passbook.core.models import Policy from passbook.core.models import Policy
from passbook.policies.exceptions import PolicyException from passbook.policies.exceptions import PolicyException
from passbook.policies.struct import PolicyRequest, PolicyResult from passbook.policies.types import PolicyRequest, PolicyResult
LOGGER = get_logger() LOGGER = get_logger()

View File

@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
from passbook.core.models import Policy, User from passbook.core.models import Policy, User
from passbook.lib.utils.http import get_client_ip 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): class ReputationPolicy(Policy):

View File

@ -1,4 +0,0 @@
"""autodiscover admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister("passbook_policies_sso")

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