Compare commits

...

40 Commits

Author SHA1 Message Date
48a04744e0 new release: 0.6.4-beta 2019-10-10 16:09:38 +02:00
6446ca8bb2 Merge branch '19-lockout-prevention' into 'master'
add lockout prevention

See merge request BeryJu.org/passbook!27
2019-10-10 12:37:14 +00:00
b9991465ee recovery(new): add recovery app to create recovery links 2019-10-10 14:05:16 +02:00
3d8242be06 core(minor): add new, optional description field to nonce 2019-10-10 14:04:58 +02:00
ca3bcc565d ui(minor): simplify top navigation 2019-10-10 10:02:48 +02:00
432176ea2f docker(minor): give user a fixed UID, use --chown flag for docker COPY 2019-10-10 09:36:28 +02:00
c1dae0b599 sources/oauth(minor): fix wrong settings reference 2019-10-09 19:46:23 +02:00
e70d3b6286 new release: 0.6.3-beta 2019-10-09 14:44:50 +02:00
17e6bc921b core(minor): fix import order 2019-10-09 14:37:40 +02:00
46111e7cac deploy(minor): downgrade kombu to fix redis error
https://github.com/celery/kombu/issues/1063
2019-10-09 14:32:20 +02:00
3b7e47dbe2 settings(minor): use cached_db for session, use localhost as domain 2019-10-09 14:30:53 +02:00
fff99f0e3d deploy(minor): use SERVER_TAG, fix static container 2019-10-09 14:29:44 +02:00
2e15b24f0a *(minor): switch has_user_settings to return Optional dataclass instead of tuple 2019-10-09 12:47:14 +02:00
088b9592cd core(minor): remove unused code 2019-10-08 15:04:38 +02:00
b1e4e32b83 providers/oidc(minor): correctly create audit entry on authz 2019-10-08 14:34:59 +02:00
d91a852eda factors/email(minor): start rebuilding email integration as factor 2019-10-08 14:30:17 +02:00
171c5b9759 factors/password(minor): remove form from core 2019-10-08 14:23:02 +02:00
64290b2a37 admin(minor): add view to create user 2019-10-08 11:27:19 +02:00
72769b8a0a lib(minor): cleanup default settings 2019-10-08 10:44:44 +02:00
1018309413 helm(minor): cleanup configmap, move secret_key to k8s secret 2019-10-08 10:44:25 +02:00
6d0ecd228e new release: 0.6.2-beta 2019-10-07 21:24:56 +02:00
40a651e66c docker(minor): ensure passbook user can write 2019-10-07 21:23:38 +02:00
a390bb7b59 factors/otp(minor): fix old URLs 2019-10-07 21:23:25 +02:00
245ec65cbb helm(minor): remove default postgres password 2019-10-07 21:23:15 +02:00
17eea4a10c new release: 0.6.1-beta 2019-10-07 18:53:04 +02:00
862fb0f5d2 deploy(minor): deploy more servers with more resources 2019-10-07 18:41:43 +02:00
ec73b53340 providers/saml(minor): fix last wrong urls names 2019-10-07 18:36:09 +02:00
9110f7fee3 helm(minor): fix worker not starting correctly 2019-10-07 17:41:26 +02:00
54cc1fdeef helm(minor): re-add volumes 2019-10-07 17:22:35 +02:00
8f42a7f0b4 new release: 0.6.0-beta 2019-10-07 17:18:19 +02:00
2c221ea819 providers/oauth(minor): fix import order 2019-10-07 17:14:52 +02:00
93e0441b58 helm(minor): don't directly mount configmap 2019-10-07 17:14:08 +02:00
7f1455cb12 helm(minor): disable redis cluster & persistence by default 2019-10-07 17:01:27 +02:00
59fc223a85 factors/captcha(minor): load correct keys 2019-10-07 16:58:06 +02:00
0a6f555c23 otp(minor): disable autocomplete for code input 2019-10-07 16:57:54 +02:00
6a4233d6fd providers/oauth(minor): fix urls not being mounted in the right path 2019-10-07 16:57:36 +02:00
15fa7e9652 ui(minor): merge menus 2019-10-07 16:50:13 +02:00
f2acc154cd *(minor): small refactor 2019-10-07 16:33:48 +02:00
d21ec6c9a5 root(minor): get rid of duplicate settings 2019-10-04 16:09:35 +02:00
43dd858cd5 ci(minor): fix from in dockerfile 2019-10-04 14:04:51 +02:00
344 changed files with 2139 additions and 2329 deletions

View File

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

View File

@ -27,7 +27,7 @@ create-base-image:
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/base.Dockerfile --destination docker.beryju.org/passbook/base:latest --destination docker.beryju.org/passbook/base:0.5.0-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.6.4-beta
stage: build-base-image
only:
refs:
@ -41,7 +41,7 @@ build-dev-image:
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dev.Dockerfile --destination docker.beryju.org/passbook/dev:latest --destination docker.beryju.org/passbook/dev:0.5.0-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.6.4-beta
stage: build-dev-image
only:
refs:
@ -87,15 +87,15 @@ coverage:
- postgres:latest
- redis:latest
package-passbook-server:
build-passbook-server:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.5.0-beta
stage: build
- /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.6.4-beta
only:
- tags
- /^version/.*$/
@ -107,7 +107,7 @@ build-passbook-static:
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/static.Dockerfile --destination docker.beryju.org/passbook/static:latest --destination docker.beryju.org/passbook/static:0.5.0-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.6.4-beta
only:
- tags
- /^version/.*$/

View File

@ -1,9 +1,9 @@
FROM docker.beryju.org/passbook/base:test
FROM docker.beryju.org/passbook/base:latest
COPY ./passbook/ /app/passbook
COPY --chown=passbook:passbook ./passbook/ /app/passbook
COPY ./manage.py /app/
COPY ./docker/uwsgi.ini /app/
USER passbook
WORKDIR /app/
USER passbook

View File

@ -8,6 +8,7 @@ celery = "*"
cherrypy = "*"
defusedxml = "*"
django = "*"
kombu = "==4.5.0"
django-cors-middleware = "*"
django-filters = "*"
django-ipware = "*"
@ -18,7 +19,6 @@ django-otp = "*"
django-recaptcha = "*"
django-redis = "*"
django-rest-framework = "*"
djangorestframework = "==3.9.4"
drf-yasg = "*"
ldap3 = "*"
lxml = "*"

