Compare commits

..

27 Commits

Author SHA1 Message Date
c38012f147 new release: 0.4.1-beta 2019-10-02 21:04:16 +00:00
3676ff21c2 helm(minor): use postgres 4.2.2 2019-10-02 21:03:39 +00:00
920e705d75 policy(minor): lookup correct policy subclass 2019-10-02 22:28:58 +02:00
de0b137b1e policy(minor): improve error handling 2019-10-02 22:28:39 +02:00
d44ac6e2a3 static(minor): fix build path for static image 2019-10-02 22:16:48 +02:00
71039a4012 helm(minor): fix p2 to passbook 2019-10-02 22:16:32 +02:00
8745ac7932 new release: 0.4.0-beta 2019-10-01 17:01:30 +02:00
7f70048423 ci(minor): disable pylint since its currently broken upstream 2019-10-01 16:53:09 +02:00
97dbfc8885 req(minor): fix dependency issue by downgrading prospector 2019-10-01 15:54:29 +02:00
149ea22a93 k8s(minor): switch to uwsgi 2019-10-01 15:43:06 +02:00
404ed5406d k8s(minor): remove passwords from configmap 2019-10-01 15:42:55 +02:00
b8656858ec k8s(minor): load secrets as env vars 2019-10-01 15:42:14 +02:00
6b0f0e8993 deploy(minor): use 5.x postgresql chart for psql 10.x 2019-10-01 15:33:43 +02:00
aec1ccd88d root(minor): fix redis password not being loaded 2019-10-01 15:30:35 +02:00
bee5c200b6 docker(minor): fix static build failing 2019-10-01 15:30:22 +02:00
9d640efc88 new release: 0.3.0-beta 2019-10-01 13:50:50 +02:00
f0907841dd docker(minor): remove virtualenv from pipenv 2019-10-01 13:50:37 +02:00
2bffc12ef9 ci(minor): fix default settings so CI works 2019-10-01 13:22:38 +02:00
2ff9ec6522 ci(minor): fix not all packages being installed 2019-10-01 11:34:34 +02:00
43a54f5c54 ci(minor): install pipenv before testing 2019-10-01 11:12:59 +02:00
7bff2734aa lint(minor): fix all remaining pylint and prospector errors 2019-10-01 11:08:56 +02:00
84768c0ec6 helm(minor): remove rabbitmq 2019-10-01 10:48:55 +02:00
f4499a5459 *(minor): stdlib logging to structlog 2019-10-01 10:24:10 +02:00
b3aede5bba policy(minor): Move policy-related code to separate package 2019-10-01 10:17:39 +02:00
531ea1c039 build(minor): rename dockerfiles to be detected correctly 2019-09-30 18:05:42 +02:00
c2c5ff6912 config(minor): CONFIG.get -> CONFIG.y 2019-09-30 18:04:04 +02:00
9cddab8fd5 deploy(minor): switch to pipfile 2019-09-10 17:00:13 +02:00
104 changed files with 2020 additions and 915 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.2.8-beta current_version = 0.4.1-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>.*)

1
.gitignore vendored
View File

@ -191,3 +191,4 @@ pip-selfcheck.json
# End of https://www.gitignore.io/api/python,django # End of https://www.gitignore.io/api/python,django
/static/ /static/
local.env.yml local.env.yml
.vscode/

View File

@ -13,9 +13,12 @@ variables:
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77" POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
before_script: before_script:
- pip install pipenv
# Ensure all dependencies are installed, even those not included in passbook/dev # Ensure all dependencies are installed, even those not included in passbook/dev
- pip install -r requirements.txt # According to pipenv docs, -d outputs all packages, however it actually does not
- pip install -r requirements-dev.txt - pipenv lock -r > requirements-all.txt
- pipenv lock -rd >> requirements-all.txt
- pip install -r requirements-all.txt
create-base-image: create-base-image:
image: image:
@ -24,7 +27,7 @@ create-base-image:
before_script: before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script: script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.base --destination docker.beryju.org/passbook/base:latest --destination docker.beryju.org/passbook/base:0.2.8-beta - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/base.Dockerfile --destination docker.beryju.org/passbook/base:latest --destination docker.beryju.org/passbook/base:0.4.1-beta
stage: build-base-image stage: build-base-image
only: only:
refs: refs:
@ -38,7 +41,7 @@ build-dev-image:
before_script: before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script: script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.dev --destination docker.beryju.org/passbook/dev:latest --destination docker.beryju.org/passbook/dev:0.2.8-beta - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dev.Dockerfile --destination docker.beryju.org/passbook/dev:latest --destination docker.beryju.org/passbook/dev:0.4.1-beta
stage: build-dev-image stage: build-dev-image
only: only:
refs: refs:
@ -60,20 +63,20 @@ migrations:
services: services:
- postgres:latest - postgres:latest
- redis:latest - redis:latest
prospector: # prospector:
script: # script:
- prospector # - prospector
stage: test # stage: test
services: # services:
- postgres:latest # - postgres:latest
- redis:latest # - redis:latest
pylint: # pylint:
script: # script:
- pylint passbook # - pylint passbook
stage: test # stage: test
services: # services:
- postgres:latest # - postgres:latest
- redis:latest # - redis:latest
coverage: coverage:
script: script:
- coverage run manage.py test - coverage run manage.py test
@ -91,7 +94,7 @@ package-passbook-server:
before_script: before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script: script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.2.8-beta - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.4.1-beta
stage: build stage: build
only: only:
- tags - tags
@ -104,7 +107,7 @@ build-passbook-static:
before_script: before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script: script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.static --destination docker.beryju.org/passbook/static:latest --destination docker.beryju.org/passbook/static:0.2.8-beta - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/static.Dockerfile --destination docker.beryju.org/passbook/static:latest --destination docker.beryju.org/passbook/static:0.4.1-beta
only: only:
- tags - tags
- /^version/.*$/ - /^version/.*$/
@ -121,7 +124,7 @@ package-helm:
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash - curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
script: script:
- helm init --client-only - helm init --client-only
- helm dependency build helm/passbook - helm dependency update helm/passbook
- helm package helm/passbook - helm package helm/passbook
artifacts: artifacts:
paths: paths:

View File

@ -1,114 +0,0 @@
# The default ``config.py``
# flake8: noqa
def set_prefs(prefs):
"""This function is called before opening the project"""
# Specify which files and folders to ignore in the project.
# Changes to ignored resources are not added to the history and
# VCSs. Also they are not returned in `Project.get_files()`.
# Note that ``?`` and ``*`` match all characters but slashes.
# '*.pyc': matches 'test.pyc' and 'pkg/test.pyc'
# 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc'
# '.svn': matches 'pkg/.svn' and all of its children
# 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o'
# 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o'
prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject',
'.hg', '.svn', '_svn', '.git', '.tox']
# Specifies which files should be considered python files. It is
# useful when you have scripts inside your project. Only files
# ending with ``.py`` are considered to be python files by
# default.
# prefs['python_files'] = ['*.py']
# Custom source folders: By default rope searches the project
# for finding source folders (folders that should be searched
# for finding modules). You can add paths to that list. Note
# that rope guesses project source folders correctly most of the
# time; use this if you have any problems.
# The folders should be relative to project root and use '/' for
# separating folders regardless of the platform rope is running on.
# 'src/my_source_folder' for instance.
# prefs.add('source_folders', 'src')
# You can extend python path for looking up modules
# prefs.add('python_path', '~/python/')
# Should rope save object information or not.
prefs['save_objectdb'] = True
prefs['compress_objectdb'] = False
# If `True`, rope analyzes each module when it is being saved.
prefs['automatic_soa'] = True
# The depth of calls to follow in static object analysis
prefs['soa_followed_calls'] = 0
# If `False` when running modules or unit tests "dynamic object
# analysis" is turned off. This makes them much faster.
prefs['perform_doa'] = True
# Rope can check the validity of its object DB when running.
prefs['validate_objectdb'] = True
# How many undos to hold?
prefs['max_history_items'] = 32
# Shows whether to save history across sessions.
prefs['save_history'] = True
prefs['compress_history'] = False
# Set the number spaces used for indenting. According to
# :PEP:`8`, it is best to use 4 spaces. Since most of rope's
# unit-tests use 4 spaces it is more reliable, too.
prefs['indent_size'] = 4
# Builtin and c-extension modules that are allowed to be imported
# and inspected by rope.
prefs['extension_modules'] = []
# Add all standard c-extensions to extension_modules list.
prefs['import_dynload_stdmods'] = True
# If `True` modules with syntax errors are considered to be empty.
# The default value is `False`; When `False` syntax errors raise
# `rope.base.exceptions.ModuleSyntaxError` exception.
prefs['ignore_syntax_errors'] = False
# If `True`, rope ignores unresolvable imports. Otherwise, they
# appear in the importing namespace.
prefs['ignore_bad_imports'] = False
# If `True`, rope will insert new module imports as
# `from <package> import <module>` by default.
prefs['prefer_module_from_imports'] = False
# If `True`, rope will transform a comma list of imports into
# multiple separate import statements when organizing
# imports.
prefs['split_imports'] = False
# If `True`, rope will remove all top-level import statements and
# reinsert them at the top of the module when making changes.
prefs['pull_imports_to_top'] = True
# If `True`, rope will sort imports alphabetically by module name instead
# of alphabetically by import statement, with from imports after normal
# imports.
prefs['sort_imports_alphabetically'] = False
# Location of implementation of
# rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general
# case, you don't have to change this value, unless you're an rope expert.
# Change this value to inject you own implementations of interfaces
# listed in module rope.base.oi.type_hinting.providers.interfaces
# For example, you can add you own providers for Django Models, or disable
# the search type-hinting in a class hierarchy, etc.
prefs['type_hinting_factory'] = (
'rope.base.oi.type_hinting.factory.default_type_hinting_factory')
def project_opened(project):
"""This function is called after opening the project"""
# Do whatever you like here!

Binary file not shown.

14
.vscode/settings.json vendored
View File

@ -1,14 +0,0 @@
{
"python.pythonPath": "env/bin/python",
"editor.tabSize": 4,
"[html]": {
"editor.tabSize": 2
},
"[yml]": {
"editor.tabSize": 2
},
"cSpell.words": [
"SAML",
"passbook"
]
}

View File

@ -1,5 +0,0 @@
FROM docker.beryju.org/passbook/base:latest
COPY ./requirements-dev.txt /app/
RUN pip install -r /app/requirements-dev.txt --no-cache-dir

62
Pipfile Normal file
View File

@ -0,0 +1,62 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[packages]
asgiref = "*"
beautifulsoup4 = "*"
celery = "*"
channels = "*"
cherrypy = "*"
colorlog = "*"
daphne = "*"
defusedxml = "*"
django = "*"
django-cors-middleware = "*"
django-filters = "*"
django-ipware = "*"
django-model-utils = "*"
django-oauth-toolkit = "*"
django-oidc-provider = "*"
django-otp = "*"
django-recaptcha = "*"
django-redis = "*"
django-rest-framework = "*"
django-revproxy = "*"
djangorestframework = "==3.9.4"
drf-yasg = "*"
ldap3 = "*"
lxml = "*"
markdown = "*"
oauthlib = "*"
packaging = "*"
psycopg2 = "*"
pycryptodome = "*"
pyyaml = "*"
qrcode = "*"
requests-oauthlib = "*"
sentry-sdk = "*"
service_identity = "*"
signxml = "*"
urllib3 = {extras = ["secure"],version = "*"}
websocket_client = "*"
structlog = "*"
uwsgi = "*"
[requires]
python_version = "3.7"
[dev-packages]
astroid = "==2.2.5"
coverage = "*"
isort = "*"
pylint = "==2.3.1"
pylint-django = "==2.0.10"
prospector = "==1.1.7"
django-debug-toolbar = "*"
bumpversion = "*"
unittest-xml-reporting = "*"
autopep8 = "*"
bandit = "*"
colorama = "*"

1332
Pipfile.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,15 @@
FROM python:3.7-alpine FROM python:3.7-alpine
COPY ./requirements.txt /app/ COPY ./Pipfile /app/
COPY ./Pipfile.lock /app/
WORKDIR /app/ WORKDIR /app/
RUN apk update && \ RUN apk update && \
apk add --no-cache openssl-dev build-base libxml2-dev libxslt-dev libffi-dev gcc musl-dev libgcc zlib-dev postgresql-dev && \ apk add --no-cache openssl-dev build-base libxml2-dev libxslt-dev libffi-dev gcc musl-dev libgcc zlib-dev postgresql-dev && \
pip install -r /app/requirements.txt --no-cache-dir && \ pip install pipenv --no-cache-dir && \
pipenv lock -r > requirements.txt && \
pipenv --rm && \
pip install -r requirements.txt --no-cache-dir && \
adduser -S passbook && \ adduser -S passbook && \
chown -R passbook /app chown -R passbook /app

4
dev.Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM docker.beryju.org/passbook/base:latest
RUN pipenv lock --dev -r > requirements-dev.txt && \
pip install -r /app/requirements-dev.txt --no-cache-dir

