Compare commits

...

33 Commits

Author SHA1 Message Date
9d640efc88 new release: 0.3.0-beta 2019-10-01 13:50:50 +02:00
f0907841dd docker(minor): remove virtualenv from pipenv 2019-10-01 13:50:37 +02:00
2bffc12ef9 ci(minor): fix default settings so CI works 2019-10-01 13:22:38 +02:00
2ff9ec6522 ci(minor): fix not all packages being installed 2019-10-01 11:34:34 +02:00
43a54f5c54 ci(minor): install pipenv before testing 2019-10-01 11:12:59 +02:00
7bff2734aa lint(minor): fix all remaining pylint and prospector errors 2019-10-01 11:08:56 +02:00
84768c0ec6 helm(minor): remove rabbitmq 2019-10-01 10:48:55 +02:00
f4499a5459 *(minor): stdlib logging to structlog 2019-10-01 10:24:10 +02:00
b3aede5bba policy(minor): Move policy-related code to separate package 2019-10-01 10:17:39 +02:00
531ea1c039 build(minor): rename dockerfiles to be detected correctly 2019-09-30 18:05:42 +02:00
c2c5ff6912 config(minor): CONFIG.get -> CONFIG.y 2019-09-30 18:04:04 +02:00
9cddab8fd5 deploy(minor): switch to pipfile 2019-09-10 17:00:13 +02:00
06d15d8a27 new release: 0.2.8-beta 2019-07-22 17:18:07 +02:00
b5c711854b deploy: fix static deployment and static container 2019-07-22 17:17:53 +02:00
4cf6c36f34 new release: 0.2.7-beta 2019-07-22 15:54:34 +02:00
75a6f6c875 deploy: remove old files from bumpversion 2019-07-22 15:54:10 +02:00
62abe3f256 suspicious_policy: fix Request IP lookup 2019-07-22 15:46:41 +02:00
9296c41650 ci: add missing packaging dependency 2019-07-22 15:35:11 +02:00
7fb48fde6d deploy: add static deployment, add resource reservations/limits 2019-07-22 15:22:10 +02:00
174472bb45 all: get rid of individual requirements file, remove version from every module 2019-07-22 15:20:17 +02:00
17575ed921 deploy: rewrite docker files to be more stackable 2019-07-22 15:18:33 +02:00
b1b1a27444 client-packages: now deprecated 2019-07-22 15:17:58 +02:00
f97a5eeefb api: *actually* fix drf dependency issue 2019-07-15 15:05:54 +00:00
10fd96981e new release: 0.2.6-beta 2019-07-15 13:42:18 +00:00
67e3eb549c api: fix wrong django-rest-framework dependency 2019-07-15 13:42:03 +00:00
30a6d1f0b1 new release: 0.2.5-beta 2019-07-15 13:31:04 +00:00
3d1fa9f048 app_gw: Rewrite redirect responses (replace upstream location with server_name) 2019-07-09 15:28:52 +02:00
1d2be6e68b root: fix sentry sending wrong release 2019-07-05 16:00:01 +02:00
c21e343986 oidc_provider: fix error when creating a new provider 2019-07-05 15:59:52 +02:00
ff37ed095c new release: 0.2.4-beta 2019-07-05 15:30:13 +02:00
8623a2c3fc oidc_provider: fix error trying to create RSA Key before migrations are run 2019-07-05 15:27:04 +02:00
23d277eaf1 remove oidc from OAuth2, add dedicated OIDC provider 2019-07-05 15:21:48 +02:00
75ced59451 helm: fix syntax error 2019-07-05 15:21:12 +02:00
169 changed files with 2594 additions and 1856 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.2.3-beta
current_version = 0.3.0-beta
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -15,10 +15,6 @@ values =
beta
stable
[bumpversion:file:client-packages/allauth/setup.py]
[bumpversion:file:client-packages/sentry-auth-passbook/setup.py]
[bumpversion:file:helm/passbook/values.yaml]
[bumpversion:file:helm/passbook/Chart.yaml]
@ -27,33 +23,5 @@ values =
[bumpversion:file:passbook/__init__.py]
[bumpversion:file:passbook/api/__init__.py]
[bumpversion:file:passbook/core/__init__.py]
[bumpversion:file:passbook/admin/__init__.py]
[bumpversion:file:passbook/captcha_factor/__init__.py]
[bumpversion:file:passbook/oauth_client/__init__.py]
[bumpversion:file:passbook/ldap/__init__.py]
[bumpversion:file:passbook/lib/__init__.py]
[bumpversion:file:passbook/hibp_policy/__init__.py]
[bumpversion:file:passbook/password_expiry_policy/__init__.py]
[bumpversion:file:passbook/saml_idp/__init__.py]
[bumpversion:file:passbook/audit/__init__.py]
[bumpversion:file:passbook/oauth_provider/__init__.py]
[bumpversion:file:passbook/otp/__init__.py]
[bumpversion:file:passbook/app_gw/__init__.py]
[bumpversion:file:passbook/suspicious_policy/__init__.py]
[bumpversion:file:passbook/core/nginx.conf]