60
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "d03d1e494d28a90b39edd1d489afdb5e39ec09bceb18daa2a54b2cc7de61d83c"
"sha256": "53d7190ea62f504dc1a36eae952a273e0b2d9f313f23031099d039c3146235b7"
},
"pipfile-spec": 6,
"requires": {
@ -18,17 +18,17 @@
"default": {
"amqp": {
"hashes": [
"sha256:19a917e260178b8d410122712bac69cb3e6db010d68f6101e7307508aded5e68",
"sha256:19d851b879a471fcfdcf01df9936cff924f422baa77653289f7095dedd5fb26a"
"sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8",
"sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"
],
"version": "==2.5.1"
"version": "==2.5.2"
},
"asn1crypto": {
"hashes": [
"sha256:d02bf8ea1b964a5ff04ac7891fe3a39150045d1e5e4fe99273ba677d11b92a04",
"sha256:f822954b90c4c44f002e2cd46d636ab630f1fe4df22c816a82b66505c404eb2a"
"sha256:0b199f211ae690df3db4fd6c1c4ff976497fb1da689193e368eedbadc53d9292",
"sha256:bca90060bd995c3f62c4433168eab407e44bdbdb567b3f3a396a676c1a4c4a3f"
],
"version": "==1.0.0"
"version": "==1.0.1"
},
"attrs": {
"hashes": [
@ -242,11 +242,10 @@
},
"djangorestframework": {
"hashes": [
"sha256:376f4b50340a46c15ae15ddd0c853085f4e66058f97e4dbe7d43ed62f5e60651",
"sha256:c12869cfd83c33d579b17b3cb28a2ae7322a53c3ce85580c2a2ebe4e3f56c4fb"
"sha256:5488aed8f8df5ec1d70f04b2114abc52ae6729748a176c453313834a9ee179c8",
"sha256:dc81cbf9775c6898a580f6f1f387c4777d12bd87abf0f5406018d32ccae71090"
],
"index": "pypi",
"version": "==3.9.4"
"version": "==3.10.3"
},
"drf-yasg": {
"hashes": [
@ -276,13 +275,6 @@
],
"version": "==2.8"
},
"importlib-metadata": {
"hashes": [
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
],
"version": "==0.23"
},
"inflection": {
"hashes": [
"sha256:18ea7fb7a7d152853386523def08736aa8c32636b047ade55f7578c4edeb16ca"
@ -304,17 +296,18 @@
},
"jinja2": {
"hashes": [
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
],
"version": "==2.10.1"
"version": "==2.10.3"
},
"kombu": {
"hashes": [
"sha256:31edb84947996fdda065b6560c128d5673bb913ff34aa19e7b84755217a24deb",
"sha256:c9078124ce2616b29cf6607f0ac3db894c59154252dee6392cdbbe15e5c4b566"
"sha256:389ba09e03b15b55b1a7371a441c894fd8121d174f5583bbbca032b9ea8c9edd",
"sha256:7b92303af381ef02fad6899fd5f5a9a96031d781356cd8e505fa54ae5ddee181"
],
"version": "==4.6.5"
"index": "pypi",
"version": "==4.5.0"
},
"ldap3": {
"hashes": [
@ -566,10 +559,10 @@
},
"pytz": {
"hashes": [
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
"version": "==2019.2"
"version": "==2019.3"
},
"pyyaml": {
"hashes": [
@ -736,13 +729,6 @@
"sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f"
],
"version": "==2.0"
},
"zipp": {
"hashes": [
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
],
"version": "==0.6.0"
}
},
"develop": {
@ -976,10 +962,10 @@
},
"pytz": {
"hashes": [
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
"version": "==2019.2"
"version": "==2019.3"
},
"pyyaml": {
"hashes": [

View File

@ -3,7 +3,9 @@
## Quick instance
```
export PASSBOOK_DOMAIN=domain.tld
docker-compose pull
docker-compose up -d
docker-compose exec server ./manage.py migrate
docker-compose exec server ./manage.py createsuperuser
```

View File

@ -15,5 +15,4 @@ RUN apt-get update && \
RUN pipenv lock -r > requirements.txt && \
pipenv --rm && \
pip install -r requirements.txt --no-cache-dir && \
adduser --system --no-create-home passbook && \
chown -R passbook /app
adduser --system --no-create-home --uid 1000 --group --home /app passbook

View File

@ -20,28 +20,15 @@ services:
- internal
labels:
- traefik.enable=false
database-migrate:
build:
context: .
image: docker.beryju.org/passbook/server:${TAG:-test}
command:
- ./manage.py
- migrate
networks:
- internal
restart: 'no'
environment:
- PASSBOOK_REDIS__HOST=redis
- PASSBOOK_POSTGRESQL__HOST=postgresql
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
server:
build:
context: .
image: docker.beryju.org/passbook/server:${TAG:-test}
image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest}
command:
- uwsgi
- uwsgi.ini
environment:
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
- PASSBOOK_REDIS__HOST=redis
- PASSBOOK_POSTGRESQL__HOST=postgresql
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
@ -54,15 +41,20 @@ services:
- traefik.docker.network=internal
- traefik.frontend.rule=PathPrefix:/
worker:
image: docker.beryju.org/passbook/server:${TAG:-test}
image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest}
command:
- ./manage.py
- celery
- worker
- --autoscale=10,3
- -E
- -B
- -A=passbook.root.celery
networks:
- internal
labels:
- traefik.enable=false
environment:
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
- PASSBOOK_REDIS__HOST=redis
- PASSBOOK_POSTGRESQL__HOST=postgresql
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
@ -70,7 +62,7 @@ services:
build:
context: .
dockerfile: static.Dockerfile
image: docker.beryju.org/passbook/static:${TAG:-test}
image: docker.beryju.org/passbook/static:latest
networks:
- internal
labels:

View File

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

View File

@ -2,7 +2,8 @@
http = 0.0.0.0:8000
chdir = /app
wsgi-file = passbook/root/wsgi.py
processes = 4
processes = 2
master = true
threads = 2
enable-threads = true
uid = passbook

View File

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

View File

@ -12,101 +12,5 @@ data:
host: "{{ .Release.Name }}-redis-master"
cache_db: 0
message_queue_db: 1
# Error reporting, sends stacktrace to sentry.beryju.org
error_report_enabled: {{ .Values.config.error_reporting }}
{{- if .Values.config.secret_key }}
secret_key: {{ .Values.config.secret_key }}
{{- else }}
secret_key: {{ randAlphaNum 50 }}
{{- end }}
primary_domain: {{ .Values.primary_domain }}
domains:
{{- range .Values.ingress.hosts }}
- {{ . | quote }}
{{- end }}
- kubernetes-healthcheck-host
passbook:
sign_up:
# Enables signup, created users are stored in internal Database and created in LDAP if ldap.create_users is true
enabled: true
password_reset:
# Enable password reset, passwords are reset in internal Database and in LDAP if ldap.reset_password is true
enabled: true
# Verification the user has to provide in order to be able to reset passwords. Can be any combination of `email`, `2fa`, `security_questions`
verification:
- email
# Text used in title, on login page and multiple other places
branding: passbook
login:
# Override URL used for logo
logo_url: null
# Override URL used for Background on Login page
bg_url: null
# Optionally add a subtext, placed below logo on the login page
subtext: null
footer:
links:
# Optionally add links to the footer on the login page
# - name: test
# href: https://test
# Specify which fields can be used to authenticate. Can be any combination of `username` and `email`
uid_fields:
- username
- email
session:
remember_age: 2592000 # 60 * 60 * 24 * 30, one month
# Provider-specific settings
ldap:
# # Completely enable or disable LDAP provider
# enabled: false
# # AD Domain, used to generate `userPrincipalName`
# domain: corp.contoso.com
# # Base DN in which passbook should look for users
# base_dn: dn=corp,dn=contoso,dn=com
# # LDAP field which is used to set the django username
# username_field: sAMAccountName
# # LDAP server to connect to, can be set to `<domain_name>`
# server:
# name: corp.contoso.com
# use_tls: false
# # Bind credentials, used for account creation
# bind:
# username: Administraotr@corp.contoso.com
# password: VerySecurePassword!
# Which field from `uid_fields` maps to which LDAP Attribute
login_field_map:
username: sAMAccountName
email: mail # or userPrincipalName
user_attribute_map:
active_directory:
username: "%(sAMAccountName)s"
email: "%(mail)s"
name: "%(displayName)"
# # Create new users in LDAP upon sign-up
# create_users: true
# # Reset LDAP password when user reset their password
# reset_password: true
oauth_client:
# List of python packages with sources types to load.
types:
- passbook.oauth_client.source_types.discord
- passbook.oauth_client.source_types.facebook
- passbook.oauth_client.source_types.github
- passbook.oauth_client.source_types.google
- passbook.oauth_client.source_types.reddit
- passbook.oauth_client.source_types.supervisr
- passbook.oauth_client.source_types.twitter
- passbook.oauth_client.source_types.azure_ad
saml_idp:
signing: true
autosubmit: false
issuer: passbook
assertion_valid_for: 86400
# List of python packages with provider types to load.
types:
- passbook.saml_idp.processors.generic
- passbook.saml_idp.processors.salesforce
domain: ".{{ .Values.ingress.hosts[0] }}"

View File

@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: {{ include "passbook.fullname" . }}-secret-key
data:
{{- if .Values.config.secret_key }}
secret_key: {{ .Values.config.secret_key | b64enc | quote }}
{{- else }}
secret_key: {{ randAlphaNum 50 | b64enc | quote}}
{{- end }}

View File

@ -8,7 +8,7 @@ metadata:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
@ -31,11 +31,19 @@ spec:
- ./manage.py
args:
- migrate
volumeMounts:
- mountPath: /etc/passbook
name: config-volume
envFrom:
- configMapRef:
name: {{ include "passbook.fullname" . }}-config
prefix: PASSBOOK_
env:
- name: PASSBOOK_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "passbook.fullname" . }}-secret-key
key: secret_key
- name: PASSBOOK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
@ -46,9 +54,6 @@ spec:
secretKeyRef:
name: "{{ .Release.Name }}-postgresql"
key: postgresql-password
volumeMounts:
- mountPath: /etc/passbook
name: config-volume
containers:
- name: {{ .Chart.Name }}
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
@ -57,11 +62,19 @@ spec:
- uwsgi
args:
- uwsgi.ini
volumeMounts:
- mountPath: /etc/passbook
name: config-volume
envFrom:
- configMapRef:
name: {{ include "passbook.fullname" . }}-config
prefix: PASSBOOK_
env:
- name: PASSBOOK_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "passbook.fullname" . }}-secret-key
key: secret_key
- name: PASSBOOK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
@ -76,9 +89,6 @@ spec:
- name: http
containerPort: 8000
protocol: TCP
volumeMounts:
- mountPath: /etc/passbook
name: config-volume
livenessProbe:
httpGet:
path: /
@ -95,8 +105,8 @@ spec:
value: kubernetes-healthcheck-host
resources:
requests:
cpu: 50m
memory: 150M
cpu: 100m
memory: 200M
limits:
cpu: 200m
memory: 300M
cpu: 300m
memory: 350M

View File

@ -8,7 +8,7 @@ metadata:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicaCount }}
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
@ -29,14 +29,26 @@ spec:
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command:
- ./manage.py
- celery
args:
- worker
- --autoscale=10,3
- -E
- -B
- -A=passbook.root.celery
volumeMounts:
- mountPath: /etc/passbook
name: config-volume
envFrom:
- configMapRef:
name: {{ include "passbook.fullname" . }}-config
prefix: PASSBOOK_
env:
- name: PASSBOOK_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ include "passbook.fullname" . }}-secret-key
key: secret_key
- name: PASSBOOK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
@ -47,13 +59,6 @@ spec:
secretKeyRef:
name: "{{ .Release.Name }}-postgresql"
key: postgresql-password
ports:
- name: http
containerPort: 8000
protocol: TCP
volumeMounts:
- mountPath: /etc/passbook
name: config-volume
resources:
requests:
cpu: 150m

View File

@ -1,11 +1,8 @@
# Default values for passbook.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
tag: 0.5.0-beta
tag: 0.6.4-beta
nameOverride: ""
@ -19,7 +16,13 @@ config:
postgresql:
postgresqlDatabase: passbook
postgresqlPassword: foo
redis:
cluster:
enabled: false
master:
persistence:
enabled: false
service:
type: ClusterIP

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = '0.5.0-beta'
__version__ = '0.6.4-beta'

View File

@ -7,6 +7,10 @@
<div class="container">
<h1><span class="pficon-users"></span> {% trans "Users" %}</h1>
<hr>
<a href="{% url 'passbook_admin:user-create' %}?back={{ request.get_full_path }}" class="btn btn-primary">
{% trans 'Create...' %}
</a>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>

View File

@ -61,6 +61,7 @@ urlpatterns = [
# Users
path('users/', users.UserListView.as_view(),
name='users'),
path('users/create/', users.UserCreateView.as_view(), name='user-create'),
path('users/<int:pk>/update/',
users.UserUpdateView.as_view(), name='user-update'),
path('users/<int:pk>/delete/',

View File

@ -3,8 +3,8 @@ from django.core.cache import cache
from django.shortcuts import redirect, reverse
from django.views.generic import TemplateView
from passbook import __version__
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core import __version__
from passbook.core.models import (Application, Factor, Invitation, Policy,
Provider, Source, User)
from passbook.root.celery import CELERY_APP

View File

@ -12,7 +12,7 @@ from passbook.admin.forms.policies import PolicyTestForm
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Policy
from passbook.lib.utils.reflection import path_to_class
from passbook.policy.engine import PolicyEngine
from passbook.policies.engine import PolicyEngine
class PolicyListView(AdminRequiredMixin, ListView):

View File

@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext as _
from django.views import View
from django.views.generic import DeleteView, ListView, UpdateView
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from passbook.admin.forms.users import UserForm
from passbook.admin.mixins import AdminRequiredMixin
@ -19,6 +19,17 @@ class UserListView(AdminRequiredMixin, ListView):
template_name = 'administration/user/list.html'
class UserCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create user"""
model = User
form_class = UserForm
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:users')
success_message = _('Successfully created User')
class UserUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update user"""

Binary file not shown.

View File

@ -1,11 +0,0 @@
"""passbook Application Security Gateway app"""
from django.apps import AppConfig
class PassbookApplicationApplicationGatewayConfig(AppConfig):
"""passbook app_gw app"""
name = 'passbook.app_gw'
label = 'passbook_app_gw'
verbose_name = 'passbook Application Security Gateway'
# mountpoint = 'app_gw/'

Binary file not shown.

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-21 15:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_app_gw', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='rewriterule',
name='conditions',
field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2 on 2019-04-11 13:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_app_gw', '0002_auto_20190321_1521'),
]
operations = [
migrations.AlterField(
model_name='applicationgatewayprovider',
name='authentication_header',
field=models.TextField(blank=True, default='X-Remote-User'),
),
]

View File

@ -1,7 +1,8 @@
# Generated by Django 2.1.7 on 2019-02-16 09:13
# Generated by Django 2.2.6 on 2019-10-07 14:07
import uuid
import django.contrib.postgres.fields.jsonb
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
@ -23,7 +24,7 @@ class Migration(migrations.Migration):
('action', models.TextField(choices=[('login', 'login'), ('login_failed', 'login_failed'), ('logout', 'logout'), ('authorize_application', 'authorize_application'), ('suspicious_request', 'suspicious_request'), ('sign_up', 'sign_up'), ('password_reset', 'password_reset'), ('invitation_created', 'invitation_created'), ('invitation_used', 'invitation_used')])),
('date', models.DateTimeField(auto_now_add=True)),
('app', models.TextField()),
('_context', models.TextField()),
('context', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
('request_ip', models.GenericIPAddressField()),
('created', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
@ -33,19 +34,4 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'Audit Entries',
},
),
migrations.CreateModel(
name='LoginAttempt',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('target_uid', models.CharField(max_length=254)),
('request_ip', models.GenericIPAddressField()),
('attempts', models.IntegerField(default=1)),
],
),
migrations.AlterUniqueTogether(
name='loginattempt',
unique_together={('target_uid', 'request_ip', 'created')},
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-21 12:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='loginattempt',
name='created',
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-21 12:40
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0002_auto_20190221_1201'),
]
operations = [
migrations.RemoveField(
model_name='auditentry',
name='_context',
),
migrations.AddField(
model_name='auditentry',
name='context',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
),
]