9
helm/passbook/Chart.lock Normal file
View File

@ -0,0 +1,9 @@
dependencies:
- name: postgresql
repository: https://kubernetes-charts.storage.googleapis.com/
version: 6.3.10
- name: redis
repository: https://kubernetes-charts.storage.googleapis.com/
version: 9.2.1
digest: sha256:bdde250e1401dccdd5161e39c807f9e88b05e3e8e72e74df767a1bbb5fc39a4a
generated: "2019-10-01T10:46:06.542706+02:00"

View File

@ -1,6 +1,6 @@
apiVersion: v1 apiVersion: v1
appVersion: "0.2.8-beta" appVersion: "0.4.1-beta"
description: A Helm chart for passbook. description: A Helm chart for passbook.
name: passbook name: passbook
version: "0.2.8-beta" version: "0.4.1-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

@ -1 +0,0 @@
# passbook

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,98 +0,0 @@
---
categories:
- Authentication
- SSO
questions:
- default: "true"
variable: config.error_reporting
type: boolean
description: "Enable error-reporting to sentry.services.beryju.org"
group: "passbook Configuration"
label: "Error Reporting"
####################################################################
### PostgreSQL
####################################################################
- variable: postgresql.enabled
default: true
description: "Deploy a database server as part of this deployment, or set to false and configure an external database connection."
type: boolean
required: true
label: Install PostgreSQL
show_subquestion_if: true
group: "Database Settings"
subquestions:
- variable: postgresql.postgresqlDatabase
default: "passbook"
description: "Database name to create"
type: string
label: PostgreSQL Database
- variable: postgresql.postgresqlUsername
default: "passbook"
description: "Database user to create"
type: string
label: PostgreSQL User
- variable: postgresql.postgresqlPassword
default: ""
description: "password will be auto-generated if not specified"
type: password
label: PostgreSQL Password
- variable: externalDatabase.host
default: ""
description: "Host of the external database"
type: string
label: External Database Host
show_if: "postgresql.enabled=false"
group: "Database Settings"
- variable: externalDatabase.user
default: ""
description: "Existing username in the external DB"
type: string
label: External Database username
show_if: "postgresql.enabled=false"
group: "Database Settings"
- variable: externalDatabase.password
default: ""
description: "External database password"
type: password
label: External Database password
show_if: "postgresql.enabled=false"
group: "Database Settings"
- variable: externalDatabase.database
default: ""
description: "Name of the existing database"
type: string
label: External Database
show_if: "postgresql.enabled=false"
group: "Database Settings"
- variable: externalDatabase.port
default: "3306"
description: "External database port number"
type: string
label: External Database Port
show_if: "postgresql.enabled=false"
group: "Database Settings"
- variable: postgresql.persistence.enabled
default: false
description: "Enable persistent volume for PostgreSQL"
type: boolean
required: true
label: PostgreSQL Persistent Volume Enabled
show_if: "postgresql.enabled=true"
show_subquestion_if: true
group: "Database Settings"
subquestions:
- variable: postgresql.master.persistence.size
default: "8Gi"
description: "PostgreSQL Persistent Volume Size"
type: string
label: PostgreSQL Volume Size
- variable: postgresql.master.persistence.storageClass
default: ""
description: "If undefined or null, uses the default StorageClass. Default to null"
type: storageclass
label: Default StorageClass for PostgreSQL
- variable: postgresql.master.persistence.existingClaim
default: ""
description: "If not empty, uses the specified existing PVC instead of creating new one"
type: string
label: Existing Persistent Volume Claim for PostgreSQL

View File

@ -1,12 +1,9 @@
dependencies: dependencies:
- name: rabbitmq
repository: https://kubernetes-charts.storage.googleapis.com/
version: 4.3.2
- name: postgresql - name: postgresql
repository: https://kubernetes-charts.storage.googleapis.com/ repository: https://kubernetes-charts.storage.googleapis.com/
version: 3.10.1 version: 4.2.2
- name: redis - name: redis
repository: https://kubernetes-charts.storage.googleapis.com/ repository: https://kubernetes-charts.storage.googleapis.com/
version: 5.1.0 version: 9.2.1
digest: sha256:8bf68bc928a2e3c0f05139635be05fa0840554c7bde4cecd624fac78fb5fa5a3 digest: sha256:8782e974a1094eaeecf1d68f093ca4fb84977217b2bd38b09790a05ec289aec2
generated: 2019-03-21T11:06:51.553379+01:00 generated: "2019-10-02T21:03:25.90491153Z"

View File

@ -1,10 +1,7 @@
dependencies: dependencies:
- name: rabbitmq
version: 4.3.2
repository: https://kubernetes-charts.storage.googleapis.com/
- name: postgresql - name: postgresql
version: 3.10.1 version: 4.2.2
repository: https://kubernetes-charts.storage.googleapis.com/ repository: https://kubernetes-charts.storage.googleapis.com/
- name: redis - name: redis
version: 5.1.0 version: 9.2.1
repository: https://kubernetes-charts.storage.googleapis.com/ repository: https://kubernetes-charts.storage.googleapis.com/

View File

@ -32,6 +32,21 @@ spec:
- ./manage.py - ./manage.py
args: args:
- app_gw_web - app_gw_web
envFrom:
- configMapRef:
name: {{ include "passbook.fullname" . }}-config
prefix: PASSBOOK_
env:
- name: PASSBOOK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-redis"
key: redis-password
- name: PASSBOOK_POSTGRESQL__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-postgresql"
key: postgresql-password
ports: ports:
- name: http - name: http
containerPort: 8000 containerPort: 8000

View File

@ -4,41 +4,16 @@ metadata:
name: {{ include "passbook.fullname" . }}-config name: {{ include "passbook.fullname" . }}-config
data: data:
config.yml: | config.yml: |
# Env for Docker images postgresql:
databases: host: "{{ .Release.Name }}-postgresql"
default: name: "{{ .Values.postgresql.postgresqlDatabase }}"
engine: django.db.backends.postgresql
name: {{ .Values.postgresql.postgresqlDatabase }}
user: postgres user: postgres
password: {{ .Values.postgresql.postgresqlPassword }} redis:
host: {{ .Release.Name }}-postgresql host: "{{ .Release.Name }}-redis-master"
port: '' cache_db: 0
log: message_queue_db: 1
level:
console: WARNING # Error reporting, sends stacktrace to sentry.beryju.org
file: WARNING
file: /dev/null
syslog:
host: 127.0.0.1
port: 514
email:
host: {{ .Values.config.email.host }}
port: 25
user: ''
password: ''
use_tls: false
use_ssl: false
from: passbook <passbook@domain.tld>
web:
listen: 0.0.0.0
port: 8000
threads: 30
debug: false
secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https
rabbitmq: "user:{{ .Values.rabbitmq.rabbitmq.password }}@{{ .Release.Name }}-rabbitmq"
redis: ":{{ .Values.redis.password }}@{{ .Release.Name }}-redis-master/0"
# Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: {{ .Values.config.error_reporting }} error_report_enabled: {{ .Values.config.error_reporting }}
{{- if .Values.config.secret_key }} {{- if .Values.config.secret_key }}

View File

@ -31,6 +31,21 @@ spec:
- ./manage.py - ./manage.py
args: args:
- migrate - migrate
envFrom:
- configMapRef:
name: {{ include "passbook.fullname" . }}-config
prefix: PASSBOOK_
env:
- name: PASSBOOK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-redis"
key: redis-password
- name: PASSBOOK_POSTGRESQL__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-postgresql"
key: postgresql-password
volumeMounts: volumeMounts:
- mountPath: /etc/passbook - mountPath: /etc/passbook
name: config-volume name: config-volume
@ -39,9 +54,31 @@ spec:
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}" image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: command:
- ./manage.py - uwsgi
args: args:
- web - --http 0.0.0.0:8000
- --wsgi-file passbook/root/wsgi.py
- --master
- --processes 24
- --threads 2
- --offload-threads 4
- --stats 0.0.0.0:8001
- --stats-http
envFrom:
- configMapRef:
name: {{ include "passbook.fullname" . }}-config
prefix: PASSBOOK_
env:
- name: PASSBOOK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-redis"
key: redis-password
- name: PASSBOOK_POSTGRESQL__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-postgresql"
key: postgresql-password
ports: ports:
- name: http - name: http
containerPort: 8000 containerPort: 8000

View File

@ -32,6 +32,21 @@ spec:
- ./manage.py - ./manage.py
args: args:
- worker - worker
envFrom:
- configMapRef:
name: {{ include "passbook.fullname" . }}-config
prefix: PASSBOOK_
env:
- name: PASSBOOK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-redis"
key: redis-password
- name: PASSBOOK_POSTGRESQL__PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Release.Name }}-postgresql"
key: postgresql-password
ports: ports:
- name: http - name: http
containerPort: 8000 containerPort: 8000

View File

@ -5,7 +5,7 @@
replicaCount: 1 replicaCount: 1
image: image:
tag: 0.2.8-beta tag: 0.4.1-beta
nameOverride: "" nameOverride: ""

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = '0.2.8-beta' __version__ = '0.4.1-beta'

View File

@ -1,14 +1,14 @@
"""passbook admin templatetags""" """passbook admin templatetags"""
import inspect import inspect
from logging import getLogger
from django import template from django import template
from django.db.models import Model from django.db.models import Model
from structlog import get_logger
from passbook.lib.utils.template import render_to_string from passbook.lib.utils.template import render_to_string
register = template.Library() register = template.Library()
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
@register.simple_tag() @register.simple_tag()
def get_links(model_instance): def get_links(model_instance):

View File

@ -11,8 +11,8 @@ from django.views.generic.detail import DetailView
from passbook.admin.forms.policies import PolicyTestForm from passbook.admin.forms.policies import PolicyTestForm
from passbook.admin.mixins import AdminRequiredMixin from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Policy from passbook.core.models import Policy
from passbook.core.policies import PolicyEngine
from passbook.lib.utils.reflection import path_to_class from passbook.lib.utils.reflection import path_to_class
from passbook.policy.engine import PolicyEngine
class PolicyListView(AdminRequiredMixin, ListView): class PolicyListView(AdminRequiredMixin, ListView):

View File

@ -1,14 +1,13 @@
"""passbook app_gw webserver management command""" """passbook app_gw webserver management command"""
from logging import getLogger
from daphne.cli import CommandLineInterface from daphne.cli import CommandLineInterface
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import autoreload from django.utils import autoreload
from structlog import get_logger
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class Command(BaseCommand): class Command(BaseCommand):

View File

@ -27,7 +27,7 @@ class ApplicationGatewayMiddleware:
handler = RequestHandler(app_gw, request) handler = RequestHandler(app_gw, request)
if not handler.check_permission(): if not handler.check_permission():
to_url = 'https://%s/?next=%s' % (CONFIG.get('domains')[0], request.get_full_path()) to_url = 'https://%s/?next=%s' % (CONFIG.y('domains')[0], request.get_full_path())
return RedirectView.as_view(url=to_url)(request) return RedirectView.as_view(url=to_url)(request)
return handler.get_response() return handler.get_response()

View File

