Compare commits

...

79 Commits

Author SHA1 Message Date
b21fd10093 new release: 0.7.5-beta 2019-12-16 22:05:22 +01:00
6f9c19b142 misc: update bumpversion config 2019-12-16 22:05:16 +01:00
f45643ca87 Merge branch '45-helm-3' into 'master'
Resolve "Upgrade to helm 3 for packaging"

Closes #45

See merge request BeryJu.org/passbook!35
2019-12-16 20:49:34 +00:00
85f8bea784 ci: replace helm with helm3 2019-12-14 14:34:34 +01:00
b428ec5237 providers/oidc: remove duplicate fields 2019-12-14 14:28:36 +01:00
92428529ad docs: add sentry 2019-12-14 14:28:14 +01:00
f6761b5b0b docs: fix harbor site not being included 2019-12-13 15:45:50 +01:00
307b04f4ca docs: add harbor integration, cleanup 2019-12-13 15:36:09 +01:00
6a520a5697 docs: add rancher integration 2019-12-13 13:53:30 +01:00
f22dbba931 providers/saml: add UID field 2019-12-13 13:45:10 +01:00
82cf482fba Merge branch 'docs' into 'master'
Docs

See merge request BeryJu.org/passbook!33
2019-12-12 22:06:20 +00:00
a6afb99edd docs: build docs on new version 2019-12-12 18:13:38 +01:00
ac5f8465b9 docs: add GitLab integration docs 2019-12-12 18:12:14 +01:00
218acb9e38 docs: add providers and sources 2019-12-12 18:00:23 +01:00
927c718fdd docs: add some more info to mkdocs 2019-12-12 09:55:10 +01:00
b7a6d6e739 docs: add docs for property mappings, switch to material theme 2019-12-10 11:25:34 +01:00
0946d6a25d docs: add initial structure, add docs for policies and factors 2019-12-09 21:00:45 +01:00
c1e98e2f0c Merge branch 'master' into docs 2019-12-09 16:49:05 +01:00
807cbbeaaf audit: rewrite to be independent of django http requests, allow custom actions 2019-12-05 16:14:08 +01:00
6c358c4e0a misc: run coverage before other tasks to find bugs easier 2019-12-05 16:03:31 +01:00
74cd0bc08f all(minor): remove old, unused code 2019-12-05 15:07:37 +01:00
b08ec0477e all(minor): replace django-ipware with custom implementation 2019-12-05 14:33:55 +01:00
328c999cb9 ci(minor): reenable prospector 2019-12-05 14:31:51 +01:00
c37e382c15 root(minor): fix incorrect user IP being shown 2019-12-02 18:05:06 +01:00
784dd0fdd6 root(minor): fix unnecessary redirect for prometheus 2019-12-02 18:04:55 +01:00
e6256cb9c8 root(minor): add script to run coverage 2019-12-02 16:43:50 +01:00
4520e3f8b8 deploy(minor): fix wrong health-check for static deployment 2019-11-20 15:55:39 +01:00
23146de2bf new release: 0.7.4-beta 2019-11-20 13:15:46 +01:00
e24f4fe3a8 compose(minor): add error_reporting flag 2019-11-20 13:13:10 +01:00
8e6b69f96f ci(minor): disable gatekeeper build due to quay issue 2019-11-20 13:12:51 +01:00
979bea17ed root(minor): disable error reporting by default 2019-11-20 13:12:37 +01:00
30dba285d9 root(minor): remove build stanza from docker-compose as it causes issues 2019-11-20 13:08:32 +01:00
99fadf2e55 new release: 0.7.3-beta 2019-11-19 18:14:06 +01:00
b606e3d0cb static(minor): remove nginx config from bumpversion 2019-11-19 18:14:03 +01:00
be642bc874 root(major): fix dbbackup not working with prometheus 2019-11-19 18:08:25 +01:00
49a347b32f static(major): switch to pixie for static files 2019-11-19 18:00:29 +01:00
089b48aad1 Merge branch 'agw' 2019-11-11 18:14:03 +01:00
2997cb83b1 providers/appgw(major): rewrite to use oauth2_proxy 2019-11-11 18:13:46 +01:00
08f0aca894 provider/oidc(minor): include claims in id_token 2019-11-11 13:19:54 +01:00
80ea7c40b7 helm(minor): fix monitoring username not being in b64 2019-11-08 15:00:08 +01:00
019a0cb14d new release: 0.7.2-beta 2019-11-08 14:26:52 +01:00
97290755e7 root(major): re-add missing dependencies 2019-11-08 14:26:49 +01:00
7f150c96b4 new release: 0.7.1-beta 2019-11-08 14:04:59 +01:00
73558f30d1 root(minor): revert to django_redis cache 2019-11-08 13:58:10 +01:00
dfcfd87644 root(minor): remove old deps from pipfile 2019-11-08 13:55:58 +01:00
2c0f0a68a8 helm(major): add prometheus rules, add switch to enable/disable monitoring 2019-11-08 13:49:28 +01:00
3d73aac3ab helm(minor): add service monitors 2019-11-08 12:24:42 +01:00
e4fbcd3735 root(major): add prometheus 2019-11-08 12:23:51 +01:00
44c0eb37cf sources/saml(minor): fix lint issue 2019-11-07 18:02:59 +01:00
adc3dcc2c4 sources/saml(minor): disallow login if source is not enabled 2019-11-07 17:35:25 +01:00
bac8227371 sources/saml(minor): fix fields not being shown 2019-11-07 17:28:59 +01:00
73d4d9dfe0 admin(major): fix incorrect permissions being set 2019-11-07 17:25:36 +01:00
afdac5f3f8 Merge branch '10-saml-sp' into 'master'
Resolve "Add SAML SP"

Closes #10

See merge request BeryJu.org/passbook!31
2019-11-07 16:05:28 +00:00
dabce36667 sources/saml(major): add saml SP 2019-11-07 17:02:56 +01:00
3bd56ce522 api(minor): fix invalid fieldls being selected 2019-11-07 10:30:22 +01:00
540419d5c1 helm(minor): update chart to use apps/v1, remove deps from git 2019-11-07 10:24:27 +01:00
ed1fcc3930 new release: 0.7.0-beta 2019-11-02 16:31:23 +00:00
c22ddc5394 root(minor): catch keyboardinput and s3 error from sentry 2019-11-02 16:27:28 +00:00
0544864a3f Merge branch '42-attributeerror-nonetype-object-has-no-attribute-startswith' into 'master'
Resolve "AttributeError: 'NoneType' object has no attribute 'startswith'"

Closes #42

See merge request BeryJu.org/passbook!32
2019-11-01 12:53:43 +00:00
0b9fc9e444 root(minor): fallback to empty string if no Host header ise set 2019-11-01 12:50:38 +00:00
e862b97005 all(major): add API for all objects 2019-10-28 17:55:36 +01:00
cffe09b02e all(major): add most models to API 2019-10-28 17:40:57 +01:00
846a86fb62 fix lint 2019-10-28 14:44:46 +01:00
463c130351 core(major): add api for most simple objects 2019-10-28 14:27:43 +01:00
ffca957838 audit(major): AuditEntry -> Event 2019-10-28 14:26:34 +01:00
543e949a48 api(minor): start with api v2 2019-10-28 14:26:07 +01:00
feb80049aa Merge branch 'master' into guardian 2019-10-25 22:18:13 +02:00
5c59c8ccb6 new release: 0.6.11-beta 2019-10-15 16:56:24 +02:00
1fadd82c65 Merge branch '41-commandconnectorerror-error-running-pg_dump-passbook-host-passbook-postgresql-username-pos' into 'master'
Resolve "CommandConnectorError: Error running:  pg_dump passbook --host=passbook-postgresql --username=pos..."

Closes #41

See merge request BeryJu.org/passbook!30
2019-10-15 14:53:59 +00:00
7e7736126d docker(minor): install pg client for pg_dump 2019-10-15 16:51:42 +02:00
5e0915afce helm(major): update postgresql to 11 2019-10-15 16:51:21 +02:00
261d57ad7b Merge branch 'master' into guardian
# Conflicts:
#	Pipfile
#	Pipfile.lock
#	passbook/admin/views/invitations.py
#	passbook/admin/views/policy.py
#	passbook/admin/views/providers.py
#	passbook/admin/views/sources.py
#	passbook/admin/views/users.py
2019-10-15 15:09:11 +02:00
37111fd07b core(minor): merge migrations 2019-10-10 17:41:22 +02:00
143a575369 Merge branch 'master' into guardian
# Conflicts:
#	Pipfile
#	Pipfile.lock
#	passbook/core/models.py
2019-10-10 17:29:34 +02:00
344a8817c3 admin(minor): fix linting 2019-10-10 13:05:03 +02:00
3afb0d4f6d admin(minor): remove partial API 2019-10-10 13:04:20 +02:00
c9714893bb admin(major): rewrite all views to use guardian mixins 2019-10-10 13:01:49 +02:00
3185a86b22 core(minor): add separate permission to reset user's password 2019-10-10 13:01:36 +02:00
a53f7a49ac root(minor): start implementing guardian 2019-10-10 10:45:51 +02:00
186 changed files with 3757 additions and 1779 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.6.10-beta
current_version = 0.7.5-beta
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -15,13 +15,11 @@ values =
beta
stable
[bumpversion:file:helm/passbook/values.yaml]
[bumpversion:file:helm/values.yaml]
[bumpversion:file:helm/passbook/Chart.yaml]
[bumpversion:file:helm/Chart.yaml]
[bumpversion:file:.gitlab-ci.yml]
[bumpversion:file:passbook/__init__.py]
[bumpversion:file:docker/nginx.conf]

6
.gitignore vendored
View File