View File

@ -1,16 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-08 14:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0003_auto_20190221_1240'),
]
operations = [
migrations.DeleteModel(
name='LoginAttempt',
),
]

View File

@ -1,10 +0,0 @@
"""passbook captcha app"""
from django.apps import AppConfig
class PassbookCaptchaFactorConfig(AppConfig):
"""passbook captcha app"""
name = 'passbook.captcha_factor'
label = 'passbook_captcha_factor'
verbose_name = 'passbook Captcha'

View File

@ -1,2 +0,0 @@
"""passbook core"""
__version__ = '0.2.6-beta'

View File

@ -1,12 +1,6 @@
"""passbook core app config"""
from importlib import import_module
from django.apps import AppConfig
from structlog import get_logger
from passbook.lib.config import CONFIG
LOGGER = get_logger()
class PassbookCoreConfig(AppConfig):
"""passbook core app config"""
@ -15,13 +9,3 @@ class PassbookCoreConfig(AppConfig):
label = 'passbook_core'
verbose_name = 'passbook Core'
mountpoint = ''
def ready(self):
import_module('passbook.policy.engine')
factors_to_load = CONFIG.y('passbook.factors', [])
for factors_to_load in factors_to_load:
try:
import_module(factors_to_load)
LOGGER.info("Loaded factor", factor_class=factors_to_load)
except ImportError as exc:
LOGGER.debug(exc)

View File

@ -16,7 +16,7 @@ class ApplicationForm(forms.ModelForm):
model = Application
fields = ['name', 'slug', 'launch_url', 'icon_url',
'policies', 'provider', 'skip_authorization']
'provider', 'policies', 'skip_authorization']
widgets = {
'name': forms.TextInput(),
'launch_url': forms.TextInput(),

View File

@ -81,13 +81,3 @@ class SignUpForm(forms.Form):
if password != password_repeat:
raise ValidationError(_("Passwords don't match"))
return self.cleaned_data.get('password_repeat')
class PasswordFactorForm(forms.Form):
"""Password authentication form"""
password = forms.CharField(widget=forms.PasswordInput(attrs={
'placeholder': _('Password'),
'autofocus': 'autofocus',
'autocomplete': 'current-password'
}))

View File

@ -3,40 +3,8 @@
from django import forms
from django.utils.translation import gettext as _
from passbook.core.models import (DebugPolicy, FieldMatcherPolicy,
GroupMembershipPolicy, PasswordPolicy,
SSOLoginPolicy, WebhookPolicy)
GENERAL_FIELDS = ['name', 'action', 'negate', 'order', 'timeout']
class FieldMatcherPolicyForm(forms.ModelForm):
"""FieldMatcherPolicy Form"""
class Meta:
model = FieldMatcherPolicy
fields = GENERAL_FIELDS + ['user_field', 'match_action', 'value', ]
widgets = {
'name': forms.TextInput(),
'value': forms.TextInput(),
}
class WebhookPolicyForm(forms.ModelForm):
"""WebhookPolicyForm Form"""
class Meta:
model = WebhookPolicy
fields = GENERAL_FIELDS + ['url', 'method', 'json_body', 'json_headers',
'result_jsonpath', 'result_json_value', ]
widgets = {
'name': forms.TextInput(),
'json_body': forms.TextInput(),
'json_headers': forms.TextInput(),
'result_jsonpath': forms.TextInput(),
'result_json_value': forms.TextInput(),
}
from passbook.core.models import DebugPolicy
from passbook.policies.forms import GENERAL_FIELDS
class DebugPolicyForm(forms.ModelForm):
@ -52,49 +20,3 @@ class DebugPolicyForm(forms.ModelForm):
labels = {
'result': _('Allow user')
}
class GroupMembershipPolicyForm(forms.ModelForm):
"""GroupMembershipPolicy Form"""
class Meta:
model = GroupMembershipPolicy
fields = GENERAL_FIELDS + ['group', ]
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
}
class SSOLoginPolicyForm(forms.ModelForm):
"""Edit SSOLoginPolicy instances"""
class Meta:
model = SSOLoginPolicy
fields = GENERAL_FIELDS
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
}
class PasswordPolicyForm(forms.ModelForm):
"""PasswordPolicy Form"""
class Meta:
model = PasswordPolicy
fields = GENERAL_FIELDS + ['amount_uppercase', 'amount_lowercase',
'amount_symbols', 'length_min', 'symbol_charset',
'error_message']
widgets = {
'name': forms.TextInput(),
'symbol_charset': forms.TextInput(),
'error_message': forms.TextInput(),
}
labels = {
'amount_uppercase': _('Minimum amount of Uppercase Characters'),
'amount_lowercase': _('Minimum amount of Lowercase Characters'),
'amount_symbols': _('Minimum amount of Symbols Characters'),
'length_min': _('Minimum Length'),
}

View File

@ -1,45 +0,0 @@
"""passbook import_users management command"""
from csv import DictReader
from django.core.management.base import BaseCommand
from django.core.validators import EmailValidator, ValidationError
from structlog import get_logger
from passbook.core.models import User
LOGGER = get_logger()
class Command(BaseCommand):
"""Import users from CSV file"""
def add_arguments(self, parser):
# Positional arguments
parser.add_argument('file', nargs='+', type=str)
def handle(self, *args, **options):
"""Create Users from CSV file"""
for file in options.get('file'):
with open(file, 'r') as _file:
reader = DictReader(_file)
for user in reader:
LOGGER.debug('User %s', user.get('username'))
try:
# only import users with valid email addresses
if user.get('email'):
validator = EmailValidator()
validator(user.get('email'))
# use combination of username and email to check for existing user
if User.objects.filter(
username=user.get('username'),
email=user.get('email')).exists():
LOGGER.debug('User %s exists already, skipping', user.get('username'))
# Create user
User.objects.create(
username=user.get('username'),
email=user.get('email'),
name=user.get('name'),
password=user.get('password'))
LOGGER.debug('Created User %s', user.get('username'))
except ValidationError as exc:
LOGGER.warning('User %s caused %r, skipping', user.get('username'), exc)
continue

View File

@ -1,35 +0,0 @@
"""passbook Webserver management command"""
import cherrypy
from django.conf import settings
from django.core.management.base import BaseCommand
from structlog import get_logger
from passbook.lib.config import CONFIG
from passbook.root.wsgi import application
LOGGER = get_logger()
class Command(BaseCommand):
"""Run CherryPy webserver"""
def handle(self, *args, **options):
"""passbook cherrypy server"""
cherrypy.config.update(CONFIG.y('web'))
cherrypy.tree.graft(application, '/')
# Mount NullObject to serve static files
cherrypy.tree.mount(None, settings.STATIC_URL, config={
'/': {
'tools.staticdir.on': True,
'tools.staticdir.dir': settings.STATIC_ROOT,
'tools.expires.on': True,
'tools.expires.secs': 86400,
'tools.gzip.on': True,
}
})
cherrypy.engine.start()
for file in CONFIG.loaded_file:
cherrypy.engine.autoreload.files.add(file)
LOGGER.info("Added '%s' to autoreload triggers", file)
cherrypy.engine.block()

View File

@ -1,22 +0,0 @@
"""passbook Worker management command"""
from django.core.management.base import BaseCommand
from django.utils import autoreload
from structlog import get_logger
from passbook.root.celery import CELERY_APP
LOGGER = get_logger()
class Command(BaseCommand):
"""Run Celery Worker"""
def handle(self, *args, **options):
"""celery worker"""
autoreload.run_with_reloader(self.celery_worker)
def celery_worker(self):
"""Run celery worker within autoreload"""
autoreload.raise_last_exception()
CELERY_APP.worker_main(['worker', '--autoscale=10,3', '-E', '-B'])

View File