@ -1,6 +1,5 @@
"""passbook app_gw request handler""" """passbook app_gw request handler"""
import mimetypes import mimetypes
from logging import getLogger
from random import SystemRandom from random import SystemRandom
from urllib.parse import urlparse from urllib.parse import urlparse
@ -8,6 +7,7 @@ import certifi
import urllib3 import urllib3
from django.core.cache import cache from django.core.cache import cache
from django.utils.http import urlencode from django.utils.http import urlencode
from structlog import get_logger
from passbook.app_gw.models import ApplicationGatewayProvider from passbook.app_gw.models import ApplicationGatewayProvider
from passbook.app_gw.proxy.exceptions import InvalidUpstream from passbook.app_gw.proxy.exceptions import InvalidUpstream
@ -15,11 +15,11 @@ from passbook.app_gw.proxy.response import get_django_response
from passbook.app_gw.proxy.rewrite import Rewriter from passbook.app_gw.proxy.rewrite import Rewriter
from passbook.app_gw.proxy.utils import encode_items, normalize_request_headers from passbook.app_gw.proxy.utils import encode_items, normalize_request_headers
from passbook.core.models import Application from passbook.core.models import Application
from passbook.core.policies import PolicyEngine from passbook.policy.engine import PolicyEngine
SESSION_UPSTREAM_KEY = 'passbook_app_gw_upstream' SESSION_UPSTREAM_KEY = 'passbook_app_gw_upstream'
IGNORED_HOSTNAMES_KEY = 'passbook_app_gw_ignored' IGNORED_HOSTNAMES_KEY = 'passbook_app_gw_ignored'
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
QUOTE_SAFE = r'<.;>\(}*+|~=-$/_:^@)[{]&\'!,"`' QUOTE_SAFE = r'<.;>\(}*+|~=-$/_:^@)[{]&\'!,"`'
ERRORS_MESSAGES = { ERRORS_MESSAGES = {
'upstream-no-scheme': ("Upstream URL scheme must be either " 'upstream-no-scheme': ("Upstream URL scheme must be either "
@ -204,8 +204,8 @@ class RequestHandler:
def _set_content_type(self, proxy_response): def _set_content_type(self, proxy_response):
content_type = proxy_response.headers.get('Content-Type') content_type = proxy_response.headers.get('Content-Type')
if not content_type: if not content_type:
content_type = (mimetypes.guess_type(self.request.path)[0] or content_type = (mimetypes.guess_type(self.request.path)
self.app_gw.default_content_type) [0] or self.app_gw.default_content_type)
proxy_response.headers['Content-Type'] = content_type proxy_response.headers['Content-Type'] = content_type
# LOGGER.debug("Proxy response CONTENT-TYPE: %s", # LOGGER.debug("Proxy response CONTENT-TYPE: %s",
# proxy_response.headers['Content-Type']) # proxy_response.headers['Content-Type'])

View File

@ -1,7 +1,6 @@
"""response functions from django-revproxy""" """response functions from django-revproxy"""
import logging
from django.http import HttpResponse, StreamingHttpResponse from django.http import HttpResponse, StreamingHttpResponse
from structlog import get_logger
from passbook.app_gw.proxy.utils import (cookie_from_string, from passbook.app_gw.proxy.utils import (cookie_from_string,
set_response_headers, should_stream) set_response_headers, should_stream)
@ -9,7 +8,7 @@ from passbook.app_gw.proxy.utils import (cookie_from_string,
#: Default number of bytes that are going to be read in a file lecture #: Default number of bytes that are going to be read in a file lecture
DEFAULT_AMT = 2 ** 16 DEFAULT_AMT = 2 ** 16
logger = logging.getLogger(__name__) logger = get_logger(__name__)
def get_django_response(proxy_response, strict_cookies=False): def get_django_response(proxy_response, strict_cookies=False):

View File

@ -1,8 +1,9 @@
"""Utils from django-revproxy, slightly adjusted""" """Utils from django-revproxy, slightly adjusted"""
import logging
import re import re
from wsgiref.util import is_hop_by_hop from wsgiref.util import is_hop_by_hop
from structlog import get_logger
try: try:
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
COOKIE_PREFIX = '' COOKIE_PREFIX = ''
@ -155,7 +156,7 @@ def encode_items(items):
return encoded return encoded
logger = logging.getLogger('revproxy.cookies') logger = get_logger()
def cookie_from_string(cookie_string, strict_cookies=False): def cookie_from_string(cookie_string, strict_cookies=False):

View File

@ -1,15 +1,14 @@
"""passbook app_gw cache clean signals""" """passbook app_gw cache clean signals"""
from logging import getLogger
from django.core.cache import cache from django.core.cache import cache
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from structlog import get_logger
from passbook.app_gw.models import ApplicationGatewayProvider from passbook.app_gw.models import ApplicationGatewayProvider
from passbook.app_gw.proxy.handler import IGNORED_HOSTNAMES_KEY from passbook.app_gw.proxy.handler import IGNORED_HOSTNAMES_KEY
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
@receiver(post_save) @receiver(post_save)
# pylint: disable=unused-argument # pylint: disable=unused-argument

View File

@ -1,14 +1,14 @@
"""websocket proxy consumer""" """websocket proxy consumer"""
import threading import threading
from logging import getLogger
from ssl import CERT_NONE from ssl import CERT_NONE
import websocket import websocket
from channels.generic.websocket import WebsocketConsumer from channels.generic.websocket import WebsocketConsumer
from structlog import get_logger
from passbook.app_gw.models import ApplicationGatewayProvider from passbook.app_gw.models import ApplicationGatewayProvider
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class ProxyConsumer(WebsocketConsumer): class ProxyConsumer(WebsocketConsumer):
"""Proxy websocket connection to upstream""" """Proxy websocket connection to upstream"""

View File

@ -1,6 +1,4 @@
"""passbook audit models""" """passbook audit models"""
from logging import getLogger
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.postgres.fields import JSONField from django.contrib.postgres.fields import JSONField
@ -8,10 +6,11 @@ from django.core.exceptions import ValidationError
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 ipware import get_client_ip from ipware import get_client_ip
from structlog import get_logger
from passbook.lib.models import UUIDModel from passbook.lib.models import UUIDModel
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class AuditEntry(UUIDModel): class AuditEntry(UUIDModel):
"""An individual audit log entry""" """An individual audit log entry"""

View File

@ -1,12 +1,12 @@
"""passbook core app config""" """passbook core app config"""
from importlib import import_module from importlib import import_module
from logging import getLogger
from django.apps import AppConfig from django.apps import AppConfig
from structlog import get_logger
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class PassbookCoreConfig(AppConfig): class PassbookCoreConfig(AppConfig):
"""passbook core app config""" """passbook core app config"""
@ -17,7 +17,7 @@ class PassbookCoreConfig(AppConfig):
mountpoint = '' mountpoint = ''
def ready(self): def ready(self):
import_module('passbook.core.policies') import_module('passbook.policy.engine')
factors_to_load = CONFIG.y('passbook.factors', []) factors_to_load = CONFIG.y('passbook.factors', [])
for factors_to_load in factors_to_load: for factors_to_load in factors_to_load:
try: try:

View File

@ -19,7 +19,7 @@ class AuthenticationFactor(TemplateView):
self.authenticator = authenticator self.authenticator = authenticator
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.get('passbook') kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True kwargs['is_login'] = True
kwargs['title'] = _('Log in to your account') kwargs['title'] = _('Log in to your account')
kwargs['primary_action'] = _('Log in') kwargs['primary_action'] = _('Log in')

View File

@ -1,9 +1,9 @@
"""passbook multi-factor authentication engine""" """passbook multi-factor authentication engine"""
from logging import getLogger from structlog import get_logger
from passbook.core.auth.factor import AuthenticationFactor from passbook.core.auth.factor import AuthenticationFactor
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class DummyFactor(AuthenticationFactor): class DummyFactor(AuthenticationFactor):

View File

@ -1,6 +1,5 @@
"""passbook multi-factor authentication engine""" """passbook multi-factor authentication engine"""
from inspect import Signature from inspect import Signature
from logging import getLogger
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import _clean_credentials from django.contrib.auth import _clean_credentials
@ -10,6 +9,7 @@ from django.forms.utils import ErrorList
from django.shortcuts import redirect, reverse from django.shortcuts import redirect, reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView from django.views.generic import FormView
from structlog import get_logger
from passbook.core.auth.factor import AuthenticationFactor from passbook.core.auth.factor import AuthenticationFactor
from passbook.core.auth.view import AuthenticationView from passbook.core.auth.view import AuthenticationView
@ -19,7 +19,7 @@ from passbook.core.tasks import send_email
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
from passbook.lib.utils.reflection import path_to_class from passbook.lib.utils.reflection import path_to_class
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
def authenticate(request, backends, **credentials): def authenticate(request, backends, **credentials):

View File

@ -1,19 +1,20 @@
"""passbook multi-factor authentication engine""" """passbook multi-factor authentication engine"""
from logging import getLogger from typing import List, 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.shortcuts import get_object_or_404, redirect, 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 passbook.core.models import Factor, User from passbook.core.models import Factor, User
from passbook.core.policies import PolicyEngine
from passbook.core.views.utils import PermissionDeniedView from passbook.core.views.utils import PermissionDeniedView
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.policy.engine import PolicyEngine
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
def _redirect_with_qs(view, get_query_set=None): def _redirect_with_qs(view, get_query_set=None):
"""Wrapper to redirect whilst keeping GET Parameters""" """Wrapper to redirect whilst keeping GET Parameters"""
@ -31,12 +32,12 @@ class AuthenticationView(UserPassesTestMixin, View):
SESSION_USER_BACKEND = 'passbook_user_backend' SESSION_USER_BACKEND = 'passbook_user_backend'
SESSION_IS_SSO_LOGIN = 'passbook_sso_login' SESSION_IS_SSO_LOGIN = 'passbook_sso_login'
pending_user = None pending_user: User
pending_factors = [] pending_factors: List[Tuple[str, str]] = []
_current_factor_class = None _current_factor_class: Factor
current_factor = None 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):

View File

@ -1,16 +1,15 @@
"""passbook core authentication forms""" """passbook core authentication forms"""
from logging import getLogger
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import validate_email from django.core.validators import validate_email
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from structlog import get_logger
from passbook.core.models import User from passbook.core.models import User
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
from passbook.lib.utils.ui import human_list from passbook.lib.utils.ui import human_list
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class LoginForm(forms.Form): class LoginForm(forms.Form):
"""Allow users to login""" """Allow users to login"""

View File

@ -1,13 +1,13 @@
"""passbook import_users management command""" """passbook import_users management command"""
from csv import DictReader from csv import DictReader
from logging import getLogger
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.core.validators import EmailValidator, ValidationError from django.core.validators import EmailValidator, ValidationError
from structlog import get_logger
from passbook.core.models import User from passbook.core.models import User
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class Command(BaseCommand): class Command(BaseCommand):
"""Import users from CSV file""" """Import users from CSV file"""

View File

@ -1,15 +1,14 @@
"""passbook Webserver management command""" """passbook Webserver management command"""
from logging import getLogger
import cherrypy import cherrypy
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from structlog import get_logger
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
from passbook.root.wsgi import application from passbook.root.wsgi import application
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class Command(BaseCommand): class Command(BaseCommand):
@ -17,7 +16,7 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
"""passbook cherrypy server""" """passbook cherrypy server"""
cherrypy.config.update(CONFIG.get('web')) cherrypy.config.update(CONFIG.y('web'))
cherrypy.tree.graft(application, '/') cherrypy.tree.graft(application, '/')
# Mount NullObject to serve static files # Mount NullObject to serve static files
cherrypy.tree.mount(None, settings.STATIC_URL, config={ cherrypy.tree.mount(None, settings.STATIC_URL, config={

View File

@ -1,13 +1,12 @@
"""passbook Worker management command""" """passbook Worker management command"""
from logging import getLogger
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import autoreload from django.utils import autoreload
from structlog import get_logger
from passbook.root.celery import CELERY_APP from passbook.root.celery import CELERY_APP
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class Command(BaseCommand): class Command(BaseCommand):

View File

@ -1,10 +1,9 @@
"""passbook core models""" """passbook core models"""
import re import re
from datetime import timedelta from datetime import timedelta
from logging import getLogger
from random import SystemRandom from random import SystemRandom
from time import sleep from time import sleep
from typing import Tuple, Union from typing import List
from uuid import uuid4 from uuid import uuid4
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
@ -14,17 +13,33 @@ 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 as _
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from structlog import get_logger
from passbook.policy.exceptions import PolicyException
from passbook.core.signals import password_changed from passbook.core.signals import password_changed
from passbook.lib.models import CreatedUpdatedModel, UUIDModel from passbook.lib.models import CreatedUpdatedModel, UUIDModel
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
def default_nonce_duration(): def default_nonce_duration():
"""Default duration a Nonce is valid""" """Default duration a Nonce is valid"""
return now() + timedelta(hours=4) return now() + timedelta(hours=4)
class PolicyResult:
"""Small data-class to hold policy results"""
passing: bool = False
messages: List[str] = []
def __init__(self, passing: bool, *messages: str):
self.passing = passing
self.messages = messages
def __str__(self):
return f"<PolicyResult passing={self.passing}>"
class Group(UUIDModel): class Group(UUIDModel):
"""Custom Group model which supports a basic hierarchy""" """Custom Group model which supports a basic hierarchy"""
@ -229,9 +244,9 @@ class Policy(UUIDModel, CreatedUpdatedModel):
return self.name return self.name
return "%s action %s" % (self.name, self.action) return "%s action %s" % (self.name, self.action)
def passes(self, user: User) -> Union[bool, Tuple[bool, str]]: def passes(self, user: User) -> PolicyResult:
"""Check if user instance passes this policy""" """Check if user instance passes this policy"""
raise NotImplementedError() raise PolicyException()
class FieldMatcherPolicy(Policy): class FieldMatcherPolicy(Policy):
"""Policy which checks if a field of the User model matches/doesn't match a """Policy which checks if a field of the User model matches/doesn't match a
@ -273,7 +288,7 @@ class FieldMatcherPolicy(Policy):
description = "%s: %s" % (self.name, description) description = "%s: %s" % (self.name, description)
return description return description
def passes(self, user: User) -> Union[bool, Tuple[bool, str]]: def passes(self, user: User) -> PolicyResult:
"""Check if user instance passes this role""" """Check if user instance passes this role"""
if not hasattr(user, self.user_field): if not hasattr(user, self.user_field):
raise ValueError("Field does not exist") raise ValueError("Field does not exist")
@ -294,7 +309,7 @@ class FieldMatcherPolicy(Policy):
passes = user_field_value == self.value passes = user_field_value == self.value
LOGGER.debug("User got '%r'", passes) LOGGER.debug("User got '%r'", passes)
return passes return PolicyResult(passes)
class Meta: class Meta:
@ -313,10 +328,10 @@ class PasswordPolicy(Policy):
form = 'passbook.core.forms.policies.PasswordPolicyForm' form = 'passbook.core.forms.policies.PasswordPolicyForm'
def passes(self, user: User) -> Union[bool, Tuple[bool, str]]: def passes(self, user: User) -> PolicyResult:
# Only check if password is being set # Only check if password is being set
if not hasattr(user, '__password__'): if not hasattr(user, '__password__'):
return True return PolicyResult(True)
password = getattr(user, '__password__') password = getattr(user, '__password__')
filter_regex = r'' filter_regex = r''
@ -329,8 +344,8 @@ class PasswordPolicy(Policy):
result = bool(re.compile(filter_regex).match(password)) result = bool(re.compile(filter_regex).match(password))
LOGGER.debug("User got %r", result) LOGGER.debug("User got %r", result)
if not result: if not result:
return result, self.error_message return PolicyResult(result, self.error_message)
return result return PolicyResult(result)
class Meta: class Meta:
@ -364,7 +379,7 @@ class WebhookPolicy(Policy):
form = 'passbook.core.forms.policies.WebhookPolicyForm' form = 'passbook.core.forms.policies.WebhookPolicyForm'
def passes(self, user: User): def passes(self, user: User) -> PolicyResult:
"""Call webhook asynchronously and report back""" """Call webhook asynchronously and report back"""
raise NotImplementedError() raise NotImplementedError()
@ -383,12 +398,12 @@ class DebugPolicy(Policy):
form = 'passbook.core.forms.policies.DebugPolicyForm' form = 'passbook.core.forms.policies.DebugPolicyForm'
def passes(self, user: User): def passes(self, user: User) -> PolicyResult:
"""Wait random time then return result""" """Wait random time then return result"""
wait = SystemRandom().randrange(self.wait_min, self.wait_max) wait = SystemRandom().randrange(self.wait_min, self.wait_max)
LOGGER.debug("Policy '%s' waiting for %ds", self.name, wait) LOGGER.debug("Policy '%s' waiting for %ds", self.name, wait)
sleep(wait) sleep(wait)
return self.result, 'Debugging' return PolicyResult(self.result, 'Debugging')
class Meta: class Meta:
@ -402,8 +417,8 @@ class GroupMembershipPolicy(Policy):
form = 'passbook.core.forms.policies.GroupMembershipPolicyForm' form = 'passbook.core.forms.policies.GroupMembershipPolicyForm'
def passes(self, user: User) -> Union[bool, Tuple[bool, str]]: def passes(self, user: User) -> PolicyResult:
return self.group.user_set.filter(pk=user.pk).exists() return PolicyResult(self.group.user_set.filter(pk=user.pk).exists())
class Meta: class Meta:
@ -415,10 +430,10 @@ class SSOLoginPolicy(Policy):
form = 'passbook.core.forms.policies.SSOLoginPolicyForm' form = 'passbook.core.forms.policies.SSOLoginPolicyForm'
def passes(self, user): def passes(self, user) -> PolicyResult:
"""Check if user instance passes this policy""" """Check if user instance passes this policy"""
from passbook.core.auth.view import AuthenticationView from passbook.core.auth.view import AuthenticationView
return user.session.get(AuthenticationView.SESSION_IS_SSO_LOGIN, False), "" return PolicyResult(user.session.get(AuthenticationView.SESSION_IS_SSO_LOGIN, False))
class Meta: class Meta:

View File

@ -39,7 +39,7 @@ http {
gzip on; gzip on;
gzip_types application/javascript image/* text/css; gzip_types application/javascript image/* text/css;
gunzip on; gunzip on;
add_header X-passbook-Version 0.2.8-beta; add_header X-passbook-Version 0.4.1-beta;
add_header Vary X-passbook-Version; add_header Vary X-passbook-Version;
root /static/; root /static/;

View File

@ -1,134 +0,0 @@
"""passbook core policy engine"""
from logging import getLogger
from amqp.exceptions import UnexpectedFrame
from celery import group
from celery.exceptions import TimeoutError as CeleryTimeoutError
from django.core.cache import cache
from ipware import get_client_ip
from passbook.core.models import Policy, User
from passbook.root.celery import CELERY_APP
LOGGER = getLogger(__name__)
def _cache_key(policy, user):
return "policy_%s#%s" % (policy.uuid, user.pk)
@CELERY_APP.task()
def _policy_engine_task(user_pk, policy_pk, **kwargs):
"""Task wrapper to run policy checking"""
if not user_pk:
raise ValueError()
policy_obj = Policy.objects.filter(pk=policy_pk).select_subclasses().first()
user_obj = User.objects.get(pk=user_pk)
for key, value in kwargs.items():
setattr(user_obj, key, value)
LOGGER.debug("Running policy `%s`#%s for user %s...", policy_obj.name,
policy_obj.pk.hex, user_obj)
policy_result = policy_obj.passes(user_obj)
# Handle policy result correctly if result, message or just result
message = None
if isinstance(policy_result, (tuple, list)):
policy_result, message = policy_result
# Invert result if policy.negate is set
if policy_obj.negate:
policy_result = not policy_result
LOGGER.debug("Policy %r#%s got %s", policy_obj.name, policy_obj.pk.hex, policy_result)
cache_key = _cache_key(policy_obj, user_obj)
cache.set(cache_key, (policy_obj.action, policy_result, message))
LOGGER.debug("Cached entry as %s", cache_key)
return policy_obj.action, policy_result, message
class PolicyEngine:
"""Orchestrate policy checking, launch tasks and return result"""
__group = None
__cached = None
policies = None
__get_timeout = 0
__request = None
__user = None
def __init__(self, policies):
self.policies = policies
self.__request = None
self.__user = None
def for_user(self, user):
"""Check policies for user"""
self.__user = user
return self
def with_request(self, request):
"""Set request"""
self.__request = request
return self
def build(self):
"""Build task group"""
if not self.__user:
raise ValueError("User not set.")
signatures = []
cached_policies = []
kwargs = {
'__password__': getattr(self.__user, '__password__', None),
'session': dict(getattr(self.__request, 'session', {}).items()),
}
if self.__request:
kwargs['remote_ip'], _ = get_client_ip(self.__request)
if not kwargs['remote_ip']:
kwargs['remote_ip'] = '255.255.255.255'
for policy in self.policies:
cached_policy = cache.get(_cache_key(policy, self.__user), None)
if cached_policy:
LOGGER.debug("Taking result from cache for %s", policy.pk.hex)
cached_policies.append(cached_policy)
else:
LOGGER.debug("Evaluating policy %s", policy.pk.hex)
signatures.append(_policy_engine_task.signature(
args=(self.__user.pk, policy.pk.hex),
kwargs=kwargs,
time_limit=policy.timeout))
self.__get_timeout += policy.timeout
LOGGER.debug("Set total policy timeout to %r", self.__get_timeout)
# If all policies are cached, we have an empty list here.
if signatures:
self.__group = group(signatures)()
self.__get_timeout += 3
self.__get_timeout = (self.__get_timeout / len(self.policies)) * 1.5
self.__cached = cached_policies
return self
@property
def result(self):
"""Get policy-checking result"""
messages = []
result = []
try:
if self.__group:
# ValueError can be thrown from _policy_engine_task when user is None
result += self.__group.get(timeout=self.__get_timeout)
result += self.__cached
except ValueError as exc:
# ValueError can be thrown from _policy_engine_task when user is None
return False, [str(exc)]
except UnexpectedFrame as exc:
return False, [str(exc)]
except CeleryTimeoutError as exc:
return False, [str(exc)]
for policy_action, policy_result, policy_message in result:
passing = (policy_action == Policy.ACTION_ALLOW and policy_result) or \
(policy_action == Policy.ACTION_DENY and not policy_result)
LOGGER.debug('Action=%s, Result=%r => %r', policy_action, policy_result, passing)
if policy_message:
messages.append(policy_message)
if not passing:
return False, messages
return True, messages
@property
def passing(self):
"""Only get true/false if user passes"""
return self.result[0]

View File

@ -1,14 +1,13 @@
"""passbook core signals""" """passbook core signals"""
from logging import getLogger
from django.core.cache import cache from django.core.cache import cache
from django.core.signals import Signal from django.core.signals import Signal
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from structlog import get_logger
from passbook.core.exceptions import PasswordPolicyInvalid from passbook.core.exceptions import PasswordPolicyInvalid
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
user_signed_up = Signal(providing_args=['request', 'user']) user_signed_up = Signal(providing_args=['request', 'user'])
invitation_created = Signal(providing_args=['request', 'invitation']) invitation_created = Signal(providing_args=['request', 'invitation'])
@ -20,7 +19,7 @@ password_changed = Signal(providing_args=['user', 'password'])
def password_policy_checker(sender, password, **kwargs): def password_policy_checker(sender, password, **kwargs):
"""Run password through all password policies which are applied to the user""" """Run password through all password policies which are applied to the user"""
from passbook.core.models import PasswordFactor from passbook.core.models import PasswordFactor
from passbook.core.policies import PolicyEngine from passbook.policy.engine import PolicyEngine
setattr(sender, '__password__', password) setattr(sender, '__password__', password)
_all_factors = PasswordFactor.objects.filter(enabled=True).order_by('order') _all_factors = PasswordFactor.objects.filter(enabled=True).order_by('order')
for factor in _all_factors: for factor in _all_factors:

View File

@ -1,16 +1,16 @@
"""passbook core tasks""" """passbook core tasks"""
from datetime import datetime from datetime import datetime
from logging import getLogger
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags from django.utils.html import strip_tags
from structlog import get_logger
from passbook.core.models import Nonce from passbook.core.models import Nonce
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
from passbook.root.celery import CELERY_APP from passbook.root.celery import CELERY_APP
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
@CELERY_APP.task() @CELERY_APP.task()
def send_email(to_address, subject, template, context): def send_email(to_address, subject, template, context):

View File

@ -3,7 +3,7 @@
from django import template from django import template
from passbook.core.models import Factor, Source from passbook.core.models import Factor, Source
from passbook.core.policies import PolicyEngine from passbook.policy.engine import PolicyEngine
register = template.Library() register = template.Library()

View File

@ -77,7 +77,7 @@ class TestFactorAuthentication(TestCase):
request.user = AnonymousUser() request.user = AnonymousUser()
middleware = SessionMiddleware() middleware = SessionMiddleware()
middleware.process_request(request) middleware.process_request(request)
request.session.save() request.session.save() # pylint: disable=no-member
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
response = AuthenticationView.as_view()(request) response = AuthenticationView.as_view()(request)
@ -93,7 +93,7 @@ class TestFactorAuthentication(TestCase):
request.user = AnonymousUser() request.user = AnonymousUser()
middleware = SessionMiddleware() middleware = SessionMiddleware()
middleware.process_request(request) middleware.process_request(request)
request.session.save() request.session.save() # pylint: disable=no-member
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
response = AuthenticationView.as_view()(request) response = AuthenticationView.as_view()(request)
@ -111,7 +111,7 @@ class TestFactorAuthentication(TestCase):
request.user = AnonymousUser() request.user = AnonymousUser()
middleware = SessionMiddleware() middleware = SessionMiddleware()
middleware.process_request(request) middleware.process_request(request)
request.session.save() request.session.save() # pylint: disable=no-member
request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk request.session[AuthenticationView.SESSION_PENDING_USER] = self.user.pk
response = AuthenticationView.as_view()(request) response = AuthenticationView.as_view()(request)
@ -127,7 +127,7 @@ class TestFactorAuthentication(TestCase):
middleware.process_request(request) middleware.process_request(request)
for key, value in session_copy: for key, value in session_copy:
request.session[key] = value request.session[key] = value
request.session.save() request.session.save() # pylint: disable=no-member
response = AuthenticationView.as_view()(request) response = AuthenticationView.as_view()(request)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('passbook_core:overview')) self.assertEqual(response.url, reverse('passbook_core:overview'))

View File

@ -1,12 +1,11 @@
"""passbook URL Configuration""" """passbook URL Configuration"""
from logging import getLogger
from django.urls import path from django.urls import path
from structlog import get_logger
from passbook.core.auth import view from passbook.core.auth import view
from passbook.core.views import authentication, overview, user from passbook.core.views import authentication, overview, user
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
urlpatterns = [ urlpatterns = [
# Authentication views # Authentication views

View File

@ -1,13 +1,12 @@
"""passbook access helper classes""" """passbook access helper classes"""
from logging import getLogger
from django.contrib import messages from django.contrib import messages
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Application from passbook.core.models import Application
from passbook.core.policies import PolicyEngine from passbook.policy.engine import PolicyEngine
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class AccessMixin: class AccessMixin:
"""Mixin class for usage in Authorization views. """Mixin class for usage in Authorization views.

View File

@ -1,5 +1,4 @@
"""passbook core authentication views""" """passbook core authentication views"""
from logging import getLogger
from typing import Dict from typing import Dict
from django.contrib import messages from django.contrib import messages
@ -11,6 +10,7 @@ from django.shortcuts import get_object_or_404, redirect, reverse
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.generic import FormView from django.views.generic import FormView
from structlog import get_logger
from passbook.core.auth.view import AuthenticationView, _redirect_with_qs from passbook.core.auth.view import AuthenticationView, _redirect_with_qs
from passbook.core.exceptions import PasswordPolicyInvalid from passbook.core.exceptions import PasswordPolicyInvalid
@ -20,7 +20,7 @@ from passbook.core.signals import invitation_used, user_signed_up
from passbook.core.tasks import send_email from passbook.core.tasks import send_email
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class LoginView(UserPassesTestMixin, FormView): class LoginView(UserPassesTestMixin, FormView):
@ -40,7 +40,7 @@ class LoginView(UserPassesTestMixin, FormView):
return redirect(reverse('passbook_core:overview')) return redirect(reverse('passbook_core:overview'))
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.get('passbook') kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True kwargs['is_login'] = True
kwargs['title'] = _('Log in to your account') kwargs['title'] = _('Log in to your account')
kwargs['primary_action'] = _('Log in') kwargs['primary_action'] = _('Log in')
@ -135,7 +135,7 @@ class SignUpView(UserPassesTestMixin, FormView):
return super().get_initial() return super().get_initial()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.get('passbook') kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True kwargs['is_login'] = True
kwargs['title'] = _('Sign Up') kwargs['title'] = _('Sign Up')
kwargs['primary_action'] = _('Sign up') kwargs['primary_action'] = _('Sign up')

View File

@ -4,7 +4,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView from django.views.generic import TemplateView
from passbook.core.models import Application from passbook.core.models import Application
from passbook.core.policies import PolicyEngine from passbook.policy.engine import PolicyEngine
class OverviewView(LoginRequiredMixin, TemplateView): class OverviewView(LoginRequiredMixin, TemplateView):

View File

@ -66,7 +66,7 @@ class UserChangePasswordView(LoginRequiredMixin, FormView):
return redirect('passbook_core:overview') return redirect('passbook_core:overview')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.get('passbook') kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True kwargs['is_login'] = True
kwargs['title'] = _('Change Password') kwargs['title'] = _('Change Password')
kwargs['primary_action'] = _('Change') kwargs['primary_action'] = _('Change')

View File

@ -1,14 +1,14 @@
"""passbook HIBP Models""" """passbook HIBP Models"""
from hashlib import sha1 from hashlib import sha1
from logging import getLogger
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 requests import get from requests import get
from structlog import get_logger
from passbook.core.models import Policy, User from passbook.core.models import Policy, PolicyResult, User
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class HaveIBeenPwendPolicy(Policy): class HaveIBeenPwendPolicy(Policy):
"""Check if password is on HaveIBeenPwned's list by upload the first """Check if password is on HaveIBeenPwned's list by upload the first
@ -18,13 +18,13 @@ class HaveIBeenPwendPolicy(Policy):
form = 'passbook.hibp_policy.forms.HaveIBeenPwnedPolicyForm' form = 'passbook.hibp_policy.forms.HaveIBeenPwnedPolicyForm'
def passes(self, user: User) -> bool: def passes(self, user: User) -> PolicyResult:
"""Check if password is in HIBP DB. Hashes given Password with SHA1, uses the first 5 """Check if password is in HIBP DB. Hashes given Password with SHA1, uses the first 5
characters of Password in request and checks if full hash is in response. Returns 0 characters of Password in request and checks if full hash is in response. Returns 0
if Password is not in result otherwise the count of how many times it was used.""" if Password is not in result otherwise the count of how many times it was used."""
# Only check if password is being set # Only check if password is being set
if not hasattr(user, '__password__'): if not hasattr(user, '__password__'):
return True return PolicyResult(True)
password = getattr(user, '__password__') password = getattr(user, '__password__')
pw_hash = sha1(password.encode('utf-8')).hexdigest() # nosec pw_hash = sha1(password.encode('utf-8')).hexdigest() # nosec
url = 'https://api.pwnedpasswords.com/range/%s' % pw_hash[:5] url = 'https://api.pwnedpasswords.com/range/%s' % pw_hash[:5]
@ -36,8 +36,9 @@ class HaveIBeenPwendPolicy(Policy):
final_count = int(count) final_count = int(count)
LOGGER.debug("Got count %d for hash %s", final_count, pw_hash[:5]) LOGGER.debug("Got count %d for hash %s", final_count, pw_hash[:5])
if final_count > self.allowed_count: if final_count > self.allowed_count:
return False, _("Password exists on %(count)d online lists." % {'count': final_count}) message = _("Password exists on %(count)d online lists." % {'count': final_count})
return True return PolicyResult(False, message)
return PolicyResult(True)
class Meta: class Meta:

View File

@ -1,12 +1,11 @@
"""passbook LDAP Authentication Backend""" """passbook LDAP Authentication Backend"""
from logging import getLogger
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
from structlog import get_logger
from passbook.ldap.ldap_connector import LDAPConnector from passbook.ldap.ldap_connector import LDAPConnector
from passbook.ldap.models import LDAPSource from passbook.ldap.models import LDAPSource
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class LDAPBackend(ModelBackend): class LDAPBackend(ModelBackend):

View File

@ -1,15 +1,15 @@
"""Wrapper for ldap3 to easily manage user""" """Wrapper for ldap3 to easily manage user"""
from logging import getLogger
from time import time from time import time
import ldap3 import ldap3
import ldap3.core.exceptions import ldap3.core.exceptions
from structlog import get_logger
from passbook.core.models import User from passbook.core.models import User
from passbook.ldap.models import LDAPSource from passbook.ldap.models import LDAPSource
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
USERNAME_FIELD = CONFIG.y('ldap.username_field', 'sAMAccountName') USERNAME_FIELD = CONFIG.y('ldap.username_field', 'sAMAccountName')
LOGIN_FIELD = CONFIG.y('ldap.login_field', 'userPrincipalName') LOGIN_FIELD = CONFIG.y('ldap.login_field', 'userPrincipalName')
@ -166,7 +166,7 @@ class LDAPConnector:
if not self._source.enabled: if not self._source.enabled:
return None return None
# FIXME: Adapt user_uid # FIXME: Adapt user_uid
# email = filters.pop(CONFIG.get('passport').get('ldap').get, '') # email = filters.pop(CONFIG.y('passport').get('ldap').get, '')
email = filters.pop('email') email = filters.pop('email')
user_dn = self.lookup(self.generate_filter(**{LOGIN_FIELD: email})) user_dn = self.lookup(self.generate_filter(**{LOGIN_FIELD: email}))
if not user_dn: if not user_dn:

View File

@ -1,37 +1,40 @@
"""passbook lib config loader""" """passbook core config loader"""
import os import os
from collections.abc import Mapping from collections.abc import Mapping
from contextlib import contextmanager from contextlib import contextmanager
from glob import glob from glob import glob
from logging import getLogger
from typing import Any from typing import Any
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 django.utils.autoreload import autoreload_started
from structlog import get_logger
SEARCH_PATHS = [ SEARCH_PATHS = [
'passbook/lib/default.yml', 'passbook/lib/default.yml',
'/etc/passbook/config.yml', '/etc/passbook/config.yml',
'.', '',
] + glob('/etc/passbook/config.d/*.yml', recursive=True) ] + glob('/etc/passbook/config.d/*.yml', recursive=True)
LOGGER = getLogger(__name__) LOGGER = get_logger()
ENVIRONMENT = os.getenv('PASSBOOK_ENV', 'local') ENV_PREFIX = 'PASSBOOK'
ENVIRONMENT = os.getenv(f'{ENV_PREFIX}_ENV', 'local')
class ConfigLoader: class ConfigLoader:
"""Search through SEARCH_PATHS and load configuration""" """Search through SEARCH_PATHS and load configuration. Environment variables starting with
`ENV_PREFIX` are also applied.
A variable like PASSBOOK_POSTGRESQL__HOST would translate to postgresql.host"""
loaded_file = [] loaded_file = []
__config = {} __config = {}
__context_default = None
__sub_dicts = [] __sub_dicts = []
def __init__(self): def __init__(self):
super().__init__() super().__init__()
base_dir = os.path.realpath(os.path.join( base_dir = os.path.realpath(os.path.join(os.path.dirname(__file__), '../..'))
os.path.dirname(__file__), '../..'))
for path in SEARCH_PATHS: for path in SEARCH_PATHS:
# Check if path is relative, and if so join with base_dir # Check if path is relative, and if so join with base_dir
if not os.path.isabs(path): if not os.path.isabs(path):
@ -47,15 +50,7 @@ class ConfigLoader:
if os.path.isfile(env_file) and os.path.exists(env_file): if os.path.isfile(env_file) and os.path.exists(env_file):
# Update config with env file # Update config with env file
self.update_from_file(env_file) self.update_from_file(env_file)
self.handle_secret_key() self.update_from_env()
def handle_secret_key(self):
"""Handle `secret_key_file`"""
if 'secret_key_file' in self.__config:
secret_key_file = self.__config.get('secret_key_file')
if os.path.isfile(secret_key_file) and os.path.exists(secret_key_file):
with open(secret_key_file) as file:
self.__config['secret_key'] = file.read().replace('\n', '')
def update(self, root, updatee): def update(self, root, updatee):
"""Recursively update dictionary""" """Recursively update dictionary"""
@ -63,16 +58,25 @@ class ConfigLoader:
if isinstance(value, Mapping): if isinstance(value, Mapping):
root[key] = self.update(root.get(key, {}), value) root[key] = self.update(root.get(key, {}), value)
else: else:
if isinstance(value, str):
value = self.parse_uri(value)
root[key] = value root[key] = value
return root return root
def parse_uri(self, value):
"""Parse string values which start with a URI"""
url = urlparse(value)
if url.scheme == 'env':
value = os.getenv(url.netloc, url.query)
return value
def update_from_file(self, path: str): def update_from_file(self, path: str):
"""Update config from file contents""" """Update config from file contents"""
try: try:
with open(path) as file: with open(path) as file:
try: try:
self.update(self.__config, yaml.safe_load(file)) self.update(self.__config, yaml.safe_load(file))
LOGGER.debug("Loaded %s", path) LOGGER.debug("Loaded config", file=path)
self.loaded_file.append(path) self.loaded_file.append(path)
except yaml.YAMLError as exc: except yaml.YAMLError as exc:
raise ImproperlyConfigured from exc raise ImproperlyConfigured from exc
@ -83,12 +87,26 @@ class ConfigLoader:
"""Update config from dict""" """Update config from dict"""
self.__config.update(update) self.__config.update(update)
@contextmanager def update_from_env(self):
def default(self, value: Any): """Check environment variables"""
"""Contextmanage that sets default""" outer = {}
self.__context_default = value idx = 0
yield for key, value in os.environ.items():
self.__context_default = None if not key.startswith(ENV_PREFIX):
continue
relative_key = key.replace(f"{ENV_PREFIX}_", '').replace('__', '.').lower()
# Recursively convert path from a.b.c into outer[a][b][c]
current_obj = outer
dot_parts = relative_key.split('.')
for dot_part in dot_parts[:-1]:
if dot_part not in current_obj:
current_obj[dot_part] = {}
current_obj = current_obj[dot_part]
current_obj[dot_parts[-1]] = value
idx += 1
if idx > 0:
LOGGER.debug("Loaded environment variables", count=idx)
self.update(self.__config, outer)
@contextmanager @contextmanager
# pylint: disable=invalid-name # pylint: disable=invalid-name
@ -98,15 +116,6 @@ class ConfigLoader:
yield yield
self.__sub_dicts.pop() self.__sub_dicts.pop()
def get(self, key: str, default=None) -> Any:
"""Get value from loaded config file"""
if default is None:
default = self.__context_default
config_copy = self.raw
for sub in self.__sub_dicts:
config_copy = config_copy.get(sub, None)
return config_copy.get(key, default)
@property @property
def raw(self) -> dict: def raw(self) -> dict:
"""Get raw config dictionary""" """Get raw config dictionary"""
@ -115,8 +124,6 @@ class ConfigLoader:
# pylint: disable=invalid-name # pylint: disable=invalid-name
def y(self, path: str, default=None, sep='.') -> Any: def y(self, path: str, default=None, sep='.') -> Any:
"""Access attribute by using yaml path""" """Access attribute by using yaml path"""
if default is None:
default = self.__context_default
# Walk sub_dicts before parsing path # Walk sub_dicts before parsing path
root = self.raw root = self.raw
for sub in self.__sub_dicts: for sub in self.__sub_dicts:
@ -129,11 +136,14 @@ class ConfigLoader:
return default return default
return root return root
def y_bool(self, path: str, default=False) -> bool:
"""Wrapper for y that converts value into boolean"""
return str(self.y(path, default)).lower() == 'true'
CONFIG = ConfigLoader() CONFIG = ConfigLoader()
# pylint: disable=unused-argument def signal_handler(sender, **_):
def signal_handler(sender, **kwargs):
"""Add all loaded config files to autoreload watcher""" """Add all loaded config files to autoreload watcher"""
for path in CONFIG.loaded_file: for path in CONFIG.loaded_file:
sender.watch_file(path) sender.watch_file(path)

View File

@ -32,8 +32,7 @@ def reauth_required(view_function):
if RE_AUTH_KEY not in request.session: if RE_AUTH_KEY not in request.session:
# Timestamp not in session, force user to reauth # Timestamp not in session, force user to reauth
return redirect(reverse('account-reauth') + '?' + return redirect(reverse('account-reauth') + '?' + urlencode({'next': request.path}))
urlencode({'next': request.path}))
if RE_AUTH_KEY in request.session and \ if RE_AUTH_KEY in request.session and \
request.session[RE_AUTH_KEY] >= (now - RE_AUTH_MARGAIN) and \ request.session[RE_AUTH_KEY] >= (now - RE_AUTH_MARGAIN) and \

View File

@ -1,43 +1,20 @@
# This is the default configuration file # This is the default configuration file
databases: postgresql:
default: host: localhost
engine: 'django.db.backends.postgresql'
name: passbook name: passbook
user: passbook user: passbook
password: 'EK-5jnKfjrGRm<77' password: 'env://POSTGRES_PASSWORD'
redis:
host: localhost host: localhost
log:
level:
console: DEBUG
file: DEBUG
file: /dev/null
syslog:
host: 127.0.0.1
port: 514
email:
host: localhost
port: 25
user: ''
password: '' password: ''
use_tls: false cache_db: 0
use_ssl: false message_queue_db: 1
from: passbook <passbook@domain.tld>
web:
server.socket_host: 0.0.0.0
server.socket_port: 8000
server.thread_pool: 20
log.screen: false
log.access_file: ''
log.error_file: ''
debug: false debug: false
secure_proxy_header:
HTTP_X_FORWARDED_PROTO: https
rabbitmq: guest:guest@localhost/passbook
redis: localhost/0
# Error reporting, sends stacktrace to sentry.services.beryju.org # Error reporting, sends stacktrace to sentry.services.beryju.org
error_report_enabled: true error_report_enabled: true
secret_key: 9$@r!d^1^jrn#fk#1#@ks#9&i$^s#1)_13%$rwjrhd=e8jfi_s
domains: domains:
- passbook.local - passbook.local

View File

@ -1,7 +1,7 @@
"""passbook sentry integration""" """passbook sentry integration"""
from logging import getLogger from structlog import get_logger
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
def before_send(event, hint): def before_send(event, hint):

View File

@ -1,12 +1,11 @@
"""passbook lib navbar Templatetag""" """passbook lib navbar Templatetag"""
from logging import getLogger
from django import template from django import template
from django.urls import reverse from django.urls import reverse
from structlog import get_logger
register = template.Library() register = template.Library()
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)

View File

@ -1,10 +1,9 @@
"""passbook Core Reflection templatetags Templatetag""" """passbook Core Reflection templatetags Templatetag"""
from logging import getLogger
from django import template from django import template
from structlog import get_logger
register = template.Library() register = template.Library()
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
def get_key_unique(context): def get_key_unique(context):

View File

@ -2,9 +2,9 @@
from django.template import Context, Template, loader from django.template import Context, Template, loader
def render_from_string(template: str, ctx: Context) -> str: def render_from_string(tmpl: str, ctx: Context) -> str:
"""Render template from string to string""" """Render template from string to string"""
template = Template(template) template = Template(tmpl)
return template.render(ctx) return template.render(ctx)

View File

@ -1,12 +1,12 @@
"""passbook oauth_client config""" """passbook oauth_client config"""
from importlib import import_module from importlib import import_module
from logging import getLogger
from django.apps import AppConfig from django.apps import AppConfig
from structlog import get_logger
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class PassbookOAuthClientConfig(AppConfig): class PassbookOAuthClientConfig(AppConfig):
"""passbook oauth_client config""" """passbook oauth_client config"""

View File

@ -1,7 +1,6 @@
"""OAuth Clients""" """OAuth Clients"""
import json import json
from logging import getLogger
from urllib.parse import parse_qs, urlencode from urllib.parse import parse_qs, urlencode
from django.conf import settings from django.conf import settings
@ -10,8 +9,9 @@ from django.utils.encoding import force_text
from requests import Session from requests import Session
from requests.exceptions import RequestException from requests.exceptions import RequestException
from requests_oauthlib import OAuth1 from requests_oauthlib import OAuth1
from structlog import get_logger
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class BaseOAuthClient: class BaseOAuthClient:
@ -120,9 +120,9 @@ class OAuthClient(BaseOAuthClient):
"Parse token and secret from raw token response." "Parse token and secret from raw token response."
if raw_token is None: if raw_token is None:
return (None, None) return (None, None)
qs = parse_qs(raw_token) query_string = parse_qs(raw_token)
token = qs.get('oauth_token', [None])[0] token = query_string.get('oauth_token', [None])[0]
secret = qs.get('oauth_token_secret', [None])[0] secret = query_string.get('oauth_token_secret', [None])[0]
return (token, secret) return (token, secret)
def request(self, method, url, **kwargs): def request(self, method, url, **kwargs):
@ -217,8 +217,7 @@ class OAuth2Client(BaseOAuthClient):
try: try:
token_data = json.loads(raw_token) token_data = json.loads(raw_token)
except ValueError: except ValueError:
qs = parse_qs(raw_token) token = parse_qs(raw_token).get('access_token', [None])[0]
token = qs.get('access_token', [None])[0]
else: else:
token = token_data.get('access_token', None) token = token_data.get('access_token', None)
return (token, None) return (token, None)

View File

@ -1,16 +1,16 @@
"""AzureAD OAuth2 Views""" """AzureAD OAuth2 Views"""
import json import json
import uuid import uuid
from logging import getLogger
from requests.exceptions import RequestException from requests.exceptions import RequestException
from structlog import get_logger
from passbook.oauth_client.clients import OAuth2Client from passbook.oauth_client.clients import OAuth2Client
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
from passbook.oauth_client.utils import user_get_or_create from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback from passbook.oauth_client.views.core import OAuthCallback
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class AzureADOAuth2Client(OAuth2Client): class AzureADOAuth2Client(OAuth2Client):

View File

@ -1,15 +1,15 @@
"""Discord OAuth Views""" """Discord OAuth Views"""
import json import json
from logging import getLogger
from requests.exceptions import RequestException from requests.exceptions import RequestException
from structlog import get_logger
from passbook.oauth_client.clients import OAuth2Client from passbook.oauth_client.clients import OAuth2Client
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
from passbook.oauth_client.utils import user_get_or_create from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
@MANAGER.source(kind=RequestKind.redirect, name='Discord') @MANAGER.source(kind=RequestKind.redirect, name='Discord')

View File

@ -1,10 +1,11 @@
"""Source type manager""" """Source type manager"""
from enum import Enum from enum import Enum
from logging import getLogger
from structlog import get_logger
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class RequestKind(Enum): class RequestKind(Enum):
"""Enum of OAuth Request types""" """Enum of OAuth Request types"""

View File

@ -1,16 +1,16 @@
"""Reddit OAuth Views""" """Reddit OAuth Views"""
import json import json
from logging import getLogger
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from requests.exceptions import RequestException from requests.exceptions import RequestException
from structlog import get_logger
from passbook.oauth_client.clients import OAuth2Client from passbook.oauth_client.clients import OAuth2Client
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
from passbook.oauth_client.utils import user_get_or_create from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect from passbook.oauth_client.views.core import OAuthCallback, OAuthRedirect
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
@MANAGER.source(kind=RequestKind.redirect, name='reddit') @MANAGER.source(kind=RequestKind.redirect, name='reddit')

View File

@ -1,16 +1,16 @@
"""Supervisr OAuth2 Views""" """Supervisr OAuth2 Views"""
import json import json
from logging import getLogger
from requests.exceptions import RequestException from requests.exceptions import RequestException
from structlog import get_logger
from passbook.oauth_client.clients import OAuth2Client from passbook.oauth_client.clients import OAuth2Client
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
from passbook.oauth_client.utils import user_get_or_create from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback from passbook.oauth_client.views.core import OAuthCallback
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class SupervisrOAuth2Client(OAuth2Client): class SupervisrOAuth2Client(OAuth2Client):

View File

@ -1,15 +1,14 @@
"""Twitter OAuth Views""" """Twitter OAuth Views"""
from logging import getLogger
from requests.exceptions import RequestException from requests.exceptions import RequestException
from structlog import get_logger
from passbook.oauth_client.clients import OAuthClient from passbook.oauth_client.clients import OAuthClient
from passbook.oauth_client.source_types.manager import MANAGER, RequestKind from passbook.oauth_client.source_types.manager import MANAGER, RequestKind
from passbook.oauth_client.utils import user_get_or_create from passbook.oauth_client.utils import user_get_or_create
from passbook.oauth_client.views.core import OAuthCallback from passbook.oauth_client.views.core import OAuthCallback
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class TwitterOAuthClient(OAuthClient): class TwitterOAuthClient(OAuthClient):

View File

@ -1,7 +1,5 @@
"""Core OAauth Views""" """Core OAauth Views"""
from logging import getLogger
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
@ -11,13 +9,14 @@ from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import RedirectView, View from django.views.generic import RedirectView, View
from structlog import get_logger
from passbook.core.auth.view import AuthenticationView, _redirect_with_qs from passbook.core.auth.view import AuthenticationView, _redirect_with_qs
from passbook.lib.utils.reflection import app from passbook.lib.utils.reflection import app
from passbook.oauth_client.clients import get_client from passbook.oauth_client.clients import get_client
from passbook.oauth_client.models import OAuthSource, UserOAuthSourceConnection from passbook.oauth_client.models import OAuthSource, UserOAuthSourceConnection
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods

View File

@ -1,5 +1,4 @@
"""passbook OAuth2 Views""" """passbook OAuth2 Views"""
from logging import getLogger
from urllib.parse import urlencode from urllib.parse import urlencode
from django.contrib import messages from django.contrib import messages
@ -7,6 +6,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404, redirect, reverse from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from oauth2_provider.views.base import AuthorizationView from oauth2_provider.views.base import AuthorizationView
from structlog import get_logger
from passbook.audit.models import AuditEntry from passbook.audit.models import AuditEntry
from passbook.core.models import Application from passbook.core.models import Application
@ -14,7 +14,7 @@ from passbook.core.views.access import AccessMixin
from passbook.core.views.utils import LoadingView, PermissionDeniedView from passbook.core.views.utils import LoadingView, PermissionDeniedView
from passbook.oauth_provider.models import OAuth2Provider from passbook.oauth_provider.models import OAuth2Provider
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class PassbookAuthorizationLoadingView(LoginRequiredMixin, LoadingView): class PassbookAuthorizationLoadingView(LoginRequiredMixin, LoadingView):

View File

@ -1,11 +1,10 @@
"""passbook auth oidc provider app config""" """passbook auth oidc provider app config"""
from logging import getLogger
from django.apps import AppConfig from django.apps import AppConfig
from django.db.utils import InternalError, OperationalError, ProgrammingError from django.db.utils import InternalError, OperationalError, ProgrammingError
from django.urls import include, path from django.urls import include, path
from structlog import get_logger
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class PassbookOIDCProviderConfig(AppConfig): class PassbookOIDCProviderConfig(AppConfig):
"""passbook auth oidc provider app config""" """passbook auth oidc provider app config"""

View File

@ -1,13 +1,12 @@
"""OIDC Permission checking""" """OIDC Permission checking"""
from logging import getLogger
from django.contrib import messages from django.contrib import messages
from django.shortcuts import redirect from django.shortcuts import redirect
from structlog import get_logger
from passbook.core.models import Application from passbook.core.models import Application
from passbook.core.policies import PolicyEngine from passbook.policy.engine import PolicyEngine
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
def check_permissions(request, user, client): def check_permissions(request, user, client):
"""Check permissions, used for """Check permissions, used for

View File

@ -1,16 +1,15 @@
"""OTP Factor logic""" """OTP Factor logic"""
from logging import getLogger
from django.contrib import messages from django.contrib import messages
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView from django.views.generic import FormView
from django_otp import match_token, user_has_device from django_otp import match_token, user_has_device
from structlog import get_logger
from passbook.core.auth.factor import AuthenticationFactor from passbook.core.auth.factor import AuthenticationFactor
from passbook.otp.forms import OTPVerifyForm from passbook.otp.forms import OTPVerifyForm
from passbook.otp.views import OTP_SETTING_UP_KEY, EnableView from passbook.otp.views import OTP_SETTING_UP_KEY, EnableView
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class OTPFactor(FormView, AuthenticationFactor): class OTPFactor(FormView, AuthenticationFactor):
"""OTP Factor View""" """OTP Factor View"""

View File

@ -1,7 +1,6 @@
"""passbook OTP Views""" """passbook OTP Views"""
from base64 import b32encode from base64 import b32encode
from binascii import unhexlify from binascii import unhexlify
from logging import getLogger
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
@ -15,6 +14,7 @@ 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
from qrcode import make from qrcode import make
from qrcode.image.svg import SvgPathImage from qrcode.image.svg import SvgPathImage
from structlog import get_logger
from passbook.lib.boilerplate import NeverCacheMixin from passbook.lib.boilerplate import NeverCacheMixin
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
@ -23,7 +23,7 @@ from passbook.otp.utils import otpauth_url
OTP_SESSION_KEY = 'passbook_otp_key' OTP_SESSION_KEY = 'passbook_otp_key'
OTP_SETTING_UP_KEY = 'passbook_otp_setup' OTP_SETTING_UP_KEY = 'passbook_otp_setup'
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class UserSettingsView(LoginRequiredMixin, TemplateView): class UserSettingsView(LoginRequiredMixin, TemplateView):
"""View for user settings to control OTP""" """View for user settings to control OTP"""
@ -75,7 +75,7 @@ class EnableView(LoginRequiredMixin, FormView):
# TODO: Check if OTP Factor exists and applies to user # TODO: Check if OTP Factor exists and applies to user
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.get('passbook') kwargs['config'] = CONFIG.y('passbook')
kwargs['is_login'] = True kwargs['is_login'] = True
kwargs['title'] = _('Configue OTP') kwargs['title'] = _('Configue OTP')
kwargs['primary_action'] = _('Setup') kwargs['primary_action'] = _('Setup')

View File

@ -1,14 +1,14 @@
"""passbook password_expiry_policy Models""" """passbook password_expiry_policy Models"""
from datetime import timedelta from datetime import timedelta
from logging import getLogger
from django.db import models from django.db import models
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 as _
from structlog import get_logger
from passbook.core.models import Policy, User from passbook.core.models import Policy, PolicyResult, User
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class PasswordExpiryPolicy(Policy): class PasswordExpiryPolicy(Policy):
@ -20,7 +20,7 @@ class PasswordExpiryPolicy(Policy):
form = 'passbook.password_expiry_policy.forms.PasswordExpiryPolicyForm' form = 'passbook.password_expiry_policy.forms.PasswordExpiryPolicyForm'
def passes(self, user: User) -> bool: def passes(self, user: User) -> PolicyResult:
"""If password change date is more than x days in the past, call set_unusable_password """If password change date is more than x days in the past, call set_unusable_password
and show a notice""" and show a notice"""
actual_days = (now() - user.password_change_date).days actual_days = (now() - user.password_change_date).days
@ -29,12 +29,13 @@ class PasswordExpiryPolicy(Policy):
if not self.deny_only: if not self.deny_only:
user.set_unusable_password() user.set_unusable_password()
user.save() user.save()
return False, _(('Password expired %(days)d days ago. ' message = _(('Password expired %(days)d days ago. '
'Please update your password.') % { 'Please update your password.') % {
'days': days_since_expiry 'days': days_since_expiry
}) })
return False, _('Password has expired.') return PolicyResult(False, message)
return True return PolicyResult(False, _('Password has expired.'))
return PolicyResult(True)
class Meta: class Meta:

View File

100
passbook/policy/engine.py Normal file
View File

@ -0,0 +1,100 @@
"""passbook policy engine"""
from multiprocessing import Pipe
from multiprocessing.connection import Connection
from typing import List, Tuple
from django.core.cache import cache
from django.http import HttpRequest
from structlog import get_logger
from passbook.core.models import Policy, PolicyResult, User
from passbook.policy.task import PolicyTask
LOGGER = get_logger()
def _cache_key(policy, user):
return "policy_%s#%s" % (policy.uuid, user.pk)
class PolicyEngine:
"""Orchestrate policy checking, launch tasks and return result"""
# __group = None
# __cached = None
policies: List[Policy] = []
__request: HttpRequest
__user: User
__proc_list: List[Tuple[Connection, PolicyTask]] = []
def __init__(self, policies, user: User = None, request: HttpRequest = None):
self.policies = policies
self.__request = request
self.__user = user
def for_user(self, user: User) -> 'PolicyEngine':
"""Check policies for user"""
self.__user = user
return self
def with_request(self, request: HttpRequest) -> 'PolicyEngine':
"""Set request"""
self.__request = request
return self
def build(self) -> 'PolicyEngine':
"""Build task group"""
if not self.__user:
raise ValueError("User not set.")
cached_policies = []
kwargs = {
'__password__': getattr(self.__user, '__password__', None),
'session': dict(getattr(self.__request, 'session', {}).items()),
'request': self.__request,
}
for policy in self.policies:
cached_policy = cache.get(_cache_key(policy, self.__user), None)
if cached_policy:
LOGGER.debug("Taking result from cache for %s", policy.pk.hex)
cached_policies.append(cached_policy)
else:
LOGGER.debug("Looking up real class of policy...")
# TODO: Rewrite this to lookup all policies at once
policy = Policy.objects.get_subclass(pk=policy.id)
LOGGER.debug("Evaluating policy %s", policy.pk.hex)
our_end, task_end = Pipe(False)
task = PolicyTask()
task.ret = task_end
task.user = self.__user
task.policy = policy
task.params = kwargs
LOGGER.debug("Starting Process %s", task.__class__.__name__)
task.start()
self.__proc_list.append((our_end, task))
# If all policies are cached, we have an empty list here.
if self.__proc_list:
for _, running_proc in self.__proc_list:
running_proc.join()
return self
@property
def result(self):
"""Get policy-checking result"""
results: List[PolicyResult] = []
messages: List[str] = []
for our_end, _ in self.__proc_list:
results.append(our_end.recv())
for policy_result in results:
# passing = (policy_action == Policy.ACTION_ALLOW and policy_result) or \
# (policy_action == Policy.ACTION_DENY and not policy_result)
LOGGER.debug('Result=%r => %r', policy_result, policy_result.passing)
if policy_result.messages:
messages += policy_result.messages
if not policy_result.passing:
return False, messages
return True, messages
@property
def passing(self):
"""Only get true/false if user passes"""
return self.result[0]

View File

@ -0,0 +1,4 @@
"""policy exceptions"""
class PolicyException(Exception):
"""Exception that should be raised during Policy Evaluation, and can be recovered from."""

44
passbook/policy/task.py Normal file
View File

@ -0,0 +1,44 @@
"""passbook policy task"""
from multiprocessing import Process
from multiprocessing.connection import Connection
from typing import Any, Dict
from structlog import get_logger
from passbook.core.models import Policy, User, PolicyResult
from passbook.policy.exceptions import PolicyException
LOGGER = get_logger(__name__)
def _cache_key(policy, user):
return "policy_%s#%s" % (policy.uuid, user.pk)
class PolicyTask(Process):
"""Evaluate a single policy within a seprate process"""
ret: Connection
user: User
policy: Policy
params: Dict[str, Any]
def run(self):
"""Task wrapper to run policy checking"""
for key, value in self.params.items():
setattr(self.user, key, value)
LOGGER.debug("Running policy `%s`#%s for user %s...", self.policy.name,
self.policy.pk.hex, self.user)
try:
policy_result = self.policy.passes(self.user)
except PolicyException as exc:
LOGGER.debug(exc)
policy_result = PolicyResult(False, str(exc))
# Invert result if policy.negate is set
if self.policy.negate:
policy_result = not policy_result
LOGGER.debug("Policy %r#%s got %s", self.policy.name, self.policy.pk.hex, policy_result)
# cache_key = _cache_key(self.policy, self.user)
# cache.set(cache_key, (self.policy.action, policy_result, message))
# LOGGER.debug("Cached entry as %s", cache_key)
self.ret.send(policy_result)
self.ret.close()

View File

@ -1,15 +1,15 @@
"""passbook core celery""" """passbook core celery"""
import logging
import os import os
from logging.config import dictConfig
from celery import Celery, signals from celery import Celery, signals
from django.conf import settings from django.conf import settings
from structlog import get_logger
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
LOGGER = logging.getLogger(__name__) LOGGER = get_logger(__name__)
CELERY_APP = Celery('passbook') CELERY_APP = Celery('passbook')
@ -19,7 +19,7 @@ CELERY_APP = Celery('passbook')
@signals.setup_logging.connect @signals.setup_logging.connect
def config_loggers(*args, **kwags): def config_loggers(*args, **kwags):
"""Apply logging settings from settings.py to celery""" """Apply logging settings from settings.py to celery"""
logging.config.dictConfig(settings.LOGGING) dictConfig(settings.LOGGING)
# pylint: disable=unused-argument # pylint: disable=unused-argument

View File

@ -11,23 +11,18 @@ https://docs.djangoproject.com/en/2.1/ref/settings/
""" """
import importlib import importlib
import logging
import os import os
import sys import sys
from celery.schedules import crontab import structlog
from django.contrib import messages
from sentry_sdk import init as sentry_init from sentry_sdk import init as sentry_init
from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from passbook import __version__ from passbook import __version__
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
from passbook.lib.sentry import before_send from passbook.lib.sentry import before_send
VERSION = __version__
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
STATIC_ROOT = BASE_DIR + '/static' STATIC_ROOT = BASE_DIR + '/static'
@ -36,12 +31,13 @@ STATIC_ROOT = BASE_DIR + '/static'
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = CONFIG.get('secret_key') SECRET_KEY = CONFIG.y('secret_key',
"9$@r!d^1^jrn#fk#1#@ks#9&i$^s#1)_13%$rwjrhd=e8jfi_s") # noqa Debug
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.get('debug') DEBUG = CONFIG.y_bool('debug')
INTERNAL_IPS = ['127.0.0.1'] INTERNAL_IPS = ['127.0.0.1']
# ALLOWED_HOSTS = CONFIG.get('domains', []) + [CONFIG.get('primary_domain')] # ALLOWED_HOSTS = CONFIG.y('domains', []) + [CONFIG.y('primary_domain')]
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
@ -53,7 +49,7 @@ AUTH_USER_MODEL = 'passbook_core.User'
CSRF_COOKIE_NAME = 'passbook_csrf' CSRF_COOKIE_NAME = 'passbook_csrf'
SESSION_COOKIE_NAME = 'passbook_session' SESSION_COOKIE_NAME = 'passbook_session'
SESSION_COOKIE_DOMAIN = CONFIG.get('primary_domain') SESSION_COOKIE_DOMAIN = CONFIG.y('primary_domain')
SESSION_ENGINE = "django.contrib.sessions.backends.cache" SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default" SESSION_CACHE_ALIAS = "default"
LANGUAGE_COOKIE_NAME = 'passbook_language' LANGUAGE_COOKIE_NAME = 'passbook_language'
@ -72,8 +68,8 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.postgres', 'django.contrib.postgres',
'rest_framework', # 'rest_framework',
'drf_yasg', # 'drf_yasg',
'passbook.core.apps.PassbookCoreConfig', 'passbook.core.apps.PassbookCoreConfig',
'passbook.admin.apps.PassbookAdminConfig', 'passbook.admin.apps.PassbookAdminConfig',
'passbook.api.apps.PassbookAPIConfig', 'passbook.api.apps.PassbookAPIConfig',
@ -93,16 +89,6 @@ INSTALLED_APPS = [
'passbook.app_gw.apps.PassbookApplicationApplicationGatewayConfig', 'passbook.app_gw.apps.PassbookApplicationApplicationGatewayConfig',
] ]
# Message Tag fix for bootstrap CSS Classes
MESSAGE_TAGS = {
messages.DEBUG: 'primary',
messages.INFO: 'info',
messages.SUCCESS: 'success',
messages.WARNING: 'warning',
messages.ERROR: 'danger',
}
REST_FRAMEWORK = { REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions, # Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users. # or allow read-only access for unauthenticated users.
@ -114,17 +100,21 @@ REST_FRAMEWORK = {
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django_redis.cache.RedisCache", "BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://%s" % CONFIG.get('redis'), "LOCATION": (f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}:6379"
f"/{CONFIG.y('redis.cache_db')}"),
"OPTIONS": { "OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient", "CLIENT_CLASS": "django_redis.client.DefaultClient",
} }
} }
} }
DJANGO_REDIS_IGNORE_EXCEPTIONS = True
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
MIDDLEWARE = [ MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'passbook.app_gw.middleware.ApplicationGatewayMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
@ -150,21 +140,19 @@ TEMPLATES = [
}, },
] ]
WSGI_APPLICATION = 'passbook.core.wsgi.application' WSGI_APPLICATION = 'passbook.root.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases # https://docs.djangoproject.com/en/2.1/ref/settings/#databases
DATABASES = {} DATABASES = {
for db_alias, db_config in CONFIG.get('databases').items(): 'default': {
DATABASES[db_alias] = { 'ENGINE': 'django.db.backends.postgresql',
'ENGINE': db_config.get('engine'), 'HOST': CONFIG.y('postgresql.host'),
'HOST': db_config.get('host'), 'NAME': CONFIG.y('postgresql.name'),
'NAME': db_config.get('name'), 'USER': CONFIG.y('postgresql.user'),
'USER': db_config.get('user'), 'PASSWORD': CONFIG.y('postgresql.password'),
'PASSWORD': db_config.get('password'), }
'OPTIONS': db_config.get('options', {}),
} }
# Password validation # Password validation
@ -203,20 +191,13 @@ USE_TZ = True
# Celery settings # Celery settings
# Add a 10 minute timeout to all Celery tasks. # Add a 10 minute timeout to all Celery tasks.
CELERY_TASK_SOFT_TIME_LIMIT = 600 CELERY_TASK_SOFT_TIME_LIMIT = 600
CELERY_TIMEZONE = TIME_ZONE
CELERY_BEAT_SCHEDULE = {} CELERY_BEAT_SCHEDULE = {}
CELERY_CREATE_MISSING_QUEUES = True CELERY_CREATE_MISSING_QUEUES = True
CELERY_TASK_DEFAULT_QUEUE = 'passbook' CELERY_TASK_DEFAULT_QUEUE = 'passbook'
CELERY_BROKER_URL = 'amqp://%s' % CONFIG.get('rabbitmq') CELERY_BROKER_URL = (f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}"
CELERY_RESULT_BACKEND = 'rpc://' f":6379/{CONFIG.y('redis.message_queue_db')}")
CELERY_ACKS_LATE = True CELERY_RESULT_BACKEND = (f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}"
CELERY_BROKER_HEARTBEAT = 0 f":6379/{CONFIG.y('redis.message_queue_db')}")
CELERY_BEAT_SCHEDULE = {
'cleanup-expired-nonces': {
'task': 'passbook.core.tasks.clean_nonces',
'schedule': crontab(hour=1, minute=1)
}
}
if not DEBUG: if not DEBUG:
@ -224,11 +205,7 @@ if not DEBUG:
dsn="https://33cdbcb23f8b436dbe0ee06847410b67@sentry.beryju.org/3", dsn="https://33cdbcb23f8b436dbe0ee06847410b67@sentry.beryju.org/3",
integrations=[ integrations=[
DjangoIntegration(), DjangoIntegration(),
CeleryIntegration(), CeleryIntegration()
LoggingIntegration(
level=logging.INFO,
event_level=logging.ERROR
)
], ],
send_default_pii=True, send_default_pii=True,
before_send=before_send, before_send=before_send,
@ -240,95 +217,76 @@ if not DEBUG:
STATIC_URL = '/static/' STATIC_URL = '/static/'
structlog.configure_once(
processors=[
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(),
structlog.processors.StackInfoRenderer(),
# structlog.processors.format_exc_info,
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
context_class=structlog.threadlocal.wrap_dict(dict),
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
LOG_PRE_CHAIN = [
# Add the log level and a timestamp to the event_dict if the log entry
# is not from structlog.
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(),
]
with CONFIG.cd('log'): with CONFIG.cd('log'):
LOGGING_HANDLER_MAP = {
'passbook': 'DEBUG',
'django': 'WARNING',
'celery': 'WARNING',
'grpc': 'DEBUG',
'oauthlib': 'DEBUG',
'oauth2_provider': 'DEBUG',
'daphne': 'INFO',
}
LOGGING = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': True, 'disable_existing_loggers': False,
'formatters': { 'formatters': {
'verbose': { "plain": {
'format': ('%(asctime)s %(levelname)-8s %(name)-55s ' "()": structlog.stdlib.ProcessorFormatter,
'%(funcName)-20s %(message)s'), "processor": structlog.processors.JSONRenderer(),
"foreign_pre_chain": LOG_PRE_CHAIN,
}, },
'color': { "colored": {
'()': 'colorlog.ColoredFormatter', "()": structlog.stdlib.ProcessorFormatter,
'format': ('%(log_color)s%(asctime)s %(levelname)-8s %(name)-55s ' "processor": structlog.dev.ConsoleRenderer(colors=DEBUG),
'%(funcName)-20s %(message)s'), "foreign_pre_chain": LOG_PRE_CHAIN,
'log_colors': {
'DEBUG': 'bold_black',
'INFO': 'white',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'bold_red',
'SUCCESS': 'green',
}, },
}
}, },
'handlers': { 'handlers': {
'console': { 'console': {
'level': CONFIG.get('level').get('console'), 'level': DEBUG,
'class': 'logging.StreamHandler', 'class': 'logging.StreamHandler',
'formatter': 'color', 'formatter': "colored" if DEBUG else "plain",
},
'syslog': {
'level': CONFIG.get('level').get('file'),
'class': 'logging.handlers.SysLogHandler',
'formatter': 'verbose',
'address': (CONFIG.get('syslog').get('host'),
CONFIG.get('syslog').get('port'))
},
'file': {
'level': CONFIG.get('level').get('file'),
'class': 'logging.FileHandler',
'formatter': 'verbose',
'filename': CONFIG.get('file'),
}, },
'queue': { 'queue': {
'level': CONFIG.get('level').get('console'), 'level': DEBUG,
'class': 'passbook.lib.log.QueueListenerHandler', 'class': 'passbook.lib.log.QueueListenerHandler',
'handlers': [ 'handlers': [
'cfg://handlers.console', 'cfg://handlers.console',
# 'cfg://handlers.syslog',
'cfg://handlers.file',
], ],
} }
}, },
'loggers': { 'loggers': {
'passbook': {
'handlers': ['queue'],
'level': 'DEBUG',
'propagate': True,
},
'django': {
'handlers': ['queue'],
'level': 'INFO',
'propagate': True,
},
'tasks': {
'handlers': ['queue'],
'level': 'DEBUG',
'propagate': True,
},
'cherrypy': {
'handlers': ['queue'],
'level': 'DEBUG',
'propagate': True,
},
'oauthlib': {
'handlers': ['queue'],
'level': 'DEBUG',
'propagate': True,
},
'oauth2_provider': {
'handlers': ['queue'],
'level': 'DEBUG',
'propagate': True,
},
'daphne': {
'handlers': ['queue'],
'level': 'INFO',
'propagate': True,
} }
} }
for handler_name, level in LOGGING_HANDLER_MAP.items():
LOGGING['loggers'][handler_name] = {
'handlers': ['console'],
'level': level,
'propagate': True,
} }
TEST = False TEST = False
@ -342,6 +300,7 @@ if any('test' in arg for arg in sys.argv):
TEST = True TEST = True
CELERY_TASK_ALWAYS_EAGER = True CELERY_TASK_ALWAYS_EAGER = True
_DISALLOWED_ITEMS = ['INSTALLED_APPS', 'MIDDLEWARE', 'AUTHENTICATION_BACKENDS'] _DISALLOWED_ITEMS = ['INSTALLED_APPS', 'MIDDLEWARE', 'AUTHENTICATION_BACKENDS']
# Load subapps's INSTALLED_APPS # Load subapps's INSTALLED_APPS
for _app in INSTALLED_APPS: for _app in INSTALLED_APPS:

View File

@ -1,15 +1,14 @@
"""passbook URL Configuration""" """passbook URL Configuration"""
from logging import getLogger
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic import RedirectView from django.views.generic import RedirectView
from structlog import get_logger
from passbook.core.views import error from passbook.core.views import error
from passbook.lib.utils.reflection import get_apps from passbook.lib.utils.reflection import get_apps
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
admin.autodiscover() admin.autodiscover()
admin.site.login = RedirectView.as_view(pattern_name='passbook_core:auth-login') admin.site.login = RedirectView.as_view(pattern_name='passbook_core:auth-login')

View File

@ -12,6 +12,6 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'passbook.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'passbook.root.settings')
application = SentryWsgiMiddleware(get_wsgi_application()) application = SentryWsgiMiddleware(get_wsgi_application())

View File

@ -1,12 +1,12 @@
"""passbook mod saml_idp app config""" """passbook mod saml_idp app config"""
from importlib import import_module from importlib import import_module
from logging import getLogger
from django.apps import AppConfig from django.apps import AppConfig
from structlog import get_logger
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class PassbookSAMLIDPConfig(AppConfig): class PassbookSAMLIDPConfig(AppConfig):
"""passbook saml_idp app config""" """passbook saml_idp app config"""

View File

@ -2,9 +2,9 @@
import time import time
import uuid import uuid
from logging import getLogger
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from structlog import get_logger
from passbook.saml_idp import exceptions, utils, xml_render from passbook.saml_idp import exceptions, utils, xml_render
@ -65,7 +65,7 @@ class Processor:
def __init__(self, remote): def __init__(self, remote):
self.name = remote.name self.name = remote.name
self._remote = remote self._remote = remote
self._logger = getLogger(__name__) self._logger = get_logger(__name__)
self._system_params['ISSUER'] = self._remote.issuer self._system_params['ISSUER'] = self._remote.issuer
self._logger.debug('processor configured') self._logger.debug('processor configured')
@ -260,7 +260,6 @@ class Processor:
def _validate_user(self): def _validate_user(self):
"""Validates the User. Sub-classes should override this and """Validates the User. Sub-classes should override this and
throw an CannotHandleAssertion Exception if the validation does not succeed.""" throw an CannotHandleAssertion Exception if the validation does not succeed."""
pass
def can_handle(self, request): def can_handle(self, request):
"""Returns true if this processor can handle this request.""" """Returns true if this processor can handle this request."""

View File

@ -3,9 +3,7 @@
class CannotHandleAssertion(Exception): class CannotHandleAssertion(Exception):
"""This processor does not handle this assertion.""" """This processor does not handle this assertion."""
pass
class UserNotAuthorized(Exception): class UserNotAuthorized(Exception):
"""User not authorized for SAML 2.0 authentication.""" """User not authorized for SAML 2.0 authentication."""
pass

View File

@ -1,16 +1,15 @@
"""passbook saml_idp Models""" """passbook saml_idp Models"""
from logging import getLogger
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from django.shortcuts import reverse from django.shortcuts import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import PropertyMapping, Provider from passbook.core.models import PropertyMapping, Provider
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.saml_idp.base import Processor from passbook.saml_idp.base import Processor
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
class SAMLProvider(Provider): class SAMLProvider(Provider):

View File

@ -1,6 +1,4 @@
"""passbook SAML IDP Views""" """passbook SAML IDP Views"""
from logging import getLogger
from django.contrib.auth import logout from django.contrib.auth import logout
from django.contrib.auth.mixins import AccessMixin from django.contrib.auth.mixins import AccessMixin
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -13,16 +11,17 @@ from django.utils.translation import gettext as _
from django.views import View from django.views import View
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from signxml.util import strip_pem_header from signxml.util import strip_pem_header
from structlog import get_logger
from passbook.audit.models import AuditEntry from passbook.audit.models import AuditEntry
from passbook.core.models import Application from passbook.core.models import Application
from passbook.core.policies import PolicyEngine
from passbook.lib.mixins import CSRFExemptMixin from passbook.lib.mixins import CSRFExemptMixin
from passbook.lib.utils.template import render_to_string from passbook.lib.utils.template import render_to_string
from passbook.policy.engine import PolicyEngine
from passbook.saml_idp import exceptions from passbook.saml_idp import exceptions
from passbook.saml_idp.models import SAMLProvider from passbook.saml_idp.models import SAMLProvider
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
URL_VALIDATOR = URLValidator(schemes=('http', 'https')) URL_VALIDATOR = URLValidator(schemes=('http', 'https'))

View File

@ -1,11 +1,11 @@
"""Functions for creating XML output.""" """Functions for creating XML output."""
from logging import getLogger from structlog import get_logger
from passbook.lib.utils.template import render_to_string from passbook.lib.utils.template import render_to_string
from passbook.saml_idp.xml_signing import get_signature_xml, sign_with_signxml from passbook.saml_idp.xml_signing import get_signature_xml, sign_with_signxml
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
def _get_attribute_statement(params): def _get_attribute_statement(params):

View File

@ -1,14 +1,13 @@
"""Signing code goes here.""" """Signing code goes here."""
from logging import getLogger
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from lxml import etree # nosec from lxml import etree # nosec
from signxml import XMLSigner, XMLVerifier from signxml import XMLSigner, XMLVerifier
from structlog import get_logger
from passbook.lib.utils.template import render_to_string from passbook.lib.utils.template import render_to_string
LOGGER = getLogger(__name__) LOGGER = get_logger(__name__)
def sign_with_signxml(private_key, data, cert, reference_uri=None): def sign_with_signxml(private_key, data, cert, reference_uri=None):

View File

@ -2,7 +2,7 @@
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 Policy, User from passbook.core.models import Policy, PolicyResult, User
class SuspiciousRequestPolicy(Policy): class SuspiciousRequestPolicy(Policy):
@ -14,7 +14,7 @@ class SuspiciousRequestPolicy(Policy):
form = 'passbook.suspicious_policy.forms.SuspiciousRequestPolicyForm' form = 'passbook.suspicious_policy.forms.SuspiciousRequestPolicyForm'
def passes(self, user: User): def passes(self, user: User) -> PolicyResult:
remote_ip = user.remote_ip remote_ip = user.remote_ip
passing = True passing = True
if self.check_ip: if self.check_ip:
@ -23,7 +23,7 @@ class SuspiciousRequestPolicy(Policy):
if self.check_username: if self.check_username:
user_scores = UserScore.objects.filter(user=user, score__lte=self.threshold) user_scores = UserScore.objects.filter(user=user, score__lte=self.threshold)
passing = passing and user_scores.exists() passing = passing and user_scores.exists()
return passing return PolicyResult(passing)
class Meta: class Meta:

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