@ -63,6 +63,7 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
unittest.xml
# Translations
*.mo
@ -184,7 +185,6 @@ dmypy.json
[Ii]nclude
[Ll]ib64
[Ll]ocal
[Ss]cripts
pyvenv.cfg
pip-selfcheck.json
@ -192,3 +192,7 @@ pip-selfcheck.json
/static/
local.env.yml
.vscode/
### Helm ###
# Chart dependencies
**/charts/*.tgz

View File

@ -49,44 +49,41 @@ build-dev-image:
- tags
- /^version/.*$/
isort:
script:
- isort -c -sg env
stage: test
services:
- postgres:latest
- redis:latest
- postgres:latest
- redis:latest
migrations:
script:
- python manage.py migrate
stage: test
services:
- postgres:latest
- redis:latest
# prospector:
# script:
# - prospector
# stage: test
# services:
# - postgres:latest
# - redis:latest
- postgres:latest
- redis:latest
prospector:
script:
- prospector
stage: test
services:
- postgres:latest
- redis:latest
pylint:
script:
- pylint passbook
stage: test
services:
- postgres:latest
- redis:latest
- postgres:latest
- redis:latest
coverage:
script:
- coverage run --concurrency=multiprocessing manage.py test
- coverage combine
- coverage report
- ./scripts/coverage.sh
stage: test
services:
- postgres:latest
- redis:latest
- postgres:latest
- redis:latest
build-passbook-server:
stage: build
@ -96,7 +93,19 @@ build-passbook-server:
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.6.10-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.7.5-beta
only:
- tags
- /^version/.*$/
build-docs:
stage: build
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/docs/Dockerfile --destination docker.beryju.org/passbook/docs:latest --destination docker.beryju.org/passbook/docs:0.7.5-beta
only:
- tags
- /^version/.*$/
@ -108,7 +117,7 @@ build-passbook-static:
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/static.Dockerfile --destination docker.beryju.org/passbook/static:latest --destination docker.beryju.org/passbook/static:0.6.10-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.7.5-beta
only:
- tags
- /^version/.*$/
@ -122,11 +131,10 @@ package-helm:
stage: package
before_script:
- apt update && apt install -y curl
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
script:
- helm init --client-only
- helm dependency update helm/passbook
- helm package helm/passbook
- helm dependency update helm
- helm package helm
artifacts:
paths:
- passbook-*.tgz
@ -145,8 +153,8 @@ notify-sentry:
before_script:
- apk add curl
script:
- sentry-cli releases new passbook@0.6.10-beta
- sentry-cli releases set-commits --auto passbook@0.6.10-beta
- sentry-cli releases new passbook@0.7.5-beta
- sentry-cli releases set-commits --auto passbook@0.7.5-beta
only:
- tags
- /^version/.*$/

37
Pipfile
View File

@ -4,54 +4,55 @@ url = "https://pypi.org/simple"
verify_ssl = true
[packages]
boto3 = "*"
celery = "*"
cherrypy = "*"
defusedxml = "*"
django = "*"
kombu = "==4.5.0"
django-cors-middleware = "*"
django-filters = "*"
django-ipware = "*"
django-dbbackup = "*"
django-filter = "*"
django-guardian = "*"
django-model-utils = "*"
django-oauth-toolkit = "*"
django-oidc-provider = "*"
django-otp = "*"
django-prometheus = "*"
django-recaptcha = "*"
django-redis = "*"
django-rest-framework = "*"
django-storages = "*"
djangorestframework-guardian = "*"
drf-yasg = "*"
kombu = "==4.5.0"
ldap3 = "*"
lxml = "*"
markdown = "*"
oauthlib = "*"
packaging = "*"
psycopg2-binary = "*"
pycryptodome = "*"
pyuwsgi = "*"
pyyaml = "*"
qrcode = "*"
requests-oauthlib = "*"
sentry-sdk = "*"
service_identity = "*"
signxml = "*"
urllib3 = {extras = ["secure"],version = "*"}
structlog = "*"
pyuwsgi = "*"
django-dbbackup = "*"
boto3 = "*"
django-storages = "*"
swagger-spec-validator = "*"
urllib3 = {extras = ["secure"],version = "*"}
[requires]
python_version = "3.7"
[dev-packages]
coverage = "*"
isort = "*"
pylint = "==2.3.1"
pylint-django = "*"
prospector = "*"
django-debug-toolbar = "*"
bumpversion = "*"
unittest-xml-reporting = "*"
autopep8 = "*"
bandit = "*"
bumpversion = "*"
colorama = "*"
coverage = "*"
django-debug-toolbar = "*"
isort = "*"
prospector = "*"
pylint = "==2.3.1"
pylint-django = "*"
unittest-xml-reporting = "*"

671
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,8 @@
```
export PASSBOOK_DOMAIN=domain.tld
# Optionally enable Error-reporting
# export PASSBOOK_ERROR_REPORTING=true
docker-compose pull
docker-compose up -d
docker-compose exec server ./manage.py migrate

View File

@ -16,5 +16,8 @@ COPY --from=locker /app/requirements-dev.txt /app/
WORKDIR /app/
RUN pip install -r requirements.txt --no-cache-dir && \
RUN apt-get update && \
apt-get install -y --no-install-recommends postgresql-client-11 && \
rm -rf /var/lib/apt/ && \
pip install -r requirements.txt --no-cache-dir && \
adduser --system --no-create-home --uid 1000 --group --home /app passbook

View File

@ -21,8 +21,6 @@ services:
labels:
- traefik.enable=false
server:
build:
context: .
image: docker.beryju.org/passbook/server:${SERVER_TAG:-latest}
command:
- uwsgi
@ -30,6 +28,7 @@ services:
environment:
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
- PASSBOOK_REDIS__HOST=redis
- PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false}
- PASSBOOK_POSTGRESQL__HOST=postgresql
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
ports:
@ -57,18 +56,16 @@ services:
environment:
- PASSBOOK_DOMAIN=${PASSBOOK_DOMAIN}
- PASSBOOK_REDIS__HOST=redis
- PASSBOOK_ERROR_REPORTING=${PASSBOOK_ERROR_REPORTING:-false}
- PASSBOOK_POSTGRESQL__HOST=postgresql
- PASSBOOK_POSTGRESQL__PASSWORD=${PG_PASS:-thisisnotagoodpassword}
static:
build:
context: .
dockerfile: static.Dockerfile
image: docker.beryju.org/passbook/static:latest
networks:
- internal
labels:
- traefik.frontend.rule=PathPrefix:/static, /robots.txt
- traefik.port=80
- traefik.port=8080
- traefik.docker.network=internal
traefik:
image: traefik:1.7

View File

@ -1,66 +0,0 @@
user nginx;
worker_processes 1;
error_log stderr warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format json_combined escape=json
'{'
'"time_local":"$time_local",'
'"remote_addr":"$remote_addr",'
'"remote_user":"$remote_user",'
'"request":"$request",'
'"status": "$status",'
'"body_bytes_sent":"$body_bytes_sent",'
'"request_time":"$request_time",'
'"http_referrer":"$http_referer",'
'"http_user_agent":"$http_user_agent"'
'}';
access_log /dev/stdout json_combined;
sendfile on;
tcp_nopush on;
keepalive_timeout 65;
server {
server_name _;
gzip on;
gzip_types application/javascript image/* text/css;
gunzip on;
add_header X-passbook-Version 0.6.10-beta;
add_header Vary X-passbook-Version;
root /data/;
location /_/healthz {
return 204;
}
location ~* \.(jpg|jpeg|png|gif|ico)$ {
expires 30d;
}
location ~* \.(css|js)$ {
expires 7d;
}
}
server {
listen 8080;
location = /stub_status {
stub_status;
}
}
}

View File

@ -1,6 +1,5 @@
[uwsgi]
http = 0.0.0.0:8000
chdir = /app
wsgi-file = passbook/root/wsgi.py
processes = 2
master = true

14
docs/Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM python:3.7-slim-buster as builder
WORKDIR /mkdocs
RUN pip install mkdocs mkdocs-material
COPY docs/ docs
COPY mkdocs.yml .
RUN mkdocs build
FROM nginx
COPY --from=builder /mkdocs/site /usr/share/nginx/html

23
docs/factors.md Normal file
View File

@ -0,0 +1,23 @@
# Factors
A factor represents a single authenticating factor for a user. Common examples of this would be a password or an OTP. These factors can be combined in any order, and can be dynamically enabled using policies.
## Password Factor
This is the standard Password Factor. It allows you to select which Backend the password is checked with. here you can also specify which Policies are used to check the password. You can also specify which Factors a User has to pass to recover their account.
## Dummy Factor
This factor waits a random amount of time. Mostly used for debugging.
## E-Mail Factor
This factor is mostly for recovery, and used in conjunction with the Password Factor.
## OTP Factor
This is your typical One-Time Password implementation, compatible with Authy and Google Authenticator. You can enfore this Factor so that every user has to configure it, or leave it optional.
## Captcha Factor
While this factor doesn't really authenticate a user, it is part of the Authentication Flow. passbook uses Google's reCaptcha implementation.

55
docs/images/logo.svg Normal file
View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path style="fill:#57565C;" d="M407,512H105C47.103,512,0,464.897,0,407V105C0,47.103,47.103,0,105,0h302
c57.897,0,105,47.103,105,105v302C512,464.897,464.897,512,407,512z"/>
<path style="fill:#3E3D42;" d="M407,0H256v512h151c57.897,0,105-47.103,105-105V105C512,47.103,464.897,0,407,0z"/>
<rect x="91" y="141" style="fill:#00C3FF;" width="330" height="44"/>
<rect x="256" y="141" style="fill:#00AAF0;" width="165" height="44"/>
<rect x="91" y="176" style="fill:#FFDC40;" width="330" height="44"/>
<rect x="256" y="176" style="fill:#FFAB15;" width="165" height="44"/>
<rect x="91" y="206" style="fill:#87E694;" width="330" height="44"/>
<rect x="256" y="206" style="fill:#66CC70;" width="165" height="44"/>
<path style="fill:#F2F2F2;" d="M421,381c0,8.284-6.716,15-15,15H106c-8.284,0-15-6.716-15-15v-85h89.997
c9.31,0,17.688,4.938,21.868,12.888C213.277,328.695,233.638,341,256,341s42.723-12.305,53.135-32.111
c4.18-7.95,12.559-12.889,21.868-12.889H421V381z"/>
<path style="fill:#FF6849;" d="M421,266h-89.997c-20.487,0-39.041,11.085-48.423,28.929C277.369,304.842,267.185,311,256,311
s-21.369-6.158-26.58-16.071C220.038,277.085,201.484,266,180.997,266H91v-30h330V266z"/>
<path style="fill:#F2F2F2;" d="M421,146H91v-15c0-8.284,6.716-15,15-15h300c8.284,0,15,6.716,15,15V146z"/>
<path style="fill:#E5E5E5;" d="M331.003,296c-9.31,0-17.688,4.938-21.868,12.889C298.723,328.695,278.362,341,256,341v55h150
c8.284,0,15-6.716,15-15v-85H331.003z"/>
<path style="fill:#FD4B2D;" d="M256,236v75c11.185,0,21.369-6.158,26.58-16.071C291.962,277.085,310.516,266,331.003,266H421v-30
H256z"/>
<path style="fill:#E5E5E5;" d="M406,116H256v30h165v-15C421,122.716,414.284,116,406,116z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

31
docs/index.md Executable file
View File

@ -0,0 +1,31 @@
# Welcome
Welcome to the passbook Documentation. passbook is an open-source Identity Provider and Usermanagement software. It can be used as a central directory for users or customers and it can integrate with your existing Directory.
passbook can also be used as part of an Application to facilitate User Enrollment, Password recovery and Social Login.
passbook uses the following Terminology:
### Policy
A Policy is at a base level a yes/no gate. It will either evaluate to True or False depending on the Policy Kind and settings. For example, a "Group Membership Policy" evaluates to True if the User is member of the specified Group and False if not. This can be used to conditionally apply Factors and grant/deny access.
### Provider
A Provider is a way for other Applications to authenticate against passbook. Common Providers are OpenID Connect (OIDC) and SAML.
### Source
Sources are ways to get users into passbook. This might be an LDAP Connection to import Users from Active Directory, or an OAuth2 Connection to allow Social Logins.
### Application
An application links together Policies with a Provider, allowing you to control access. It also holds Information like UI Name, Icon and more.
### Factors
Factors represent Authentication Factors, like a Password or OTP. These Factors can be dynamically enabled using policies. This allows you to, for example, force users from a certain IP ranges to complete a Captcha to authenticate.
### Property Mappings
Property Mappings allow you to make Information available for external Applications. For example, if you want to login to AWS with passbook, you'd use Property Mappings to set the User's Roles based on their Groups.

View File

@ -0,0 +1,26 @@
# docker-compose
This installation Method is for test-setups and small-scale productive setups.
## Prerequisites
- docker
- docker-compose
## Install
Download the latest `docker-compose.yml` from [here](https://git.beryju.org/BeryJu.org/passbook/raw/master/docker-compose.yml). Place it in a directory of your choice.
passbook needs to know it's primary URL to create links in E-Mails and set cookies, so you have to run the following command:
```
export PASSBOOK_DOMAIN=domain.tld # this can be any domain or IP, it just needs to point to passbook.
```
The compose file references the current latest version, which can be overridden with the `SERVER_TAG` Environment variable.
If you plan to use this setup for production, it is also advised to change the PostgreSQL Password by setting `PG_PASS` to a password of your choice.
Now you can pull the Docker images needed by running `docker-compose pull`. After this has finished, run `docker-compose up -d` to start passbook.
passbook will then be reachable on Port 80. You can optionally configure the packaged traefik to use Let's Encrypt for TLS Encryption.

6
docs/installation/install.md Executable file
View File

@ -0,0 +1,6 @@
# Installation
There are two supported ways to install passbook:
- [docker-compose](docker-compose.md) for test- or small productive setups
- [Kubernetes](./kubernetes.md) for larger Productive setups

View File

@ -0,0 +1,3 @@
# Kubernetes
For a mid to high-load Installation, Kubernetes is recommended. passbook is installed using a helm-chart.

View File

@ -0,0 +1,59 @@
# GitLab Integration
## What is GitLab
From https://about.gitlab.com/what-is-gitlab/
```
GitLab is a complete DevOps platform, delivered as a single application. This makes GitLab unique and makes Concurrent DevOps possible, unlocking your organization from the constraints of a pieced together toolchain. Join us for a live Q&A to learn how GitLab can give you unmatched visibility and higher levels of efficiency in a single application across the DevOps lifecycle.
```
## Preparation
The following placeholders will be used:
- `gitlab.company` is the FQDN of the GitLab Install
- `passbook.company` is the FQDN of the passbook Install
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
- ACS URL: `https://gitlab.company/users/auth/saml/callback`
- Audience: `https://gitlab.company`
- Issuer: `https://gitlab.company`
You can of course use a custom Signing Certificate, and adjust the Assertion Length. To get the value for `idp_cert_fingerprint`, you can use a tool like [this](https://www.samltool.com/fingerprint.php).
## GitLab Configuration
Paste the following block in your `gitlab.rb` file, after replacing the placeholder values from above. The file is located in `/etc/gitlab`.
```ruby
gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
gitlab_rails['omniauth_sync_email_from_provider'] = 'saml'
gitlab_rails['omniauth_sync_profile_from_provider'] = ['saml']
gitlab_rails['omniauth_sync_profile_attributes'] = ['email']
gitlab_rails['omniauth_auto_sign_in_with_provider'] = 'saml'
gitlab_rails['omniauth_block_auto_created_users'] = false
gitlab_rails['omniauth_auto_link_saml_user'] = true
gitlab_rails['omniauth_providers'] = [
{
name: 'saml',
args: {
assertion_consumer_service_url: 'https://gitlab.company/users/auth/saml/callback',
idp_cert_fingerprint: '4E:1E:CD:67:4A:67:5A:E9:6A:D0:3C:E6:DD:7A:F2:44:2E:76:00:6A',
idp_sso_target_url: 'https://passbook.company/application/saml/<passbook application slug>/login/',
issuer: 'https://gitlab.company',
name_identifier_format: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
attribute_statements: {
email: ['urn:oid:1.3.6.1.4.1.5923.1.1.1.6'],
first_name: ['urn:oid:2.5.4.3'],
nickname: ['urn:oid:2.16.840.1.113730.3.1.241']
}
},
label: 'passbook'
}
]
```
Afterwards, either run `gitlab-ctl reconfigure` if you're running GitLab Omnibus, or restart the container if you're using the container.

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View File

@ -0,0 +1,28 @@
# Harbor Integration
## What is Harbor
From https://goharbor.io
```
Harbor is an open source container image registry that secures images with role-based access control, scans images for vulnerabilities, and signs images as trusted. A CNCF Incubating project, Harbor delivers compliance, performance, and interoperability to help you consistently and securely manage images across cloud native compute platforms like Kubernetes and Docker.
```
## Preparation
The following placeholders will be used:
- `harbor.company` is the FQDN of the Harbor Install
- `passbook.company` is the FQDN of the passbook Install
Create an application in passbook. Create an OpenID Provider with the following Parameters:
- Client Type: `Confidential`
- Response types: `code (Authorization Code Flow)`
- JWT Algorithm: `RS256`
- Redirect URIs: `https://harbor.company/c/oidc/callback`
- Scopes: `openid`
## Harbor
![](./harbor.png)

View File

@ -0,0 +1,29 @@
# Rancher Integration
## What is Rancher
From https://rancher.com/products/rancher
```
An Enterprise Platform for Managing Kubernetes Everywhere
Rancher is a platform built to address the needs of the DevOps teams deploying applications with Kubernetes, and the IT staff responsible for delivering an enterprise-critical service.
```
## Preparation
The following placeholders will be used:
- `rancher.company` is the FQDN of the Rancher Install
- `passbook.company` is the FQDN of the passbook Install
Create an application in passbook and note the slug, as this will be used later. Create a SAML Provider with the following Parameters:
- ACS URL: `https://rancher.company/v1-saml/adfs/saml/acs`
- Audience: `https://rancher.company/v1-saml/adfs/saml/metadata`
- Issuer: `passbook`
You can of course use a custom Signing Certificate, and adjust the Assertion Length.
## Rancher
![](./rancher.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

View File

@ -0,0 +1,42 @@
# Sentry Integration
## What is Sentry
From https://sentry.io
```
Sentry provides self-hosted and cloud-based error monitoring that helps all software
teams discover, triage, and prioritize errors in real-time.
One million developers at over fifty thousand companies already ship
better software faster with Sentry. Wont you join them?
```
## Preparation
The following placeholders will be used:
- `sentry.company` is the FQDN of the Sentry Install
- `passbook.company` is the FQDN of the passbook Install
Create an application in passbook. Create an OpenID Provider with the following Parameters:
- Client Type: `Confidential`
- Response types: `code (Authorization Code Flow)`
- JWT Algorithm: `RS256`
- Redirect URIs: `https://sentry.company/auth/sso/`
- Scopes: `openid email`
## Sentry
**This guide assumes you've installed Sentry using [getsentry/onpremise](https://github.com/getsentry/onpremise)**
- Add `sentry-auth-oidc` to `onpremise/sentry/requirements.txt` (Create the file if it doesn't exist yet)
- Add the following block to your `onpremise/sentry/sentry.conf.py`:
```
OIDC_ISSUER = "passbook"
OIDC_CLIENT_ID = "<Client ID from passbook>"
OIDC_CLIENT_SECRET = "<Client Secret from passbook>"
OIDC_SCOPE = "openid email"
OIDC_DOMAIN = "https://passbook.company/application/oidc/"
```

33
docs/k8s/deployment.yml Normal file
View File

@ -0,0 +1,33 @@
---
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: passbook-docs
namespace: prod-passbook-docs
labels:
app.kubernetes.io/name: passbook-docs
app.kubernetes.io/managed-by: passbook-docs
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: passbook-docs
template:
metadata:
labels:
app.kubernetes.io/name: passbook-docs
spec:
containers:
- name: passbook-docs
image: "docker.beryju.org/passbook/docs:latest"
ports:
- name: http
containerPort: 80
protocol: TCP
resources:
limits:
cpu: 10m
memory: 20Mi
requests:
cpu: 10m
memory: 20Mi

21
docs/k8s/ingress.yml Normal file
View File

@ -0,0 +1,21 @@
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
labels:
app.kubernetes.io/name: passbook-docs
name: passbook-docs
namespace: prod-passbook-docs
spec:
rules:
- host: docs.passbook.beryju.org
http:
paths:
- backend:
serviceName: passbook-docs-http
servicePort: http
path: /
tls:
- hosts:
- docs.passbook.beryju.org
secretName: passbook-docs-acme

17
docs/k8s/service.yml Normal file
View File

@ -0,0 +1,17 @@
---
apiVersion: v1
kind: Service
metadata:
name: passbook-docs-http
namespace: prod-passbook-docs
labels:
app.kubernetes.io/name: passbook-docs
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: passbook-docs

68
docs/policies.md Normal file
View File

@ -0,0 +1,68 @@
# Policies
## Kinds
There are two different Kind of policies, a Standard Policy and a Password Policy. Normal Policies just evaluate to True or False, and can be used everywhere. Password Policies apply when a Password is set (during User enrollment, Recovery or anywhere else). These policies can be used to apply Password Rules like length, etc. The can also be used to expire passwords after a certain amount of time.
## Standard Policies
---
### Group-Membership Policy
This policy evaluates to True if the current user is a Member of the selected group.
### Reputation Policy
passbook keeps track of failed login attempts by Source IP and Attempted Username. These values are saved as scores. Each failed login decreases the Score for the Client IP as well as the targeted Username by one.
This policy can be used to for example prompt Clients with a low score to pass a Captcha before they can continue.
### Field matcher Policy
This policy allows you to evaluate arbitrary comparisons against the User instance. Currently supported fields are:
- Username
- E-Mail
- Name
- Is_active
- Date joined
Any of the following operations are supported:
- Starts with
- Ends with
- Contains
- Regexp (standard Python engine)
- Exact
### SSO Policy
This policy evaluates to True if the current Authentication Flow has been initiated through an external Source, like OAuth and SAML.
### Webhook Policy
This policy allows you to send an arbitrary HTTP Request to any URL. You can then use JSONPath to extract the result you need.
## Password Policies
---
### Password Policy
This Policy allows you to specify Password rules, like Length and required Characters.
The following rules can be set:
- Minimum amount of Uppercase Characters
- Minimum amount of Lowercase Characters
- Minimum amount of Symbols Characters
- Minimum Length
- Symbol charset (define which characters are counted as symbols)
### Have I Been Pwned Policy
This Policy checks the hashed Password against the [Have I Been Pwned](https://haveibeenpwned.com/) API. This only sends the first 5 characters of the hashed password. The remaining comparison is done within passbook.
### Password-Expiry Policy
This policy can enforce regular password rotation by expiring set Passwords after a finite amount of time. This forces users to set a new password.

21
docs/property-mappings.md Normal file
View File

@ -0,0 +1,21 @@
# Property Mappings
Property Mappings allow you to pass information to external Applications. For example, pass the current user's Groups as a SAML Parameter. Property Mappings are also used to map Source fields to passbook fields, for example when using LDAP.
## SAML Property Mapping
SAML Property Mappings allow you embed Information into the SAML AuthN Request. THis Information can then be used by the Application to assign permissions for example.
You can find examples [here](integrations/)
## LDAP Property Mapping
LDAP Property Mappings are used when you define a LDAP Source. These Mappings define which LDAP Property maps to which passbook Property. By default, these mappings are created:
- Autogenerated LDAP Mapping: givenName -> first_name
- Autogenerated LDAP Mapping: mail -> email
- Autogenerated LDAP Mapping: name -> name
- Autogenerated LDAP Mapping: sAMAccountName -> username
- Autogenerated LDAP Mapping: sn -> last_name
These are configured for the most common LDAP Setups.

23
docs/providers.md Normal file
View File

@ -0,0 +1,23 @@
# Providers
Providers allow external Applications to authenticate against passbook and use its User Information.
## OpenID Provider
This provider uses the commonly used OpenID Connect variation of OAuth2.
## OAuth2 Provider
This provider is slightly different than the OpenID Provider. While it uses the same basic OAuth2 Protocol, it provides a GitHub-compatible Endpoint. This allows you to integrate Applications, which don't support Custom OpenID Providers.
The API exposes Username, E-Mail, Name and Groups in a GitHub-compatible format.
## SAML Provider
This provider allows you to integrate Enterprise Software using the SAML2 Protocol. It supports signed Requests. This Provider also has [Property Mappings](property-mappings.md#saml-property-mapping), which allows you to expose Vendor-specific Fields.
Default fields are:
- `eduPersonPrincipalName`: User's E-Mail
- `cn`: User's Full Name
- `mail`: User's E-Mail
- `displayName`: User's Username
- `uid`: User Unique Identifier

39
docs/sources.md Normal file
View File

@ -0,0 +1,39 @@
# Sources
Sources allow you to connect passbook to an existing User directory. They can also be used for Social-Login, using external Providers like Facebook, Twitter, etc.
## Generic OAuth Source
**All Integration-specific Sources are documented in the Integrations Section**
This source allows users to enroll themselves with an External OAuth-based Identity Provider. The Generic Provider expects the Endpoint to return OpenID-Connect compatible Information. Vendor specific Implementations have their own OAuth Source.
- Policies: Allow/Forbid Users from linking their Accounts with this Provider
- Request Token URL: This field is used for OAuth v1 Implementations and will be provided by the Provider.
- Authorization URL: This value will be provided by the Provider.
- Access Token URL: This value will be provided by the Provider.
- Profile URL: This URL is called by passbook to retrieve User information upon successful authentication.
- Consumer key/Consumer secret: These values will be provided by the Provider.
## SAML Source
This source allows passbook to act as a SAML Service Provider. Just like the SAML Provider, it supports signed Requests. Vendor specific documentation can be found in the Integrations Section
## LDAP Source
This source allows you to import Users and Groups from an LDAP Server
- Server URI: URI to your LDAP Server/Domain Controller
- Bind CN: CN to bind as, this can also be a UPN in the format of `user@domain.tld`
- Bind password: Password used during the bind process
- Enable Start TLS: Enables StartTLS functionality. To use SSL instead, use port `636`
- Base DN: Base DN used for all LDAP queries
- Addition User DN: Prepended to Base DN for User-queries.
- Addition Group DN: Prepended to Base DN for Group-queries.
- User object filter: Consider Objects matching this filter to be Users.
- Group object filter: Consider Objects matching this filter to be Groups.
- User group membership field: Field which contains Groups of user.
- Object uniqueness field: Field which contains a unique Identifier.
- Sync groups: Enable/disable Group synchronization. Groups are synced in the background every 5 minutes.
- Sync parent group: Optionally set this Group as parent Group for all synced Groups (allows you to, for example, import AD Groups under a root `imported-from-ad` group.)
- Property mappings: Define which LDAP Properties map to which passbook Properties. The default set of Property Mappings is generated for Active Directory. See also [LDAP Property Mappings](property-mappings.md#ldap-property-mapping)

8
gatekeeper/Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM quay.io/pusher/oauth2_proxy
COPY templates /templates
ENV OAUTH2_PROXY_EMAIL_DOMAINS=*
ENV OAUTH2_PROXY_PROVIDER=oidc
ENV OAUTH2_PROXY_CUSTOM_TEMPLATES_DIR=/templates
ENV OAUTH2_PROXY_HTTP_ADDRESS=:4180

View File

@ -0,0 +1,18 @@
{{define "error.html"}}
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>{{.Title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
</head>
<body>
<h2>{{.Title}}</h2>
<p>{{.Message}}</p>
<hr>
<p><a href="{{.ProxyPrefix}}/sign_in">Sign In</a></p>
</body>
</html>
{{end}}

View File

@ -0,0 +1,119 @@
{{define "sign_in.html"}}
<!DOCTYPE html>
<html lang="en" charset="utf-8">
<head>
<title>Sign In with passbook</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<style>
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.42857143;
color: #333;
background: #f0f0f0;
}
.signin {
display: block;
margin: 20px auto;
max-width: 400px;
background: #fff;
border: 1px solid #ccc;
border-radius: 10px;
padding: 20px;
}
.center {
text-align: center;
}
.btn {
color: #fff;
background-color: #428bca;
border: 1px solid #357ebd;
-webkit-border-radius: 4;
-moz-border-radius: 4;
border-radius: 4px;
font-size: 14px;
padding: 6px 12px;
text-decoration: none;
cursor: pointer;
}
.btn:hover {
background-color: #3071a9;
border-color: #285e8e;
text-decoration: none;
}
label {
display: inline-block;
max-width: 100%;
margin-bottom: 5px;
font-weight: 700;
}
input {
display: block;
width: 100%;
height: 34px;
padding: 6px 12px;
font-size: 14px;
line-height: 1.42857143;
color: #555;
background-color: #fff;
background-image: none;
border: 1px solid #ccc;
border-radius: 4px;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
-webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
-o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
margin: 0;
box-sizing: border-box;
}
footer {
display: block;
font-size: 10px;
color: #aaa;
text-align: center;
margin-bottom: 10px;
}
footer a {
display: inline-block;
height: 25px;
line-height: 25px;
color: #aaa;
text-decoration: underline;
}
footer a:hover {
color: #aaa;
}
</style>
</head>
<body>
<div class="signin center">
<form method="GET" action="{{.ProxyPrefix}}/start">
<input type="hidden" name="rd" value="{{.Redirect}}">
<button type="submit" class="btn">Sign in with passbook</button><br />
</form>
</div>
<script>
if (window.location.hash) {
(function () {
var inputs = document.getElementsByName('rd');
for (var i = 0; i < inputs.length; i++) {
inputs[i].value += window.location.hash;
}
})();
}
</script>
</body>
</html>
{{end}}

View File

@ -1,9 +1,9 @@
dependencies:
- name: postgresql
repository: https://kubernetes-charts.storage.googleapis.com/
version: 4.2.2
version: 6.5.8
- name: redis
repository: https://kubernetes-charts.storage.googleapis.com/
version: 9.2.1
digest: sha256:8782e974a1094eaeecf1d68f093ca4fb84977217b2bd38b09790a05ec289aec2
generated: "2019-10-02T21:03:25.90491153Z"
version: 9.5.1
digest: sha256:f18b5dc8d0be13d584407405c60d10b6b84d25f7fa8aaa3dd0e5385c38f5c516
generated: "2019-12-14T13:33:48.4341939Z"

View File

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

Binary file not shown.

View File

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

View File

@ -1,7 +1,7 @@
dependencies:
- name: postgresql
version: 4.2.2
version: 6.5.8
repository: https://kubernetes-charts.storage.googleapis.com/
- name: redis
version: 9.2.1
version: 9.5.1
repository: https://kubernetes-charts.storage.googleapis.com/

View File

@ -12,5 +12,5 @@ data:
host: "{{ .Release.Name }}-redis-master"
cache_db: 0
message_queue_db: 1
error_report_enabled: {{ .Values.config.error_reporting }}
error_reporting: {{ .Values.config.error_reporting }}
domain: ".{{ index .Values.ingress.hosts 0 }}"

View File

@ -0,0 +1,121 @@
{{- if .Values.monitoring.enabled -}}
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: {{ include "passbook.fullname" . }}-static-rules
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
groups:
- name: Aggregate request counters
rules:
- record: job:django_http_requests_before_middlewares_total:sum_rate30s
expr: sum(rate(django_http_requests_before_middlewares_total[30s])) by (job)
- record: job:django_http_requests_unknown_latency_total:sum_rate30s
expr: sum(rate(django_http_requests_unknown_latency_total[30s])) by (job)
- record: job:django_http_ajax_requests_total:sum_rate30s
expr: sum(rate(django_http_ajax_requests_total[30s])) by (job)
- record: job:django_http_responses_before_middlewares_total:sum_rate30s
expr: sum(rate(django_http_responses_before_middlewares_total[30s])) by (job)
- record: job:django_http_requests_unknown_latency_including_middlewares_total:sum_rate30s
expr: sum(rate(django_http_requests_unknown_latency_including_middlewares_total[30s])) by (job)
- record: job:django_http_requests_body_total_bytes:sum_rate30s
expr: sum(rate(django_http_requests_body_total_bytes[30s])) by (job)
- record: job:django_http_responses_streaming_total:sum_rate30s
expr: sum(rate(django_http_responses_streaming_total[30s])) by (job)
- record: job:django_http_responses_body_total_bytes:sum_rate30s
expr: sum(rate(django_http_responses_body_total_bytes[30s])) by (job)
- record: job:django_http_requests_total:sum_rate30s
expr: sum(rate(django_http_requests_total_by_method[30s])) by (job)
- record: job:django_http_requests_total_by_method:sum_rate30s
expr: sum(rate(django_http_requests_total_by_method[30s])) by (job,method)
- record: job:django_http_requests_total_by_transport:sum_rate30s
expr: sum(rate(django_http_requests_total_by_transport[30s])) by (job,transport)
- record: job:django_http_requests_total_by_view:sum_rate30s
expr: sum(rate(django_http_requests_total_by_view_transport_method[30s])) by (job,view)
- record: job:django_http_requests_total_by_view_transport_method:sum_rate30s
expr: sum(rate(django_http_requests_total_by_view_transport_method[30s])) by (job,view,transport,method)
- record: job:django_http_responses_total_by_templatename:sum_rate30s
expr: sum(rate(django_http_responses_total_by_templatename[30s])) by (job,templatename)
- record: job:django_http_responses_total_by_status:sum_rate30s
expr: sum(rate(django_http_responses_total_by_status[30s])) by (job,status)
- record: job:django_http_responses_total_by_status_name_method:sum_rate30s
expr: sum(rate(django_http_responses_total_by_status_name_method[30s])) by (job,status,name,method)
- record: job:django_http_responses_total_by_charset:sum_rate30s
expr: sum(rate(django_http_responses_total_by_charset[30s])) by (job,charset)
- record: job:django_http_exceptions_total_by_type:sum_rate30s
expr: sum(rate(django_http_exceptions_total_by_type[30s])) by (job,type)
- record: job:django_http_exceptions_total_by_view:sum_rate30s
expr: sum(rate(django_http_exceptions_total_by_view[30s])) by (job,view)
- name: Aggregate latency histograms
rules:
- record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s
expr: histogram_quantile(0.50, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le))
labels:
quantile: "50"
- record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s
expr: histogram_quantile(0.95, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le))
labels:
quantile: "95"
- record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s
expr: histogram_quantile(0.99, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le))
labels:
quantile: "99"
- record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s
expr: histogram_quantile(0.999, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le))
labels:
quantile: "99.9"
- record: job:django_http_requests_latency_seconds:quantile_rate30s
expr: histogram_quantile(0.50, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le))
labels:
quantile: "50"
- record: job:django_http_requests_latency_seconds:quantile_rate30s
expr: histogram_quantile(0.95, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le))
labels:
quantile: "95"
- record: job:django_http_requests_latency_seconds:quantile_rate30s
expr: histogram_quantile(0.99, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le))
labels:
quantile: "99"
- record: job:django_http_requests_latency_seconds:quantile_rate30s
expr: histogram_quantile(0.999, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le))
labels:
quantile: "99.9"
- name: Aggregate model operations
rules:
- record: job:django_model_inserts_total:sum_rate1m
expr: sum(rate(django_model_inserts_total[1m])) by (job, model)
- record: job:django_model_updates_total:sum_rate1m
expr: sum(rate(django_model_updates_total[1m])) by (job, model)
- record: job:django_model_deletes_total:sum_rate1m
expr: sum(rate(django_model_deletes_total[1m])) by (job, model)
- name: Aggregate database operations
rules:
- record: job:django_db_new_connections_total:sum_rate30s
expr: sum(rate(django_db_new_connections_total[30s])) by (alias, vendor)
- record: job:django_db_new_connection_errors_total:sum_rate30s
expr: sum(rate(django_db_new_connection_errors_total[30s])) by (alias, vendor)
- record: job:django_db_execute_total:sum_rate30s
expr: sum(rate(django_db_execute_total[30s])) by (alias, vendor)
- record: job:django_db_execute_many_total:sum_rate30s
expr: sum(rate(django_db_execute_many_total[30s])) by (alias, vendor)
- record: job:django_db_errors_total:sum_rate30s
expr: sum(rate(django_db_errors_total[30s])) by (alias, vendor, type)
- name: Aggregate migrations
rules:
- record: job:django_migrations_applied_total:max
expr: max(django_migrations_applied_total) by (job, connection)
- record: job:django_migrations_unapplied_total:max
expr: max(django_migrations_unapplied_total) by (job, connection)
- name: Alerts
rules:
- alert: UnappliedMigrations
expr: job:django_migrations_unapplied_total:max > 0
for: 1m
labels:
severity: testing
{{- end }}

View File

@ -4,6 +4,7 @@ type: Opaque
metadata:
name: {{ include "passbook.fullname" . }}-secret-key
data:
monitoring_username: bW9uaXRvcg== # monitor in base64
{{- if .Values.config.secret_key }}
secret_key: {{ .Values.config.secret_key | b64enc | quote }}
{{- else }}

View File

@ -1,4 +1,4 @@
apiVersion: apps/v1beta2
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "passbook.fullname" . }}-static
@ -18,10 +18,6 @@ spec:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
k8s.passbook.io/component: static
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: '9113'
field.cattle.io/workloadMetrics: '[{"path":"/metrics","port":9113,"schema":"HTTP"}]'
spec:
containers:
- name: {{ .Chart.Name }}-static
@ -29,19 +25,19 @@ spec:
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
containerPort: 8080
protocol: TCP
livenessProbe:
initialDelaySeconds: 10
timeoutSeconds: 5
httpGet:
path: /_/healthz
path: /-/ping
port: http
readinessProbe:
initialDelaySeconds: 10
timeoutSeconds: 5
httpGet:
path: /_/healthz
path: /-/ping
port: http
resources:
requests:
@ -50,6 +46,3 @@ spec:
limits:
cpu: 20m
memory: 20M
- name: {{ .Chart.Name }}-static-prometheus
image: nginx/nginx-prometheus-exporter:0.4.1
imagePullPolicy: IfNotPresent

View File

@ -11,7 +11,7 @@ metadata:
spec:
type: ClusterIP
ports:
- port: 80
- port: 8080
targetPort: http
protocol: TCP
name: http

View File

@ -0,0 +1,17 @@
{{- if .Values.monitoring.enabled -}}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
name: {{ include "passbook.fullname" . }}-static-monitoring
spec:
endpoints:
- port: http
selector:
matchLabels:
k8s.passbook.io/component: static
{{- end }}

View File

@ -1,4 +1,4 @@
apiVersion: apps/v1beta2
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "passbook.fullname" . }}-web

View File

@ -4,13 +4,14 @@ metadata:
name: {{ include "passbook.fullname" . }}-web
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ include "passbook.chart" . }}
k8s.passbook.io/component: web
spec:
type: {{ .Values.service.type }}
type: ClusterIP
ports:
- port: {{ .Values.service.port }}
- port: 80
targetPort: http
protocol: TCP
name: http

View File

@ -0,0 +1,25 @@
{{- if .Values.monitoring.enabled -}}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
helm.sh/chart: {{ include "passbook.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
name: {{ include "passbook.fullname" . }}-web-monitoring
spec:
endpoints:
- basicAuth:
password:
name: {{ include "passbook.fullname" . }}-secret-key
key: secret_key
username:
name: {{ include "passbook.fullname" . }}-secret-key
key: monitoring_username
port: http
interval: 10s
selector:
matchLabels:
k8s.passbook.io/component: web
{{- end }}

View File

@ -1,4 +1,4 @@
apiVersion: apps/v1beta2
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "passbook.fullname" . }}-worker

View File

@ -2,7 +2,7 @@
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
image:
tag: 0.6.10-beta
tag: 0.7.5-beta
nameOverride: ""
@ -10,23 +10,14 @@ config:
# Optionally specify fixed secret_key, otherwise generated automatically
# secret_key: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o
# Enable error reporting
error_reporting: true
error_reporting: false
email:
host: localhost
postgresql:
postgresqlDatabase: passbook
redis:
cluster:
enabled: false
master:
persistence:
enabled: false
service:
type: ClusterIP
port: 80
# This Helm chart ships with built-in Prometheus ServiceMonitors and Rules.
# This requires the CoreOS Prometheus Operator.
monitoring:
enabled: false
ingress:
enabled: false
@ -40,3 +31,14 @@ ingress:
# - secretName: chart-example-tls
# hosts:
# - passbook.k8s.local
# These settings configure the packaged PostgreSQL and Redis chart.
postgresql:
postgresqlDatabase: passbook
redis:
cluster:
enabled: false
master:
persistence:
enabled: false

31
mkdocs.yml Normal file
View File

@ -0,0 +1,31 @@
site_name: passbook Docs
site_url: https://docs.passbook.beryju.org
copyright: "Copyright &copy; 2019 - 2020 BeryJu.org"
nav:
- Home: index.md
- Installation:
- Installation: installation/install.md
- docker-compose: installation/docker-compose.md
- Kubernetes: installation/kubernetes.md
- Sources: sources.md
- Providers: providers.md
- Property Mappings: property-mappings.md
- Factors: factors.md
- Policies: policies.md
- Integrations:
- as Provider:
- GitLab: integrations/services/gitlab/index.md
- Rancher: integrations/services/rancher/index.md
- Harbor: integrations/services/harbor/index.md
- Sentry: integrations/services/sentry/index.md
repo_name: "BeryJu.org/passbook"
repo_url: https://git.beryju.org/BeryJu.org/passbook
theme:
name: "material"
logo: "images/logo.svg"
markdown_extensions:
- toc:
permalink: "¶"

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = '0.6.10-beta'
__version__ = '0.7.5-beta'

View File

@ -1,6 +0,0 @@
"""Versioned Admin API Urls"""
from django.conf.urls import include, url
urlpatterns = [
url(r'^v1/', include('passbook.admin.api.v1.urls', namespace='v1')),
]

View File

@ -1,36 +0,0 @@
"""passbook admin gorup API"""
from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.viewsets import ModelViewSet
from passbook.core.models import Group
class RecursiveField(Serializer):
"""Recursive field for manytomanyfield"""
def to_representation(self, value):
serializer = self.parent.parent.__class__(value, context=self.context)
return serializer.data
def create(self):
raise NotImplementedError()
def update(self):
raise NotImplementedError()
class GroupSerializer(ModelSerializer):
"""Group Serializer"""
children = RecursiveField(many=True)
class Meta:
model = Group
fields = '__all__'
class GroupViewSet(ModelViewSet):
"""Group Viewset"""
permission_classes = [IsAdminUser]
serializer_class = GroupSerializer
queryset = Group.objects.filter(parent__isnull=True)

View File

@ -1,33 +0,0 @@
"""passbook admin API URLs"""
from django.urls import path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions
from rest_framework.routers import DefaultRouter
from passbook.admin.api.v1.applications import ApplicationViewSet
from passbook.admin.api.v1.groups import GroupViewSet
from passbook.admin.api.v1.users import UserViewSet
router = DefaultRouter()
router.register('applications', ApplicationViewSet)
router.register('groups', GroupViewSet)
router.register('users', UserViewSet)
SchemaView = get_schema_view(
openapi.Info(
title="passbook Administration API",
default_version='v1',
description="Internal passbook API for Administration Interface",
contact=openapi.Contact(email="contact@snippets.local"),
license=openapi.License(name="MIT License"),
),
public=True,
permission_classes=(permissions.IsAdminUser,),
)
urlpatterns = router.urls + [
path('swagger.yml', SchemaView.without_ui(cache_timeout=0), name='schema-json'),
path('swagger/', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
]
app_name = 'passbook.admin'

View File

@ -1,4 +1,4 @@
"""p2 form helpers"""
"""passbook form helpers"""
from django import forms
from passbook.admin.fields import YAMLField

View File

@ -2,5 +2,6 @@
# from django import forms
SOURCE_FORM_FIELDS = ['name', 'slug', 'enabled', 'policies']
SOURCE_SERIALIZER_FIELDS = ['pk', 'name', 'slug', 'enabled', 'policies']
# class SourceForm(forms.Form)

View File

@ -1,5 +1,5 @@
"""passbook URL Configuration"""
from django.urls import include, path
from django.urls import path
from passbook.admin.views import (applications, audit, debug, factors, groups,
invitations, overview, policy,
@ -74,11 +74,9 @@ urlpatterns = [
path('group/<uuid:pk>/update/', groups.GroupUpdateView.as_view(), name='group-update'),
path('group/<uuid:pk>/delete/', groups.GroupDeleteView.as_view(), name='group-delete'),
# Audit Log
path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'),
path('audit/', audit.EventListView.as_view(), name='audit-log'),
# Groups
path('groups/', groups.GroupListView.as_view(), name='groups'),
# API
path('api/', include('passbook.admin.api.urls')),
# Debug
path('debug/request/', debug.DebugRequestView.as_view(), name='debug-request'),
]

View File

@ -1,19 +1,24 @@
"""passbook Application administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \
PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.forms.applications import ApplicationForm
from passbook.core.models import Application
from passbook.lib.views import CreateAssignPermView
class ApplicationListView(AdminRequiredMixin, ListView):
class ApplicationListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all applications"""
model = Application
permission_required = 'passbook_core.view_application'
ordering = 'name'
paginate_by = 40
template_name = 'administration/application/list.html'
@ -22,10 +27,13 @@ class ApplicationListView(AdminRequiredMixin, ListView):
return super().get_queryset().select_subclasses()
class ApplicationCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
class ApplicationCreateView(SuccessMessageMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, CreateAssignPermView):
"""Create new Application"""
model = Application
form_class = ApplicationForm
permission_required = 'passbook_core.add_application'
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:applications')
@ -36,21 +44,25 @@ class ApplicationCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView)
return super().get_context_data(**kwargs)
class ApplicationUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
class ApplicationUpdateView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, UpdateView):
"""Update application"""
model = Application
form_class = ApplicationForm
permission_required = 'passbook_core.change_application'
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:applications')
success_message = _('Successfully updated Application')
class ApplicationDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
class ApplicationDeleteView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, DeleteView):
"""Delete application"""
model = Application
permission_required = 'passbook_core.delete_application'
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:applications')

View File

@ -1,16 +1,18 @@
"""passbook AuditEntry administration"""
"""passbook Event administration"""
from django.views.generic import ListView
from guardian.mixins import PermissionListMixin
from passbook.admin.mixins import AdminRequiredMixin
from passbook.audit.models import AuditEntry
from passbook.audit.models import Event
class AuditEntryListView(AdminRequiredMixin, ListView):
class EventListView(PermissionListMixin, ListView):
"""Show list of all invitations"""
model = AuditEntry
model = Event
template_name = 'administration/audit/list.html'
permission_required = 'passbook_audit.view_event'
ordering = '-created'
paginate_by = 10
def get_queryset(self):
return AuditEntry.objects.all().order_by('-created')
return Event.objects.all().order_by('-created')

View File

@ -1,11 +1,9 @@
"""passbook administration debug views"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import TemplateView
from passbook.admin.mixins import AdminRequiredMixin
class DebugRequestView(AdminRequiredMixin, TemplateView):
class DebugRequestView(LoginRequiredMixin, TemplateView):
"""Show debug info about request"""
template_name = 'administration/debug/request.html'

View File

@ -1,14 +1,18 @@
"""passbook Factor administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \
PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Factor
from passbook.lib.utils.reflection import path_to_class
from passbook.lib.views import CreateAssignPermView
def all_subclasses(cls):
@ -16,11 +20,13 @@ def all_subclasses(cls):
return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
class FactorListView(AdminRequiredMixin, ListView):
class FactorListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all factors"""
model = Factor
template_name = 'administration/factor/list.html'
permission_required = 'passbook_core.view_factor'
ordering = 'order'
paginate_by = 40
@ -32,10 +38,15 @@ class FactorListView(AdminRequiredMixin, ListView):
def get_queryset(self):
return super().get_queryset().select_subclasses()
class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
class FactorCreateView(SuccessMessageMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, CreateAssignPermView):
"""Create new Factor"""
model = Factor
template_name = 'generic/create.html'
permission_required = 'passbook_core.add_factor'
success_url = reverse_lazy('passbook_admin:factors')
success_message = _('Successfully created Factor')
@ -53,10 +64,13 @@ class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
raise Http404
return path_to_class(model.form)
class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
class FactorUpdateView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, UpdateView):
"""Update factor"""
model = Factor
permission_required = 'passbook_core.update_application'
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:factors')
success_message = _('Successfully updated Factor')
@ -69,11 +83,14 @@ class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
def get_object(self, queryset=None):
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
class FactorDeleteView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, DeleteView):
"""Delete factor"""
model = Factor
template_name = 'generic/delete.html'
permission_required = 'passbook_core.delete_factor'
success_url = reverse_lazy('passbook_admin:factors')
success_message = _('Successfully deleted Factor')

View File

@ -1,28 +1,36 @@
"""passbook Group administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \
PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.forms.groups import GroupForm
from passbook.core.models import Group
from passbook.lib.views import CreateAssignPermView
class GroupListView(AdminRequiredMixin, ListView):
class GroupListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all groups"""
model = Group
permission_required = 'passbook_core.view_group'
ordering = 'name'
paginate_by = 40
template_name = 'administration/group/list.html'
class GroupCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
class GroupCreateView(SuccessMessageMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, CreateAssignPermView):
"""Create new Group"""
model = Group
form_class = GroupForm
permission_required = 'passbook_core.add_group'
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:groups')
@ -33,18 +41,20 @@ class GroupCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
return super().get_context_data(**kwargs)
class GroupUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
class GroupUpdateView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, UpdateView):
"""Update group"""
model = Group
form_class = GroupForm
permission_required = 'passbook_core.change_group'
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:groups')
success_message = _('Successfully updated Group')
class GroupDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
class GroupDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView):
"""Delete group"""
model = Group

View File

@ -1,33 +1,40 @@
"""passbook Invitation administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \
PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.http import HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import CreateView, DeleteView, ListView
from django.views.generic import DeleteView, ListView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.forms.invitations import InvitationForm
from passbook.core.models import Invitation
from passbook.core.signals import invitation_created
from passbook.lib.views import CreateAssignPermView
class InvitationListView(AdminRequiredMixin, ListView):
class InvitationListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all invitations"""
model = Invitation
ordering = 'expires'
paginate_by = 40
permission_required = 'passbook_core.view_invitation'
template_name = 'administration/invitation/list.html'
class InvitationCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
class InvitationCreateView(SuccessMessageMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, CreateAssignPermView):
"""Create new Invitation"""
model = Invitation
form_class = InvitationForm
permission_required = 'passbook_core.add_invitation'
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:invitations')
success_message = _('Successfully created Invitation')
form_class = InvitationForm
def get_context_data(self, **kwargs):
kwargs['type'] = 'Invitation'
@ -43,10 +50,14 @@ class InvitationCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
invitation=obj)
return HttpResponseRedirect(self.success_url)
class InvitationDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
class InvitationDeleteView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, DeleteView):
"""Delete invitation"""
model = Invitation
permission_required = 'passbook_core.delete_invitation'
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:invitations')
success_message = _('Successfully deleted Invitation')

View File

@ -1,26 +1,29 @@
"""passbook Policy administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \
PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import (CreateView, DeleteView, FormView, ListView,
UpdateView)
from django.views.generic import DeleteView, FormView, ListView, UpdateView
from django.views.generic.detail import DetailView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.forms.policies import PolicyTestForm
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Policy
from passbook.lib.utils.reflection import path_to_class
from passbook.lib.views import CreateAssignPermView
from passbook.policies.engine import PolicyEngine
class PolicyListView(AdminRequiredMixin, ListView):
class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all policies"""
model = Policy
ordering = 'name'
paginate_by = 40
permission_required = 'passbook_core.view_policy'
template_name = 'administration/policy/list.html'
def get_context_data(self, **kwargs):
@ -32,9 +35,13 @@ class PolicyListView(AdminRequiredMixin, ListView):
return super().get_queryset().order_by('order').select_subclasses()
class PolicyCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
class PolicyCreateView(SuccessMessageMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, CreateAssignPermView):
"""Create new Policy"""
model = Policy
permission_required = 'passbook_core.add_policy'
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:policies')
success_message = _('Successfully created Policy')
@ -48,10 +55,13 @@ class PolicyCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
return path_to_class(model.form)
class PolicyUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
class PolicyUpdateView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, UpdateView):
"""Update policy"""
model = Policy
permission_required = 'passbook_core.change_policy'
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:policies')
success_message = _('Successfully updated Policy')
@ -65,10 +75,13 @@ class PolicyUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class PolicyDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
class PolicyDeleteView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, DeleteView):
"""Delete policy"""
model = Policy
permission_required = 'passbook_core.delete_policy'
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:policies')
success_message = _('Successfully deleted Policy')
@ -81,11 +94,12 @@ class PolicyDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
return super().delete(request, *args, **kwargs)
class PolicyTestView(AdminRequiredMixin, DetailView, FormView):
class PolicyTestView(LoginRequiredMixin, DetailView, PermissionRequiredMixin, FormView):
"""View to test policy(s)"""
model = Policy
form_class = PolicyTestForm
permission_required = 'passbook_core.view_policy'
template_name = 'administration/policy/test.html'
object = None

View File

@ -1,14 +1,18 @@
"""passbook PropertyMapping administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \
PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import PropertyMapping
from passbook.lib.utils.reflection import path_to_class
from passbook.lib.views import CreateAssignPermView
def all_subclasses(cls):
@ -17,10 +21,11 @@ def all_subclasses(cls):
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
class PropertyMappingListView(AdminRequiredMixin, ListView):
class PropertyMappingListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all property_mappings"""
model = PropertyMapping
permission_required = 'passbook_core.view_propertymapping'
template_name = 'administration/property_mapping/list.html'
ordering = 'name'
paginate_by = 40
@ -34,9 +39,13 @@ class PropertyMappingListView(AdminRequiredMixin, ListView):
return super().get_queryset().select_subclasses()
class PropertyMappingCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
class PropertyMappingCreateView(SuccessMessageMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, CreateAssignPermView):
"""Create new PropertyMapping"""
model = PropertyMapping
permission_required = 'passbook_core.add_propertymapping'
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:property-mappings')
success_message = _('Successfully created Property Mapping')
@ -58,10 +67,13 @@ class PropertyMappingCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateV
return path_to_class(model.form)
class PropertyMappingUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
class PropertyMappingUpdateView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, UpdateView):
"""Update property_mapping"""
model = PropertyMapping
permission_required = 'passbook_core.change_propertymapping'
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:property-mappings')
success_message = _('Successfully updated Property Mapping')
@ -75,10 +87,13 @@ class PropertyMappingUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateV
return PropertyMapping.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class PropertyMappingDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
class PropertyMappingDeleteView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, DeleteView):
"""Delete property_mapping"""
model = PropertyMapping
permission_required = 'passbook_core.delete_propertymapping'
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:property-mappings')
success_message = _('Successfully deleted Property Mapping')

View File

@ -1,21 +1,25 @@
"""passbook Provider administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \
PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Provider
from passbook.lib.utils.reflection import path_to_class
from passbook.lib.views import CreateAssignPermView
class ProviderListView(AdminRequiredMixin, ListView):
class ProviderListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all providers"""
model = Provider
paginate_by = 40
permission_required = 'passbook_core.add_provider'
template_name = 'administration/provider/list.html'
def get_context_data(self, **kwargs):
@ -27,9 +31,13 @@ class ProviderListView(AdminRequiredMixin, ListView):
return super().get_queryset().select_subclasses()
class ProviderCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
class ProviderCreateView(SuccessMessageMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, CreateAssignPermView):
"""Create new Provider"""
model = Provider
permission_required = 'passbook_core.add_provider'
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:providers')
success_message = _('Successfully created Provider')
@ -43,10 +51,13 @@ class ProviderCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
return path_to_class(model.form)
class ProviderUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
class ProviderUpdateView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, UpdateView):
"""Update provider"""
model = Provider
permission_required = 'passbook_core.change_provider'
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:providers')
success_message = _('Successfully updated Provider')
@ -60,10 +71,13 @@ class ProviderUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
return Provider.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class ProviderDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
class ProviderDeleteView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, DeleteView):
"""Delete provider"""
model = Provider
permission_required = 'passbook_core.delete_provider'
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:providers')
success_message = _('Successfully deleted Provider')

View File

@ -1,14 +1,18 @@
"""passbook Source administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \
PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.http import Http404
from django.urls import reverse_lazy
from django.utils.translation import ugettext as _
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from django.views.generic import DeleteView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Source
from passbook.lib.utils.reflection import path_to_class
from passbook.lib.views import CreateAssignPermView
def all_subclasses(cls):
@ -16,10 +20,11 @@ def all_subclasses(cls):
return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
class SourceListView(AdminRequiredMixin, ListView):
class SourceListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all sources"""
model = Source
permission_required = 'passbook_core.view_source'
ordering = 'name'
paginate_by = 40
template_name = 'administration/source/list.html'
@ -33,9 +38,13 @@ class SourceListView(AdminRequiredMixin, ListView):
return super().get_queryset().select_subclasses()
class SourceCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
class SourceCreateView(SuccessMessageMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, CreateAssignPermView):
"""Create new Source"""
model = Source
permission_required = 'passbook_core.add_source'
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:sources')
success_message = _('Successfully created Source')
@ -48,10 +57,13 @@ class SourceCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
return path_to_class(model.form)
class SourceUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
class SourceUpdateView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, UpdateView):
"""Update source"""
model = Source
permission_required = 'passbook_core.change_source'
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:sources')
success_message = _('Successfully updated Source')
@ -65,10 +77,13 @@ class SourceUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
return Source.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class SourceDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
class SourceDeleteView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, DeleteView):
"""Delete source"""
model = Source
permission_required = 'passbook_core.delete_source'
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:sources')
success_message = _('Successfully deleted Source')

View File

@ -1,54 +1,65 @@
"""passbook User administration"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import \
PermissionRequiredMixin as DjangoPermissionRequiredMixin
from django.contrib.messages.views import SuccessMessageMixin
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext as _
from django.views import View
from django.views.generic import CreateView, DeleteView, ListView, UpdateView
from django.views.generic import DeleteView, DetailView, ListView, UpdateView
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
from passbook.admin.forms.users import UserForm
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Nonce, User
from passbook.lib.views import CreateAssignPermView
class UserListView(AdminRequiredMixin, ListView):
class UserListView(LoginRequiredMixin, PermissionListMixin, ListView):
"""Show list of all users"""
model = User
permission_required = 'passbook_core.view_user'
ordering = 'username'
paginate_by = 40
template_name = 'administration/user/list.html'
class UserCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
class UserCreateView(SuccessMessageMixin, LoginRequiredMixin,
DjangoPermissionRequiredMixin, CreateAssignPermView):
"""Create user"""
model = User
form_class = UserForm
permission_required = 'passbook_core.add_user'
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, LoginRequiredMixin,
PermissionRequiredMixin, UpdateView):
"""Update user"""
model = User
form_class = UserForm
permission_required = 'passbook_core.change_user'
context_object_name = 'object' # By default the object's name
# is user which is used by other checks
# By default the object's name is user which is used by other checks
context_object_name = 'object'
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:users')
success_message = _('Successfully updated User')
class UserDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
class UserDeleteView(SuccessMessageMixin, LoginRequiredMixin,
PermissionRequiredMixin, DeleteView):
"""Delete user"""
model = User
permission_required = 'passbook_core.delete_user'
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:users')
success_message = _('Successfully deleted User')
@ -58,14 +69,16 @@ class UserDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
return super().delete(request, *args, **kwargs)
class UserPasswordResetView(AdminRequiredMixin, View):
class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""Get Password reset link for user"""
# pylint: disable=invalid-name
def get(self, request, pk):
model = User
permission_required = 'passbook_core.reset_user_password'
def get(self, request, *args, **kwargs):
"""Create nonce for user and return link"""
user = get_object_or_404(User, pk=pk)
nonce = Nonce.objects.create(user=user)
super().get(request, *args, **kwargs)
nonce = Nonce.objects.create(user=self.object)
link = request.build_absolute_uri(reverse(
'passbook_core:auth-password-reset', kwargs={'nonce': nonce.uuid}))
messages.success(request, _('Password reset link: <pre>%(link)s</pre>' % {'link': link}))

View File

@ -0,0 +1,31 @@
"""permission classes for django restframework"""
from rest_framework.permissions import BasePermission, DjangoObjectPermissions
from passbook.core.models import PolicyModel
from passbook.policies.engine import PolicyEngine
class CustomObjectPermissions(DjangoObjectPermissions):
"""Similar to `DjangoObjectPermissions`, but adding 'view' permissions."""
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
class PolicyPermissions(BasePermission):
"""Permission checker based on PolicyEngine"""
policy_engine: PolicyEngine
def has_object_permission(self, request, view, obj: PolicyModel) -> bool:
# if not obj.po
self.policy_engine = PolicyEngine(obj.policies, request.user, request)
self.policy_engine.request.obj = obj
return self.policy_engine.build().passing

View File

@ -2,7 +2,9 @@
from django.urls import include, path
from passbook.api.v1.urls import urlpatterns as v1_urls
from passbook.api.v2.urls import urlpatterns as v2_urls
urlpatterns = [
path('v1/', include(v1_urls))
path('v1/', include(v1_urls)),
path('v2/', include(v2_urls)),
]

103
passbook/api/v2/urls.py Normal file
View File

@ -0,0 +1,103 @@
"""api v2 urls"""
from django.conf.urls import url
from django.urls import path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import routers
from structlog import get_logger
from passbook.api.permissions import CustomObjectPermissions
from passbook.audit.api.events import EventViewSet
from passbook.core.api.applications import ApplicationViewSet
from passbook.core.api.factors import FactorViewSet
from passbook.core.api.groups import GroupViewSet
from passbook.core.api.invitations import InvitationViewSet
from passbook.core.api.policies import PolicyViewSet
from passbook.core.api.propertymappings import PropertyMappingViewSet
from passbook.core.api.providers import ProviderViewSet
from passbook.core.api.sources import SourceViewSet
from passbook.core.api.users import UserViewSet
from passbook.factors.captcha.api import CaptchaFactorViewSet
from passbook.factors.dummy.api import DummyFactorViewSet
from passbook.factors.email.api import EmailFactorViewSet
from passbook.factors.otp.api import OTPFactorViewSet
from passbook.factors.password.api import PasswordFactorViewSet
from passbook.lib.utils.reflection import get_apps
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
from passbook.policies.group.api import GroupMembershipPolicyViewSet
from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
from passbook.policies.matcher.api import FieldMatcherPolicyViewSet
from passbook.policies.password.api import PasswordPolicyViewSet
from passbook.policies.reputation.api import ReputationPolicyViewSet
from passbook.policies.sso.api import SSOLoginPolicyViewSet
from passbook.policies.webhook.api import WebhookPolicyViewSet
from passbook.providers.app_gw.api import ApplicationGatewayProviderViewSet
from passbook.providers.oauth.api import OAuth2ProviderViewSet
from passbook.providers.oidc.api import OpenIDProviderViewSet
from passbook.providers.saml.api import (SAMLPropertyMappingViewSet,
SAMLProviderViewSet)
from passbook.sources.ldap.api import (LDAPPropertyMappingViewSet,
LDAPSourceViewSet)
from passbook.sources.oauth.api import OAuthSourceViewSet
LOGGER = get_logger()
router = routers.DefaultRouter()
for _passbook_app in get_apps():
if hasattr(_passbook_app, 'api_mountpoint'):
for prefix, viewset in _passbook_app.api_mountpoint:
router.register(prefix, viewset)
LOGGER.debug("Mounted API URLs", app_name=_passbook_app.name)
router.register('core/applications', ApplicationViewSet)
router.register('core/invitations', InvitationViewSet)
router.register('core/groups', GroupViewSet)
router.register('core/users', UserViewSet)
router.register('audit/events', EventViewSet)
router.register('sources/all', SourceViewSet)
router.register('sources/ldap', LDAPSourceViewSet)
router.register('sources/oauth', OAuthSourceViewSet)
router.register('policies/all', PolicyViewSet)
router.register('policies/passwordexpiry', PasswordExpiryPolicyViewSet)
router.register('policies/groupmembership', GroupMembershipPolicyViewSet)
router.register('policies/haveibeenpwned', HaveIBeenPwendPolicyViewSet)
router.register('policies/fieldmatcher', FieldMatcherPolicyViewSet)
router.register('policies/password', PasswordPolicyViewSet)
router.register('policies/reputation', ReputationPolicyViewSet)
router.register('policies/ssologin', SSOLoginPolicyViewSet)
router.register('policies/webhook', WebhookPolicyViewSet)
router.register('providers/all', ProviderViewSet)
router.register('providers/applicationgateway', ApplicationGatewayProviderViewSet)
router.register('providers/oauth', OAuth2ProviderViewSet)
router.register('providers/openid', OpenIDProviderViewSet)
router.register('providers/saml', SAMLProviderViewSet)
router.register('propertymappings/all', PropertyMappingViewSet)
router.register('propertymappings/ldap', LDAPPropertyMappingViewSet)
router.register('propertymappings/saml', SAMLPropertyMappingViewSet)
router.register('factors/all', FactorViewSet)
router.register('factors/captcha', CaptchaFactorViewSet)
router.register('factors/dummy', DummyFactorViewSet)
router.register('factors/email', EmailFactorViewSet)
router.register('factors/otp', OTPFactorViewSet)
router.register('factors/password', PasswordFactorViewSet)
info = openapi.Info(
title="passbook API",
default_version='v2',
# description="Test description",
# terms_of_service="https://www.google.com/policies/terms/",
contact=openapi.Contact(email="hello@beryju.org"),
license=openapi.License(name="MIT License"),
)
SchemaView = get_schema_view(
info,
public=True,
permission_classes=(CustomObjectPermissions,),
)
urlpatterns = [
url(r'^swagger(?P<format>\.json|\.yaml)$',
SchemaView.without_ui(cache_timeout=0), name='schema-json'),
path('swagger/', SchemaView.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('redoc/', SchemaView.with_ui('redoc', cache_timeout=0), name='schema-redoc'),
] + router.urls

View File

@ -0,0 +1,21 @@
"""Audit API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ReadOnlyModelViewSet
from passbook.audit.models import Event
class EventSerializer(ModelSerializer):
"""Event Serializer"""
class Meta:
model = Event
fields = ['pk', 'user', 'action', 'date', 'app', 'context', 'request_ip', 'created', ]
class EventViewSet(ReadOnlyModelViewSet):
"""Event Read-Only Viewset"""
queryset = Event.objects.all()
serializer_class = EventSerializer

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.6 on 2019-10-28 08:29
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('passbook_audit', '0001_initial'),
]
operations = [
migrations.RenameModel(
old_name='AuditEntry',
new_name='Event',
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.8 on 2019-12-05 14:07
from django.db import migrations, models
import passbook.audit.models
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0002_auto_20191028_0829'),
]
operations = [
migrations.AlterModelOptions(
name='event',
options={'verbose_name': 'Audit Event', 'verbose_name_plural': 'Audit Events'},
),
migrations.AlterField(
model_name='event',
name='action',
field=models.TextField(choices=[('LOGIN', 'login'), ('LOGIN_FAILED', 'login_failed'), ('LOGOUT', 'logout'), ('AUTHORIZE_APPLICATION', 'authorize_application'), ('SUSPICIOUS_REQUEST', 'suspicious_request'), ('SIGN_UP', 'sign_up'), ('PASSWORD_RESET', 'password_reset'), ('INVITE_CREATED', 'invitation_created'), ('INVITE_USED', 'invitation_used'), ('CUSTOM', 'custom')]),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.8 on 2019-12-05 15:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0003_auto_20191205_1407'),
]
operations = [
migrations.RemoveField(
model_name='event',
name='request_ip',
),
migrations.AddField(
model_name='event',
name='client_ip',
field=models.GenericIPAddressField(null=True),
),
]

View File

@ -1,75 +1,103 @@
"""passbook audit models"""
from enum import Enum
from inspect import getmodule, stack
from typing import Optional
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError
from django.db import models
from django.http import HttpRequest
from django.utils.translation import gettext as _
from ipware import get_client_ip
from guardian.shortcuts import get_anonymous_user
from structlog import get_logger
from passbook.lib.models import UUIDModel
from passbook.lib.utils.http import get_client_ip
LOGGER = get_logger()
class AuditEntry(UUIDModel):
"""An individual audit log entry"""
class EventAction(Enum):
"""All possible actions to save into the audit log"""
ACTION_LOGIN = 'login'
ACTION_LOGIN_FAILED = 'login_failed'
ACTION_LOGOUT = 'logout'
ACTION_AUTHORIZE_APPLICATION = 'authorize_application'
ACTION_SUSPICIOUS_REQUEST = 'suspicious_request'
ACTION_SIGN_UP = 'sign_up'
ACTION_PASSWORD_RESET = 'password_reset' # noqa # nosec
ACTION_INVITE_CREATED = 'invitation_created'
ACTION_INVITE_USED = 'invitation_used'
ACTIONS = (
(ACTION_LOGIN, ACTION_LOGIN),
(ACTION_LOGIN_FAILED, ACTION_LOGIN_FAILED),
(ACTION_LOGOUT, ACTION_LOGOUT),
(ACTION_AUTHORIZE_APPLICATION, ACTION_AUTHORIZE_APPLICATION),
(ACTION_SUSPICIOUS_REQUEST, ACTION_SUSPICIOUS_REQUEST),
(ACTION_SIGN_UP, ACTION_SIGN_UP),
(ACTION_PASSWORD_RESET, ACTION_PASSWORD_RESET),
(ACTION_INVITE_CREATED, ACTION_INVITE_CREATED),
(ACTION_INVITE_USED, ACTION_INVITE_USED),
)
LOGIN = 'login'
LOGIN_FAILED = 'login_failed'
LOGOUT = 'logout'
AUTHORIZE_APPLICATION = 'authorize_application'
SUSPICIOUS_REQUEST = 'suspicious_request'
SIGN_UP = 'sign_up'
PASSWORD_RESET = 'password_reset' # noqa # nosec
INVITE_CREATED = 'invitation_created'
INVITE_USED = 'invitation_used'
CUSTOM = 'custom'
@staticmethod
def as_choices():
"""Generate choices of actions used for database"""
return tuple((x, y.value) for x, y in EventAction.__members__.items())
class Event(UUIDModel):
"""An individual audit log event"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)
action = models.TextField(choices=ACTIONS)
action = models.TextField(choices=EventAction.as_choices())
date = models.DateTimeField(auto_now_add=True)
app = models.TextField()
context = JSONField(default=dict, blank=True)
request_ip = models.GenericIPAddressField()
client_ip = models.GenericIPAddressField(null=True)
created = models.DateTimeField(auto_now_add=True)
@staticmethod
def create(action, request, **kwargs):
"""Create AuditEntry from arguments"""
client_ip, _ = get_client_ip(request)
if not hasattr(request, 'user'):
user = None
else:
user = request.user
if isinstance(user, AnonymousUser):
user = kwargs.get('user', None)
entry = AuditEntry.objects.create(
action=action,
user=user,
# User 255.255.255.255 as fallback if IP cannot be determined
request_ip=client_ip or '255.255.255.255',
def _get_app_from_request(request: HttpRequest) -> str:
if not isinstance(request, HttpRequest):
return ""
return request.resolver_match.app_name
@staticmethod
def new(action: EventAction,
app: Optional[str] = None,
_inspect_offset: int = 1,
**kwargs) -> 'Event':
"""Create new Event instance from arguments. Instance is NOT saved."""
if not isinstance(action, EventAction):
raise ValueError(f"action must be EventAction instance but was {type(action)}")
if not app:
app = getmodule(stack()[_inspect_offset][0]).__name__
event = Event(
action=action.value,
app=app,
context=kwargs)
LOGGER.debug("Created Audit entry", action=action,
user=user, from_ip=client_ip, context=kwargs)
return entry
LOGGER.debug("Created Audit event", action=action, context=kwargs)
return event
def from_http(self, request: HttpRequest,
user: Optional[settings.AUTH_USER_MODEL] = None) -> 'Event':
"""Add data from a Django-HttpRequest, allowing the creation of
Events independently from requests.
`user` arguments optionally overrides user from requests."""
if hasattr(request, 'user'):
if isinstance(request.user, AnonymousUser):
self.user = get_anonymous_user()
else:
self.user = request.user
if user:
self.user = user
# User 255.255.255.255 as fallback if IP cannot be determined
self.client_ip = get_client_ip(request) or '255.255.255.255'
# If there's no app set, we get it from the requests too
if not self.app:
self.app = Event._get_app_from_request(request)
self.save()
return self
def save(self, *args, **kwargs):
if not self._state.adding:
raise ValidationError("you may not edit an existing %s" % self._meta.model_name)
super().save(*args, **kwargs)
return super().save(*args, **kwargs)
class Meta:
verbose_name = _('Audit Entry')
verbose_name_plural = _('Audit Entries')
verbose_name = _('Audit Event')
verbose_name_plural = _('Audit Events')

View File

@ -2,7 +2,7 @@
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.dispatch import receiver
from passbook.audit.models import AuditEntry
from passbook.audit.models import Event, EventAction
from passbook.core.signals import (invitation_created, invitation_used,
user_signed_up)
@ -10,26 +10,24 @@ from passbook.core.signals import (invitation_created, invitation_used,
@receiver(user_logged_in)
def on_user_logged_in(sender, request, user, **kwargs):
"""Log successful login"""
AuditEntry.create(AuditEntry.ACTION_LOGIN, request)
Event.new(EventAction.LOGIN).from_http(request)
@receiver(user_logged_out)
def on_user_logged_out(sender, request, user, **kwargs):
"""Log successfully logout"""
AuditEntry.create(AuditEntry.ACTION_LOGOUT, request)
Event.new(EventAction.LOGOUT).from_http(request)
@receiver(user_signed_up)
def on_user_signed_up(sender, request, user, **kwargs):
"""Log successfully signed up"""
AuditEntry.create(AuditEntry.ACTION_SIGN_UP, request)
Event.new(EventAction.SIGN_UP).from_http(request)
@receiver(invitation_created)
def on_invitation_created(sender, request, invitation, **kwargs):
"""Log Invitation creation"""
AuditEntry.create(AuditEntry.ACTION_INVITE_CREATED, request,
invitation_uuid=invitation.uuid.hex)
Event.new(EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex).from_http(request)
@receiver(invitation_used)
def on_invitation_used(sender, request, invitation, **kwargs):
"""Log Invitation usage"""
AuditEntry.create(AuditEntry.ACTION_INVITE_USED, request,
invitation_uuid=invitation.uuid.hex)
Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http(request)

View File

View File

@ -1,5 +1,4 @@
"""passbook admin application API"""
from rest_framework.permissions import IsAdminUser
"""Application API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
@ -10,13 +9,14 @@ class ApplicationSerializer(ModelSerializer):
"""Application Serializer"""
class Meta:
model = Application
fields = '__all__'
fields = ['pk', 'name', 'slug', 'launch_url', 'icon_url',
'provider', 'policies', 'skip_authorization']
class ApplicationViewSet(ModelViewSet):
"""Application Viewset"""
permission_classes = [IsAdminUser]
serializer_class = ApplicationSerializer
queryset = Application.objects.all()
serializer_class = ApplicationSerializer

View File

@ -0,0 +1,30 @@
"""Factor API Views"""
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ReadOnlyModelViewSet
from passbook.core.models import Factor
class FactorSerializer(ModelSerializer):
"""Factor Serializer"""
__type__ = SerializerMethodField(method_name='get_type')
def get_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace('factor', '')
class Meta:
model = Factor
fields = ['pk', 'name', 'slug', 'order', 'enabled', '__type__']
class FactorViewSet(ReadOnlyModelViewSet):
"""Factor Viewset"""
queryset = Factor.objects.all()
serializer_class = FactorSerializer
def get_queryset(self):
return Factor.objects.select_subclasses()

View File

@ -0,0 +1,21 @@
"""Groups API Viewset"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.core.models import Group
class GroupSerializer(ModelSerializer):
"""Group Serializer"""
class Meta:
model = Group
fields = ['pk', 'name', 'parent', 'user_set', 'attributes']
class GroupViewSet(ModelViewSet):
"""Group Viewset"""
queryset = Group.objects.all()
serializer_class = GroupSerializer

View File

@ -0,0 +1,21 @@
"""Invitation API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.core.models import Invitation
class InvitationSerializer(ModelSerializer):
"""Invitation Serializer"""
class Meta:
model = Invitation
fields = ['pk', 'expires', 'fixed_username', 'fixed_email', 'needs_confirmation']
class InvitationViewSet(ModelViewSet):
"""Invitation Viewset"""
queryset = Invitation.objects.all()
serializer_class = InvitationSerializer

View File

@ -0,0 +1,31 @@
"""Policy API Views"""
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ReadOnlyModelViewSet
from passbook.core.models import Policy
from passbook.policies.forms import GENERAL_FIELDS
class PolicySerializer(ModelSerializer):
"""Policy Serializer"""
__type__ = SerializerMethodField(method_name='get_type')
def get_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace('policy', '')
class Meta:
model = Policy
fields = ['pk'] + GENERAL_FIELDS + ['__type__']
class PolicyViewSet(ReadOnlyModelViewSet):
"""Policy Viewset"""
queryset = Policy.objects.all()
serializer_class = PolicySerializer
def get_queryset(self):
return Policy.objects.select_subclasses()

View File

@ -0,0 +1,30 @@
"""PropertyMapping API Views"""
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ReadOnlyModelViewSet
from passbook.core.models import PropertyMapping
class PropertyMappingSerializer(ModelSerializer):
"""PropertyMapping Serializer"""
__type__ = SerializerMethodField(method_name='get_type')
def get_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace('propertymapping', '')
class Meta:
model = PropertyMapping
fields = ['pk', 'name', '__type__']
class PropertyMappingViewSet(ReadOnlyModelViewSet):
"""PropertyMapping Viewset"""
queryset = PropertyMapping.objects.all()
serializer_class = PropertyMappingSerializer
def get_queryset(self):
return PropertyMapping.objects.select_subclasses()

View File

@ -0,0 +1,30 @@
"""Provider API Views"""
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ReadOnlyModelViewSet
from passbook.core.models import Provider
class ProviderSerializer(ModelSerializer):
"""Provider Serializer"""
__type__ = SerializerMethodField(method_name='get_type')
def get_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace('provider', '')
class Meta:
model = Provider
fields = ['pk', 'property_mappings', '__type__']
class ProviderViewSet(ReadOnlyModelViewSet):
"""Provider Viewset"""
queryset = Provider.objects.all()
serializer_class = ProviderSerializer
def get_queryset(self):
return Provider.objects.select_subclasses()

View File

@ -0,0 +1,31 @@
"""Source API Views"""
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ReadOnlyModelViewSet
from passbook.admin.forms.source import SOURCE_SERIALIZER_FIELDS
from passbook.core.models import Source
class SourceSerializer(ModelSerializer):
"""Source Serializer"""
__type__ = SerializerMethodField(method_name='get_type')
def get_type(self, obj):
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace('source', '')
class Meta:
model = Source
fields = SOURCE_SERIALIZER_FIELDS + ['__type__']
class SourceViewSet(ReadOnlyModelViewSet):
"""Source Viewset"""
queryset = Source.objects.all()
serializer_class = SourceSerializer
def get_queryset(self):
return Source.objects.select_subclasses()

View File

@ -1,5 +1,4 @@
"""passbook admin user API"""
from rest_framework.permissions import IsAdminUser
"""User API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
@ -10,14 +9,13 @@ class UserSerializer(ModelSerializer):
"""User Serializer"""
class Meta:
model = User
fields = ['is_superuser', 'username', 'name', 'email', 'date_joined',
'uuid']
fields = ['pk', 'username', 'name', 'email']
class UserViewSet(ModelViewSet):
"""User Viewset"""
permission_classes = [IsAdminUser]
serializer_class = UserSerializer
queryset = User.objects.all()
serializer_class = UserSerializer

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.6 on 2019-10-10 10:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='user',
options={'permissions': (('reset_user_password', 'Reset Password'),)},
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.6 on 2019-10-10 15:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0002_auto_20191010_1058'),
('passbook_core', '0002_nonce_description'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.6 on 2019-10-25 20:22
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0004_remove_policy_action'),
('passbook_core', '0003_merge_20191010_1541'),
]
operations = [
]

View File

@ -11,6 +11,7 @@ from django.db import models
from django.urls import reverse_lazy
from django.utils.timezone import now
from django.utils.translation import gettext as _
from guardian.mixins import GuardianUserMixin
from model_utils.managers import InheritanceManager
from structlog import get_logger
@ -41,7 +42,7 @@ class Group(UUIDModel):
unique_together = (('name', 'parent',),)
class User(AbstractUser):
class User(GuardianUserMixin, AbstractUser):
"""Custom User model to allow easier adding o f user-based settings"""
uuid = models.UUIDField(default=uuid4, editable=False)
@ -59,6 +60,11 @@ class User(AbstractUser):
self.password_change_date = now()
return super().set_password(password)
class Meta:
permissions = (
('reset_user_password', 'Reset Password'),
)
class Provider(models.Model):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""

View File

@ -0,0 +1,21 @@
"""CaptchaFactor API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.factors.captcha.models import CaptchaFactor
class CaptchaFactorSerializer(ModelSerializer):
"""CaptchaFactor Serializer"""
class Meta:
model = CaptchaFactor
fields = ['pk', 'name', 'slug', 'order', 'enabled', 'public_key', 'private_key']
class CaptchaFactorViewSet(ModelViewSet):
"""CaptchaFactor Viewset"""
queryset = CaptchaFactor.objects.all()
serializer_class = CaptchaFactorSerializer

View File

@ -0,0 +1,21 @@
"""DummyFactor API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.factors.dummy.models import DummyFactor
class DummyFactorSerializer(ModelSerializer):
"""DummyFactor Serializer"""
class Meta:
model = DummyFactor
fields = ['pk', 'name', 'slug', 'order', 'enabled']
class DummyFactorViewSet(ModelViewSet):
"""DummyFactor Viewset"""
queryset = DummyFactor.objects.all()
serializer_class = DummyFactorSerializer

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