@ -1,21 +1,24 @@
# Generated by Django 2.1.7 on 2019-02-16 09:10
# Generated by Django 2.2.6 on 2019-10-07 14:06
import uuid
import django.contrib.auth.models
import django.contrib.auth.validators
import django.contrib.postgres.fields.jsonb
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
import passbook.core.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0009_alter_user_last_name_max_length'),
('auth', '0011_update_proxy_permissions'),
]
operations = [
@ -34,6 +37,8 @@ class Migration(migrations.Migration):
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False)),
('name', models.TextField()),
('password_change_date', models.DateTimeField(auto_now_add=True)),
],
options={
'verbose_name': 'user',
@ -44,39 +49,17 @@ class Migration(migrations.Migration):
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='Group',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=80, verbose_name='name')),
('extra_data', models.TextField(blank=True)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='passbook_core.Group')),
],
),
migrations.CreateModel(
name='Invitation',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.DateTimeField(blank=True, default=None, null=True)),
('fixed_username', models.TextField(blank=True, default=None)),
('fixed_email', models.TextField(blank=True, default=None)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Invitation',
'verbose_name_plural': 'Invitations',
},
),
migrations.CreateModel(
name='Policy',
fields=[
('created', models.DateField(auto_now_add=True)),
('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField(blank=True, null=True)),
('action', models.CharField(choices=[('allow', 'allow'), ('deny', 'deny')], max_length=20)),
('negate', models.BooleanField(default=False)),
('order', models.IntegerField(default=0)),
('timeout', models.IntegerField(default=30)),
],
options={
'abstract': False,
@ -85,28 +68,136 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='PolicyModel',
fields=[
('created', models.DateField(auto_now_add=True)),
('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('policies', models.ManyToManyField(blank=True, to='passbook_core.Policy')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='PropertyMapping',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField()),
],
options={
'verbose_name': 'Property Mapping',
'verbose_name_plural': 'Property Mappings',
},
),
migrations.CreateModel(
name='DebugPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('result', models.BooleanField(default=False)),
('wait_min', models.IntegerField(default=5)),
('wait_max', models.IntegerField(default=30)),
],
options={
'verbose_name': 'Debug Policy',
'verbose_name_plural': 'Debug Policies',
},
bases=('passbook_core.policy',),
),
migrations.CreateModel(
name='Factor',
fields=[
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField(unique=True)),
('order', models.IntegerField()),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='Source',
fields=[
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField()),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('property_mappings', models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping')),
],
),
migrations.CreateModel(
name='Nonce',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.DateTimeField(default=passbook.core.models.default_nonce_duration)),
('expiring', models.BooleanField(default=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Nonce',
'verbose_name_plural': 'Nonces',
},
),
migrations.CreateModel(
name='Invitation',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.DateTimeField(blank=True, default=None, null=True)),
('fixed_username', models.TextField(blank=True, default=None)),
('fixed_email', models.TextField(blank=True, default=None)),
('needs_confirmation', models.BooleanField(default=True)),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Invitation',
'verbose_name_plural': 'Invitations',
},
),
migrations.CreateModel(
name='Group',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=80, verbose_name='name')),
('tags', django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict)),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='passbook_core.Group')),
],
options={
'unique_together': {('name', 'parent')},
},
),
migrations.AddField(
model_name='user',
name='groups',
field=models.ManyToManyField(to='passbook_core.Group'),
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
),
migrations.CreateModel(
name='UserSourceConnection',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('created', models.DateTimeField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Source')),
],
options={
'unique_together': {('user', 'source')},
},
),
migrations.CreateModel(
name='Application',
@ -124,131 +215,9 @@ class Migration(migrations.Migration):
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='DebugPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('result', models.BooleanField(default=False)),
('wait_min', models.IntegerField(default=5)),
('wait_max', models.IntegerField(default=30)),
],
options={
'verbose_name': 'Debug Policy',
'verbose_name_plural': 'Debug Policys',
},
bases=('passbook_core.policy',),
),
migrations.CreateModel(
name='Factor',
fields=[
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField(unique=True)),
('order', models.IntegerField()),
('type', models.TextField(unique=True)),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='FieldMatcherPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('user_field', models.TextField(choices=[('username', 'Username'), ('first_name', 'First Name'), ('last_name', 'Last Name'), ('email', 'E-Mail'), ('is_staff', 'Is staff'), ('is_active', 'Is active'), ('data_joined', 'Date joined')])),
('match_action', models.CharField(choices=[('startswith', 'Starts with'), ('endswith', 'Ends with'), ('endswith', 'Contains'), ('regexp', 'Regexp'), ('exact', 'Exact')], max_length=50)),
('value', models.TextField()),
],
options={
'verbose_name': 'Field matcher Policy',
'verbose_name_plural': 'Field matcher Policys',
},
bases=('passbook_core.policy',),
),
migrations.CreateModel(
name='PasswordPolicyPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('amount_uppercase', models.IntegerField(default=0)),
('amount_lowercase', models.IntegerField(default=0)),
('amount_symbols', models.IntegerField(default=0)),
('length_min', models.IntegerField(default=0)),
('symbol_charset', models.TextField(default='!\\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ')),
],
options={
'verbose_name': 'Password Policy Policy',
'verbose_name_plural': 'Password Policy Policys',
},
bases=('passbook_core.policy',),
),
migrations.CreateModel(
name='Source',
fields=[
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField()),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='WebhookPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('url', models.URLField()),
('method', models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PATCH', 'PATCH'), ('DELETE', 'DELETE'), ('PUT', 'PUT')], max_length=10)),
('json_body', models.TextField()),
('json_headers', models.TextField()),
('result_jsonpath', models.TextField()),
('result_json_value', models.TextField()),
],
options={
'verbose_name': 'Webhook Policy',
'verbose_name_plural': 'Webhook Policys',
},
bases=('passbook_core.policy',),
),
migrations.AddField(
model_name='policymodel',
name='policies',
field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
),
migrations.AddField(
model_name='user',
name='groups',
field=models.ManyToManyField(to='passbook_core.Group'),
),
migrations.AddField(
model_name='user',
name='user_permissions',
field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'),
),
migrations.AddField(
model_name='usersourceconnection',
name='source',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='passbook_core.Source'),
),
migrations.AlterUniqueTogether(
name='group',
unique_together={('name', 'parent')},
),
migrations.AddField(
model_name='user',
name='applications',
field=models.ManyToManyField(to='passbook_core.Application'),
),
migrations.AddField(
model_name='user',
name='sources',
field=models.ManyToManyField(through='passbook_core.UserSourceConnection', to='passbook_core.Source'),
),
migrations.AlterUniqueTogether(
name='usersourceconnection',
unique_together={('user', 'source')},
),
]

View File

@ -1,29 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-16 10:02
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='debugpolicy',
options={'verbose_name': 'Debug Policy', 'verbose_name_plural': 'Debug Policies'},
),
migrations.AlterModelOptions(
name='fieldmatcherpolicy',
options={'verbose_name': 'Field matcher Policy', 'verbose_name_plural': 'Field matcher Policies'},
),
migrations.AlterModelOptions(
name='passwordpolicypolicy',
options={'verbose_name': 'Password Policy Policy', 'verbose_name_plural': 'Password Policy Policies'},
),
migrations.AlterModelOptions(
name='webhookpolicy',
options={'verbose_name': 'Webhook Policy', 'verbose_name_plural': 'Webhook Policies'},
),
]

View File

@ -1,4 +1,4 @@
# Generated by Django 2.2 on 2019-04-18 09:09
# Generated by Django 2.2.6 on 2019-10-10 11:48
from django.db import migrations, models
@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_saml_idp', '0002_samlpropertymapping'),
('passbook_core', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='samlprovider',
name='audience',
model_name='nonce',
name='description',
field=models.TextField(blank=True, default=''),
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-16 10:04
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0002_auto_20190216_1002'),
]
operations = [
migrations.RenameModel(
old_name='PasswordPolicyPolicy',
new_name='PasswordPolicy',
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-16 10:13
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0003_auto_20190216_1004'),
]
operations = [
migrations.AlterModelOptions(
name='passwordpolicy',
options={'verbose_name': 'Password Policy', 'verbose_name_plural': 'Password Policies'},
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-21 12:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0004_auto_20190216_1013'),
]
operations = [
migrations.AlterField(
model_name='policy',
name='created',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='policymodel',
name='created',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='usersourceconnection',
name='created',
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-21 12:32
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0005_auto_20190221_1201'),
]
operations = [
migrations.AddField(
model_name='factor',
name='arguments',
field=django.contrib.postgres.fields.jsonb.JSONField(default=dict),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-21 12:33
import django.contrib.postgres.fields.jsonb
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0006_factor_arguments'),
]
operations = [
migrations.AlterField(
model_name='factor',
name='arguments',
field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, default=dict),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-21 15:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0007_auto_20190221_1233'),
]
operations = [
migrations.AlterField(
model_name='fieldmatcherpolicy',
name='match_action',
field=models.CharField(choices=[('startswith', 'Starts with'), ('endswith', 'Ends with'), ('contains', 'Contains'), ('regexp', 'Regexp'), ('exact', 'Exact')], max_length=50),
),
]

View File

@ -1,44 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-24 09:50
import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0008_auto_20190221_1516'),
]
operations = [
migrations.CreateModel(
name='DummyFactor',
fields=[
('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')),
],
options={
'abstract': False,
},
bases=('passbook_core.factor',),
),
migrations.CreateModel(
name='PasswordFactor',
fields=[
('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')),
('backends', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)),
],
options={
'abstract': False,
},
bases=('passbook_core.factor',),
),
migrations.RemoveField(
model_name='factor',
name='arguments',
),
migrations.RemoveField(
model_name='factor',
name='type',
),
]

View File