1
.gitignore vendored
View File

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

View File

@ -1,14 +1,11 @@
# Global Variables
stages:
- build-buildimage
- build-base-image
- build-dev-image
- test
- build
- docs
- deploy
image: docker.beryju.org/passbook/build-base:latest
services:
- postgres:latest
- redis:latest
- package
image: docker.beryju.org/passbook/dev:latest
variables:
POSTGRES_DB: passbook
@ -16,98 +13,123 @@ variables:
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
before_script:
# Ensure all dependencies are installed, even those not included in passbook/build-base
- pip install -r requirements-dev.txt
- pip install pipenv
# Ensure all dependencies are installed, even those not included in passbook/dev
# According to pipenv docs, -d outputs all packages, however it actually does not
- pipenv lock -r > requirements-all.txt
- pipenv lock -rd >> requirements-all.txt
- pip install -r requirements-all.txt
create-build-image:
create-base-image:
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.build-base --destination docker.beryju.org/passbook/build-base:latest --destination docker.beryju.org/passbook/build-base:0.2.3-beta
stage: build-buildimage
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/base.Dockerfile --destination docker.beryju.org/passbook/base:latest --destination docker.beryju.org/passbook/base:0.3.0-beta
stage: build-base-image
only:
refs:
- tags
- /^version/.*$/
build-dev-image:
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/dev.Dockerfile --destination docker.beryju.org/passbook/dev:latest --destination docker.beryju.org/passbook/dev:0.3.0-beta
stage: build-dev-image
only:
refs:
- tags
- /^version/.*$/
isort:
script:
- isort -c -sg env
stage: test
services:
- 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
pylint:
script:
- pylint passbook
stage: test
services:
- postgres:latest
- redis:latest
coverage:
script:
- python manage.py collectstatic --no-input
- coverage run manage.py test
- coverage report
- coverage html
stage: test
bandit:
script:
- bandit -r passbook
stage: test
services:
- postgres:latest
- redis:latest
package-docker:
package-passbook-server:
image:
name: gcr.io/kaniko-project/executor:debug
entrypoint: [""]
before_script:
- echo "{\"auths\":{\"docker.beryju.org\":{\"auth\":\"$DOCKER_AUTH\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.beryju.org/passbook/server:latest --destination docker.beryju.org/passbook/server:0.2.3-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.3.0-beta
stage: build
only:
- tags
- /^version/.*$/
package-helm:
build-passbook-static:
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/static.Dockerfile --destination docker.beryju.org/passbook/static:latest --destination docker.beryju.org/passbook/static:0.3.0-beta
only:
- tags
- /^version/.*$/
# running collectstatic fully initialises django, hence we need that databases
services:
- postgres:latest
- redis:latest
package-helm:
image: debian:stretch-slim
stage: package
before_script:
- apt update && apt install -y curl
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
script:
- helm init --client-only
- helm dependency build helm/passbook
- helm package helm/passbook
artifacts:
paths:
- passbook-*.tgz
expire_in: 2 days
expire_in: 1 week
only:
- tags
- /^version/.*$/
package-client-package-allauth:
script:
- cd client-packages/allauth
- python setup.py sdist
- twine upload --username $TWINE_USERNAME --password $TWINE_PASSWORD dist/*
stage: build
only:
refs:
- tags
- /^version/.*$/
changes:
- client-packages/allauth/**
package-client-package-sentry:
script:
- cd client-packages/sentry-auth-passbook
- python setup.py sdist
- twine upload --username $TWINE_USERNAME --password $TWINE_PASSWORD dist/*
stage: build
only:
refs:
- tags
- /^version/.*$/
changes:
- client-packages/sentry-auth-passbook/**

View File

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

Binary file not shown.

14
.vscode/settings.json vendored
View File

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

View File

@ -1,34 +1,8 @@
FROM python:3.6-slim-stretch as build
FROM docker.beryju.org/passbook/base:latest
COPY ./passbook/ /app/passbook
COPY ./manage.py /app/
COPY ./requirements.txt /app/
WORKDIR /app/
RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev libpq-dev -y && \
mkdir /app/static/ && \
pip install -r requirements.txt && \
pip install psycopg2 && \
./manage.py collectstatic --no-input && \
apt-get remove --purge -y build-essential && \
apt-get autoremove --purge -y
FROM python:3.6-slim-stretch
COPY ./passbook/ /app/passbook
COPY ./manage.py /app/
COPY ./requirements.txt /app/
COPY --from=build /app/static /app/static/
WORKDIR /app/
RUN apt-get update && apt-get install build-essential libssl-dev libffi-dev libpq-dev -y && \
pip install -r requirements.txt && \
pip install psycopg2 && \
adduser --system --home /app/ passbook && \
chown -R passbook /app/ && \
apt-get remove --purge -y build-essential && \
apt-get autoremove --purge -y
USER passbook
WORKDIR /app/

View File

@ -1,12 +0,0 @@
FROM python:3.6
COPY ./passbook/ /app/passbook
COPY ./client-packages/ /app/client-packages
COPY ./requirements.txt /app/
COPY ./requirements-dev.txt /app/
WORKDIR /app/
RUN apt-get update && apt-get install libssl-dev libffi-dev libpq-dev -y && \
pip install -U -r requirements-dev.txt && \
rm -rf /app/*

61
Pipfile Normal file
View File

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

1348
Pipfile.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
base.Dockerfile Normal file
View File

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

View File

@ -1,35 +0,0 @@
"""passbook provider"""
from allauth.socialaccount.providers.base import ProviderAccount
from allauth.socialaccount.providers.oauth2.provider import OAuth2Provider
class PassbookAccount(ProviderAccount):
"""passbook account"""
def to_str(self):
dflt = super().to_str()
return self.account.extra_data.get('username', dflt)
class PassbookProvider(OAuth2Provider):
"""passbook provider"""
id = 'passbook'
name = 'passbook'
account_class = PassbookAccount
def extract_uid(self, data):
return str(data['sub'])
def extract_common_fields(self, data):
return {
'email': data.get('email'),
'username': data.get('preferred_username'),
'name': data.get('name'),
}
def get_default_scope(self):
return ['openid:userinfo']
provider_classes = [PassbookProvider] # noqa

View File

@ -1,6 +0,0 @@
"""passbook provider"""
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from allauth_passbook.provider import PassbookProvider
urlpatterns = default_urlpatterns(PassbookProvider)

View File

@ -1,37 +0,0 @@
"""passbook adapter"""
import requests
from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
OAuth2CallbackView,
OAuth2LoginView)
from allauth_passbook.provider import PassbookProvider
class PassbookOAuth2Adapter(OAuth2Adapter):
"""passbook OAuth2 Adapter"""
provider_id = PassbookProvider.id
# pylint: disable=no-member
settings = app_settings.PROVIDERS.get(provider_id, {}) # noqa
provider_base_url = settings.get("PASSBOOK_URL", 'https://id.beryju.org')
access_token_url = '{0}/application/oauth/token/'.format(provider_base_url)
authorize_url = '{0}/application/oauth/authorize/'.format(provider_base_url)
profile_url = '{0}/api/v1/openid/'.format(
provider_base_url)
def complete_login(self, request, app, access_token, **kwargs):
headers = {
'Authorization': 'Bearer {0}'.format(access_token.token),
'Content-Type': 'application/json',
}
extra_data = requests.get(self.profile_url, headers=headers)
return self.get_provider().sociallogin_from_response(
request,
extra_data.json()
)
oauth2_login = OAuth2LoginView.adapter_view(PassbookOAuth2Adapter) # noqa
oauth2_callback = OAuth2CallbackView.adapter_view(PassbookOAuth2Adapter) # noqa

View File

@ -1 +0,0 @@
django-allauth

View File

@ -1,33 +0,0 @@
"""passbook allauth setup.py"""
from setuptools import setup
setup(
name='django-allauth-passbook',
version='0.2.3-beta',
description='passbook support for django-allauth',
# long_description='\n'.join(read_simple('docs/index.md')[2:]),
long_description_content_type='text/markdown',
author='BeryJu.org',
author_email='hello@beryju.org',
packages=['allauth_passbook'],
include_package_data=True,
install_requires=['django-allauth'],
keywords='django allauth passbook',
license='MIT',
classifiers=[
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries :: Python Modules',
'Environment :: Web Environment',
'Topic :: Internet',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Framework :: Django',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1',
],
)

View File

@ -1,5 +0,0 @@
*.pyc
*.egg-info/
*.eggs
/dist
/build

View File

@ -1,32 +0,0 @@
sudo: false
language: python
services:
- memcached
- postgresql
- redis-server
python:
- '2.7'
cache:
directories:
- node_modules
- "$HOME/.cache/pip"
deploy:
provider: pypi
user: getsentry
password:
secure: kVmxKHkBWRLYyZme05p+WZSJmb8GjHV9uyuaSCVMRlqWCW+GXRB7P1xXR2jb9URTlNdcs56Ab/UrwzCbMFGC8LmwCeFVgIR/ltytVZG2FgXZPWaeA4dH25qK2oGWgzJ/xeiMpmuJqN9hRl25MX6jG7FZKvrrOkG7+8tpPd1yO+uYWZQbnebZMjcPBqEpn7CC0hR39GSoyVAbydpMe5hwENGQM26CepcicdrelfawItoUrXrkJzBHkIQQTO/xRSbCtRJOtzI5lwtv3GP0hcbOy5tI5dhG/93pLwZRc5+dZaCaP7oaVeOcBjN0zfINRQobt8d6h2Qgvd/YyFkGi0/xKn1zMmKIVLOG6VsYwEAUq8wNOsP4A/jdm4Y0J/1oEZStCkpaGpx85TYi4kq1hWQdyqaVJSPhh4Tk4roIaS2zOYQl+nIpbHqmJ4FJrg1il+TCdjBXobATQ1mKRBUrjD+RDzH/r4ogbd8+UwvvvevpqS2K+/wgT6UD0MzDInv9S29CUQvuFhPoqyJb5XRddHMRE9EEK/2Z8tFN91sDATnqfXHgwnvu00q/nKP5JnijBPzGmx7ydgUViIukklDrlPvo9BbRJz0Vr2vbAvMTrLMLCXqi5CwTm+v+iaOf/YaCziaG2vx0eVASYjpOLCedSgRZBubPM8z4E/HMXhChN7sVDWk=
on:
tags: true
distributions: sdist bdist_wheel
env:
global:
- PIP_DOWNLOAD_CACHE=".pip_download_cache"
before_install:
- pip install codecov
install:
- make develop
script:
- PYFLAKES_NODOCTEST=1 flake8
- coverage run --source=. -m py.test tests
after_success:
- codecov

View File

@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2016 Functional Software, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -1,3 +0,0 @@
include setup.py package.json webpack.config.js README.rst MANIFEST.in LICENSE AUTHORS
recursive-include sentry_auth_supervisr/templates *
global-exclude *~

View File

@ -1,26 +0,0 @@
.PHONY: clean develop install-tests lint publish test
develop:
pip install "pip>=7"
pip install -e .
make install-tests
install-tests:
pip install .[tests]
lint:
@echo "--> Linting python"
flake8
@echo ""
test:
@echo "--> Running Python tests"
py.test tests || exit 1
@echo ""
publish:
python setup.py sdist bdist_wheel upload
clean:
rm -rf *.egg-info src/*.egg-info
rm -rf dist build

View File

@ -1,55 +0,0 @@
GitHub Auth for Sentry
======================
An SSO provider for Sentry which enables GitHub organization-restricted authentication.
Install
-------
::
$ pip install https://github.com/getsentry/sentry-auth-github/archive/master.zip
Setup
-----
Create a new application under your organization in GitHub. Enter the **Authorization
callback URL** as the prefix to your Sentry installation:
::
https://example.sentry.com
Once done, grab your API keys and drop them in your ``sentry.conf.py``:
.. code-block:: python
GITHUB_APP_ID = ""
GITHUB_API_SECRET = ""
Verified email addresses can optionally be required:
.. code-block:: python
GITHUB_REQUIRE_VERIFIED_EMAIL = True
Optionally you may also specify the domain (for GHE users):
.. code-block:: python
GITHUB_BASE_DOMAIN = "git.example.com"
GITHUB_API_DOMAIN = "api.git.example.com"
If Subdomain isolation is disabled in GHE:
.. code-block:: python
GITHUB_BASE_DOMAIN = "git.example.com"
GITHUB_API_DOMAIN = "git.example.com/api/v3"

View File

@ -1,14 +0,0 @@
from __future__ import absolute_import
# Run tests against sqlite for simplicity
import os
import os.path
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
os.environ.setdefault('DB', 'sqlite')
pytest_plugins = [
'sentry.utils.pytest'
]

View File

@ -1,7 +0,0 @@
from __future__ import absolute_import
from sentry.auth import register
from .provider import PassbookOAuth2Provider
register('passbook', PassbookOAuth2Provider)

View File

@ -1,45 +0,0 @@
from __future__ import absolute_import, print_function
from requests.exceptions import RequestException
from sentry import http
from sentry.utils import json
from .constants import BASE_DOMAIN
class PassbookApiError(Exception):
def __init__(self, message='', status=0):
super(PassbookApiError, self).__init__(message)
self.status = status
class PassbookClient(object):
def __init__(self, client_id, client_secret):
self.client_id = client_id
self.client_secret = client_secret
self.http = http.build_session()
def _request(self, path, access_token):
params = {
'client_id': self.client_id,
'client_secret': self.client_secret,
}
headers = {
'Authorization': 'Bearer {0}'.format(access_token),
}
try:
req = self.http.get('https://{0}/{1}'.format(BASE_DOMAIN, path.lstrip('/')),
params=params,
headers=headers,
)
except RequestException as e:
raise PassbookApiError(unicode(e), status=getattr(e, 'status_code', 0))
if req.status_code < 200 or req.status_code >= 300:
raise PassbookApiError(req.content, status=req.status_code)
return json.loads(req.content)
def get_user(self, access_token):
return self._request('/api/v1/openid/', access_token)

View File

@ -1,14 +0,0 @@
from __future__ import absolute_import, print_function
from django.conf import settings
CLIENT_ID = getattr(settings, 'PASSBOOK_APP_ID', None)
CLIENT_SECRET = getattr(settings, 'PASSBOOK_API_SECRET', None)
SCOPE = 'openid:userinfo'
BASE_DOMAIN = getattr(settings, 'PASSBOOK_BASE_DOMAIN', 'id.beryju.org')
ACCESS_TOKEN_URL = 'https://{0}/application/oauth/token/'.format(BASE_DOMAIN)
AUTHORIZE_URL = 'https://{0}/application/oauth/authorize/'.format(BASE_DOMAIN)

View File

@ -1,62 +0,0 @@
from __future__ import absolute_import, print_function
from sentry.auth.exceptions import IdentityNotValid
from sentry.auth.providers.oauth2 import (OAuth2Callback, OAuth2Login,
OAuth2Provider)
from .client import PassbookApiError, PassbookClient
from .constants import (ACCESS_TOKEN_URL, AUTHORIZE_URL, CLIENT_ID,
CLIENT_SECRET, SCOPE)
from .views import FetchUser, PassbookConfigureView
class PassbookOAuth2Provider(OAuth2Provider):
access_token_url = ACCESS_TOKEN_URL
authorize_url = AUTHORIZE_URL
name = 'Passbook'
client_id = CLIENT_ID
client_secret = CLIENT_SECRET
def __init__(self, **config):
super(PassbookOAuth2Provider, self).__init__(**config)
def get_configure_view(self):
return PassbookConfigureView.as_view()
def get_auth_pipeline(self):
return [
OAuth2Login(
authorize_url=self.authorize_url,
client_id=self.client_id,
scope=SCOPE,
),
OAuth2Callback(
access_token_url=self.access_token_url,
client_id=self.client_id,
client_secret=self.client_secret,
),
FetchUser(
client_id=self.client_id,
client_secret=self.client_secret,
),
]
def get_refresh_token_url(self):
return ACCESS_TOKEN_URL
def build_identity(self, state):
data = state['data']
user_data = state['user']
return {
'id': user_data['email'],
'email': user_data['email'],
'name': user_data['name'],
'data': self.get_oauth_data(data),
}
def build_config(self, state):
return {}
def refresh_identity(self, auth_identity):
client = PassbookClient(self.client_id, self.client_secret)
access_token = auth_identity.data['access_token']

View File

@ -1,75 +0,0 @@
from __future__ import absolute_import, print_function
from django import forms
from sentry.auth.view import AuthView, ConfigureView
from sentry.models import AuthIdentity
from .client import PassbookClient
def _get_name_from_email(email):
"""
Given an email return a capitalized name. Ex. john.smith@example.com would return John Smith.
"""
name = email.rsplit('@', 1)[0]
name = ' '.join([n_part.capitalize() for n_part in name.split('.')])
return name
class FetchUser(AuthView):
def __init__(self, client_id, client_secret, *args, **kwargs):
self.client = PassbookClient(client_id, client_secret)
super(FetchUser, self).__init__(*args, **kwargs)
def handle(self, request, helper):
access_token = helper.fetch_state('data')['access_token']
user = self.client.get_user(access_token)
# A user hasn't set their name in their Passbook profile so it isn't
# populated in the response
if not user.get('name'):
user['name'] = _get_name_from_email(user['email'])
helper.bind_state('user', user)
return helper.next_step()
class ConfirmEmailForm(forms.Form):
email = forms.EmailField(label='Email')
class ConfirmEmail(AuthView):
def handle(self, request, helper):
user = helper.fetch_state('user')
# TODO(dcramer): this isnt ideal, but our current flow doesnt really
# support this behavior;
try:
auth_identity = AuthIdentity.objects.select_related('user').get(
auth_provider=helper.auth_provider,
ident=user['id'],
)
except AuthIdentity.DoesNotExist:
pass
else:
user['email'] = auth_identity.user.email
if user.get('email'):
return helper.next_step()
form = ConfirmEmailForm(request.POST or None)
if form.is_valid():
user['email'] = form.cleaned_data['email']
helper.bind_state('user', user)
return helper.next_step()
return self.respond('sentry_auth_passbook/enter-email.html', {
'form': form,
})
class PassbookConfigureView(ConfigureView):
def dispatch(self, request, organization, auth_provider):
return self.render('sentry_auth_passbook/configure.html')

View File

@ -1,12 +0,0 @@
[wheel]
universal = 1
[pytest]
python_files = test*.py
addopts = --tb=native -p no:doctest
norecursedirs = bin dist docs htmlcov script hooks node_modules .* {args}
[flake8]
ignore = F999,E501,E128,E124,E402,W503,E731,C901
max-line-length = 100
exclude = .tox,.git,*/migrations/*,node_modules/*,docs/*

View File

@ -1,46 +0,0 @@
#!/usr/bin/env python
"""
sentry-auth-passbook
==================
:copyright: (c) 2016 Functional Software, Inc
"""
from setuptools import find_packages, setup
install_requires = [
'sentry>=7.0.0',
]
tests_require = [
'mock',
'flake8>=2.0,<2.1',
]
setup(
name='sentry-auth-passbook',
version='0.2.3-beta',
author='BeryJu.org',
author_email='support@beryju.org',
url='https://passbook.beryju.org',
description='passbook authentication provider for Sentry',
long_description=__doc__,
long_description_content_type='text/markdown',
license='MIT',
packages=find_packages(exclude=['tests']),
zip_safe=False,
install_requires=install_requires,
tests_require=tests_require,
extras_require={'tests': tests_require},
include_package_data=True,
entry_points={
'sentry.apps': [
'auth_passbook = sentry_auth_passbook',
],
},
classifiers=[
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'Operating System :: OS Independent',
'Topic :: Software Development'
],
)

View File

@ -1,6 +0,0 @@
from sentry.testutils import TestCase
class GitHubOAuth2ProviderTest(TestCase):
def test_simple(self):
pass

View File

@ -1,17 +0,0 @@
from __future__ import absolute_import, print_function
import pytest
from sentry_auth_sentry.views import _get_name_from_email
expected_data = [
('john.smith@example.com', 'John Smith'),
('john@example.com', 'John'),
('XYZ-234=3523@example.com', 'Xyz-234=3523'),
('XYZ.1111@example.com', 'Xyz 1111'),
('JOHN@example.com', 'John'),
]
@pytest.mark.parametrize("email,expected_name", expected_data)
def test_get_name_from_email(email, expected_name):
assert _get_name_from_email(email) == expected_name

4
dev.Dockerfile Normal file
View File

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

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

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

View File

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

View File

@ -1 +0,0 @@
# passbook

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

@ -28,8 +28,10 @@ spec:
- name: {{ .Chart.Name }}
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command: ["/bin/sh","-c"]
args: ["./manage.py migrate && ./manage.py app_gw_web"]
command:
- ./manage.py
args:
- app_gw_web
ports:
- name: http
containerPort: 8000
@ -52,16 +54,9 @@ spec:
- name: Host
value: kubernetes-healthcheck-host
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
requests:
cpu: 150m
memory: 300M
limits:
cpu: 500m
memory: 500M

View File

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

View File

@ -1,6 +1,5 @@
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "passbook.fullname" . -}}
{{- $ingressPath := .Values.ingress.path -}}
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
@ -30,16 +29,20 @@ spec:
- host: {{ . | quote }}
http:
paths:
- path: {{ $ingressPath }}
- path: /
backend:
serviceName: {{ $fullName }}-web
servicePort: http
- path: /static/
backend:
serviceName: {{ $fullName }}-static
servicePort: http
{{- end }}
{{- range .Values.ingress.app_gw_hosts }}}
{{- range .Values.ingress.app_gw_hosts }}
- host: {{ . | quote }}
http:
paths:
- path: {{ $ingressPath }}
- path: /
backend:
serviceName: {{ $fullName }}-appgw
servicePort: http

View File

@ -0,0 +1,55 @@
apiVersion: apps/v1beta2
kind: Deployment
metadata:
name: {{ include "passbook.fullname" . }}-static
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:
selector:
matchLabels:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
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
image: "docker.beryju.org/passbook/static:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 80
protocol: TCP
livenessProbe:
initialDelaySeconds: 10
timeoutSeconds: 5
httpGet:
path: /_/healthz
port: http
readinessProbe:
initialDelaySeconds: 10
timeoutSeconds: 5
httpGet:
path: /_/healthz
port: http
resources:
requests:
cpu: 10m
memory: 10M
limits:
cpu: 20m
memory: 20M
- name: {{ .Chart.Name }}-static-prometheus
image: nginx/nginx-prometheus-exporter:0.4.1
imagePullPolicy: IfNotPresent

View File

@ -0,0 +1,21 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "passbook.fullname" . }}-static
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 }}
k8s.passbook.io/component: static
spec:
type: ClusterIP
ports:
- port: 80
targetPort: http
protocol: TCP
name: http
selector:
app.kubernetes.io/name: {{ include "passbook.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
k8s.passbook.io/component: static

View File

@ -24,12 +24,24 @@ spec:
- name: config-volume
configMap:
name: {{ include "passbook.fullname" . }}-config
initContainers:
- name: passbook-database-migrations
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
command:
- ./manage.py
args:
- migrate
volumeMounts:
- mountPath: /etc/passbook
name: config-volume
containers:
- name: {{ .Chart.Name }}
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command: ["/bin/sh","-c"]
args: ["./manage.py migrate && ./manage.py web"]
command:
- ./manage.py
args:
- web
ports:
- name: http
containerPort: 8000
@ -52,16 +64,9 @@ spec:
- name: Host
value: kubernetes-healthcheck-host
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
requests:
cpu: 50m
memory: 150M
limits:
cpu: 200m
memory: 300M

View File

@ -28,7 +28,10 @@ spec:
- name: {{ .Chart.Name }}
image: "docker.beryju.org/passbook/server:{{ .Values.image.tag }}"
imagePullPolicy: IfNotPresent
command: ["./manage.py", "worker"]
command:
- ./manage.py
args:
- worker
ports:
- name: http
containerPort: 8000
@ -37,16 +40,9 @@ spec:
- mountPath: /etc/passbook
name: config-volume
resources:
{{ toYaml .Values.resources | indent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{ toYaml . | indent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{ toYaml . | indent 8 }}
{{- end }}
requests:
cpu: 150m
memory: 300M
limits:
cpu: 300m
memory: 500M

View File

@ -5,7 +5,7 @@
replicaCount: 1
image:
tag: 0.2.3-beta
tag: 0.3.0-beta
nameOverride: ""

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = '0.2.3-beta'
__version__ = '0.3.0-beta'

View File

@ -1,2 +0,0 @@
"""passbook admin"""
__version__ = '0.2.3-beta'

View File

@ -1,2 +0,0 @@
django-rest-framework
drf_yasg

View File

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

View File

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

View File

@ -1,2 +0,0 @@
"""passbook api"""
__version__ = '0.2.3-beta'

View File

@ -1,3 +0,0 @@
django-rest-framework
drf_yasg
django-filters

View File

@ -1,2 +0,0 @@
"""passbook Application Security Gateway Header"""
__version__ = '0.2.3-beta'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
django-revproxy
urllib3[secure]
channels
service_identity
websocket-client
daphne<2.3.0
asgiref~=2.3

View File

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

View File

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

View File

@ -1,2 +0,0 @@
"""passbook audit Header"""
__version__ = '0.2.3-beta'

View File

@ -1,6 +1,4 @@
"""passbook audit models"""
from logging import getLogger
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.contrib.postgres.fields import JSONField
@ -8,10 +6,11 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext as _
from ipware import get_client_ip
from structlog import get_logger
from passbook.lib.models import UUIDModel
LOGGER = getLogger(__name__)
LOGGER = get_logger(__name__)
class AuditEntry(UUIDModel):
"""An individual audit log entry"""

View File

@ -1,2 +0,0 @@
"""passbook captcha_factor Header"""
__version__ = '0.2.3-beta'

View File

@ -1 +0,0 @@
django-recaptcha

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,19 +1,20 @@
"""passbook multi-factor authentication engine"""
from logging import getLogger
from typing import List, Tuple
from django.contrib.auth import login
from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import get_object_or_404, redirect, reverse
from django.utils.http import urlencode
from django.views.generic import View
from structlog import get_logger
from passbook.core.models import Factor, User
from passbook.core.policies import PolicyEngine
from passbook.core.views.utils import PermissionDeniedView
from passbook.lib.utils.reflection import class_to_path, path_to_class
from passbook.lib.utils.urls import is_url_absolute
from passbook.policy.engine import PolicyEngine
LOGGER = getLogger(__name__)
LOGGER = get_logger(__name__)
def _redirect_with_qs(view, get_query_set=None):
"""Wrapper to redirect whilst keeping GET Parameters"""
@ -31,12 +32,12 @@ class AuthenticationView(UserPassesTestMixin, View):
SESSION_USER_BACKEND = 'passbook_user_backend'
SESSION_IS_SSO_LOGIN = 'passbook_sso_login'
pending_user = None
pending_factors = []
pending_user: User
pending_factors: List[Tuple[str, str]] = []
_current_factor_class = None
_current_factor_class: Factor
current_factor = None
current_factor: Factor
# Allow only not authenticated users to login
def test_func(self):

View File

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

View File

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

View File

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

View File

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

View File

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

66
passbook/core/nginx.conf Normal file
View File

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

View File

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

View File

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

View File

@ -0,0 +1,54 @@
{% load static %}
{% load i18n %}
{% load utils %}
<!DOCTYPE html>
<html lang="en" class="layout-pf layout-pf-fixed transitions">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
{% block title %}
{% title %}
{% endblock %}
</title>
<link rel="icon" type="image/png" href="{% static 'img/logo.png' %}">
<link rel="shortcut icon" type="image/png" href="{% static 'img/logo.png' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly-additions.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/passbook.css' %}">
<style>
.login-pf {
background-attachment: fixed;
scroll-behavior: smooth;
background-size: cover;
}
</style>
{% block head %}
{% endblock %}
</head>
<body class="login-pf">
{% if 'impersonate_id' in request.session %}
<div class="experimental-pf-bar">
<span id="experimentalBar" class="experimental-pf-text">
{% blocktrans with user=user %}You're currently impersonating {{ user }}.{% endblocktrans %}
<a href="?__unimpersonate=True" id="acceptMessage">{% trans 'Stop impersonation' %}</a>
</span>
</div>
{% endif %}
{% block body %}
{% endblock %}
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/patternfly.min.js' %}"></script>
<script src="{% static 'js/passbook.js' %}"></script>
{% block scripts %}
{% endblock %}
<div class="modals">
{% include 'partials/about_modal.html' %}
</div>
</body>
</html>

View File

@ -1,4 +1,4 @@
{% extends 'base/skeleton.html' %}
{% extends 'base/skeleton_login.html' %}
{% load static %}
{% load i18n %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,2 +0,0 @@
"""passbook hibp_policy"""
__version__ = '0.2.3-beta'

View File

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

View File

@ -1,2 +0,0 @@
"""Passbook ldap app Header"""
__version__ = '0.2.3-beta'

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