Compare commits
24 Commits
version/0.
...
version/0.
| Author | SHA1 | Date | |
|---|---|---|---|
| 48a04744e0 | |||
| 6446ca8bb2 | |||
| b9991465ee | |||
| 3d8242be06 | |||
| ca3bcc565d | |||
| 432176ea2f | |||
| c1dae0b599 | |||
| e70d3b6286 | |||
| 17e6bc921b | |||
| 46111e7cac | |||
| 3b7e47dbe2 | |||
| fff99f0e3d | |||
| 2e15b24f0a | |||
| 088b9592cd | |||
| b1e4e32b83 | |||
| d91a852eda | |||
| 171c5b9759 | |||
| 64290b2a37 | |||
| 72769b8a0a | |||
| 1018309413 | |||
| 6d0ecd228e | |||
| 40a651e66c | |||
| a390bb7b59 | |||
| 245ec65cbb |
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.6.1-beta
|
current_version = 0.6.4-beta
|
||||||
tag = True
|
tag = True
|
||||||
commit = True
|
commit = True
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
|
||||||
|
|||||||
@ -27,7 +27,7 @@ create-base-image:
|
|||||||
before_script:
|
before_script:
|
||||||
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
||||||
script:
|
script:
|
||||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/base.Dockerfile --destination docker.beryju.org/passbook/base:latest --destination docker.beryju.org/passbook/base:0.6.1-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
|
stage: build-base-image
|
||||||
only:
|
only:
|
||||||
refs:
|
refs:
|
||||||
@ -41,7 +41,7 @@ build-dev-image:
|
|||||||
before_script:
|
before_script:
|
||||||
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
||||||
script:
|
script:
|
||||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/dev.Dockerfile --destination docker.beryju.org/passbook/dev:latest --destination docker.beryju.org/passbook/dev:0.6.1-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
|
stage: build-dev-image
|
||||||
only:
|
only:
|
||||||
refs:
|
refs:
|
||||||
@ -95,7 +95,7 @@ build-passbook-server:
|
|||||||
before_script:
|
before_script:
|
||||||
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
||||||
script:
|
script:
|
||||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.6.1-beta
|
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.6.4-beta
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
||||||
- /^version/.*$/
|
- /^version/.*$/
|
||||||
@ -107,7 +107,7 @@ build-passbook-static:
|
|||||||
before_script:
|
before_script:
|
||||||
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
|
||||||
script:
|
script:
|
||||||
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/static.Dockerfile --destination docker.beryju.org/passbook/static:latest --destination docker.beryju.org/passbook/static:0.6.1-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:
|
only:
|
||||||
- tags
|
- tags
|
||||||
- /^version/.*$/
|
- /^version/.*$/
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
FROM docker.beryju.org/passbook/base:latest
|
FROM docker.beryju.org/passbook/base:latest
|
||||||
|
|
||||||
COPY ./passbook/ /app/passbook
|
COPY --chown=passbook:passbook ./passbook/ /app/passbook
|
||||||
COPY ./manage.py /app/
|
COPY ./manage.py /app/
|
||||||
COPY ./docker/uwsgi.ini /app/
|
COPY ./docker/uwsgi.ini /app/
|
||||||
|
|
||||||
USER passbook
|
|
||||||
|
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
|
|
||||||
|
USER passbook
|
||||||
|
|||||||
2
Pipfile
2
Pipfile
@ -8,6 +8,7 @@ celery = "*"
|
|||||||
cherrypy = "*"
|
cherrypy = "*"
|
||||||
defusedxml = "*"
|
defusedxml = "*"
|
||||||
django = "*"
|
django = "*"
|
||||||
|
kombu = "==4.5.0"
|
||||||
django-cors-middleware = "*"
|
django-cors-middleware = "*"
|
||||||
django-filters = "*"
|
django-filters = "*"
|
||||||
django-ipware = "*"
|
django-ipware = "*"
|
||||||
@ -18,7 +19,6 @@ django-otp = "*"
|
|||||||
django-recaptcha = "*"
|
django-recaptcha = "*"
|
||||||
django-redis = "*"
|
django-redis = "*"
|
||||||
django-rest-framework = "*"
|
django-rest-framework = "*"
|
||||||
djangorestframework = "==3.9.4"
|
|
||||||
drf-yasg = "*"
|
drf-yasg = "*"
|
||||||
ldap3 = "*"
|
ldap3 = "*"
|
||||||
lxml = "*"
|
lxml = "*"
|
||||||
|
|||||||
60
Pipfile.lock
generated
60
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "d03d1e494d28a90b39edd1d489afdb5e39ec09bceb18daa2a54b2cc7de61d83c"
|
"sha256": "53d7190ea62f504dc1a36eae952a273e0b2d9f313f23031099d039c3146235b7"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -18,17 +18,17 @@
|
|||||||
"default": {
|
"default": {
|
||||||
"amqp": {
|
"amqp": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:19a917e260178b8d410122712bac69cb3e6db010d68f6101e7307508aded5e68",
|
"sha256:6e649ca13a7df3faacdc8bbb280aa9a6602d22fd9d545336077e573a1f4ff3b8",
|
||||||
"sha256:19d851b879a471fcfdcf01df9936cff924f422baa77653289f7095dedd5fb26a"
|
"sha256:77f1aef9410698d20eaeac5b73a87817365f457a507d82edf292e12cbb83b08d"
|
||||||
],
|
],
|
||||||
"version": "==2.5.1"
|
"version": "==2.5.2"
|
||||||
},
|
},
|
||||||
"asn1crypto": {
|
"asn1crypto": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:d02bf8ea1b964a5ff04ac7891fe3a39150045d1e5e4fe99273ba677d11b92a04",
|
"sha256:0b199f211ae690df3db4fd6c1c4ff976497fb1da689193e368eedbadc53d9292",
|
||||||
"sha256:f822954b90c4c44f002e2cd46d636ab630f1fe4df22c816a82b66505c404eb2a"
|
"sha256:bca90060bd995c3f62c4433168eab407e44bdbdb567b3f3a396a676c1a4c4a3f"
|
||||||
],
|
],
|
||||||
"version": "==1.0.0"
|
"version": "==1.0.1"
|
||||||
},
|
},
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -242,11 +242,10 @@
|
|||||||
},
|
},
|
||||||
"djangorestframework": {
|
"djangorestframework": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:376f4b50340a46c15ae15ddd0c853085f4e66058f97e4dbe7d43ed62f5e60651",
|
"sha256:5488aed8f8df5ec1d70f04b2114abc52ae6729748a176c453313834a9ee179c8",
|
||||||
"sha256:c12869cfd83c33d579b17b3cb28a2ae7322a53c3ce85580c2a2ebe4e3f56c4fb"
|
"sha256:dc81cbf9775c6898a580f6f1f387c4777d12bd87abf0f5406018d32ccae71090"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"version": "==3.10.3"
|
||||||
"version": "==3.9.4"
|
|
||||||
},
|
},
|
||||||
"drf-yasg": {
|
"drf-yasg": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -276,13 +275,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.8"
|
"version": "==2.8"
|
||||||
},
|
},
|
||||||
"importlib-metadata": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:aa18d7378b00b40847790e7c27e11673d7fed219354109d0e7b9e5b25dc3ad26",
|
|
||||||
"sha256:d5f18a79777f3aa179c145737780282e27b508fc8fd688cb17c7a813e8bd39af"
|
|
||||||
],
|
|
||||||
"version": "==0.23"
|
|
||||||
},
|
|
||||||
"inflection": {
|
"inflection": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:18ea7fb7a7d152853386523def08736aa8c32636b047ade55f7578c4edeb16ca"
|
"sha256:18ea7fb7a7d152853386523def08736aa8c32636b047ade55f7578c4edeb16ca"
|
||||||
@ -304,17 +296,18 @@
|
|||||||
},
|
},
|
||||||
"jinja2": {
|
"jinja2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
|
"sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f",
|
||||||
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
|
"sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"
|
||||||
],
|
],
|
||||||
"version": "==2.10.1"
|
"version": "==2.10.3"
|
||||||
},
|
},
|
||||||
"kombu": {
|
"kombu": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:31edb84947996fdda065b6560c128d5673bb913ff34aa19e7b84755217a24deb",
|
"sha256:389ba09e03b15b55b1a7371a441c894fd8121d174f5583bbbca032b9ea8c9edd",
|
||||||
"sha256:c9078124ce2616b29cf6607f0ac3db894c59154252dee6392cdbbe15e5c4b566"
|
"sha256:7b92303af381ef02fad6899fd5f5a9a96031d781356cd8e505fa54ae5ddee181"
|
||||||
],
|
],
|
||||||
"version": "==4.6.5"
|
"index": "pypi",
|
||||||
|
"version": "==4.5.0"
|
||||||
},
|
},
|
||||||
"ldap3": {
|
"ldap3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -566,10 +559,10 @@
|
|||||||
},
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
|
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
|
||||||
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
|
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
|
||||||
],
|
],
|
||||||
"version": "==2019.2"
|
"version": "==2019.3"
|
||||||
},
|
},
|
||||||
"pyyaml": {
|
"pyyaml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -736,13 +729,6 @@
|
|||||||
"sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f"
|
"sha256:cc33599b549f0c8a248cb72f3bf32d77712de1ff7ee8814312eb6456b42c015f"
|
||||||
],
|
],
|
||||||
"version": "==2.0"
|
"version": "==2.0"
|
||||||
},
|
|
||||||
"zipp": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e",
|
|
||||||
"sha256:f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"
|
|
||||||
],
|
|
||||||
"version": "==0.6.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {
|
"develop": {
|
||||||
@ -976,10 +962,10 @@
|
|||||||
},
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:26c0b32e437e54a18161324a2fca3c4b9846b74a8dccddd843113109e1116b32",
|
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
|
||||||
"sha256:c894d57500a4cd2d5c71114aaab77dbab5eabd9022308ce5ac9bb93a60a6f0c7"
|
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
|
||||||
],
|
],
|
||||||
"version": "==2019.2"
|
"version": "==2019.3"
|
||||||
},
|
},
|
||||||
"pyyaml": {
|
"pyyaml": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|||||||
@ -3,7 +3,9 @@
|
|||||||
## Quick instance
|
## Quick instance
|
||||||
|
|
||||||
```
|
```
|
||||||
|
export PASSBOOK_DOMAIN=domain.tld
|
||||||
docker-compose pull
|
docker-compose pull
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
docker-compose exec server ./manage.py migrate
|
||||||
docker-compose exec server ./manage.py createsuperuser
|
docker-compose exec server ./manage.py createsuperuser
|
||||||
```
|
```
|
||||||
|
|||||||
@ -15,5 +15,4 @@ RUN apt-get update && \
|
|||||||
RUN pipenv lock -r > requirements.txt && \
|
RUN pipenv lock -r > requirements.txt && \
|
||||||
pipenv --rm && \
|
pipenv --rm && \
|
||||||
pip install -r requirements.txt --no-cache-dir && \
|
pip install -r requirements.txt --no-cache-dir && \
|
||||||
adduser --system --no-create-home passbook && \
|
adduser --system --no-create-home --uid 1000 --group --home /app passbook
|
||||||
chown -R passbook /app
|
|
||||||
|
|||||||
@ -20,28 +20,15 @@ services:
|
|||||||
- internal
|
- internal
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=false
|
- 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:
|
server:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
image: docker.beryju.org/passbook/server:${TAG:-test}
|
image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest}
|
||||||
command:
|
command:
|
||||||
- uwsgi
|
- uwsgi
|
||||||
- uwsgi.ini
|
- uwsgi.ini
|
||||||
environment:
|
environment:
|
||||||
|
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
|
||||||
- PASSBOOK_REDIS__HOST=redis
|
- PASSBOOK_REDIS__HOST=redis
|
||||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||||
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||||
@ -54,15 +41,20 @@ services:
|
|||||||
- traefik.docker.network=internal
|
- traefik.docker.network=internal
|
||||||
- traefik.frontend.rule=PathPrefix:/
|
- traefik.frontend.rule=PathPrefix:/
|
||||||
worker:
|
worker:
|
||||||
image: docker.beryju.org/passbook/server:${TAG:-test}
|
image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest}
|
||||||
command:
|
command:
|
||||||
- ./manage.py
|
- celery
|
||||||
- worker
|
- worker
|
||||||
|
- --autoscale=10,3
|
||||||
|
- -E
|
||||||
|
- -B
|
||||||
|
- -A=passbook.root.celery
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
labels:
|
labels:
|
||||||
- traefik.enable=false
|
- traefik.enable=false
|
||||||
environment:
|
environment:
|
||||||
|
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
|
||||||
- PASSBOOK_REDIS__HOST=redis
|
- PASSBOOK_REDIS__HOST=redis
|
||||||
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
- PASSBOOK_POSTGRESQL__HOST=postgresql
|
||||||
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
|
||||||
@ -70,7 +62,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: static.Dockerfile
|
dockerfile: static.Dockerfile
|
||||||
image: docker.beryju.org/passbook/static:${TAG:-test}
|
image: docker.beryju.org/passbook/static:latest
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@ -39,7 +39,7 @@ http {
|
|||||||
gzip on;
|
gzip on;
|
||||||
gzip_types application/javascript image/* text/css;
|
gzip_types application/javascript image/* text/css;
|
||||||
gunzip on;
|
gunzip on;
|
||||||
add_header X-passbook-Version 0.6.1-beta;
|
add_header X-passbook-Version 0.6.4-beta;
|
||||||
add_header Vary X-passbook-Version;
|
add_header Vary X-passbook-Version;
|
||||||
root /data/;
|
root /data/;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
appVersion: "0.6.1-beta"
|
appVersion: "0.6.4-beta"
|
||||||
description: A Helm chart for passbook.
|
description: A Helm chart for passbook.
|
||||||
name: passbook
|
name: passbook
|
||||||
version: "0.6.1-beta"
|
version: "0.6.4-beta"
|
||||||
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
icon: https://git.beryju.org/uploads/-/system/project/avatar/108/logo.png
|
||||||
|
|||||||
@ -12,87 +12,5 @@ data:
|
|||||||
host: "{{ .Release.Name }}-redis-master"
|
host: "{{ .Release.Name }}-redis-master"
|
||||||
cache_db: 0
|
cache_db: 0
|
||||||
message_queue_db: 1
|
message_queue_db: 1
|
||||||
|
|
||||||
# Error reporting, sends stacktrace to sentry.beryju.org
|
|
||||||
error_report_enabled: {{ .Values.config.error_reporting }}
|
error_report_enabled: {{ .Values.config.error_reporting }}
|
||||||
|
domain: ".{{ .Values.ingress.hosts[0] }}"
|
||||||
{{- 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
|
|
||||||
saml_idp:
|
|
||||||
signing: true
|
|
||||||
autosubmit: false
|
|
||||||
issuer: passbook
|
|
||||||
assertion_valid_for: 86400
|
|
||||||
# List of python packages with provider types to load.
|
|
||||||
|
|||||||
11
helm/passbook/templates/secret.yaml
Normal file
11
helm/passbook/templates/secret.yaml
Normal 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 }}
|
||||||
@ -39,6 +39,11 @@ spec:
|
|||||||
name: {{ include "passbook.fullname" . }}-config
|
name: {{ include "passbook.fullname" . }}-config
|
||||||
prefix: PASSBOOK_
|
prefix: PASSBOOK_
|
||||||
env:
|
env:
|
||||||
|
- name: PASSBOOK_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ include "passbook.fullname" . }}-secret-key
|
||||||
|
key: secret_key
|
||||||
- name: PASSBOOK_REDIS__PASSWORD
|
- name: PASSBOOK_REDIS__PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
@ -65,6 +70,11 @@ spec:
|
|||||||
name: {{ include "passbook.fullname" . }}-config
|
name: {{ include "passbook.fullname" . }}-config
|
||||||
prefix: PASSBOOK_
|
prefix: PASSBOOK_
|
||||||
env:
|
env:
|
||||||
|
- name: PASSBOOK_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ include "passbook.fullname" . }}-secret-key
|
||||||
|
key: secret_key
|
||||||
- name: PASSBOOK_REDIS__PASSWORD
|
- name: PASSBOOK_REDIS__PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
|
|||||||
@ -44,6 +44,11 @@ spec:
|
|||||||
name: {{ include "passbook.fullname" . }}-config
|
name: {{ include "passbook.fullname" . }}-config
|
||||||
prefix: PASSBOOK_
|
prefix: PASSBOOK_
|
||||||
env:
|
env:
|
||||||
|
- name: PASSBOOK_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: {{ include "passbook.fullname" . }}-secret-key
|
||||||
|
key: secret_key
|
||||||
- name: PASSBOOK_REDIS__PASSWORD
|
- name: PASSBOOK_REDIS__PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
# This is a YAML-formatted file.
|
# This is a YAML-formatted file.
|
||||||
# Declare variables to be passed into your templates.
|
# Declare variables to be passed into your templates.
|
||||||
image:
|
image:
|
||||||
tag: 0.6.1-beta
|
tag: 0.6.4-beta
|
||||||
|
|
||||||
nameOverride: ""
|
nameOverride: ""
|
||||||
|
|
||||||
@ -16,7 +16,6 @@ config:
|
|||||||
|
|
||||||
postgresql:
|
postgresql:
|
||||||
postgresqlDatabase: passbook
|
postgresqlDatabase: passbook
|
||||||
postgresqlPassword: foo
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
cluster:
|
cluster:
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
"""passbook"""
|
"""passbook"""
|
||||||
__version__ = '0.6.1-beta'
|
__version__ = '0.6.4-beta'
|
||||||
|
|||||||
@ -7,6 +7,10 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h1><span class="pficon-users"></span> {% trans "Users" %}</h1>
|
<h1><span class="pficon-users"></span> {% trans "Users" %}</h1>
|
||||||
<hr>
|
<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">
|
<table class="table table-striped table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -61,6 +61,7 @@ urlpatterns = [
|
|||||||
# Users
|
# Users
|
||||||
path('users/', users.UserListView.as_view(),
|
path('users/', users.UserListView.as_view(),
|
||||||
name='users'),
|
name='users'),
|
||||||
|
path('users/create/', users.UserCreateView.as_view(), name='user-create'),
|
||||||
path('users/<int:pk>/update/',
|
path('users/<int:pk>/update/',
|
||||||
users.UserUpdateView.as_view(), name='user-update'),
|
users.UserUpdateView.as_view(), name='user-update'),
|
||||||
path('users/<int:pk>/delete/',
|
path('users/<int:pk>/delete/',
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from django.shortcuts import get_object_or_404, redirect
|
|||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic import DeleteView, ListView, UpdateView
|
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
|
||||||
|
|
||||||
from passbook.admin.forms.users import UserForm
|
from passbook.admin.forms.users import UserForm
|
||||||
from passbook.admin.mixins import AdminRequiredMixin
|
from passbook.admin.mixins import AdminRequiredMixin
|
||||||
@ -19,6 +19,17 @@ class UserListView(AdminRequiredMixin, ListView):
|
|||||||
template_name = 'administration/user/list.html'
|
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):
|
class UserUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
|
||||||
"""Update user"""
|
"""Update user"""
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,5 @@
|
|||||||
"""passbook core app config"""
|
"""passbook core app config"""
|
||||||
from importlib import import_module
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.conf import settings
|
|
||||||
from structlog import get_logger
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
class PassbookCoreConfig(AppConfig):
|
class PassbookCoreConfig(AppConfig):
|
||||||
@ -15,11 +9,3 @@ class PassbookCoreConfig(AppConfig):
|
|||||||
label = 'passbook_core'
|
label = 'passbook_core'
|
||||||
verbose_name = 'passbook Core'
|
verbose_name = 'passbook Core'
|
||||||
mountpoint = ''
|
mountpoint = ''
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
for factors_to_load in settings.PASSBOOK_CORE_FACTORS:
|
|
||||||
try:
|
|
||||||
import_module(factors_to_load)
|
|
||||||
LOGGER.info("Loaded factor", factor_class=factors_to_load)
|
|
||||||
except ImportError as exc:
|
|
||||||
LOGGER.debug(exc)
|
|
||||||
|
|||||||
@ -81,13 +81,3 @@ class SignUpForm(forms.Form):
|
|||||||
if password != password_repeat:
|
if password != password_repeat:
|
||||||
raise ValidationError(_("Passwords don't match"))
|
raise ValidationError(_("Passwords don't match"))
|
||||||
return self.cleaned_data.get('password_repeat')
|
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'
|
|
||||||
}))
|
|
||||||
|
|||||||
18
passbook/core/migrations/0002_nonce_description.py
Normal file
18
passbook/core/migrations/0002_nonce_description.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 2.2.6 on 2019-10-10 11:48
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='nonce',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(blank=True, default=''),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -2,6 +2,7 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
from typing import Optional
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
@ -56,6 +57,7 @@ class User(AbstractUser):
|
|||||||
self.password_change_date = now()
|
self.password_change_date = now()
|
||||||
return super().set_password(password)
|
return super().set_password(password)
|
||||||
|
|
||||||
|
|
||||||
class Provider(models.Model):
|
class Provider(models.Model):
|
||||||
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
|
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
|
||||||
|
|
||||||
@ -69,11 +71,26 @@ class Provider(models.Model):
|
|||||||
return getattr(self, 'name')
|
return getattr(self, 'name')
|
||||||
return super().__str__()
|
return super().__str__()
|
||||||
|
|
||||||
|
|
||||||
class PolicyModel(UUIDModel, CreatedUpdatedModel):
|
class PolicyModel(UUIDModel, CreatedUpdatedModel):
|
||||||
"""Base model which can have policies applied to it"""
|
"""Base model which can have policies applied to it"""
|
||||||
|
|
||||||
policies = models.ManyToManyField('Policy', blank=True)
|
policies = models.ManyToManyField('Policy', blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class UserSettings:
|
||||||
|
"""Dataclass for Factor and Source's user_settings"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
icon: str
|
||||||
|
view_name: str
|
||||||
|
|
||||||
|
def __init__(self, name: str, icon: str, view_name: str):
|
||||||
|
self.name = name
|
||||||
|
self.icon = icon
|
||||||
|
self.view_name = view_name
|
||||||
|
|
||||||
|
|
||||||
class Factor(PolicyModel):
|
class Factor(PolicyModel):
|
||||||
"""Authentication factor, multiple instances of the same Factor can be used"""
|
"""Authentication factor, multiple instances of the same Factor can be used"""
|
||||||
|
|
||||||
@ -86,11 +103,10 @@ class Factor(PolicyModel):
|
|||||||
type = ''
|
type = ''
|
||||||
form = ''
|
form = ''
|
||||||
|
|
||||||
def has_user_settings(self):
|
def user_settings(self) -> Optional[UserSettings]:
|
||||||
"""Entrypoint to integrate with User settings. Can either return False if no
|
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||||
user settings are available, or a tuple or string, string, string where the first string
|
user settings are available, or an instanace of UserSettings."""
|
||||||
is the name the item has, the second string is the icon and the third is the view-name."""
|
return None
|
||||||
return False
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Factor {self.slug}"
|
return f"Factor {self.slug}"
|
||||||
@ -147,11 +163,10 @@ class Source(PolicyModel):
|
|||||||
"""Return additional Info, such as a callback URL. Show in the administration interface."""
|
"""Return additional Info, such as a callback URL. Show in the administration interface."""
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def has_user_settings(self):
|
def user_settings(self) -> Optional[UserSettings]:
|
||||||
"""Entrypoint to integrate with User settings. Can either return False if no
|
"""Entrypoint to integrate with User settings. Can either return None if no
|
||||||
user settings are available, or a tuple or string, string, string where the first string
|
user settings are available, or an instanace of UserSettings."""
|
||||||
is the name the item has, the second string is the icon and the third is the view-name."""
|
return None
|
||||||
return False
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@ -242,21 +257,29 @@ class Invitation(UUIDModel):
|
|||||||
verbose_name = _('Invitation')
|
verbose_name = _('Invitation')
|
||||||
verbose_name_plural = _('Invitations')
|
verbose_name_plural = _('Invitations')
|
||||||
|
|
||||||
|
|
||||||
class Nonce(UUIDModel):
|
class Nonce(UUIDModel):
|
||||||
"""One-time link for password resets/sign-up-confirmations"""
|
"""One-time link for password resets/sign-up-confirmations"""
|
||||||
|
|
||||||
expires = models.DateTimeField(default=default_nonce_duration)
|
expires = models.DateTimeField(default=default_nonce_duration)
|
||||||
user = models.ForeignKey('User', on_delete=models.CASCADE)
|
user = models.ForeignKey('User', on_delete=models.CASCADE)
|
||||||
expiring = models.BooleanField(default=True)
|
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):
|
def __str__(self):
|
||||||
return f"Nonce f{self.uuid.hex} (expires={self.expires})"
|
return f"Nonce f{self.uuid.hex} {self.description} (expires={self.expires})"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _('Nonce')
|
verbose_name = _('Nonce')
|
||||||
verbose_name_plural = _('Nonces')
|
verbose_name_plural = _('Nonces')
|
||||||
|
|
||||||
|
|
||||||
class PropertyMapping(UUIDModel):
|
class PropertyMapping(UUIDModel):
|
||||||
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
|
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +0,0 @@
|
|||||||
"""core settings"""
|
|
||||||
|
|
||||||
PASSBOOK_CORE_FACTORS = [
|
|
||||||
|
|
||||||
]
|
|
||||||
@ -1,28 +1,15 @@
|
|||||||
"""passbook core tasks"""
|
"""passbook core tasks"""
|
||||||
from datetime import datetime
|
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 structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import Nonce
|
from passbook.core.models import Nonce
|
||||||
from passbook.lib.config import CONFIG
|
|
||||||
from passbook.root.celery import CELERY_APP
|
from passbook.root.celery import CELERY_APP
|
||||||
|
|
||||||
LOGGER = get_logger()
|
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()
|
@CELERY_APP.task()
|
||||||
def clean_nonces():
|
def clean_nonces():
|
||||||
"""Remove expired nonces"""
|
"""Remove expired nonces"""
|
||||||
amount, _ = Nonce.objects.filter(expires__lt=datetime.now(), expiring=True).delete()
|
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)
|
||||||
|
|||||||
@ -46,9 +46,6 @@
|
|||||||
<script src="{% static 'js/passbook.js' %}"></script>
|
<script src="{% static 'js/passbook.js' %}"></script>
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<div class="modals">
|
|
||||||
{% include 'partials/about_modal.html' %}
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -46,9 +46,6 @@
|
|||||||
<script src="{% static 'js/passbook.js' %}"></script>
|
<script src="{% static 'js/passbook.js' %}"></script>
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
<div class="modals">
|
|
||||||
{% include 'partials/about_modal.html' %}
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -23,37 +23,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav class="collapse navbar-collapse">
|
<nav class="collapse navbar-collapse">
|
||||||
<ul class="nav navbar-nav navbar-right navbar-iconic navbar-utility">
|
<ul class="nav navbar-nav navbar-right navbar-iconic navbar-utility">
|
||||||
<li class="dropdown">
|
<a href="{% url 'passbook_core:auth-logout' %}" class="btn btn-link nav-item-iconic" aria-haspopup="true" aria-expanded="true">
|
||||||
<button class="btn btn-link dropdown-toggle nav-item-iconic" id="dropdownMenu1" data-toggle="dropdown"
|
<span title="Username" class="fa fa-sign-out"></span>
|
||||||
aria-haspopup="true" aria-expanded="true">
|
<span class="dropdown-title">
|
||||||
<span title="Help" class="fa pficon-help"></span>
|
{% trans 'Logout' %}
|
||||||
</button>
|
</span>
|
||||||
<ul class="dropdown-menu" aria-labelledby="dropdownMenu1">
|
</a>
|
||||||
{% comment %} <li><a href="#0">Help</a></li> {% endcomment %}
|
<a href="{% url 'passbook_core:user-settings' %}" class="btn btn-link nav-item-iconic" aria-haspopup="true" aria-expanded="true">
|
||||||
<li><a data-toggle="modal" data-target="#about-modal" href="#0">{% trans 'About' %}</a></li>
|
<span title="Username" class="fa pficon-user"></span>
|
||||||
</ul>
|
<span class="dropdown-title">
|
||||||
</li>
|
{{ user.username }}
|
||||||
<li class="dropdown">
|
</span>
|
||||||
<button class="btn btn-link dropdown-toggle nav-item-iconic" id="dropdownMenu2" data-toggle="dropdown"
|
</a>
|
||||||
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>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</nav>
|
</nav>
|
||||||
@ -141,18 +122,6 @@
|
|||||||
<span class="list-group-item-value">{% trans 'Audit Log' %}</span>
|
<span class="list-group-item-value">{% trans 'Audit Log' %}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="list-group-item {% is_active_app 'admin' %}">
|
|
||||||
<a href="{% url 'admin:index' %}">
|
|
||||||
<span class="fa fa-database" data-toggle="tooltip" title="{% trans 'Django' %}"></span>
|
|
||||||
<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="fa fa-bug" data-toggle="tooltip" title="{% trans 'Debug' %}"></span>
|
|
||||||
<span class="list-group-item-value">{% trans 'Debug' %}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -16,23 +16,27 @@
|
|||||||
<i class="fa pficon-edit"></i> {% trans 'Details' %}
|
<i class="fa pficon-edit"></i> {% trans 'Details' %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-divider"></li>
|
|
||||||
{% user_factors as uf %}
|
{% user_factors as uf %}
|
||||||
{% for name, icon, link in uf %}
|
{% if uf %}
|
||||||
<li class="{% is_active link %}">
|
<li class="nav-divider"></li>
|
||||||
<a href="{% url link %}">
|
{% endif %}
|
||||||
<i class="{{ icon }}"></i> {{ name }}
|
{% for user_settings in uf %}
|
||||||
</a>
|
<li class="{% is_active user_settings.view_name %}">
|
||||||
</li>
|
<a href="{% url user_settings.view_name %}">
|
||||||
|
<i class="{{ user_settings.icon }}"></i> {{ user_settings.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="nav-divider"></li>
|
|
||||||
{% user_sources as us %}
|
{% user_sources as us %}
|
||||||
{% for name, icon, link in us %}
|
{% if us %}
|
||||||
<li class="{% if link == request.get_full_path %} active {% endif %}">
|
<li class="nav-divider"></li>
|
||||||
<a href="{{ link }}">
|
{% endif %}
|
||||||
<i class="{{ icon }}"></i> {{ name }}
|
{% for user_settings in us %}
|
||||||
</a>
|
<li class="{% if user_settings.view_name == request.get_full_path %} active {% endif %}">
|
||||||
</li>
|
<a href="{{ user_settings.view_name }}">
|
||||||
|
<i class="{{ user_settings.icon }}"></i> {{ user_settings.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,37 +1,38 @@
|
|||||||
"""passbook user settings template tags"""
|
"""passbook user settings template tags"""
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
from django.template.context import RequestContext
|
from django.template.context import RequestContext
|
||||||
|
|
||||||
from passbook.core.models import Factor, Source
|
from passbook.core.models import Factor, Source, UserSettings
|
||||||
from passbook.policies.engine import PolicyEngine
|
from passbook.policies.engine import PolicyEngine
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def user_factors(context: RequestContext):
|
def user_factors(context: RequestContext) -> List[UserSettings]:
|
||||||
"""Return list of all factors which apply to user"""
|
"""Return list of all factors which apply to user"""
|
||||||
user = context.get('request').user
|
user = context.get('request').user
|
||||||
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
|
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
|
||||||
matching_factors = []
|
matching_factors: List[UserSettings] = []
|
||||||
for factor in _all_factors:
|
for factor in _all_factors:
|
||||||
_link = factor.has_user_settings()
|
user_settings = factor.user_settings()
|
||||||
policy_engine = PolicyEngine(factor.policies.all())
|
policy_engine = PolicyEngine(factor.policies.all())
|
||||||
policy_engine.for_user(user).with_request(context.get('request')).build()
|
policy_engine.for_user(user).with_request(context.get('request')).build()
|
||||||
if policy_engine.passing and _link:
|
if policy_engine.passing and user_settings:
|
||||||
matching_factors.append(_link)
|
matching_factors.append(user_settings)
|
||||||
return matching_factors
|
return matching_factors
|
||||||
|
|
||||||
@register.simple_tag(takes_context=True)
|
@register.simple_tag(takes_context=True)
|
||||||
def user_sources(context: RequestContext):
|
def user_sources(context: RequestContext) -> List[UserSettings]:
|
||||||
"""Return a list of all sources which are enabled for the user"""
|
"""Return a list of all sources which are enabled for the user"""
|
||||||
user = context.get('request').user
|
user = context.get('request').user
|
||||||
_all_sources = Source.objects.filter(enabled=True).select_subclasses()
|
_all_sources = Source.objects.filter(enabled=True).select_subclasses()
|
||||||
matching_sources = []
|
matching_sources: List[UserSettings] = []
|
||||||
for factor in _all_sources:
|
for factor in _all_sources:
|
||||||
_link = factor.has_user_settings()
|
user_settings = factor.user_settings()
|
||||||
policy_engine = PolicyEngine(factor.policies.all())
|
policy_engine = PolicyEngine(factor.policies.all())
|
||||||
policy_engine.for_user(user).with_request(context.get('request')).build()
|
policy_engine.for_user(user).with_request(context.get('request')).build()
|
||||||
if policy_engine.passing and _link:
|
if policy_engine.passing and user_settings:
|
||||||
matching_sources.append(_link)
|
matching_sources.append(user_settings)
|
||||||
return matching_sources
|
return matching_sources
|
||||||
|
|||||||
@ -15,7 +15,6 @@ from structlog import get_logger
|
|||||||
from passbook.core.forms.authentication import LoginForm, SignUpForm
|
from passbook.core.forms.authentication import LoginForm, SignUpForm
|
||||||
from passbook.core.models import Invitation, Nonce, Source, User
|
from passbook.core.models import Invitation, Nonce, Source, User
|
||||||
from passbook.core.signals import invitation_used, user_signed_up
|
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.password.exceptions import PasswordPolicyInvalid
|
||||||
from passbook.factors.view import AuthenticationView, _redirect_with_qs
|
from passbook.factors.view import AuthenticationView, _redirect_with_qs
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
@ -97,7 +96,7 @@ class SignUpView(UserPassesTestMixin, FormView):
|
|||||||
template_name = 'login/form.html'
|
template_name = 'login/form.html'
|
||||||
form_class = SignUpForm
|
form_class = SignUpForm
|
||||||
success_url = '.'
|
success_url = '.'
|
||||||
# Invitation insatnce, if invitation link was used
|
# Invitation instance, if invitation link was used
|
||||||
_invitation = None
|
_invitation = None
|
||||||
# Instance of newly created user
|
# Instance of newly created user
|
||||||
_user = None
|
_user = None
|
||||||
@ -152,23 +151,23 @@ class SignUpView(UserPassesTestMixin, FormView):
|
|||||||
for error in exc.messages:
|
for error in exc.messages:
|
||||||
errors.append(error)
|
errors.append(error)
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
needs_confirmation = True
|
# needs_confirmation = True
|
||||||
if self._invitation and not self._invitation.needs_confirmation:
|
# if self._invitation and not self._invitation.needs_confirmation:
|
||||||
needs_confirmation = False
|
# needs_confirmation = False
|
||||||
if needs_confirmation:
|
# if needs_confirmation:
|
||||||
nonce = Nonce.objects.create(user=self._user)
|
# nonce = Nonce.objects.create(user=self._user)
|
||||||
LOGGER.debug(str(nonce.uuid))
|
# LOGGER.debug(str(nonce.uuid))
|
||||||
# Send email to user
|
# # Send email to user
|
||||||
send_email.delay(self._user.email, _('Confirm your account.'),
|
# send_email.delay(self._user.email, _('Confirm your account.'),
|
||||||
'email/account_confirm.html', {
|
# 'email/account_confirm.html', {
|
||||||
'url': self.request.build_absolute_uri(
|
# 'url': self.request.build_absolute_uri(
|
||||||
reverse('passbook_core:auth-sign-up-confirm', kwargs={
|
# reverse('passbook_core:auth-sign-up-confirm', kwargs={
|
||||||
'nonce': nonce.uuid
|
# 'nonce': nonce.uuid
|
||||||
})
|
# })
|
||||||
)
|
# )
|
||||||
})
|
# })
|
||||||
self._user.is_active = False
|
# self._user.is_active = False
|
||||||
self._user.save()
|
# self._user.save()
|
||||||
self.consume_invitation()
|
self.consume_invitation()
|
||||||
messages.success(self.request, _("Successfully signed up!"))
|
messages.success(self.request, _("Successfully signed up!"))
|
||||||
LOGGER.debug("Successfully signed up %s",
|
LOGGER.debug("Successfully signed up %s",
|
||||||
|
|||||||
@ -14,13 +14,14 @@ class AuthenticationFactor(TemplateView):
|
|||||||
|
|
||||||
form: ModelForm = None
|
form: ModelForm = None
|
||||||
required: bool = True
|
required: bool = True
|
||||||
authenticator: AuthenticationView = None
|
authenticator: AuthenticationView
|
||||||
pending_user: User = None
|
pending_user: User
|
||||||
request: HttpRequest = None
|
request: HttpRequest = None
|
||||||
template_name = 'login/form_with_user.html'
|
template_name = 'login/form_with_user.html'
|
||||||
|
|
||||||
def __init__(self, authenticator: AuthenticationView):
|
def __init__(self, authenticator: AuthenticationView):
|
||||||
self.authenticator = authenticator
|
self.authenticator = authenticator
|
||||||
|
self.pending_user = None
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs['config'] = CONFIG.y('passbook')
|
kwargs['config'] = CONFIG.y('passbook')
|
||||||
|
|||||||
5
passbook/factors/captcha/admin.py
Normal file
5
passbook/factors/captcha/admin.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""captcha factor admin"""
|
||||||
|
|
||||||
|
from passbook.lib.admin import admin_autoregister
|
||||||
|
|
||||||
|
admin_autoregister('passbook_factors_captcha')
|
||||||
@ -1,6 +1,8 @@
|
|||||||
"""passbook captcha factor forms"""
|
"""passbook captcha factor forms"""
|
||||||
from captcha.fields import ReCaptchaField
|
from captcha.fields import ReCaptchaField
|
||||||
from django import forms
|
from django import forms
|
||||||
|
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from passbook.factors.captcha.models import CaptchaFactor
|
from passbook.factors.captcha.models import CaptchaFactor
|
||||||
from passbook.factors.forms import GENERAL_FIELDS
|
from passbook.factors.forms import GENERAL_FIELDS
|
||||||
@ -21,6 +23,7 @@ class CaptchaFactorForm(forms.ModelForm):
|
|||||||
widgets = {
|
widgets = {
|
||||||
'name': forms.TextInput(),
|
'name': forms.TextInput(),
|
||||||
'order': forms.NumberInput(),
|
'order': forms.NumberInput(),
|
||||||
|
'policies': FilteredSelectMultiple(_('policies'), False),
|
||||||
'public_key': forms.TextInput(),
|
'public_key': forms.TextInput(),
|
||||||
'private_key': forms.TextInput(),
|
'private_key': forms.TextInput(),
|
||||||
}
|
}
|
||||||
|
|||||||
0
passbook/factors/email/__init__.py
Normal file
0
passbook/factors/email/__init__.py
Normal file
5
passbook/factors/email/admin.py
Normal file
5
passbook/factors/email/admin.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
"""email factor admin"""
|
||||||
|
|
||||||
|
from passbook.lib.admin import admin_autoregister
|
||||||
|
|
||||||
|
admin_autoregister('passbook_factors_email')
|
||||||
15
passbook/factors/email/apps.py
Normal file
15
passbook/factors/email/apps.py
Normal 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')
|
||||||
45
passbook/factors/email/factor.py
Normal file
45
passbook/factors/email/factor.py
Normal 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()
|
||||||
43
passbook/factors/email/forms.py
Normal file
43
passbook/factors/email/forms.py
Normal 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)'),
|
||||||
|
}
|
||||||
37
passbook/factors/email/migrations/0001_initial.py
Normal file
37
passbook/factors/email/migrations/0001_initial.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# Generated by Django 2.2.6 on 2019-10-08 12:23
|
||||||
|
|
||||||
|
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='EmailFactor',
|
||||||
|
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')),
|
||||||
|
('host', models.TextField(default='localhost')),
|
||||||
|
('port', models.IntegerField(default=25)),
|
||||||
|
('username', models.TextField(blank=True, default='')),
|
||||||
|
('password', models.TextField(blank=True, default='')),
|
||||||
|
('use_tls', models.BooleanField(default=False)),
|
||||||
|
('use_ssl', models.BooleanField(default=False)),
|
||||||
|
('timeout', models.IntegerField(default=0)),
|
||||||
|
('ssl_keyfile', models.TextField(blank=True, default=None, null=True)),
|
||||||
|
('ssl_certfile', models.TextField(blank=True, default=None, null=True)),
|
||||||
|
('from_address', models.EmailField(default='system@passbook.local', max_length=254)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Email Factor',
|
||||||
|
'verbose_name_plural': 'Email Factors',
|
||||||
|
},
|
||||||
|
bases=('passbook_core.factor',),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
passbook/factors/email/migrations/__init__.py
Normal file
0
passbook/factors/email/migrations/__init__.py
Normal file
48
passbook/factors/email/models.py
Normal file
48
passbook/factors/email/models.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
"""email factor models"""
|
||||||
|
from django.core.mail.backends.smtp import EmailBackend
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
|
from passbook.core.models import Factor
|
||||||
|
|
||||||
|
|
||||||
|
class EmailFactor(Factor):
|
||||||
|
"""email factor"""
|
||||||
|
|
||||||
|
host = models.TextField(default='localhost')
|
||||||
|
port = models.IntegerField(default=25)
|
||||||
|
username = models.TextField(default='', blank=True)
|
||||||
|
password = models.TextField(default='', blank=True)
|
||||||
|
use_tls = models.BooleanField(default=False)
|
||||||
|
use_ssl = models.BooleanField(default=False)
|
||||||
|
timeout = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
ssl_keyfile = models.TextField(default=None, blank=True, null=True)
|
||||||
|
ssl_certfile = models.TextField(default=None, blank=True, null=True)
|
||||||
|
|
||||||
|
from_address = models.EmailField(default='system@passbook.local')
|
||||||
|
|
||||||
|
type = 'passbook.factors.email.factor.EmailFactorView'
|
||||||
|
form = 'passbook.factors.email.forms.EmailFactorForm'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backend(self) -> EmailBackend:
|
||||||
|
"""Get fully configured EMail Backend instance"""
|
||||||
|
return EmailBackend(
|
||||||
|
host=self.host,
|
||||||
|
port=self.port,
|
||||||
|
username=self.username,
|
||||||
|
password=self.password,
|
||||||
|
use_tls=self.use_tls,
|
||||||
|
use_ssl=self.use_ssl,
|
||||||
|
timeout=self.timeout,
|
||||||
|
ssl_certfile=self.ssl_certfile,
|
||||||
|
ssl_keyfile=self.ssl_keyfile)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Email Factor {self.slug}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
|
||||||
|
verbose_name = _('Email Factor')
|
||||||
|
verbose_name_plural = _('Email Factors')
|
||||||
39
passbook/factors/email/tasks.py
Normal file
39
passbook/factors/email/tasks.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
"""email factor tasks"""
|
||||||
|
from smtplib import SMTPException
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from celery import group
|
||||||
|
from django.core.mail import EmailMessage
|
||||||
|
|
||||||
|
from passbook.factors.email.models import EmailFactor
|
||||||
|
from passbook.root.celery import CELERY_APP
|
||||||
|
|
||||||
|
|
||||||
|
def send_mails(factor: EmailFactor, *messages: List[EmailMessage]):
|
||||||
|
"""Wrapper to convert EmailMessage to dict and send it from worker"""
|
||||||
|
tasks = []
|
||||||
|
for message in messages:
|
||||||
|
tasks.append(_send_mail_task.s(factor.pk, message.__dict__))
|
||||||
|
lazy_group = group(*tasks)
|
||||||
|
promise = lazy_group()
|
||||||
|
return promise
|
||||||
|
|
||||||
|
@CELERY_APP.task(bind=True)
|
||||||
|
def _send_mail_task(self, email_factor_pk: int, message: Dict[Any, Any]):
|
||||||
|
"""Send E-Mail according to EmailFactor parameters from background worker.
|
||||||
|
Automatically retries if message couldn't be sent."""
|
||||||
|
factor: EmailFactor = EmailFactor.objects.get(pk=email_factor_pk)
|
||||||
|
backend = factor.backend
|
||||||
|
backend.open()
|
||||||
|
# Since django's EmailMessage objects are not JSON serialisable,
|
||||||
|
# we need to rebuild them from a dict
|
||||||
|
message_object = EmailMessage()
|
||||||
|
for key, value in message.items():
|
||||||
|
setattr(message_object, key, value)
|
||||||
|
message_object.from_email = factor.from_address
|
||||||
|
try:
|
||||||
|
num_sent = factor.backend.send_messages([message_object])
|
||||||
|
except SMTPException as exc:
|
||||||
|
raise self.retry(exc=exc)
|
||||||
|
if num_sent != 1:
|
||||||
|
raise self.retry()
|
||||||
28
passbook/factors/email/utils.py
Normal file
28
passbook/factors/email/utils.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""email utils"""
|
||||||
|
from django.core.mail import EmailMultiAlternatives
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.utils.html import strip_tags
|
||||||
|
|
||||||
|
|
||||||
|
class TemplateEmailMessage(EmailMultiAlternatives):
|
||||||
|
"""Wrapper around EmailMultiAlternatives with integrated template rendering"""
|
||||||
|
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
def __init__(self, subject='', body=None, from_email=None, to=None, bcc=None,
|
||||||
|
connection=None, attachments=None, headers=None, cc=None,
|
||||||
|
reply_to=None, template_name=None, template_context=None):
|
||||||
|
html_content = render_to_string(template_name, template_context)
|
||||||
|
if not body:
|
||||||
|
body = strip_tags(html_content)
|
||||||
|
super().__init__(
|
||||||
|
subject=subject,
|
||||||
|
body=body,
|
||||||
|
from_email=from_email,
|
||||||
|
to=to,
|
||||||
|
bcc=bcc,
|
||||||
|
connection=connection,
|
||||||
|
attachments=attachments,
|
||||||
|
headers=headers,
|
||||||
|
cc=cc,
|
||||||
|
reply_to=reply_to)
|
||||||
|
self.attach_alternative(html_content, "text/html")
|
||||||
@ -3,7 +3,7 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from passbook.core.models import Factor
|
from passbook.core.models import Factor, UserSettings
|
||||||
|
|
||||||
|
|
||||||
class OTPFactor(Factor):
|
class OTPFactor(Factor):
|
||||||
@ -15,8 +15,8 @@ class OTPFactor(Factor):
|
|||||||
type = 'passbook.factors.otp.factors.OTPFactor'
|
type = 'passbook.factors.otp.factors.OTPFactor'
|
||||||
form = 'passbook.factors.otp.forms.OTPFactorForm'
|
form = 'passbook.factors.otp.forms.OTPFactorForm'
|
||||||
|
|
||||||
def has_user_settings(self):
|
def user_settings(self) -> UserSettings:
|
||||||
return _('OTP'), 'pficon-locked', 'passbook_otp:otp-user-settings'
|
return UserSettings(_('OTP'), 'pficon-locked', 'passbook_factors_otp:otp-user-settings')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"OTP Factor {self.slug}"
|
return f"OTP Factor {self.slug}"
|
||||||
|
|||||||
@ -26,10 +26,10 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{% if not state %}
|
{% if not state %}
|
||||||
<a href="{% url 'passbook_otp:otp-enable' %}"
|
<a href="{% url 'passbook_factors_otp:otp-enable' %}"
|
||||||
class="btn btn-success btn-sm">{% trans "Enable OTP" %}</a>
|
class="btn btn-success btn-sm">{% trans "Enable OTP" %}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'passbook_otp:otp-disable' %}"
|
<a href="{% url 'passbook_factors_otp:otp-disable' %}"
|
||||||
class="btn btn-danger btn-sm">{% trans "Disable OTP" %}</a>
|
class="btn btn-danger btn-sm">{% trans "Disable OTP" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -21,8 +21,8 @@ from passbook.factors.otp.utils import otpauth_url
|
|||||||
from passbook.lib.boilerplate import NeverCacheMixin
|
from passbook.lib.boilerplate import NeverCacheMixin
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
OTP_SESSION_KEY = 'passbook_otp_key'
|
OTP_SESSION_KEY = 'passbook_factors_otp_key'
|
||||||
OTP_SETTING_UP_KEY = 'passbook_otp_setup'
|
OTP_SETTING_UP_KEY = 'passbook_factors_otp_setup'
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
class UserSettingsView(LoginRequiredMixin, TemplateView):
|
class UserSettingsView(LoginRequiredMixin, TemplateView):
|
||||||
@ -61,7 +61,7 @@ class DisableView(LoginRequiredMixin, View):
|
|||||||
# current=True,
|
# current=True,
|
||||||
# request=request,
|
# request=request,
|
||||||
# send_notification=True)
|
# send_notification=True)
|
||||||
return redirect(reverse('passbook_otp:otp-user-settings'))
|
return redirect(reverse('passbook_factors_otp:otp-user-settings'))
|
||||||
|
|
||||||
class EnableView(LoginRequiredMixin, FormView):
|
class EnableView(LoginRequiredMixin, FormView):
|
||||||
"""View to set up OTP"""
|
"""View to set up OTP"""
|
||||||
@ -88,7 +88,7 @@ class EnableView(LoginRequiredMixin, FormView):
|
|||||||
if finished_totp_devices.exists() and finished_static_devices.exists():
|
if finished_totp_devices.exists() and finished_static_devices.exists():
|
||||||
messages.error(request, _('You already have TOTP enabled!'))
|
messages.error(request, _('You already have TOTP enabled!'))
|
||||||
del request.session[OTP_SETTING_UP_KEY]
|
del request.session[OTP_SETTING_UP_KEY]
|
||||||
return redirect('passbook_otp:otp-user-settings')
|
return redirect('passbook_factors_otp:otp-user-settings')
|
||||||
request.session[OTP_SETTING_UP_KEY] = True
|
request.session[OTP_SETTING_UP_KEY] = True
|
||||||
# Check if there's an unconfirmed device left to set up
|
# Check if there's an unconfirmed device left to set up
|
||||||
totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False)
|
totp_devices = TOTPDevice.objects.filter(user=request.user, confirmed=False)
|
||||||
@ -121,7 +121,7 @@ class EnableView(LoginRequiredMixin, FormView):
|
|||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class=form_class)
|
form = super().get_form(form_class=form_class)
|
||||||
form.device = self.totp_device
|
form.device = self.totp_device
|
||||||
form.fields['qr_code'].initial = reverse('passbook_otp:otp-qr')
|
form.fields['qr_code'].initial = reverse('passbook_factors_otp:otp-qr')
|
||||||
tokens = [(x.token, x.token) for x in self.static_device.token_set.all()]
|
tokens = [(x.token, x.token) for x in self.static_device.token_set.all()]
|
||||||
form.fields['tokens'].choices = tokens
|
form.fields['tokens'].choices = tokens
|
||||||
return form
|
return form
|
||||||
@ -142,7 +142,7 @@ class EnableView(LoginRequiredMixin, FormView):
|
|||||||
# current=True,
|
# current=True,
|
||||||
# request=self.request,
|
# request=self.request,
|
||||||
# send_notification=True)
|
# send_notification=True)
|
||||||
return redirect('passbook_otp:otp-user-settings')
|
return redirect('passbook_factors_otp:otp-user-settings')
|
||||||
|
|
||||||
class QRView(NeverCacheMixin, View):
|
class QRView(NeverCacheMixin, View):
|
||||||
"""View returns an SVG image with the OTP token information"""
|
"""View returns an SVG image with the OTP token information"""
|
||||||
|
|||||||
@ -1,20 +1,18 @@
|
|||||||
"""passbook multi-factor authentication engine"""
|
"""passbook multi-factor authentication engine"""
|
||||||
from inspect import Signature
|
from inspect import Signature
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.contrib import messages
|
|
||||||
from django.contrib.auth import _clean_credentials
|
from django.contrib.auth import _clean_credentials
|
||||||
from django.contrib.auth.signals import user_login_failed
|
from django.contrib.auth.signals import user_login_failed
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.forms.utils import ErrorList
|
from django.forms.utils import ErrorList
|
||||||
from django.shortcuts import redirect, reverse
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.forms.authentication import PasswordFactorForm
|
from passbook.core.models import User
|
||||||
from passbook.core.models import Nonce
|
|
||||||
from passbook.core.tasks import send_email
|
|
||||||
from passbook.factors.base import AuthenticationFactor
|
from passbook.factors.base import AuthenticationFactor
|
||||||
|
from passbook.factors.password.forms import PasswordForm
|
||||||
from passbook.factors.view import AuthenticationView
|
from passbook.factors.view import AuthenticationView
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
from passbook.lib.utils.reflection import path_to_class
|
from passbook.lib.utils.reflection import path_to_class
|
||||||
@ -22,7 +20,7 @@ from passbook.lib.utils.reflection import path_to_class
|
|||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
def authenticate(request, backends, **credentials):
|
def authenticate(request, backends, **credentials) -> Optional[User]:
|
||||||
"""If the given credentials are valid, return a User object.
|
"""If the given credentials are valid, return a User object.
|
||||||
|
|
||||||
Customized version of django's authenticate, which accepts a list of backends"""
|
Customized version of django's authenticate, which accepts a list of backends"""
|
||||||
@ -55,32 +53,9 @@ def authenticate(request, backends, **credentials):
|
|||||||
class PasswordFactor(FormView, AuthenticationFactor):
|
class PasswordFactor(FormView, AuthenticationFactor):
|
||||||
"""Authentication factor which authenticates against django's AuthBackend"""
|
"""Authentication factor which authenticates against django's AuthBackend"""
|
||||||
|
|
||||||
form_class = PasswordFactorForm
|
form_class = PasswordForm
|
||||||
template_name = 'login/factors/backend.html'
|
template_name = 'login/factors/backend.html'
|
||||||
|
|
||||||
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):
|
|
||||||
if 'password-forgotten' in request.GET:
|
|
||||||
nonce = Nonce.objects.create(user=self.pending_user)
|
|
||||||
LOGGER.debug("DEBUG %s", str(nonce.uuid))
|
|
||||||
# Send mail to user
|
|
||||||
send_email.delay(self.pending_user.email, _('Forgotten password'),
|
|
||||||
'email/account_password_reset.html', {
|
|
||||||
'url': self.request.build_absolute_uri(
|
|
||||||
reverse('passbook_core:auth-password-reset',
|
|
||||||
kwargs={
|
|
||||||
'nonce': nonce.uuid
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
self.authenticator.cleanup()
|
|
||||||
messages.success(request, _('Check your E-Mails for a password reset link.'))
|
|
||||||
return redirect('passbook_core:auth-login')
|
|
||||||
return super().get(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""Authenticate against django's authentication backend"""
|
"""Authenticate against django's authentication backend"""
|
||||||
uid_fields = CONFIG.y('passbook.uid_fields')
|
uid_fields = CONFIG.y('passbook.uid_fields')
|
||||||
|
|||||||
@ -16,13 +16,23 @@ def get_authentication_backends():
|
|||||||
yield backend, getattr(klass(), 'name', '%s (%s)' % (klass.__name__, klass.__module__))
|
yield backend, getattr(klass(), 'name', '%s (%s)' % (klass.__name__, klass.__module__))
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordForm(forms.Form):
|
||||||
|
"""Password authentication form"""
|
||||||
|
|
||||||
|
password = forms.CharField(widget=forms.PasswordInput(attrs={
|
||||||
|
'placeholder': _('Password'),
|
||||||
|
'autofocus': 'autofocus',
|
||||||
|
'autocomplete': 'current-password'
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
class PasswordFactorForm(forms.ModelForm):
|
class PasswordFactorForm(forms.ModelForm):
|
||||||
"""Form to create/edit Password Factors"""
|
"""Form to create/edit Password Factors"""
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
model = PasswordFactor
|
model = PasswordFactor
|
||||||
fields = GENERAL_FIELDS + ['backends', 'password_policies']
|
fields = GENERAL_FIELDS + ['backends', 'password_policies', 'reset_factors']
|
||||||
widgets = {
|
widgets = {
|
||||||
'name': forms.TextInput(),
|
'name': forms.TextInput(),
|
||||||
'order': forms.NumberInput(),
|
'order': forms.NumberInput(),
|
||||||
@ -30,4 +40,5 @@ class PasswordFactorForm(forms.ModelForm):
|
|||||||
'backends': FilteredSelectMultiple(_('backends'), False,
|
'backends': FilteredSelectMultiple(_('backends'), False,
|
||||||
choices=get_authentication_backends()),
|
choices=get_authentication_backends()),
|
||||||
'password_policies': FilteredSelectMultiple(_('password policies'), False),
|
'password_policies': FilteredSelectMultiple(_('password policies'), False),
|
||||||
|
'reset_factors': FilteredSelectMultiple(_('reset factors'), False),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.2.6 on 2019-10-08 09:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_core', '0001_initial'),
|
||||||
|
('passbook_factors_password', '0002_auto_20191007_1411'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='passwordfactor',
|
||||||
|
name='reset_factors',
|
||||||
|
field=models.ManyToManyField(blank=True, related_name='reset_factors', to='passbook_core.Factor'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -3,7 +3,7 @@ from django.contrib.postgres.fields import ArrayField
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from passbook.core.models import Factor, Policy, User
|
from passbook.core.models import Factor, Policy, User, UserSettings
|
||||||
|
|
||||||
|
|
||||||
class PasswordFactor(Factor):
|
class PasswordFactor(Factor):
|
||||||
@ -11,12 +11,14 @@ class PasswordFactor(Factor):
|
|||||||
|
|
||||||
backends = ArrayField(models.TextField())
|
backends = ArrayField(models.TextField())
|
||||||
password_policies = models.ManyToManyField(Policy, blank=True)
|
password_policies = models.ManyToManyField(Policy, blank=True)
|
||||||
|
reset_factors = models.ManyToManyField(Factor, blank=True, related_name='reset_factors')
|
||||||
|
|
||||||
type = 'passbook.factors.password.factor.PasswordFactor'
|
type = 'passbook.factors.password.factor.PasswordFactor'
|
||||||
form = 'passbook.factors.password.forms.PasswordFactorForm'
|
form = 'passbook.factors.password.forms.PasswordFactorForm'
|
||||||
|
|
||||||
def has_user_settings(self):
|
def user_settings(self):
|
||||||
return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password'
|
return UserSettings(_('Change Password'), 'pficon-key',
|
||||||
|
'passbook_core:user-change-password')
|
||||||
|
|
||||||
def password_passes(self, user: User) -> bool:
|
def password_passes(self, user: User) -> bool:
|
||||||
"""Return true if user's password passes, otherwise False or raise Exception"""
|
"""Return true if user's password passes, otherwise False or raise Exception"""
|
||||||
|
|||||||
@ -16,9 +16,7 @@ debug: false
|
|||||||
# Error reporting, sends stacktrace to sentry.services.beryju.org
|
# Error reporting, sends stacktrace to sentry.services.beryju.org
|
||||||
error_report_enabled: true
|
error_report_enabled: true
|
||||||
|
|
||||||
domains:
|
domain: localhost
|
||||||
- passbook.local
|
|
||||||
primary_domain: 'localhost'
|
|
||||||
|
|
||||||
passbook:
|
passbook:
|
||||||
sign_up:
|
sign_up:
|
||||||
@ -48,8 +46,6 @@ passbook:
|
|||||||
uid_fields:
|
uid_fields:
|
||||||
- username
|
- username
|
||||||
- email
|
- email
|
||||||
session:
|
|
||||||
remember_age: 2592000 # 60 * 60 * 24 * 30, one month
|
|
||||||
# Provider-specific settings
|
# Provider-specific settings
|
||||||
ldap:
|
ldap:
|
||||||
# Which field from `uid_fields` maps to which LDAP Attribute
|
# Which field from `uid_fields` maps to which LDAP Attribute
|
||||||
@ -61,6 +57,3 @@ ldap:
|
|||||||
username: "%(sAMAccountName)s"
|
username: "%(sAMAccountName)s"
|
||||||
email: "%(mail)s"
|
email: "%(mail)s"
|
||||||
name: "%(displayName)"
|
name: "%(displayName)"
|
||||||
app_gw:
|
|
||||||
listen: 0.0.0.0
|
|
||||||
port: 8000
|
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
|
||||||
from django.utils.cache import patch_vary_headers
|
|
||||||
from django.utils.http import cookie_date
|
|
||||||
from structlog import get_logger
|
|
||||||
|
|
||||||
from passbook.factors.view import AuthenticationView
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
|
||||||
|
|
||||||
class SessionHostDomainMiddleware(SessionMiddleware):
|
|
||||||
|
|
||||||
def process_request(self, request):
|
|
||||||
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
|
||||||
request.session = self.SessionStore(session_key)
|
|
||||||
|
|
||||||
def process_response(self, request, response):
|
|
||||||
"""
|
|
||||||
If request.session was modified, or if the configuration is to save the
|
|
||||||
session every time, save the changes and set a session cookie.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
accessed = request.session.accessed
|
|
||||||
modified = request.session.modified
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if accessed:
|
|
||||||
patch_vary_headers(response, ('Cookie',))
|
|
||||||
if modified or settings.SESSION_SAVE_EVERY_REQUEST:
|
|
||||||
if request.session.get_expire_at_browser_close():
|
|
||||||
max_age = None
|
|
||||||
expires = None
|
|
||||||
else:
|
|
||||||
max_age = request.session.get_expiry_age()
|
|
||||||
expires_time = time.time() + max_age
|
|
||||||
expires = cookie_date(expires_time)
|
|
||||||
# Save the session data and refresh the client cookie.
|
|
||||||
# Skip session save for 500 responses, refs #3881.
|
|
||||||
if response.status_code != 500:
|
|
||||||
request.session.save()
|
|
||||||
hosts = [request.get_host().split(':')[0]]
|
|
||||||
if AuthenticationView.SESSION_FORCE_COOKIE_HOSTNAME in request.session:
|
|
||||||
hosts.append(request.session[AuthenticationView.SESSION_FORCE_COOKIE_HOSTNAME])
|
|
||||||
LOGGER.debug("Setting hosts for session", hosts=hosts)
|
|
||||||
for host in hosts:
|
|
||||||
response.set_cookie(settings.SESSION_COOKIE_NAME,
|
|
||||||
request.session.session_key, max_age=max_age,
|
|
||||||
expires=expires, domain=host,
|
|
||||||
path=settings.SESSION_COOKIE_PATH,
|
|
||||||
secure=settings.SESSION_COOKIE_SECURE or None,
|
|
||||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None)
|
|
||||||
return response
|
|
||||||
@ -1,7 +1,8 @@
|
|||||||
"""passbook app_gw views"""
|
"""passbook app_gw views"""
|
||||||
from pprint import pprint
|
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.cache import cache
|
||||||
from django.http import HttpRequest, HttpResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
@ -12,15 +13,26 @@ from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
|||||||
ORIGINAL_URL = 'HTTP_X_ORIGINAL_URL'
|
ORIGINAL_URL = 'HTTP_X_ORIGINAL_URL'
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
def cache_key(session_cookie: str, request: HttpRequest) -> str:
|
||||||
|
"""Cache Key for request fingerprinting"""
|
||||||
|
fprint = '_'.join([
|
||||||
|
session_cookie,
|
||||||
|
request.META.get('HTTP_HOST'),
|
||||||
|
request.META.get('PATH_INFO'),
|
||||||
|
])
|
||||||
|
return f"app_gw_{fprint}"
|
||||||
|
|
||||||
class NginxCheckView(AccessMixin, View):
|
class NginxCheckView(AccessMixin, View):
|
||||||
|
"""View used by nginx's auth_request module"""
|
||||||
|
|
||||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||||
pprint(request.META)
|
session_cookie = request.COOKIES.get(settings.SESSION_COOKIE_NAME, '')
|
||||||
|
_cache_key = cache_key(session_cookie, request)
|
||||||
|
if cache.get(_cache_key):
|
||||||
|
return HttpResponse(status=202)
|
||||||
parsed_url = urlparse(request.META.get(ORIGINAL_URL))
|
parsed_url = urlparse(request.META.get(ORIGINAL_URL))
|
||||||
# request.session[AuthenticationView.SESSION_ALLOW_ABSOLUTE_NEXT] = True
|
# request.session[AuthenticationView.SESSION_ALLOW_ABSOLUTE_NEXT] = True
|
||||||
# request.session[AuthenticationView.SESSION_FORCE_COOKIE_HOSTNAME] = parsed_url.hostname
|
# request.session[AuthenticationView.SESSION_FORCE_COOKIE_HOSTNAME] = parsed_url.hostname
|
||||||
print(request.user)
|
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return HttpResponse(status=401)
|
return HttpResponse(status=401)
|
||||||
matching = ApplicationGatewayProvider.objects.filter(
|
matching = ApplicationGatewayProvider.objects.filter(
|
||||||
@ -31,6 +43,7 @@ class NginxCheckView(AccessMixin, View):
|
|||||||
application = self.provider_to_application(matching.first())
|
application = self.provider_to_application(matching.first())
|
||||||
has_access, _ = self.user_has_access(application, request.user)
|
has_access, _ = self.user_has_access(application, request.user)
|
||||||
if has_access:
|
if has_access:
|
||||||
|
cache.set(_cache_key, True)
|
||||||
return HttpResponse(status=202)
|
return HttpResponse(status=202)
|
||||||
LOGGER.debug("User not passing", user=request.user)
|
LOGGER.debug("User not passing", user=request.user)
|
||||||
return HttpResponse(status=401)
|
return HttpResponse(status=401)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from django.contrib import messages
|
|||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.audit.models import AuditEntry
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.policies.engine import PolicyEngine
|
from passbook.policies.engine import PolicyEngine
|
||||||
|
|
||||||
@ -26,4 +27,10 @@ def check_permissions(request, user, client):
|
|||||||
for policy_message in policy_messages:
|
for policy_message in policy_messages:
|
||||||
messages.error(request, policy_message)
|
messages.error(request, policy_message)
|
||||||
return redirect('passbook_providers_oauth:oauth2-permission-denied')
|
return redirect('passbook_providers_oauth:oauth2-permission-denied')
|
||||||
|
|
||||||
|
AuditEntry.create(
|
||||||
|
action=AuditEntry.ACTION_AUTHORIZE_APPLICATION,
|
||||||
|
request=request,
|
||||||
|
app=application.name,
|
||||||
|
skipped_authorization=False)
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -38,7 +38,7 @@ class SAMLProvider(Provider):
|
|||||||
if not self._processor:
|
if not self._processor:
|
||||||
try:
|
try:
|
||||||
self._processor = path_to_class(self.processor_path)(self)
|
self._processor = path_to_class(self.processor_path)(self)
|
||||||
except ModuleNotFoundError as exc:
|
except ImportError as exc:
|
||||||
LOGGER.warning(exc)
|
LOGGER.warning(exc)
|
||||||
self._processor = None
|
self._processor = None
|
||||||
return self._processor
|
return self._processor
|
||||||
|
|||||||
0
passbook/recovery/__init__.py
Normal file
0
passbook/recovery/__init__.py
Normal file
11
passbook/recovery/apps.py
Normal file
11
passbook/recovery/apps.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"""passbook Recovery app config"""
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PassbookRecoveryConfig(AppConfig):
|
||||||
|
"""passbook Recovery app config"""
|
||||||
|
|
||||||
|
name = 'passbook.recovery'
|
||||||
|
label = 'passbook_recovery'
|
||||||
|
verbose_name = 'passbook Recovery'
|
||||||
|
mountpoint = 'recovery/'
|
||||||
0
passbook/recovery/management/__init__.py
Normal file
0
passbook/recovery/management/__init__.py
Normal file
0
passbook/recovery/management/commands/__init__.py
Normal file
0
passbook/recovery/management/commands/__init__.py
Normal file
46
passbook/recovery/management/commands/create_recovery_key.py
Normal file
46
passbook/recovery/management/commands/create_recovery_key.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"""passbook recovery createkey command"""
|
||||||
|
from datetime import timedelta
|
||||||
|
from getpass import getuser
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils.timezone import now
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.core.models import Nonce, User
|
||||||
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
"""Create Nonce used to recover access"""
|
||||||
|
|
||||||
|
help = _('Create a Key which can be used to restore access to passbook.')
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument('duration', default=1, action='store',
|
||||||
|
help='How long the token is valid for (in years).')
|
||||||
|
parser.add_argument('user', action='store',
|
||||||
|
help='Which user the Token gives access to.')
|
||||||
|
|
||||||
|
def get_url(self, nonce: Nonce) -> str:
|
||||||
|
"""Get full recovery link"""
|
||||||
|
path = reverse('passbook_recovery:use-nonce', kwargs={'uuid': str(nonce.uuid)})
|
||||||
|
return f"https://{CONFIG.y('domain')}{path}"
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
"""Create Nonce used to recover access"""
|
||||||
|
duration = int(options.get('duration', 1))
|
||||||
|
delta = timedelta(days=duration * 365.2425)
|
||||||
|
_now = now()
|
||||||
|
expiry = _now + delta
|
||||||
|
user = User.objects.get(username=options.get('user'))
|
||||||
|
nonce = Nonce.objects.create(
|
||||||
|
expires=expiry,
|
||||||
|
user=user,
|
||||||
|
description=f'Recovery Nonce generated by {getuser()} on {_now}')
|
||||||
|
self.stdout.write((f"Store this link safely, as it will allow"
|
||||||
|
f" anyone to access passbook as {user}."))
|
||||||
|
self.stdout.write(self.get_url(nonce))
|
||||||
9
passbook/recovery/urls.py
Normal file
9
passbook/recovery/urls.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
"""recovery views"""
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from passbook.recovery.views import UseNonceView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('use-nonce/<uuid:uuid>/', UseNonceView.as_view(), name='use-nonce'),
|
||||||
|
]
|
||||||
24
passbook/recovery/views.py
Normal file
24
passbook/recovery/views.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"""recovery views"""
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth import login
|
||||||
|
from django.http import Http404, HttpRequest, HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
|
from passbook.core.models import Nonce
|
||||||
|
|
||||||
|
|
||||||
|
class UseNonceView(View):
|
||||||
|
"""Use nonce to login"""
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, uuid: str) -> HttpResponse:
|
||||||
|
"""Check if nonce exists, log user in and delete nonce."""
|
||||||
|
nonce: Nonce = get_object_or_404(Nonce, pk=uuid)
|
||||||
|
if nonce.is_expired:
|
||||||
|
nonce.delete()
|
||||||
|
raise Http404
|
||||||
|
login(request, nonce.user, backend='django.contrib.auth.backends.ModelBackend')
|
||||||
|
nonce.delete()
|
||||||
|
messages.warning(request, _("Used recovery-link to authenticate."))
|
||||||
|
return redirect('passbook_core:overview')
|
||||||
@ -15,6 +15,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
from celery.schedules import crontab
|
||||||
from sentry_sdk import init as sentry_init
|
from sentry_sdk import init as sentry_init
|
||||||
from sentry_sdk.integrations.celery import CeleryIntegration
|
from sentry_sdk.integrations.celery import CeleryIntegration
|
||||||
from sentry_sdk.integrations.django import DjangoIntegration
|
from sentry_sdk.integrations.django import DjangoIntegration
|
||||||
@ -47,6 +48,7 @@ AUTH_USER_MODEL = 'passbook_core.User'
|
|||||||
|
|
||||||
CSRF_COOKIE_NAME = 'passbook_csrf'
|
CSRF_COOKIE_NAME = 'passbook_csrf'
|
||||||
SESSION_COOKIE_NAME = 'passbook_session'
|
SESSION_COOKIE_NAME = 'passbook_session'
|
||||||
|
SESSION_COOKIE_DOMAIN = CONFIG.y('domain', None)
|
||||||
LANGUAGE_COOKIE_NAME = 'passbook_language'
|
LANGUAGE_COOKIE_NAME = 'passbook_language'
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
@ -70,6 +72,7 @@ INSTALLED_APPS = [
|
|||||||
'passbook.api.apps.PassbookAPIConfig',
|
'passbook.api.apps.PassbookAPIConfig',
|
||||||
'passbook.lib.apps.PassbookLibConfig',
|
'passbook.lib.apps.PassbookLibConfig',
|
||||||
'passbook.audit.apps.PassbookAuditConfig',
|
'passbook.audit.apps.PassbookAuditConfig',
|
||||||
|
'passbook.recovery.apps.PassbookRecoveryConfig',
|
||||||
|
|
||||||
'passbook.sources.ldap.apps.PassbookSourceLDAPConfig',
|
'passbook.sources.ldap.apps.PassbookSourceLDAPConfig',
|
||||||
'passbook.sources.oauth.apps.PassbookSourceOAuthConfig',
|
'passbook.sources.oauth.apps.PassbookSourceOAuthConfig',
|
||||||
@ -83,6 +86,7 @@ INSTALLED_APPS = [
|
|||||||
'passbook.factors.captcha.apps.PassbookFactorCaptchaConfig',
|
'passbook.factors.captcha.apps.PassbookFactorCaptchaConfig',
|
||||||
'passbook.factors.password.apps.PassbookFactorPasswordConfig',
|
'passbook.factors.password.apps.PassbookFactorPasswordConfig',
|
||||||
'passbook.factors.dummy.apps.PassbookFactorDummyConfig',
|
'passbook.factors.dummy.apps.PassbookFactorDummyConfig',
|
||||||
|
'passbook.factors.email.apps.PassbookFactorEmailConfig',
|
||||||
|
|
||||||
'passbook.policies.expiry.apps.PassbookPolicyExpiryConfig',
|
'passbook.policies.expiry.apps.PassbookPolicyExpiryConfig',
|
||||||
'passbook.policies.reputation.apps.PassbookPolicyReputationConfig',
|
'passbook.policies.reputation.apps.PassbookPolicyReputationConfig',
|
||||||
@ -114,7 +118,7 @@ CACHES = {
|
|||||||
}
|
}
|
||||||
DJANGO_REDIS_IGNORE_EXCEPTIONS = True
|
DJANGO_REDIS_IGNORE_EXCEPTIONS = True
|
||||||
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
|
DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True
|
||||||
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
|
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
||||||
SESSION_CACHE_ALIAS = "default"
|
SESSION_CACHE_ALIAS = "default"
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -196,7 +200,12 @@ USE_TZ = True
|
|||||||
# Celery settings
|
# Celery settings
|
||||||
# Add a 10 minute timeout to all Celery tasks.
|
# Add a 10 minute timeout to all Celery tasks.
|
||||||
CELERY_TASK_SOFT_TIME_LIMIT = 600
|
CELERY_TASK_SOFT_TIME_LIMIT = 600
|
||||||
CELERY_BEAT_SCHEDULE = {}
|
CELERY_BEAT_SCHEDULE = {
|
||||||
|
'clean_nonces': {
|
||||||
|
'task': 'passbook.core.tasks.clean_nonces',
|
||||||
|
'schedule': crontab(minute='*/5') # Run every 5 minutes
|
||||||
|
}
|
||||||
|
}
|
||||||
CELERY_CREATE_MISSING_QUEUES = True
|
CELERY_CREATE_MISSING_QUEUES = True
|
||||||
CELERY_TASK_DEFAULT_QUEUE = 'passbook'
|
CELERY_TASK_DEFAULT_QUEUE = 'passbook'
|
||||||
CELERY_BROKER_URL = (f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}"
|
CELERY_BROKER_URL = (f"redis://:{CONFIG.y('redis.password')}@{CONFIG.y('redis.host')}"
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
import json
|
import json
|
||||||
from urllib.parse import parse_qs, urlencode
|
from urllib.parse import parse_qs, urlencode
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.crypto import constant_time_compare, get_random_string
|
from django.utils.crypto import constant_time_compare, get_random_string
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from requests import Session
|
from requests import Session
|
||||||
@ -11,6 +10,8 @@ from requests.exceptions import RequestException
|
|||||||
from requests_oauthlib import OAuth1
|
from requests_oauthlib import OAuth1
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook import __version__
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ class BaseOAuthClient:
|
|||||||
self.source = source
|
self.source = source
|
||||||
self.token = token
|
self.token = token
|
||||||
self._session = Session()
|
self._session = Session()
|
||||||
self._session.headers.update({'User-Agent': 'web:passbook:%s' % settings.VERSION})
|
self._session.headers.update({'User-Agent': 'web:passbook:%s' % __version__})
|
||||||
|
|
||||||
def get_access_token(self, request, callback=None):
|
def get_access_token(self, request, callback=None):
|
||||||
"Fetch access token from callback request."
|
"Fetch access token from callback request."
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from django.db import models
|
|||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from passbook.core.models import Source, UserSourceConnection
|
from passbook.core.models import Source, UserSettings, UserSourceConnection
|
||||||
from passbook.sources.oauth.clients import get_client
|
from passbook.sources.oauth.clients import get_client
|
||||||
|
|
||||||
|
|
||||||
@ -37,18 +37,15 @@ class OAuthSource(Source):
|
|||||||
reverse_lazy('passbook_sources_oauth:oauth-client-callback',
|
reverse_lazy('passbook_sources_oauth:oauth-client-callback',
|
||||||
kwargs={'source_slug': self.slug})
|
kwargs={'source_slug': self.slug})
|
||||||
|
|
||||||
def has_user_settings(self):
|
def user_settings(self) -> UserSettings:
|
||||||
"""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."""
|
|
||||||
icon_type = self.provider_type
|
icon_type = self.provider_type
|
||||||
if icon_type == 'azure ad':
|
if icon_type == 'azure ad':
|
||||||
icon_type = 'windows'
|
icon_type = 'windows'
|
||||||
icon_class = 'fa fa-%s' % icon_type
|
icon_class = 'fa fa-%s' % icon_type
|
||||||
view_name = 'passbook_sources_oauth:oauth-client-user'
|
view_name = 'passbook_sources_oauth:oauth-client-user'
|
||||||
return self.name, icon_class, reverse((view_name), kwargs={
|
return UserSettings(self.name, icon_class, reverse((view_name), kwargs={
|
||||||
'source_slug': self.slug
|
'source_slug': self.slug
|
||||||
})
|
}))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user