@ -1,21 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-24 10:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0009_auto_20190224_0950'),
]
operations = [
migrations.AlterModelOptions(
name='dummyfactor',
options={'verbose_name': 'Dummy Factor', 'verbose_name_plural': 'Dummy Factors'},
),
migrations.AlterModelOptions(
name='passwordfactor',
options={'verbose_name': 'Password Factor', 'verbose_name_plural': 'Password Factors'},
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-25 14:38
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0010_auto_20190224_1016'),
]
operations = [
migrations.AddField(
model_name='passwordfactor',
name='password_policies',
field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
),
migrations.AddField(
model_name='user',
name='password_change_date',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@ -1,31 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-25 19:12
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import passbook.core.models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0011_auto_20190225_1438'),
]
operations = [
migrations.CreateModel(
name='Nonce',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('expires', models.DateTimeField(default=passbook.core.models.default_nonce_duration)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Nonce',
'verbose_name_plural': 'Nonces',
},
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-25 19:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0012_nonce'),
]
operations = [
migrations.AddField(
model_name='invitation',
name='needs_confirmation',
field=models.BooleanField(default=True),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-26 14:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0014_auto_20190226_0850'),
]
operations = [
migrations.AddField(
model_name='passwordpolicy',
name='error_message',
field=models.TextField(default=''),
preserve_default=False,
),
]

View File

@ -1,38 +0,0 @@
# Generated by Django 2.1.7 on 2019-02-27 13:55
from django.db import migrations, models
def migrate_names(apps, schema_editor):
"""migrate first_name and last_name to name"""
User = apps.get_model("passbook_core", "User")
for user in User.objects.all():
user.name = '%s %s' % (user.first_name, user.last_name)
user.save()
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0015_passwordpolicy_error_message'),
]
operations = [
migrations.AddField(
model_name='user',
name='name',
field=models.TextField(default=''),
preserve_default=False,
),
migrations.RunPython(migrate_names),
migrations.AlterField(
model_name='user',
name='name',
field=models.TextField(),
preserve_default=False,
),
migrations.AlterField(
model_name='fieldmatcherpolicy',
name='user_field',
field=models.TextField(choices=[('username', 'Username'), ('name', 'Name'), ('email', 'E-Mail'), ('is_staff', 'Is staff'), ('is_active', 'Is active'), ('data_joined', 'Date joined')]),
),
]

View File

@ -1,26 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-08 10:40
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0016_auto_20190227_1355'),
]
operations = [
migrations.CreateModel(
name='PropertyMapping',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField()),
],
options={
'verbose_name': 'Property Mapping',
'verbose_name_plural': 'Property Mappings',
},
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-08 10:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0017_propertymapping'),
]
operations = [
migrations.AddField(
model_name='provider',
name='property_mappings',
field=models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping'),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-10 16:15
import django.contrib.postgres.fields.hstore
from django.contrib.postgres.operations import HStoreExtension
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0018_provider_property_mappings'),
]
operations = [
migrations.RemoveField(
model_name='group',
name='extra_data',
),
HStoreExtension(),
migrations.AddField(
model_name='group',
name='tags',
field=django.contrib.postgres.fields.hstore.HStoreField(default=dict),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-21 12:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0020_groupmembershippolicy'),
]
operations = [
migrations.AddField(
model_name='policy',
name='timeout',
field=models.IntegerField(default=30),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.7 on 2019-04-04 19:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0021_policy_timeout'),
]
operations = [
migrations.AddField(
model_name='nonce',
name='expiring',
field=models.BooleanField(default=True),
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 2.2 on 2019-04-13 15:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0022_nonce_expiring'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='applications',
),
]

View File

@ -1,12 +1,12 @@
"""passbook core models"""
import re
from datetime import timedelta
from random import SystemRandom
from time import sleep
from typing import Optional
from uuid import uuid4
from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import ArrayField, HStoreField
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.urls import reverse_lazy
from django.utils.timezone import now
@ -16,8 +16,8 @@ from structlog import get_logger
from passbook.core.signals import password_changed
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
from passbook.policy.exceptions import PolicyException
from passbook.policy.struct import PolicyRequest, PolicyResult
from passbook.policies.exceptions import PolicyException
from passbook.policies.struct import PolicyRequest, PolicyResult
LOGGER = get_logger()
@ -32,10 +32,10 @@ class Group(UUIDModel):
name = models.CharField(_('name'), max_length=80)
parent = models.ForeignKey('Group', blank=True, null=True,
on_delete=models.SET_NULL, related_name='children')
tags = HStoreField(default=dict)
tags = JSONField(default=dict, blank=True)
def __str__(self):
return "Group %s" % self.name
return f"Group {self.name}"
class Meta:
@ -57,6 +57,7 @@ class User(AbstractUser):
self.password_change_date = now()
return super().set_password(password)
class Provider(models.Model):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
@ -70,11 +71,26 @@ class Provider(models.Model):
return getattr(self, 'name')
return super().__str__()
class PolicyModel(UUIDModel, CreatedUpdatedModel):
"""Base model which can have policies applied to it"""
policies = models.ManyToManyField('Policy', blank=True)
class UserSettings:
"""Dataclass for Factor and Source's user_settings"""
name: str
icon: str
view_name: str
def __init__(self, name: str, icon: str, view_name: str):
self.name = name
self.icon = icon
self.view_name = view_name
class Factor(PolicyModel):
"""Authentication factor, multiple instances of the same Factor can be used"""
@ -87,55 +103,14 @@ class Factor(PolicyModel):
type = ''
form = ''
def has_user_settings(self):
"""Entrypoint to integrate with User settings. Can either return False if no
user settings are available, or a tuple or string, string, string where the first string
is the name the item has, the second string is the icon and the third is the view-name."""
return False
def user_settings(self) -> Optional[UserSettings]:
"""Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or an instanace of UserSettings."""
return None
def __str__(self):
return "Factor %s" % self.slug
return f"Factor {self.slug}"
class PasswordFactor(Factor):
"""Password-based Django-backend Authentication Factor"""
backends = ArrayField(models.TextField())
password_policies = models.ManyToManyField('Policy', blank=True)
type = 'passbook.core.auth.factors.password.PasswordFactor'
form = 'passbook.core.forms.factors.PasswordFactorForm'
def has_user_settings(self):
return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password'
def password_passes(self, user: User) -> bool:
"""Return true if user's password passes, otherwise False or raise Exception"""
for policy in self.policies.all():
if not policy.passes(user):
return False
return True
def __str__(self):
return "Password Factor %s" % self.slug
class Meta:
verbose_name = _('Password Factor')
verbose_name_plural = _('Password Factors')
class DummyFactor(Factor):
"""Dummy factor, mostly used to debug"""
type = 'passbook.core.auth.factors.dummy.DummyFactor'
form = 'passbook.core.forms.factors.DummyFactorForm'
def __str__(self):
return "Dummy Factor %s" % self.slug
class Meta:
verbose_name = _('Dummy Factor')
verbose_name_plural = _('Dummy Factors')
class Application(PolicyModel):
"""Every Application which uses passbook for authentication/identification/authorization
@ -161,6 +136,7 @@ class Application(PolicyModel):
def __str__(self):
return self.name
class Source(PolicyModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
@ -187,15 +163,15 @@ class Source(PolicyModel):
"""Return additional Info, such as a callback URL. Show in the administration interface."""
return None
def has_user_settings(self):
"""Entrypoint to integrate with User settings. Can either return False if no
user settings are available, or a tuple or string, string, string where the first string
is the name the item has, the second string is the icon and the third is the view-name."""
return False
def user_settings(self) -> Optional[UserSettings]:
"""Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or an instanace of UserSettings."""
return None
def __str__(self):
return self.name
class UserSourceConnection(CreatedUpdatedModel):
"""Connection between User and Source."""
@ -206,6 +182,7 @@ class UserSourceConnection(CreatedUpdatedModel):
unique_together = (('user', 'source'),)
class Policy(UUIDModel, CreatedUpdatedModel):
"""Policies which specify if a user is authorized to use an Application. Can be overridden by
other types to add other fields, more logic, etc."""
@ -228,148 +205,12 @@ class Policy(UUIDModel, CreatedUpdatedModel):
def __str__(self):
if self.name:
return self.name
return "%s action %s" % (self.name, self.action)
return f"{self.name} action {self.action}"
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if user instance passes this policy"""
raise PolicyException()
class FieldMatcherPolicy(Policy):
"""Policy which checks if a field of the User model matches/doesn't match a
certain pattern"""
MATCH_STARTSWITH = 'startswith'
MATCH_ENDSWITH = 'endswith'
MATCH_CONTAINS = 'contains'
MATCH_REGEXP = 'regexp'
MATCH_EXACT = 'exact'
MATCHES = (
(MATCH_STARTSWITH, _('Starts with')),
(MATCH_ENDSWITH, _('Ends with')),
(MATCH_CONTAINS, _('Contains')),
(MATCH_REGEXP, _('Regexp')),
(MATCH_EXACT, _('Exact')),
)
USER_FIELDS = (
('username', _('Username'),),
('name', _('Name'),),
('email', _('E-Mail'),),
('is_staff', _('Is staff'),),
('is_active', _('Is active'),),
('data_joined', _('Date joined'),),
)
user_field = models.TextField(choices=USER_FIELDS)
match_action = models.CharField(max_length=50, choices=MATCHES)
value = models.TextField()
form = 'passbook.core.forms.policies.FieldMatcherPolicyForm'
def __str__(self):
description = "%s, user.%s %s '%s'" % (self.name, self.user_field,
self.match_action, self.value)
if self.name:
description = "%s: %s" % (self.name, description)
return description
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if user instance passes this role"""
if not hasattr(request.user, self.user_field):
raise ValueError("Field does not exist")
user_field_value = getattr(request.user, self.user_field, None)
LOGGER.debug("Checking field", value=user_field_value,
action=self.match_action, should_be=self.value)
passes = False
if self.match_action == FieldMatcherPolicy.MATCH_STARTSWITH:
passes = user_field_value.startswith(self.value)
if self.match_action == FieldMatcherPolicy.MATCH_ENDSWITH:
passes = user_field_value.endswith(self.value)
if self.match_action == FieldMatcherPolicy.MATCH_CONTAINS:
passes = self.value in user_field_value
if self.match_action == FieldMatcherPolicy.MATCH_REGEXP:
pattern = re.compile(self.value)
passes = bool(pattern.match(user_field_value))
if self.match_action == FieldMatcherPolicy.MATCH_EXACT:
passes = user_field_value == self.value
return PolicyResult(passes)
class Meta:
verbose_name = _('Field matcher Policy')
verbose_name_plural = _('Field matcher Policies')
class PasswordPolicy(Policy):
"""Policy to make sure passwords have certain properties"""
amount_uppercase = models.IntegerField(default=0)
amount_lowercase = models.IntegerField(default=0)
amount_symbols = models.IntegerField(default=0)
length_min = models.IntegerField(default=0)
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
error_message = models.TextField()
form = 'passbook.core.forms.policies.PasswordPolicyForm'
def passes(self, request: PolicyRequest) -> PolicyResult:
# Only check if password is being set
if not hasattr(request.user, '__password__'):
return PolicyResult(True)
password = getattr(request.user, '__password__')
filter_regex = r''
if self.amount_lowercase > 0:
filter_regex += r'[a-z]{%d,}' % self.amount_lowercase
if self.amount_uppercase > 0:
filter_regex += r'[A-Z]{%d,}' % self.amount_uppercase
if self.amount_symbols > 0:
filter_regex += r'[%s]{%d,}' % (self.symbol_charset, self.amount_symbols)
result = bool(re.compile(filter_regex).match(password))
if not result:
return PolicyResult(result, self.error_message)
return PolicyResult(result)
class Meta:
verbose_name = _('Password Policy')
verbose_name_plural = _('Password Policies')
class WebhookPolicy(Policy):
"""Policy that asks webhook"""
METHOD_GET = 'GET'
METHOD_POST = 'POST'
METHOD_PATCH = 'PATCH'
METHOD_DELETE = 'DELETE'
METHOD_PUT = 'PUT'
METHODS = (
(METHOD_GET, METHOD_GET),
(METHOD_POST, METHOD_POST),
(METHOD_PATCH, METHOD_PATCH),
(METHOD_DELETE, METHOD_DELETE),
(METHOD_PUT, METHOD_PUT),
)
url = models.URLField()
method = models.CharField(max_length=10, choices=METHODS)
json_body = models.TextField()
json_headers = models.TextField()
result_jsonpath = models.TextField()
result_json_value = models.TextField()
form = 'passbook.core.forms.policies.WebhookPolicyForm'
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Call webhook asynchronously and report back"""
raise NotImplementedError()
class Meta:
verbose_name = _('Webhook Policy')
verbose_name_plural = _('Webhook Policies')
class DebugPolicy(Policy):
"""Policy used for debugging the PolicyEngine. Returns a fixed result,
@ -393,36 +234,6 @@ class DebugPolicy(Policy):
verbose_name = _('Debug Policy')
verbose_name_plural = _('Debug Policies')
class GroupMembershipPolicy(Policy):
"""Policy to check if the user is member in a certain group"""
group = models.ForeignKey('Group', on_delete=models.CASCADE)
form = 'passbook.core.forms.policies.GroupMembershipPolicyForm'
def passes(self, request: PolicyRequest) -> PolicyResult:
return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists())
class Meta:
verbose_name = _('Group Membership Policy')
verbose_name_plural = _('Group Membership Policies')
class SSOLoginPolicy(Policy):
"""Policy that applies to users that have authenticated themselves through SSO"""
form = 'passbook.core.forms.policies.SSOLoginPolicyForm'
def passes(self, request: PolicyRequest) -> PolicyResult:
"""Check if user instance passes this policy"""
from passbook.core.auth.view import AuthenticationView
is_sso_login = request.user.session.get(AuthenticationView.SESSION_IS_SSO_LOGIN, False)
return PolicyResult(is_sso_login)
class Meta:
verbose_name = _('SSO Login Policy')
verbose_name_plural = _('SSO Login Policies')
class Invitation(UUIDModel):
"""Single-use invitation link"""
@ -436,31 +247,39 @@ class Invitation(UUIDModel):
@property
def link(self):
"""Get link to use invitation"""
return reverse_lazy('passbook_core:auth-sign-up') + '?invitation=%s' % self.uuid
return reverse_lazy('passbook_core:auth-sign-up') + f'?invitation={self.uuid.hex}'
def __str__(self):
return "Invitation %s created by %s" % (self.uuid, self.created_by)
return f"Invitation {self.uuid.hex} created by {self.created_by}"
class Meta:
verbose_name = _('Invitation')
verbose_name_plural = _('Invitations')
class Nonce(UUIDModel):
"""One-time link for password resets/sign-up-confirmations"""
expires = models.DateTimeField(default=default_nonce_duration)
user = models.ForeignKey('User', on_delete=models.CASCADE)
expiring = models.BooleanField(default=True)
description = models.TextField(default='', blank=True)
@property
def is_expired(self) -> bool:
"""Check if nonce is expired yet."""
return now() > self.expires
def __str__(self):
return "Nonce %s (expires=%s)" % (self.uuid.hex, self.expires)
return f"Nonce f{self.uuid.hex} {self.description} (expires={self.expires})"
class Meta:
verbose_name = _('Nonce')
verbose_name_plural = _('Nonces')
class PropertyMapping(UUIDModel):
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
@ -470,7 +289,7 @@ class PropertyMapping(UUIDModel):
objects = InheritanceManager()
def __str__(self):
return "Property Mapping %s" % self.name
return f"Property Mapping {self.name}"
class Meta:

View File

@ -5,8 +5,6 @@ from django.db.models.signals import post_save
from django.dispatch import receiver
from structlog import get_logger
from passbook.core.exceptions import PasswordPolicyInvalid
LOGGER = get_logger()
user_signed_up = Signal(providing_args=['request', 'user'])
@ -14,24 +12,9 @@ invitation_created = Signal(providing_args=['request', 'invitation'])
invitation_used = Signal(providing_args=['request', 'invitation', 'user'])
password_changed = Signal(providing_args=['user', 'password'])
@receiver(password_changed)
# pylint: disable=unused-argument
def password_policy_checker(sender, password, **kwargs):
"""Run password through all password policies which are applied to the user"""
from passbook.core.models import PasswordFactor
from passbook.policy.engine import PolicyEngine
setattr(sender, '__password__', password)
_all_factors = PasswordFactor.objects.filter(enabled=True).order_by('order')
for factor in _all_factors:
policy_engine = PolicyEngine(factor.password_policies.all().select_subclasses())
policy_engine.for_user(sender).build()
passing, messages = policy_engine.result
if not passing:
raise PasswordPolicyInvalid(*messages)
@receiver(post_save)
# pylint: disable=unused-argument
def invalidate_policy_cache(sender, instance, **kwargs):
def invalidate_policy_cache(sender, instance, **_):
"""Invalidate Policy cache when policy is updated"""
from passbook.core.models import Policy
if isinstance(instance, Policy):

View File

@ -1,28 +1,15 @@
"""passbook core tasks"""
from datetime import datetime
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from structlog import get_logger
from passbook.core.models import Nonce
from passbook.lib.config import CONFIG
from passbook.root.celery import CELERY_APP
LOGGER = get_logger()
@CELERY_APP.task()
def send_email(to_address, subject, template, context):
"""Send Email to user(s)"""
html_content = render_to_string(template, context=context)
text_content = strip_tags(html_content)
msg = EmailMultiAlternatives(subject, text_content, CONFIG.y('email.from'), [to_address])
msg.attach_alternative(html_content, "text/html")
msg.send()
@CELERY_APP.task()
def clean_nonces():
"""Remove expired nonces"""
amount, _ = Nonce.objects.filter(expires__lt=datetime.now(), expiring=True).delete()
LOGGER.debug("Deleted expired %d nonces", amount)
LOGGER.debug("Deleted expired nonces", amount=amount)

View File

@ -46,9 +46,6 @@
<script src="{% static 'js/passbook.js' %}"></script>
{% block scripts %}
{% endblock %}
<div class="modals">
{% include 'partials/about_modal.html' %}
</div>
</body>
</html>

View File

@ -46,9 +46,6 @@
<script src="{% static 'js/passbook.js' %}"></script>
{% block scripts %}
{% endblock %}
<div class="modals">
{% include 'partials/about_modal.html' %}
</div>
</body>
</html>

View File

@ -23,37 +23,18 @@
</div>
<nav class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right navbar-iconic navbar-utility">
<li class="dropdown">
<button class="btn btn-link dropdown-toggle nav-item-iconic" id="dropdownMenu1" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<span title="Help" class="fa pficon-help"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
{% comment %} <li><a href="#0">Help</a></li> {% endcomment %}
<li><a data-toggle="modal" data-target="#about-modal" href="#0">{% trans 'About' %}</a></li>
</ul>
</li>
<li class="dropdown">
<button class="btn btn-link dropdown-toggle nav-item-iconic" id="dropdownMenu2" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<span title="Username" class="fa pficon-user"></span>
<span class="dropdown-title">
{{ user.username }} <span class="caret"></span>
</span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenu2">
<li>
<a href="{% url 'passbook_core:user-settings' %}">{% trans 'User Settings' %}</a>
</li>
<li>
<a href="{% url 'passbook_core:user-change-password' %}">{% trans 'Change Password' %}</a>
</li>
<li class="divider"></li>
<li>
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
</li>
</ul>
</li>
<a href="{% url 'passbook_core:auth-logout' %}" class="btn btn-link nav-item-iconic" aria-haspopup="true" aria-expanded="true">
<span title="Username" class="fa fa-sign-out"></span>
<span class="dropdown-title">
{% trans 'Logout' %}
</span>
</a>
<a href="{% url 'passbook_core:user-settings' %}" class="btn btn-link nav-item-iconic" aria-haspopup="true" aria-expanded="true">
<span title="Username" class="fa pficon-user"></span>
<span class="dropdown-title">
{{ user.username }}
</span>
</a>
</ul>
</nav>
</nav>
@ -65,122 +46,81 @@
<span class="list-group-item-value">{% trans 'Overview' %}</span>
</a>
</li>
{% is_active_app 'passbook_admin' as is_admin %}
{% if user.is_superuser %}
<li class="list-group-item {% is_active_app 'passbook_admin' %} secondary-nav-item-pf">
<a>
<span class="pficon pficon-user" data-toggle="tooltip" title=""
data-original-title="{% trans 'Administration' %}"></span>
<span class="list-group-item-value dropdown-title">{% trans 'Administration' %}</span>
<li class="list-group-item {% is_active_url 'passbook_admin:overview' %}">
<a href="{% url 'passbook_admin:overview' %}">
<span class="fa pficon-build" data-toggle="tooltip" title="{% trans 'System Status' %}"></span>
<span class="list-group-item-value">{% trans 'System Status' %}</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:applications' 'passbook_admin:application-create' 'passbook_admin:application-update' 'passbook_admin:application-delete' %}">
<a href="{% url 'passbook_admin:applications' %}">
<span class="fa pficon-applications" data-toggle="tooltip" title="{% trans 'Applications' %}"></span>
<span class="list-group-item-value">{% trans 'Applications' %}</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:sources' 'passbook_admin:source-create' 'passbook_admin:source-update' 'passbook_admin:source-delete' %}">
<a href="{% url 'passbook_admin:sources' %}">
<span class="fa pficon-resource-pool" data-toggle="tooltip" title="{% trans 'Sources' %}"></span>
<span class="list-group-item-value">{% trans 'Sources' %}</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
<a href="{% url 'passbook_admin:providers' %}">
<span class="fa pficon-integration" data-toggle="tooltip" title="{% trans 'Providers' %}"></span>
<span class="list-group-item-value">{% trans 'Providers' %}</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}">
<a href="{% url 'passbook_admin:property-mappings' %}">
<span class="fa fa-table" data-toggle="tooltip" title="{% trans 'Property Mappings' %}"></span>
<span class="list-group-item-value">{% trans 'Property Mappings' %}</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
<a href="{% url 'passbook_admin:factors' %}">
<span class="fa pficon-plugged" data-toggle="tooltip" title="{% trans 'Factors' %}"></span>
<span class="list-group-item-value">{% trans 'Factors' %}</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:policies' 'passbook_admin:policy-create' 'passbook_admin:policy-update' 'passbook_admin:policy-delete' 'passbook_admin:policy-test' %}">
<a href="{% url 'passbook_admin:policies' %}">
<span class="fa pficon-infrastructure" data-toggle="tooltip" title="{% trans 'Policies' %}"></span>
<span class="list-group-item-value">{% trans 'Policies' %}</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}">
<a href="{% url 'passbook_admin:invitations' %}">
<span class="fa pficon-migration" data-toggle="tooltip" title="{% trans 'Invitations' %}"></span>
<span class="list-group-item-value">{% trans 'Invitations' %}</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
<a href="{% url 'passbook_admin:users' %}">
<span class="fa pficon-users" data-toggle="tooltip" title="{% trans 'Users' %}"></span>
<span class="list-group-item-value">{% trans 'Users' %}</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
<a href="{% url 'passbook_admin:groups' %}">
<span class="fa pficon-users" data-toggle="tooltip" title="{% trans 'Groups' %}"></span>
<span class="list-group-item-value">{% trans 'Groups' %}</span>
</a>
</li>
<li class="list-group-item {% is_active 'passbook_admin:audit-log' %}">
<a href="{% url 'passbook_admin:audit-log' %}">
<span class="fa pficon-catalog" data-toggle="tooltip" title="{% trans 'Audit Log' %}"></span>
<span class="list-group-item-value">{% trans 'Audit Log' %}</span>
</a>
<div id="user-secondary" class="nav-pf-secondary-nav">
<div class="nav-item-pf-header">
<a href="#0" class="secondary-collapse-toggle-pf" data-toggle="collapse-secondary-nav"></a>
<span>{% trans 'Administration' %}</span>
</div>
<ul class="list-group">
<li class="list-group-item {% is_active 'passbook_admin:overview' %}">
<a href="{% url 'passbook_admin:overview' %}">
<span class="list-group-item-value">
{% trans 'Overview' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:applications' 'passbook_admin:application-create' 'passbook_admin:application-update' 'passbook_admin:application-delete' %}">
<a href="{% url 'passbook_admin:applications' %}">
<span class="list-group-item-value">
{% trans 'Applications' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:sources' 'passbook_admin:source-create' 'passbook_admin:source-update' 'passbook_admin:source-delete' %}">
<a href="{% url 'passbook_admin:sources' %}">
<span class="list-group-item-value">
{% trans 'Sources' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
<a href="{% url 'passbook_admin:providers' %}">
<span class="list-group-item-value">
{% trans 'Providers' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}">
<a href="{% url 'passbook_admin:property-mappings' %}">
<span class="list-group-item-value">
{% trans 'Property Mappings' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
<a href="{% url 'passbook_admin:factors' %}">
<span class="list-group-item-value">
{% trans 'Factors' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:policies' 'passbook_admin:policy-create' 'passbook_admin:policy-update' 'passbook_admin:policy-delete' 'passbook_admin:policy-test' %}">
<a href="{% url 'passbook_admin:policies' %}">
<span class="list-group-item-value">
{% trans 'Policies' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}">
<a href="{% url 'passbook_admin:invitations' %}">
<span class="list-group-item-value">
{% trans 'Invitations' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
<a href="{% url 'passbook_admin:users' %}">
<span class="list-group-item-value">
{% trans 'Users' %}
</span>
</a>
</li>
<li
class="list-group-item {% is_active 'passbook_admin:groups' 'passbook_admin:group-update' 'passbook_admin:group-delete' %}">
<a href="{% url 'passbook_admin:groups' %}">
<span class="list-group-item-value">
{% trans 'Groups' %}
</span>
</a>
</li>
<li class="list-group-item {% is_active 'passbook_admin:audit-log' %}">
<a href="{% url 'passbook_admin:audit-log' %}">
<span class="list-group-item-value">
{% trans 'Audit Log' %}
</span>
</a>
</li>
<li class="list-group-item {% is_active_app 'admin' %}">
<a href="{% url 'admin:index' %}">
<span class="list-group-item-value">
{% trans 'Django' %}
</span>
</a>
</li>
<li class="list-group-item {% is_active 'passbook_admin:debug-request' %}">
<a href="{% url 'passbook_admin:debug-request' %}">
<span class="list-group-item-value">
{% trans 'Debug' %}
</span>
</a>
</li>
</ul>
</div>
</li>
{% endif %}
</ul>

View File

@ -1,36 +0,0 @@
{% load static %}
{% load i18n %}
{% load cache %}
{% load utils %}
<div class="modal fade" id="about-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content about-modal-pf">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">
<span class="pficon pficon-close"></span>
</button>
</div>
<div class="modal-body">
<h1>{% trans 'passbook' %}</h1>
<div class="product-versions-pf">
<ul class="list-unstyled">
{% app_versions as vers %}
{% cache 600 versions %}
{% for app, ver in vers.items %}
<li><strong>{{ app }}</strong> {{ ver }}</li>
{% endfor %}
{% endcache %}
</ul>
</div>
<div class="trademark-pf">
Trademark and Copyright Information
</div>
</div>
<div class="modal-footer">
<img style="max-height:64px;" src="{% static 'img/logo.png' %}" alt=" Symbol">
</div>
</div>
</div>
</div>

View File

@ -16,23 +16,27 @@
<i class="fa pficon-edit"></i> {% trans 'Details' %}
</a>
</li>
<li class="nav-divider"></li>
{% user_factors as uf %}
{% for name, icon, link in uf %}
<li class="{% is_active link %}">
<a href="{% url link %}">
<i class="{{ icon }}"></i> {{ name }}
</a>
</li>
{% if uf %}
<li class="nav-divider"></li>
{% endif %}
{% for user_settings in uf %}
<li class="{% is_active user_settings.view_name %}">
<a href="{% url user_settings.view_name %}">
<i class="{{ user_settings.icon }}"></i> {{ user_settings.name }}
</a>
</li>
{% endfor %}
<li class="nav-divider"></li>
{% user_sources as us %}
{% for name, icon, link in us %}
<li class="{% if link == request.get_full_path %} active {% endif %}">
<a href="{{ link }}">
<i class="{{ icon }}"></i> {{ name }}
</a>
</li>
{% if us %}
<li class="nav-divider"></li>
{% endif %}
{% for user_settings in us %}
<li class="{% if user_settings.view_name == request.get_full_path %} active {% endif %}">
<a href="{{ user_settings.view_name }}">
<i class="{{ user_settings.icon }}"></i> {{ user_settings.name }}
</a>
</li>
{% endfor %}
</ul>
</div>

View File

@ -1,36 +1,38 @@
"""passbook user settings template tags"""
from typing import List
from django import template
from django.template.context import RequestContext
from passbook.core.models import Factor, Source
from passbook.policy.engine import PolicyEngine
from passbook.core.models import Factor, Source, UserSettings
from passbook.policies.engine import PolicyEngine
register = template.Library()
@register.simple_tag(takes_context=True)
def user_factors(context):
def user_factors(context: RequestContext) -> List[UserSettings]:
"""Return list of all factors which apply to user"""
user = context.get('request').user
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
matching_factors = []
matching_factors: List[UserSettings] = []
for factor in _all_factors:
_link = factor.has_user_settings()
user_settings = factor.user_settings()
policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(user).with_request(context.get('request')).build()
if policy_engine.passing and _link:
matching_factors.append(_link)
if policy_engine.passing and user_settings:
matching_factors.append(user_settings)
return matching_factors
@register.simple_tag(takes_context=True)
def user_sources(context):
def user_sources(context: RequestContext) -> List[UserSettings]:
"""Return a list of all sources which are enabled for the user"""
user = context.get('request').user
_all_sources = Source.objects.filter(enabled=True).select_subclasses()
matching_sources = []
matching_sources: List[UserSettings] = []
for factor in _all_sources:
_link = factor.has_user_settings()
user_settings = factor.user_settings()
policy_engine = PolicyEngine(factor.policies.all())
policy_engine.for_user(user).with_request(context.get('request')).build()
if policy_engine.passing and _link:
matching_sources.append(_link)
if policy_engine.passing and user_settings:
matching_sources.append(user_settings)
return matching_sources

View File

@ -2,8 +2,8 @@
from django.urls import path
from structlog import get_logger
from passbook.core.auth import view
from passbook.core.views import authentication, overview, user
from passbook.factors import view
LOGGER = get_logger()

View File

@ -1,10 +1,13 @@
"""passbook access helper classes"""
from typing import List, Tuple
from django.contrib import messages
from django.http import HttpRequest
from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Application
from passbook.policy.engine import PolicyEngine
from passbook.core.models import Application, Provider, User
from passbook.policies.engine import PolicyEngine
LOGGER = get_logger()
@ -13,9 +16,9 @@ class AccessMixin:
Provider functions to check application access, etc"""
# request is set by view but since this Mixin has no base class
request = None
request: HttpRequest = None
def provider_to_application(self, provider):
def provider_to_application(self, provider: Provider) -> Application:
"""Lookup application assigned to provider, throw error if no application assigned"""
try:
return provider.application
@ -25,9 +28,9 @@ class AccessMixin:
}))
raise exc
def user_has_access(self, application, user):
def user_has_access(self, application: Application, user: User) -> Tuple[bool, List[str]]:
"""Check if user has access to application."""
LOGGER.debug("Checking permissions of %s on application %s...", user, application)
LOGGER.debug("Checking permissions", user=user, application=application)
policy_engine = PolicyEngine(application.policies.all())
policy_engine.for_user(user).with_request(self.request).build()
return policy_engine.result

View File

@ -12,12 +12,11 @@ from django.views import View
from django.views.generic import FormView
from structlog import get_logger
from passbook.core.auth.view import AuthenticationView, _redirect_with_qs
from passbook.core.exceptions import PasswordPolicyInvalid
from passbook.core.forms.authentication import LoginForm, SignUpForm
from passbook.core.models import Invitation, Nonce, Source, User
from passbook.core.signals import invitation_used, user_signed_up
from passbook.core.tasks import send_email
from passbook.factors.password.exceptions import PasswordPolicyInvalid
from passbook.factors.view import AuthenticationView, _redirect_with_qs
from passbook.lib.config import CONFIG
LOGGER = get_logger()
@ -97,7 +96,7 @@ class SignUpView(UserPassesTestMixin, FormView):
template_name = 'login/form.html'
form_class = SignUpForm
success_url = '.'
# Invitation insatnce, if invitation link was used
# Invitation instance, if invitation link was used
_invitation = None
# Instance of newly created user
_user = None
@ -152,23 +151,23 @@ class SignUpView(UserPassesTestMixin, FormView):
for error in exc.messages:
errors.append(error)
return self.form_invalid(form)
needs_confirmation = True
if self._invitation and not self._invitation.needs_confirmation:
needs_confirmation = False
if needs_confirmation:
nonce = Nonce.objects.create(user=self._user)
LOGGER.debug(str(nonce.uuid))
# Send email to user
send_email.delay(self._user.email, _('Confirm your account.'),
'email/account_confirm.html', {
'url': self.request.build_absolute_uri(
reverse('passbook_core:auth-sign-up-confirm', kwargs={
'nonce': nonce.uuid
})
)
})
self._user.is_active = False
self._user.save()
# needs_confirmation = True
# if self._invitation and not self._invitation.needs_confirmation:
# needs_confirmation = False
# if needs_confirmation:
# nonce = Nonce.objects.create(user=self._user)
# LOGGER.debug(str(nonce.uuid))
# # Send email to user
# send_email.delay(self._user.email, _('Confirm your account.'),
# 'email/account_confirm.html', {
# 'url': self.request.build_absolute_uri(
# reverse('passbook_core:auth-sign-up-confirm', kwargs={
# 'nonce': nonce.uuid
# })
# )
# })
# self._user.is_active = False
# self._user.save()
self.consume_invitation()
messages.success(self.request, _("Successfully signed up!"))
LOGGER.debug("Successfully signed up %s",

View File

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

View File

@ -9,8 +9,8 @@ from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import DeleteView, FormView, UpdateView
from passbook.core.exceptions import PasswordPolicyInvalid
from passbook.core.forms.users import PasswordChangeForm, UserDetailForm
from passbook.factors.password.exceptions import PasswordPolicyInvalid
from passbook.lib.config import CONFIG

View File

@ -1,22 +1,27 @@
"""passbook multi-factor authentication engine"""
from django.forms import ModelForm
from django.http import HttpRequest
from django.utils.translation import gettext as _
from django.views.generic import TemplateView
from passbook.core.models import User
from passbook.factors.view import AuthenticationView
from passbook.lib.config import CONFIG
class AuthenticationFactor(TemplateView):
"""Abstract Authentication factor, inherits TemplateView but can be combined with FormView"""
form = None
required = True
authenticator = None
pending_user = None
request = None
form: ModelForm = None
required: bool = True
authenticator: AuthenticationView
pending_user: User
request: HttpRequest = None
template_name = 'login/form_with_user.html'
def __init__(self, authenticator):
def __init__(self, authenticator: AuthenticationView):
self.authenticator = authenticator
self.pending_user = None
def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.y('passbook')

View File

@ -0,0 +1,5 @@
"""captcha factor admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_factors_captcha')

View File

@ -0,0 +1,10 @@
"""passbook captcha app"""
from django.apps import AppConfig
class PassbookFactorCaptchaConfig(AppConfig):
"""passbook captcha app"""
name = 'passbook.factors.captcha'
label = 'passbook_factors_captcha'
verbose_name = 'passbook Factors.Captcha'

View File

@ -2,8 +2,8 @@
from django.views.generic import FormView
from passbook.captcha_factor.forms import CaptchaForm
from passbook.core.auth.factor import AuthenticationFactor
from passbook.factors.base import AuthenticationFactor
from passbook.factors.captcha.forms import CaptchaForm
class CaptchaFactor(FormView, AuthenticationFactor):
@ -16,7 +16,7 @@ class CaptchaFactor(FormView, AuthenticationFactor):
def get_form(self, form_class=None):
form = CaptchaForm(**self.get_form_kwargs())
form.fields['captcha'].public_key = '6Lfi1w8TAAAAAELH-YiWp0OFItmMzvjGmw2xkvUN'
form.fields['captcha'].private_key = '6Lfi1w8TAAAAAMQI3f86tGMvd1QkcqqVQyBWI23D'
form.fields['captcha'].public_key = self.authenticator.current_factor.public_key
form.fields['captcha'].private_key = self.authenticator.current_factor.private_key
form.fields['captcha'].widget.attrs["data-sitekey"] = form.fields['captcha'].public_key
return form

View File

@ -1,9 +1,11 @@
"""passbook captcha factor forms"""
from captcha.fields import ReCaptchaField
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from passbook.captcha_factor.models import CaptchaFactor
from passbook.core.forms.factors import GENERAL_FIELDS
from passbook.factors.captcha.models import CaptchaFactor
from passbook.factors.forms import GENERAL_FIELDS
class CaptchaForm(forms.Form):
@ -21,6 +23,7 @@ class CaptchaFactorForm(forms.ModelForm):
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False),
'public_key': forms.TextInput(),
'private_key': forms.TextInput(),
}

View File

@ -1,4 +1,4 @@
# Generated by Django 2.1.7 on 2019-02-24 21:35
# Generated by Django 2.2.6 on 2019-10-07 14:07
import django.db.models.deletion
from django.db import migrations, models
@ -9,7 +9,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('passbook_core', '0010_auto_20190224_1016'),
('passbook_core', '0001_initial'),
]
operations = [

View File

@ -11,11 +11,11 @@ class CaptchaFactor(Factor):
public_key = models.TextField()
private_key = models.TextField()
type = 'passbook.captcha_factor.factor.CaptchaFactor'
form = 'passbook.captcha_factor.forms.CaptchaFactorForm'
type = 'passbook.factors.captcha.factor.CaptchaFactor'
form = 'passbook.factors.captcha.forms.CaptchaFactorForm'
def __str__(self):
return "Captcha Factor %s" % self.slug
return f"Captcha Factor {self.slug}"
class Meta:

View File

@ -1,6 +1,8 @@
"""passbook captcha_facot settings"""
"""passbook captcha_factor settings"""
# https://developers.google.com/recaptcha/docs/faq#id-like-to-run-automated-tests-with-recaptcha.-what-should-i-do
RECAPTCHA_PUBLIC_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'
RECAPTCHA_PRIVATE_KEY = '6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe'
NOCAPTCHA = True
INSTALLED_APPS = [
'captcha'

View File

@ -0,0 +1,5 @@
"""dummy factor admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_factors_dummy')

View File

@ -0,0 +1,11 @@
"""passbook dummy factor config"""
from django.apps import AppConfig
class PassbookFactorDummyConfig(AppConfig):
"""passbook dummy factor config"""
name = 'passbook.factors.dummy'
label = 'passbook_factors_dummy'
verbose_name = 'passbook Factors.Dummy'

View File

@ -1,14 +1,12 @@
"""passbook multi-factor authentication engine"""
from structlog import get_logger
from django.http import HttpRequest
from passbook.core.auth.factor import AuthenticationFactor
LOGGER = get_logger()
from passbook.factors.base import AuthenticationFactor
class DummyFactor(AuthenticationFactor):
"""Dummy factor for testing with multiple factors"""
def post(self, request):
def post(self, request: HttpRequest):
"""Just redirect to next factor"""
return self.authenticator.user_ok()

View File

@ -0,0 +1,21 @@
"""passbook administration forms"""
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from passbook.factors.dummy.models import DummyFactor
from passbook.factors.forms import GENERAL_FIELDS
class DummyFactorForm(forms.ModelForm):
"""Form to create/edit Dummy Factor"""
class Meta:
model = DummyFactor
fields = GENERAL_FIELDS
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False)
}

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2.6 on 2019-10-07 14:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('passbook_core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='DummyFactor',
fields=[
('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')),
],
options={
'verbose_name': 'Dummy Factor',
'verbose_name_plural': 'Dummy Factors',
},
bases=('passbook_core.factor',),
),
]

View File

@ -0,0 +1,19 @@
"""dummy factor models"""
from django.utils.translation import gettext as _
from passbook.core.models import Factor
class DummyFactor(Factor):
"""Dummy factor, mostly used to debug"""
type = 'passbook.factors.dummy.factor.DummyFactor'
form = 'passbook.factors.dummy.forms.DummyFactorForm'
def __str__(self):
return f"Dummy Factor {self.slug}"
class Meta:
verbose_name = _('Dummy Factor')
verbose_name_plural = _('Dummy Factors')

View File

@ -0,0 +1,5 @@
"""email factor admin"""
from passbook.lib.admin import admin_autoregister
admin_autoregister('passbook_factors_email')

View File

@ -0,0 +1,15 @@
"""passbook email factor config"""
from importlib import import_module
from django.apps import AppConfig
class PassbookFactorEmailConfig(AppConfig):
"""passbook email factor config"""
name = 'passbook.factors.email'
label = 'passbook_factors_email'
verbose_name = 'passbook Factors.Email'
def ready(self):
import_module('passbook.factors.email.tasks')

View File

@ -0,0 +1,45 @@
"""passbook multi-factor authentication engine"""
from django.contrib import messages
from django.http import HttpRequest
from django.shortcuts import redirect, reverse
from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Nonce
from passbook.factors.base import AuthenticationFactor
from passbook.factors.email.tasks import send_mails
from passbook.factors.email.utils import TemplateEmailMessage
from passbook.lib.config import CONFIG
LOGGER = get_logger()
class EmailFactorView(AuthenticationFactor):
"""Dummy factor for testing with multiple factors"""
def get_context_data(self, **kwargs):
kwargs['show_password_forget_notice'] = CONFIG.y('passbook.password_reset.enabled')
return super().get_context_data(**kwargs)
def get(self, request, *args, **kwargs):
nonce = Nonce.objects.create(user=self.pending_user)
LOGGER.debug("DEBUG %s", str(nonce.uuid))
# Send mail to user
message = TemplateEmailMessage(
subject=_('Forgotten password'),
template_name='email/account_password_reset.html',
template_context={
'url': self.request.build_absolute_uri(
reverse('passbook_core:auth-password-reset',
kwargs={
'nonce': nonce.uuid
})
)})
send_mails(self.authenticator.current_factor, message)
self.authenticator.cleanup()
messages.success(request, _('Check your E-Mails for a password reset link.'))
return redirect('passbook_core:auth-login')
def post(self, request: HttpRequest):
"""Just redirect to next factor"""
return self.authenticator.user_ok()

View File

@ -0,0 +1,43 @@
"""passbook administration forms"""
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
from django.utils.translation import gettext as _
from passbook.factors.email.models import EmailFactor
from passbook.factors.forms import GENERAL_FIELDS
class EmailFactorForm(forms.ModelForm):
"""Form to create/edit Dummy Factor"""
class Meta:
model = EmailFactor
fields = GENERAL_FIELDS + [
'host',
'port',
'username',
'password',
'use_tls',
'use_ssl',
'timeout',
'from_address',
'ssl_keyfile',
'ssl_certfile',
]
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
'policies': FilteredSelectMultiple(_('policies'), False),
'host': forms.TextInput(),
'username': forms.TextInput(),
'password': forms.TextInput(),
'ssl_keyfile': forms.TextInput(),
'ssl_certfile': forms.TextInput(),
}
labels = {
'use_tls': _('Use TLS'),
'use_ssl': _('Use SSL'),
'ssl_keyfile': _('SSL Keyfile (optional)'),
'ssl_certfile': _('SSL Certfile (optional)'),
}

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