Compare commits

...

30 Commits

Author SHA1 Message Date
fec9b5cf94 bump version: 0.0.6-alpha -> 0.0.7-alpha 2019-02-25 13:20:12 +01:00
986fed3e7c add hook for Factors to show user settings. closes #5 2019-02-25 13:20:07 +01:00
da5568b571 cleanup, fix Permission Denied when Cancelling login, fix display of messages on login template 2019-02-25 13:02:50 +01:00
07f5dce97a remove todo file 2019-02-25 12:31:27 +01:00
bb81bb5a8d totp => otp, integrate with factors, new setup form 2019-02-25 12:29:40 +01:00
9c2cfd7db4 use Inheritance for Factors instead of JSONField 2019-02-24 22:39:09 +01:00
292fbecca0 add password change view 2019-02-23 20:56:41 +01:00
e5a405bf43 Register applications with Branded name for UI Dropdown 2019-02-23 20:42:14 +01:00
66c0fc9d9a Move factor base template to form_with_user 2019-02-23 20:41:43 +01:00
5fa8711bfa change hostname to localhost for k8s CI 2019-02-21 17:04:46 +01:00
dd9cd7aa0c automatically fill slug field while typing 2019-02-21 17:01:12 +01:00
8bc8765035 use postgres service for CI 2019-02-21 16:50:36 +01:00
b7ac4f1dd2 add psycopg2 as dependency 2019-02-21 16:30:56 +01:00
183308e444 fix Contains not working correctly 2019-02-21 16:21:45 +01:00
c941107d42 Rules -> Policies, more things 2019-02-21 16:06:57 +01:00
d3d75737ed switch to drf_yasg 2019-02-21 16:05:59 +01:00
458decfbb3 add prospector config 2019-02-21 16:00:39 +01:00
7601351f51 add help texts to explain naming 2019-02-16 11:25:53 +01:00
df45797b4a fix inconsistent naming again 2019-02-16 11:13:00 +01:00
744a320731 fix inconsistent naming 2019-02-16 10:59:23 +01:00
89722336e3 fix duplicate Class naming 2019-02-16 10:54:15 +01:00
d6f4832e90 Rule -> Policies 2019-02-16 10:24:31 +01:00
d32699b332 remove reversion 2019-02-16 09:53:32 +01:00
59a15c988f Move Factor instances to database 2019-02-16 09:52:37 +01:00
57e5996513 Fix Docker Image having messed up static files 2019-02-14 16:31:40 +01:00
6649eb401e bump version: 0.0.5-alpha -> 0.0.6-alpha 2019-02-13 16:41:59 +01:00
b657d7319d fix failing docker build and failing helm packaging 2019-02-13 16:41:51 +01:00
a9d29067bf bump version: 0.0.4-alpha -> 0.0.5-alpha 2019-02-11 18:01:45 +01:00
b7791f3b9a use build image in docker to generate static files 2019-02-11 18:01:29 +01:00
9161a6e41d initialize helm properly, include built static files 2019-02-11 17:58:25 +01:00
145 changed files with 2061 additions and 1278 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.0.4-alpha
current_version = 0.0.7-alpha
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -40,5 +40,5 @@ values =
[bumpversion:file:passbook/oauth_provider/__init__.py]
[bumpversion:file:passbook/totp/__init__.py]
[bumpversion:file:passbook/otp/__init__.py]

View File

@ -1,3 +1,4 @@
env
helm
passbook-ui
static

2
.gitignore vendored
View File

@ -189,5 +189,5 @@ pyvenv.cfg
pip-selfcheck.json
# End of https://www.gitignore.io/api/python,django
/static/*
/static/
local.env.yml

View File

@ -8,7 +8,15 @@ stages:
- test
- build
- docs
image: python:3.5
image: python:3.6
services:
- postgres:latest
variables:
POSTGRES_DB: passbook
POSTGRES_USER: passbook
POSTGRES_PASSWORD: 'EK-5jnKfjrGRm<77'
SUPERVISR_ENV: ci
include:
- /allauth/.gitlab-ci.yml
@ -46,7 +54,7 @@ package-docker:
before_script:
- echo "{\"auths\":{\"https://docker.$NEXUS_URL/\":{\"username\":\"$NEXUS_USER\",\"password\":\"$NEXUS_PASS\"}}}" > /kaniko/.docker/config.json
script:
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.0.4-alpha
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.0.7-alpha
stage: build
only:
- tags
@ -55,6 +63,7 @@ package-helm:
stage: build
script:
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
- helm init --client-only
- helm package helm/passbook
- ./manage.py nexus_upload --method put --url $NEXUS_URL --user $NEXUS_USER --password $NEXUS_PASS --repo helm *.tgz
only:

12
.prospector.yaml Normal file
View File

@ -0,0 +1,12 @@
strictness: medium
test-warnings: true
doc-warnings: false
ignore-paths:
- env
- migrations
- docs
- node_modules
uses:
- django

View File

@ -1,14 +1,25 @@
FROM python:3.6-slim-stretch
# LABEL version="1.8.8"
FROM python:3.6-slim-stretch as build
COPY ./passbook/ /app/passbook
COPY ./static/ /app/static
COPY ./manage.py /app/
COPY ./requirements.txt /app/
WORKDIR /app/
#RUN apk add --no-cache libffi-dev build-base py2-pip python2-dev libxml-dev && \
RUN mkdir /app/static/ && \
pip install -r requirements.txt && \
pip install psycopg2 && \
./manage.py collectstatic --no-input
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 pip install -r requirements.txt && \
pip install psycopg2 && \
adduser --system --home /app/ passbook && \

14
TODO
View File

@ -1,14 +0,0 @@
## oauth_client
- Move provider_type logic to own class, not name-based URL matching
- add provider_type field to Provider Model
- make Provider inherit core.application
- Add template for popular services like github, twitter, facebook, etc
## saml_idp
- move certificates to Provider so each provider can have different certificates
## admin
- add testing page where user can supply input and let rules run against it to debug/test

View File

@ -1,5 +1,5 @@
apiVersion: v1
appVersion: "0.0.4-alpha"
appVersion: "0.0.7-alpha"
description: A Helm chart for passbook.
name: passbook
version: 1.0.0

View File

@ -1,2 +1,2 @@
"""passbook"""
__version__ = '0.0.4-alpha'
__version__ = '0.0.7-alpha'

View File

@ -1,2 +1,2 @@
"""passbook admin"""
__version__ = '0.0.4-alpha'
__version__ = '0.0.7-alpha'

View File

@ -0,0 +1,6 @@
"""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

@ -0,0 +1,22 @@
"""passbook admin application API"""
from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.core.models import Application
class ApplicationSerializer(ModelSerializer):
"""Application Serializer"""
class Meta:
model = Application
fields = '__all__'
class ApplicationViewSet(ModelViewSet):
"""Application Viewset"""
permission_classes = [IsAdminUser]
serializer_class = ApplicationSerializer
queryset = Application.objects.all()

View File

@ -1,9 +1,33 @@
"""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(r'groups', GroupViewSet)
router.register('applications', ApplicationViewSet)
router.register('groups', GroupViewSet)
router.register('users', UserViewSet)
urlpatterns = router.urls
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

@ -0,0 +1,23 @@
"""passbook admin user API"""
from rest_framework.permissions import IsAdminUser
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.core.models import User
class UserSerializer(ModelSerializer):
"""User Serializer"""
class Meta:
model = User
fields = ['is_superuser', 'username', 'first_name', 'last_name', 'email', 'date_joined',
'uuid']
class UserViewSet(ModelViewSet):
"""User Viewset"""
permission_classes = [IsAdminUser]
serializer_class = UserSerializer
queryset = User.objects.all()

View File

@ -4,7 +4,7 @@ from django import forms
from passbook.core.models import User
class RuleTestForm(forms.Form):
"""Form to test rule against user"""
class PolicyTestForm(forms.Form):
"""Form to test policies against user"""
user = forms.ModelChoiceField(queryset=User.objects.all())

View File

@ -1,6 +1,6 @@
"""passbook core source form fields"""
# from django import forms
SOURCE_FORM_FIELDS = ['name', 'slug', 'enabled']
SOURCE_FORM_FIELDS = ['name', 'slug', 'enabled', 'policies']
# class SourceForm(forms.Form)

View File

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

View File

@ -10,6 +10,8 @@
{% block content %}
<div class="container">
<h1>{% trans "Applications" %}</h1>
<span>{% trans "External Applications which use passbook as Identity-Provider, utilizing protocols like OAuth2 and SAML." %}</span>
<hr>
<a href="{% url 'passbook_admin:application-create' %}" class="btn btn-primary">
{% trans 'Create...' %}
</a>

View File

@ -8,7 +8,6 @@
{% endblock %}
{% block content %}
<div class="container">
<h1>{% trans "Audit Log" %}</h1>
<div id="pf-list-standard" class="list-group list-view-pf list-view-pf-view">
{% for entry in object_list %}
@ -23,27 +22,31 @@
{{ entry.action }}
</div>
<div class="list-group-item-text">
{{ entry.context }}
</div>
</div>
<div class="list-view-pf-additional-info">
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-user"></span>
<strong>{{ entry.user }}</strong>
<span class="pficon pficon-user"></span>
<strong>{{ entry.user }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-screen"></span>
<strong>{{ entry.request_ip }}</strong>
<span class="pficon pficon-cluster"></span>
<strong>{{ entry.app|default:'-' }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-cluster"></span>
<strong>{{ entry.app|default:'-' }}</strong>
<span class="fa fa-clock-o"></span>
<strong>{{ entry.created }}</strong>
</div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-screen"></span>
<strong>{{ entry.request_ip }}</strong>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<script>
$(document).ready(function () {
// Row Checkbox Selection

View File

@ -17,8 +17,11 @@
<li class="{% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
<a href="{% url 'passbook_admin:providers' %}">{% trans 'Providers' %}</a>
</li>
<li class="{% is_active 'passbook_admin:rules' 'passbook_admin:rule-create' 'passbook_admin:rule-update' 'passbook_admin:rule-delete' 'passbook_admin:rule-test' %}">
<a href="{% url 'passbook_admin:rules' %}">{% trans 'Rules' %}</a>
<li class="{% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
<a href="{% url 'passbook_admin:factors' %}">{% trans 'Factors' %}</a>
</li>
<li class="{% is_active 'passbook_admin:policies' 'passbook_admin:policy-create' 'passbook_admin:policy-update' 'passbook_admin:policy-delete' 'passbook_admin:policy-test' %}">
<a href="{% url 'passbook_admin:policies' %}">{% trans 'Policies' %}</a>
</li>
<li class="{% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}">
<a href="{% url 'passbook_admin:invitations' %}">{% trans 'Invitations' %}</a>

View File

@ -0,0 +1,61 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load utils %}
{% load admin_reflection %}
{% block title %}
{% title %}
{% endblock %}
{% block content %}
<div class="container">
<h1>{% trans "Factors" %}</h1>
<span>{% trans "Factors required for a user to successfully authenticate." %}</span>
<hr>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'passbook_admin:factor-create' %}?type={{ type }}">{{ name }}</a></li>
{% endfor %}
</ul>
</div>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Type' %}</th>
<th>{% trans 'Order' %}</th>
<th>{% trans 'Enabled?' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for factor in object_list %}
<tr>
<td>{{ factor.name }} ({{ factor.slug }})</td>
<td>{{ factor.type }}</td>
<td>{{ factor.order }}</td>
<td>{{ factor.enabled }}</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:factor-update' pk=factor.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:factor-delete' pk=factor.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
{% get_links factor as links %}
{% for name, href in links.items %}
<a class="btn btn-default btn-sm"
href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -10,6 +10,8 @@
{% block content %}
<div class="container">
<h1>{% trans "Invitations" %}</h1>
<span>{% trans "Create Invitation Links which optionally force a username or expire on a set date." %}</span>
<hr>
<a href="{% url 'passbook_admin:invitation-create' %}" class="btn btn-primary">
{% trans 'Create...' %}
</a>

View File

@ -4,53 +4,89 @@
{% block content %}
<div class="container">
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Applications' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ application_count }}</a></span>
</p>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Applications' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ application_count }}</a></span>
</p>
</div>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Providers' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ provider_count }}</a></span>
</p>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Sources' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ source_count }}</a></span>
</p>
</div>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Rules' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ rule_count }}</a></span>
</p>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Providers' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ provider_count }}</a></span>
</p>
</div>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Users' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ user_count }}</a></span>
</p>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Factors' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ factor_count }}</a></span>
</p>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Invitation' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ invitation_count }}</a></span>
</p>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Policies' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ policy_count }}</a></span>
</p>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Users' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ user_count }}</a></span>
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -9,7 +9,9 @@
{% block content %}
<div class="container">
<h1>{% trans "Rules" %}</h1>
<h1>{% trans "Policies" %}</h1>
<span>{% trans "Allow users to use Applications based on properties, enforce Password Criteria and selectively apply Factors." %}</span>
<hr>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %}
@ -17,7 +19,7 @@
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'passbook_admin:rule-create' %}?type={{ type }}">{{ name }}</a></li>
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'passbook_admin:policy-create' %}?type={{ type }}">{{ name }}</a></li>
{% endfor %}
</ul>
</div>
@ -31,18 +33,18 @@
</tr>
</thead>
<tbody>
{% for rule in object_list %}
{% for policy in object_list %}
<tr>
<td>{{ rule.name }}</td>
<td>{{ rule|fieldtype }}</td>
<td>{{ policy.name }}</td>
<td>{{ policy|fieldtype }}</td>
<td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:rule-update' pk=rule.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:rule-test' pk=rule.uuid %}?back={{ request.get_full_path }}">{% trans 'Test' %}</a>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:rule-delete' pk=rule.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:policy-update' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:policy-test' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Test' %}</a>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:policy-delete' pk=policy.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'generic/form.html' %}
{% load i18n %}
{% block above_form %}
<h1>{% blocktrans with policy=policy %}Test policy {{ policy }}{% endblocktrans %}</h1>
{% endblock %}

View File

@ -11,6 +11,8 @@
{% block content %}
<div class="container">
<h1>{% trans "Providers" %}</h1>
<span>{% trans "Authentication Protocol Provider, used as Protocol behind an Application." %}</span>
<hr>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %}

View File

@ -1,7 +0,0 @@
{% extends 'generic/form.html' %}
{% load i18n %}
{% block above_form %}
<h1>{% blocktrans with rule=rule %}Test rule {{ rule }}{% endblocktrans %}</h1>
{% endblock %}

View File

@ -6,43 +6,49 @@
{% block content %}
<div class="container">
<h1>{% trans "Sources" %}</h1>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'passbook_admin:source-create' %}?type={{ type }}">{{ name }}</a></li>
{% endfor %}
</ul>
</div>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for source in object_list %}
<tr>
<td>{{ source.name }}</td>
<td>{{ source|fieldtype }}</td>
<td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:source-delete' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
{% get_links source as links %}
{% for name, href in links %}
<a class="btn btn-default btn-sm" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
<h1>{% trans "Sources" %}</h1>
<span>{% trans "External Sources which can be used to get Identities into passbook, for example Social Providers like Twiter and GitHub or Enterprise Providers like ADFS and LDAP." %}</span>
<hr>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %}
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:source-create' %}?type={{ type }}">{{ name }}</a></li>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</ul>
</div>
<hr>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for source in object_list %}
<tr>
<td>{{ source.name }}</td>
<td>{{ source|fieldtype }}</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:source-update' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:source-delete' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
{% get_links source as links %}
{% for name, href in links %}
<a class="btn btn-default btn-sm"
href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -1,12 +1,9 @@
"""passbook URL Configuration"""
from django.urls import include, path
from rest_framework_swagger.views import get_swagger_view
from passbook.admin.views import (applications, audit, groups, invitations,
overview, providers, rules, sources, users)
schema_view = get_swagger_view(title='passbook Admin Internal API')
from passbook.admin.views import (applications, audit, factors, groups,
invitations, overview, policy, providers,
sources, users)
urlpatterns = [
path('', overview.AdministrationOverviewView.as_view(), name='overview'),
@ -24,12 +21,12 @@ urlpatterns = [
path('sources/create/', sources.SourceCreateView.as_view(), name='source-create'),
path('sources/<uuid:pk>/update/', sources.SourceUpdateView.as_view(), name='source-update'),
path('sources/<uuid:pk>/delete/', sources.SourceDeleteView.as_view(), name='source-delete'),
# Rules
path('rules/', rules.RuleListView.as_view(), name='rules'),
path('rules/create/', rules.RuleCreateView.as_view(), name='rule-create'),
path('rules/<uuid:pk>/update/', rules.RuleUpdateView.as_view(), name='rule-update'),
path('rules/<uuid:pk>/delete/', rules.RuleDeleteView.as_view(), name='rule-delete'),
path('rules/<uuid:pk>/test/', rules.RuleTestView.as_view(), name='rule-test'),
# Policies
path('policies/', policy.PolicyListView.as_view(), name='policies'),
path('policies/create/', policy.PolicyCreateView.as_view(), name='policy-create'),
path('policies/<uuid:pk>/update/', policy.PolicyUpdateView.as_view(), name='policy-update'),
path('policies/<uuid:pk>/delete/', policy.PolicyDeleteView.as_view(), name='policy-delete'),
path('policies/<uuid:pk>/test/', policy.PolicyTestView.as_view(), name='policy-test'),
# Providers
path('providers/', providers.ProviderListView.as_view(), name='providers'),
path('providers/create/',
@ -38,6 +35,14 @@ urlpatterns = [
providers.ProviderUpdateView.as_view(), name='provider-update'),
path('providers/<int:pk>/delete/',
providers.ProviderDeleteView.as_view(), name='provider-delete'),
# Factors
path('factors/', factors.FactorListView.as_view(), name='factors'),
path('factors/create/',
factors.FactorCreateView.as_view(), name='factor-create'),
path('factors/<uuid:pk>/update/',
factors.FactorUpdateView.as_view(), name='factor-update'),
path('factors/<uuid:pk>/delete/',
factors.FactorDeleteView.as_view(), name='factor-delete'),
# Invitations
path('invitations/', invitations.InvitationListView.as_view(), name='invitations'),
path('invitations/create/',
@ -56,6 +61,5 @@ urlpatterns = [
# Groups
path('groups/', groups.GroupListView.as_view(), name='groups'),
# API
path('api/', schema_view),
path('api/v1/', include('passbook.admin.api.v1.urls'))
path('api/', include('passbook.admin.api.urls'))
]

View File

@ -0,0 +1,79 @@
"""passbook Factor administration"""
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 passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Factor
from passbook.lib.utils.reflection import path_to_class
def all_subclasses(cls):
"""Recursively return all subclassess of cls"""
return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in all_subclasses(c)])
class FactorListView(AdminRequiredMixin, ListView):
"""Show list of all factors"""
model = Factor
template_name = 'administration/factor/list.html'
ordering = 'order'
def get_context_data(self, **kwargs):
kwargs['types'] = {
x.__name__: x._meta.verbose_name for x in all_subclasses(Factor)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class FactorCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Factor"""
template_name = 'generic/create_inheritance.html'
success_url = reverse_lazy('passbook_admin:factors')
success_message = _('Successfully created Factor')
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
source_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type)
kwargs['type'] = model._meta.verbose_name
return kwargs
def get_form_class(self):
source_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type)
if not model:
raise Http404
return path_to_class(model.form)
class FactorUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update factor"""
model = Factor
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:factors')
success_message = _('Successfully updated Factor')
def get_form_class(self):
source_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(Factor) if x.__name__ == source_type)
if not model:
raise Http404
return path_to_class(model.form)
class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete factor"""
model = Factor
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:factors')
success_message = _('Successfully updated Factor')
def get_object(self, queryset=None):
return Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()

View File

@ -2,7 +2,8 @@
from django.views.generic import TemplateView
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Application, Provider, Rule, User
from passbook.core.models import (Application, Factor, Invitation, Policy,
Provider, Source, User)
class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
@ -12,7 +13,10 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
kwargs['application_count'] = len(Application.objects.all())
kwargs['rule_count'] = len(Rule.objects.all())
kwargs['policy_count'] = len(Policy.objects.all())
kwargs['user_count'] = len(User.objects.all())
kwargs['provider_count'] = len(Provider.objects.all())
kwargs['source_count'] = len(Source.objects.all())
kwargs['factor_count'] = len(Factor.objects.all())
kwargs['invitation_count'] = len(Invitation.objects.all())
return super().get_context_data(**kwargs)

View File

@ -0,0 +1,104 @@
"""passbook Policy administration"""
from django.contrib import messages
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.detail import DetailView
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
class PolicyListView(AdminRequiredMixin, ListView):
"""Show list of all policies"""
model = Policy
template_name = 'administration/policy/list.html'
def get_context_data(self, **kwargs):
kwargs['types'] = {
x.__name__: x._meta.verbose_name for x in Policy.__subclasses__()}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().order_by('order').select_subclasses()
class PolicyCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Policy"""
template_name = 'generic/create_inheritance.html'
success_url = reverse_lazy('passbook_admin:policies')
success_message = _('Successfully created Policy')
def get_form_class(self):
policy_type = self.request.GET.get('type')
model = next(x for x in Policy.__subclasses__()
if x.__name__ == policy_type)
if not model:
raise Http404
return path_to_class(model.form)
class PolicyUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update policy"""
model = Policy
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:policies')
success_message = _('Successfully updated Policy')
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class PolicyDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete policy"""
model = Policy
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:policies')
success_message = _('Successfully updated Policy')
def get_object(self, queryset=None):
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class PolicyTestView(AdminRequiredMixin, DetailView, FormView):
"""View to test policy(s)"""
model = Policy
form_class = PolicyTestForm
template_name = 'administration/policy/test.html'
object = None
def get_object(self, queryset=None):
return Policy.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def get_context_data(self, **kwargs):
kwargs['policy'] = self.get_object()
return super().get_context_data(**kwargs)
def post(self, *args, **kwargs):
self.object = self.get_object()
return super().post(*args, **kwargs)
def form_valid(self, form):
policy = self.get_object()
user = form.cleaned_data.get('user')
result = policy.passes(user)
if result:
messages.success(self.request, _('User successfully passed policy.'))
else:
messages.error(self.request, _("User didn't pass policy."))
return self.render_to_response(self.get_context_data(form=form, result=result))

View File

@ -1,104 +0,0 @@
"""passbook Rule administration"""
from django.contrib import messages
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.detail import DetailView
from passbook.admin.forms.rule import RuleTestForm
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import Rule
from passbook.lib.utils.reflection import path_to_class
class RuleListView(AdminRequiredMixin, ListView):
"""Show list of all rules"""
model = Rule
template_name = 'administration/rule/list.html'
def get_context_data(self, **kwargs):
kwargs['types'] = {
x.__name__: x._meta.verbose_name for x in Rule.__subclasses__()}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().order_by('order').select_subclasses()
class RuleCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new Rule"""
template_name = 'generic/create_inheritance.html'
success_url = reverse_lazy('passbook_admin:rules')
success_message = _('Successfully created Rule')
def get_form_class(self):
rule_type = self.request.GET.get('type')
model = next(x for x in Rule.__subclasses__()
if x.__name__ == rule_type)
if not model:
raise Http404
return path_to_class(model.form)
class RuleUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update rule"""
model = Rule
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:rules')
success_message = _('Successfully updated Rule')
def get_form_class(self):
form_class_path = self.get_object().form
form_class = path_to_class(form_class_path)
return form_class
def get_object(self, queryset=None):
return Rule.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class RuleDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete rule"""
model = Rule
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:rules')
success_message = _('Successfully updated Rule')
def get_object(self, queryset=None):
return Rule.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class RuleTestView(AdminRequiredMixin, DetailView, FormView):
"""View to test rule(s)"""
model = Rule
form_class = RuleTestForm
template_name = 'administration/rule/test.html'
object = None
def get_object(self, queryset=None):
return Rule.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def get_context_data(self, **kwargs):
kwargs['rule'] = self.get_object()
return super().get_context_data(**kwargs)
def post(self, *args, **kwargs):
self.object = self.get_object()
return super().post(*args, **kwargs)
def form_valid(self, form):
rule = self.get_object()
user = form.cleaned_data.get('user')
result = rule.passes(user)
if result:
messages.success(self.request, _('User successfully passed rule.'))
else:
messages.error(self.request, _("User didn't pass rule."))
return self.render_to_response(self.get_context_data(form=form, result=result))

View File

@ -5,7 +5,7 @@ from django.utils.translation import ugettext as _
from django.views.generic import DeleteView, ListView, UpdateView
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.forms.user import UserDetailForm
from passbook.core.forms.users import UserDetailForm
from passbook.core.models import User

View File

@ -1,2 +1,2 @@
"""passbook api"""
__version__ = '0.0.4-alpha'
__version__ = '0.0.7-alpha'

View File

@ -1,2 +1,2 @@
"""passbook audit Header"""
__version__ = '0.0.4-alpha'
__version__ = '0.0.7-alpha'

View File

@ -1,4 +1,4 @@
# Generated by Django 2.1.3 on 2018-11-25 10:39
# Generated by Django 2.1.7 on 2019-02-16 09:13
import uuid
@ -20,13 +20,32 @@ class Migration(migrations.Migration):
name='AuditEntry',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('action', models.TextField()),
('action', models.TextField(choices=[('login', 'login'), ('login_failed', 'login_failed'), ('logout', 'logout'), ('authorize_application', 'authorize_application'), ('suspicious_request', 'suspicious_request'), ('sign_up', 'sign_up'), ('password_reset', 'password_reset'), ('invitation_created', 'invitation_created'), ('invitation_used', 'invitation_used')])),
('date', models.DateTimeField(auto_now_add=True)),
('app', models.TextField()),
('_context', models.TextField()),
('request_ip', models.GenericIPAddressField()),
('created', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
],
options={
'abstract': False,
'verbose_name': 'Audit Entry',
'verbose_name_plural': 'Audit Entries',
},
),
migrations.CreateModel(
name='LoginAttempt',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('target_uid', models.CharField(max_length=254)),
('request_ip', models.GenericIPAddressField()),
('attempts', models.IntegerField(default=1)),
],
),
migrations.AlterUniqueTogether(
name='loginattempt',
unique_together={('target_uid', 'request_ip', 'created')},
),
]

View File

@ -1,30 +0,0 @@
# Generated by Django 2.1.4 on 2018-12-10 10:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='auditentry',
name='context',
field=models.TextField(default=''),
preserve_default=False,
),
migrations.AddField(
model_name='auditentry',
name='request_ip',
field=models.GenericIPAddressField(default=''),
preserve_default=False,
),
migrations.AlterField(
model_name='auditentry',
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')]),
),
]

View File

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

View File

@ -1,27 +0,0 @@
# Generated by Django 2.1.4 on 2018-12-10 12:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0002_auto_20181210_1039'),
]
operations = [
migrations.AlterModelOptions(
name='auditentry',
options={'verbose_name': 'Audit Entry', 'verbose_name_plural': 'Audit Entries'},
),
migrations.RenameField(
model_name='auditentry',
old_name='context',
new_name='_context',
),
migrations.AlterField(
model_name='auditentry',
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'), ('invitation_used', 'invitation_used')]),
),
]

View File

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

View File

@ -1,18 +0,0 @@
# Generated by Django 2.1.4 on 2018-12-10 13:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0003_auto_20181210_1213'),
]
operations = [
migrations.AlterField(
model_name='auditentry',
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'), ('invitation_created', 'invitation_created'), ('invitation_used', 'invitation_used')]),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 2.1.4 on 2018-12-11 15:00
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0004_auto_20181210_1348'),
]
operations = [
migrations.AddField(
model_name='auditentry',
name='created',
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
preserve_default=False,
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 2.1.4 on 2018-12-18 12:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_audit', '0005_auditentry_created'),
]
operations = [
migrations.CreateModel(
name='LoginAttempt',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
('target_uid', models.CharField(max_length=254)),
('request_ip', models.GenericIPAddressField()),
('attempts', models.IntegerField(default=1)),
],
),
migrations.AlterUniqueTogether(
name='loginattempt',
unique_together={('target_uid', 'request_ip', 'created')},
),
]

View File

@ -1,22 +1,20 @@
"""passbook audit models"""
from datetime import timedelta
from json import dumps, loads
from logging import getLogger
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.utils import timezone
from django.utils.translation import gettext as _
from ipware import get_client_ip
from reversion import register
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
LOGGER = getLogger(__name__)
@register()
class AuditEntry(UUIDModel):
"""An individual audit log entry"""
@ -45,18 +43,10 @@ class AuditEntry(UUIDModel):
action = models.TextField(choices=ACTIONS)
date = models.DateTimeField(auto_now_add=True)
app = models.TextField()
_context = models.TextField()
_context_cache = None
context = JSONField(default=dict, blank=True)
request_ip = models.GenericIPAddressField()
created = models.DateTimeField(auto_now_add=True)
@property
def context(self):
"""Load context data and load json"""
if not self._context_cache:
self._context_cache = loads(self._context)
return self._context_cache
@staticmethod
def create(action, request, **kwargs):
"""Create AuditEntry from arguments"""
@ -69,7 +59,7 @@ class AuditEntry(UUIDModel):
user=user,
# User 255.255.255.255 as fallback if IP cannot be determined
request_ip=client_ip or '255.255.255.255',
_context=dumps(kwargs))
context=kwargs)
LOGGER.debug("Logged %s from %s (%s)", action, request.user, client_ip)
return entry

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header"""
__version__ = '0.0.4-alpha'
__version__ = '0.0.7-alpha'

View File

@ -2,8 +2,25 @@
from captcha.fields import ReCaptchaField
from django import forms
from passbook.captcha_factor.models import CaptchaFactor
from passbook.core.forms.factors import GENERAL_FIELDS
class CaptchaForm(forms.Form):
"""passbook captcha factor form"""
captcha = ReCaptchaField()
class CaptchaFactorForm(forms.ModelForm):
"""Form to edit CaptchaFactor Instance"""
class Meta:
model = CaptchaFactor
fields = GENERAL_FIELDS + ['public_key', 'private_key']
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
'public_key': forms.TextInput(),
'private_key': forms.TextInput(),
}

View File

@ -0,0 +1,29 @@
# Generated by Django 2.1.7 on 2019-02-24 21:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('passbook_core', '0010_auto_20190224_1016'),
]
operations = [
migrations.CreateModel(
name='CaptchaFactor',
fields=[
('factor_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Factor')),
('public_key', models.TextField()),
('private_key', models.TextField()),
],
options={
'verbose_name': 'Captcha Factor',
'verbose_name_plural': 'Captcha Factors',
},
bases=('passbook_core.factor',),
),
]

View File

@ -0,0 +1,23 @@
"""passbook captcha factor"""
from django.db import models
from django.utils.translation import gettext as _
from passbook.core.models import Factor
class CaptchaFactor(Factor):
"""Captcha Factor instance"""
public_key = models.TextField()
private_key = models.TextField()
type = 'passbook.captcha_factor.factor.CaptchaFactor'
form = 'passbook.captcha_factor.forms.CaptchaFactorForm'
def __str__(self):
return "Captcha Factor %s" % self.slug
class Meta:
verbose_name = _('Captcha Factor')
verbose_name_plural = _('Captcha Factors')

View File

@ -1,2 +1,2 @@
"""passbook core"""
__version__ = '0.0.4-alpha'
__version__ = '0.0.7-alpha'

View File

@ -1,8 +1,12 @@
"""passbook core app config"""
from importlib import import_module
from logging import getLogger
from django.apps import AppConfig
from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__)
class PassbookCoreConfig(AppConfig):
"""passbook core app config"""
@ -12,4 +16,11 @@ class PassbookCoreConfig(AppConfig):
verbose_name = 'passbook Core'
def ready(self):
import_module('passbook.core.rules')
import_module('passbook.core.policies')
factors_to_load = CONFIG.y('passbook.factors', [])
for factors_to_load in factors_to_load:
try:
import_module(factors_to_load)
LOGGER.info("Loaded %s", factors_to_load)
except ImportError as exc:
LOGGER.debug(exc)

View File

@ -1,13 +1,9 @@
"""passbook multi-factor authentication engine"""
from logging import getLogger
from django.utils.translation import gettext as _
from django.views.generic import TemplateView
from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__)
class AuthenticationFactor(TemplateView):
"""Abstract Authentication factor, inherits TemplateView but can be combined with FormView"""
@ -15,8 +11,9 @@ class AuthenticationFactor(TemplateView):
form = None
required = True
authenticator = None
pending_user = None
request = None
template_name = 'login/form.html'
template_name = 'login/form_with_user.html'
def __init__(self, authenticator):
self.authenticator = authenticator
@ -26,4 +23,5 @@ class AuthenticationFactor(TemplateView):
kwargs['is_login'] = True
kwargs['title'] = _('Log in to your account')
kwargs['primary_action'] = _('Log in')
kwargs['user'] = self.pending_user
return super().get_context_data(**kwargs)

View File

View File

@ -8,17 +8,17 @@ from django.utils.translation import gettext as _
from django.views.generic import FormView
from passbook.core.auth.factor import AuthenticationFactor
from passbook.core.auth.mfa import MultiFactorAuthenticator
from passbook.core.forms.authentication import AuthenticationBackendFactorForm
from passbook.core.auth.view import AuthenticationView
from passbook.core.forms.authentication import PasswordFactorForm
from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__)
class AuthenticationBackendFactor(FormView, AuthenticationFactor):
class PasswordFactor(FormView, AuthenticationFactor):
"""Authentication factor which authenticates against django's AuthBackend"""
form_class = AuthenticationBackendFactorForm
form_class = PasswordFactorForm
template_name = 'login/factors/backend.html'
def form_valid(self, form):
@ -34,7 +34,7 @@ class AuthenticationBackendFactor(FormView, AuthenticationFactor):
if user:
# User instance returned from authenticate() has .backend property set
self.authenticator.pending_user = user
self.request.session[MultiFactorAuthenticator.SESSION_USER_BACKEND] = user.backend
self.request.session[AuthenticationView.SESSION_USER_BACKEND] = user.backend
return self.authenticator.user_ok()
# No user was found -> invalid credentials
LOGGER.debug("Invalid credentials")

View File

@ -1,20 +1,20 @@
"""passbook multi-factor authentication engine"""
from logging import getLogger
from django.conf import settings
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.views.generic import View
from passbook.core.models import User
from passbook.core.models import Factor, User
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
LOGGER = getLogger(__name__)
class MultiFactorAuthenticator(UserPassesTestMixin, View):
class AuthenticationView(UserPassesTestMixin, View):
"""Wizard-like Multi-factor authenticator"""
SESSION_FACTOR = 'passbook_factor'
@ -25,67 +25,84 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
pending_user = None
pending_factors = []
factors = settings.AUTHENTICATION_FACTORS.copy()
_current_factor_class = None
_current_factor = None
current_factor = None
# Allow only not authenticated users to login
def test_func(self):
return self.request.user.is_authenticated is False
def handle_no_permission(self):
# Function from UserPassesTestMixin
if 'next' in self.request.GET:
return redirect(self.request.GET.get('next'))
return redirect(reverse('passbook_core:overview'))
def dispatch(self, request, *args, **kwargs):
# Extract pending user from session (only remember uid)
if MultiFactorAuthenticator.SESSION_PENDING_USER in request.session:
if AuthenticationView.SESSION_PENDING_USER in request.session:
self.pending_user = get_object_or_404(
User, id=self.request.session[MultiFactorAuthenticator.SESSION_PENDING_USER])
User, id=self.request.session[AuthenticationView.SESSION_PENDING_USER])
else:
# No Pending user, redirect to login screen
return redirect(reverse('passbook_core:auth-login'))
# Write pending factors to session
if MultiFactorAuthenticator.SESSION_PENDING_FACTORS in request.session:
self.pending_factors = request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS]
if AuthenticationView.SESSION_PENDING_FACTORS in request.session:
self.pending_factors = request.session[AuthenticationView.SESSION_PENDING_FACTORS]
else:
self.pending_factors = self.factors.copy()
# Get an initial list of factors which are currently enabled
# and apply to the current user. We check policies here and block the request
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
self.pending_factors = []
for factor in _all_factors:
if factor.passes(self.pending_user):
self.pending_factors.append((factor.uuid.hex, factor.type))
# Read and instantiate factor from session
factor_class = None
if MultiFactorAuthenticator.SESSION_FACTOR not in request.session:
factor_class = self.pending_factors[0]
factor_uuid, factor_class = None, None
if AuthenticationView.SESSION_FACTOR not in request.session:
# Case when no factors apply to user, return error denied
if not self.pending_factors:
return self.user_invalid()
factor_uuid, factor_class = self.pending_factors[0]
else:
factor_class = request.session[MultiFactorAuthenticator.SESSION_FACTOR]
factor_uuid, factor_class = request.session[AuthenticationView.SESSION_FACTOR]
# Lookup current factor object
self.current_factor = Factor.objects.filter(uuid=factor_uuid).select_subclasses().first()
# Instantiate Next Factor and pass request
factor = path_to_class(factor_class)
self._current_factor = factor(self)
self._current_factor.request = request
self._current_factor_class = factor(self)
self._current_factor_class.pending_user = self.pending_user
self._current_factor_class.request = request
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
"""pass get request to current factor"""
LOGGER.debug("Passing GET to %s", class_to_path(self._current_factor.__class__))
return self._current_factor.get(request, *args, **kwargs)
LOGGER.debug("Passing GET to %s", class_to_path(self._current_factor_class.__class__))
return self._current_factor_class.get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""pass post request to current factor"""
LOGGER.debug("Passing POST to %s", class_to_path(self._current_factor.__class__))
return self._current_factor.post(request, *args, **kwargs)
LOGGER.debug("Passing POST to %s", class_to_path(self._current_factor_class.__class__))
return self._current_factor_class.post(request, *args, **kwargs)
def user_ok(self):
"""Redirect to next Factor"""
LOGGER.debug("Factor %s passed", class_to_path(self._current_factor.__class__))
LOGGER.debug("Factor %s passed", class_to_path(self._current_factor_class.__class__))
# Remove passed factor from pending factors
if class_to_path(self._current_factor.__class__) in self.pending_factors:
self.pending_factors.remove(class_to_path(self._current_factor.__class__))
current_factor_tuple = (self.current_factor.uuid.hex,
class_to_path(self._current_factor_class.__class__))
if current_factor_tuple in self.pending_factors:
self.pending_factors.remove(current_factor_tuple)
next_factor = None
if self.pending_factors:
next_factor = self.pending_factors.pop()
self.request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS] = \
self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] = \
self.pending_factors
self.request.session[MultiFactorAuthenticator.SESSION_FACTOR] = next_factor
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
LOGGER.debug("Rendering Factor is %s", next_factor)
return redirect(reverse('passbook_core:mfa'))
# return redirect(reverse('passbook_core:auth-process', kwargs={'factor': next_factor}))
return redirect(reverse('passbook_core:auth-process'))
# User passed all factors
LOGGER.debug("User passed all factors, logging in")
return self._user_passed()
@ -94,26 +111,30 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
"""Show error message, user cannot login.
This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
LOGGER.debug("User invalid")
return redirect(reverse('passbook_core:mfa-denied'))
self._cleanup()
return redirect(reverse('passbook_core:auth-denied'))
def _user_passed(self):
"""User Successfully passed all factors"""
# user = authenticate(request=self.request, )
backend = self.request.session[MultiFactorAuthenticator.SESSION_USER_BACKEND]
backend = self.request.session[AuthenticationView.SESSION_USER_BACKEND]
login(self.request, self.pending_user, backend=backend)
LOGGER.debug("Logged in user %s", self.pending_user)
# Cleanup
self._cleanup()
next_param = self.request.GET.get('next', None)
if next_param and is_url_absolute(next_param):
return redirect(next_param)
return redirect(reverse('passbook_core:overview'))
def _cleanup(self):
"""Remove temporary data from session"""
session_keys = ['SESSION_FACTOR', 'SESSION_PENDING_FACTORS',
'SESSION_PENDING_USER', 'SESSION_USER_BACKEND', ]
session_keys = [self.SESSION_FACTOR, self.SESSION_PENDING_FACTORS,
self.SESSION_PENDING_USER, self.SESSION_USER_BACKEND, ]
for key in session_keys:
if key in self.request.session:
del self.request.session[key]
LOGGER.debug("Cleaned up sessions")
class MFAPermissionDeniedView(PermissionDeniedView):
class FactorPermissionDeniedView(PermissionDeniedView):
"""User could not be authenticated"""

View File

@ -4,14 +4,11 @@ import logging
import os
import celery
# import pymysql
from django.conf import settings
# from raven import Client
# from raven.contrib.celery import register_logger_signal, register_signal
# pymysql.install_as_MySQLdb()
# set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.core.settings")

View File

@ -1,5 +1,6 @@
"""passbook Core Application forms"""
from django import forms
from django.utils.translation import gettext_lazy as _
from passbook.core.models import Application, Provider
@ -7,15 +8,20 @@ from passbook.core.models import Application, Provider
class ApplicationForm(forms.ModelForm):
"""Application Form"""
provider = forms.ModelChoiceField(queryset=Provider.objects.all().select_subclasses())
provider = forms.ModelChoiceField(queryset=Provider.objects.all().select_subclasses(),
required=False)
class Meta:
model = Application
fields = ['name', 'slug', 'launch_url', 'icon_url',
'rules', 'provider', 'skip_authorization']
'policies', 'provider', 'skip_authorization']
widgets = {
'name': forms.TextInput(),
'launch_url': forms.TextInput(),
'icon_url': forms.TextInput(),
}
labels = {
'launch_url': _('Launch URL'),
'icon_url': _('Icon URL'),
}

View File

@ -18,16 +18,17 @@ class LoginForm(forms.Form):
uid_field = forms.CharField(widget=forms.TextInput(attrs={'placeholder': _('UID')}))
remember_me = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if CONFIG.y('passbook.uid_fields') == ['email']:
self.fields['uid_field'] = forms.EmailField()
def clean_uid_field(self):
"""Validate uid_field after EmailValidator if 'email' is the only selected uid_fields"""
if CONFIG.y('passbook.uid_fields') == ['email']:
validate_email(self.cleaned_data.get('uid_field'))
return self.cleaned_data.get('uid_field')
class AuthenticationBackendFactorForm(forms.Form):
"""Password authentication form"""
password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': _('Password')}))
class SignUpForm(forms.Form):
"""SignUp Form"""
@ -74,6 +75,16 @@ class SignUpForm(forms.Form):
def clean_password_repeat(self):
"""Check if Password adheres to filter and if passwords matche"""
password = self.cleaned_data.get('password')
password_repeat = self.cleaned_data.get('password_repeat')
if password != password_repeat:
raise ValidationError(_("Passwords don't match"))
# TODO: Password policy? Via Plugin? via Policy?
# return check_password(self)
return self.cleaned_data.get('password_repeat')
class PasswordFactorForm(forms.Form):
"""Password authentication form"""
password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': _('Password')}))

View File

@ -0,0 +1,30 @@
"""passbook administration forms"""
from django import forms
from passbook.core.models import DummyFactor, PasswordFactor
GENERAL_FIELDS = ['name', 'slug', 'order', 'policies', 'enabled']
class PasswordFactorForm(forms.ModelForm):
"""Form to create/edit Password Factors"""
class Meta:
model = PasswordFactor
fields = GENERAL_FIELDS + ['backends']
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
}
class DummyFactorForm(forms.ModelForm):
"""Form to create/edit Dummy Factor"""
class Meta:
model = DummyFactor
fields = GENERAL_FIELDS
widgets = {
'name': forms.TextInput(),
'order': forms.NumberInput(),
}

View File

@ -1,18 +1,18 @@
"""passbook rule forms"""
"""passbook Policy forms"""
from django import forms
from django.utils.translation import gettext as _
from passbook.core.models import DebugRule, FieldMatcherRule, WebhookRule
from passbook.core.models import DebugPolicy, FieldMatcherPolicy, WebhookPolicy
GENERAL_FIELDS = ['name', 'action', 'negate', 'order', ]
class FieldMatcherRuleForm(forms.ModelForm):
"""FieldMatcherRule Form"""
class FieldMatcherPolicyForm(forms.ModelForm):
"""FieldMatcherPolicy Form"""
class Meta:
model = FieldMatcherRule
model = FieldMatcherPolicy
fields = GENERAL_FIELDS + ['user_field', 'match_action', 'value', ]
widgets = {
'name': forms.TextInput(),
@ -20,12 +20,12 @@ class FieldMatcherRuleForm(forms.ModelForm):
}
class WebhookRuleForm(forms.ModelForm):
"""WebhookRuleForm Form"""
class WebhookPolicyForm(forms.ModelForm):
"""WebhookPolicyForm Form"""
class Meta:
model = WebhookRule
model = WebhookPolicy
fields = GENERAL_FIELDS + ['url', 'method', 'json_body', 'json_headers',
'result_jsonpath', 'result_json_value', ]
widgets = {
@ -37,12 +37,12 @@ class WebhookRuleForm(forms.ModelForm):
}
class DebugRuleForm(forms.ModelForm):
"""DebugRuleForm Form"""
class DebugPolicyForm(forms.ModelForm):
"""DebugPolicyForm Form"""
class Meta:
model = DebugRule
model = DebugPolicy
fields = GENERAL_FIELDS + ['result', 'wait_min', 'wait_max']
widgets = {
'name': forms.TextInput(),

View File

@ -1,14 +0,0 @@
"""passbook core user forms"""
from django import forms
from passbook.core.models import User
class UserDetailForm(forms.ModelForm):
"""Update User Details"""
class Meta:
model = User
fields = ['username', 'first_name', 'last_name', 'email']

View File

@ -0,0 +1,35 @@
"""passbook core user forms"""
from django import forms
from django.forms import ValidationError
from django.utils.translation import gettext_lazy as _
from passbook.core.models import User
class UserDetailForm(forms.ModelForm):
"""Update User Details"""
class Meta:
model = User
fields = ['username', 'first_name', 'last_name', 'email']
class PasswordChangeForm(forms.Form):
"""Form to update password"""
password = forms.CharField(label=_('Password'),
widget=forms.PasswordInput(attrs={'placeholder': _('New Password')}))
password_repeat = forms.CharField(label=_('Repeat Password'),
widget=forms.PasswordInput(attrs={
'placeholder': _('Repeat Password')
}))
def clean_password_repeat(self):
"""Check if Password adheres to filter and if passwords matche"""
password = self.cleaned_data.get('password')
password_repeat = self.cleaned_data.get('password_repeat')
if password != password_repeat:
raise ValidationError(_("Passwords don't match"))
# TODO: Password policy check
return self.cleaned_data.get('password_repeat')

View File

@ -0,0 +1,71 @@
"""passbook nexus_upload management command"""
from getpass import getpass
import requests
from django.core.management.base import BaseCommand
class Command(BaseCommand):
"""Upload debian package to nexus repository"""
url = None
user = None
password = None
def add_arguments(self, parser):
parser.add_argument(
'--repo',
action='store',
help='Repository to upload to',
required=True)
parser.add_argument(
'--url',
action='store',
help='Nexus root URL',
required=True)
parser.add_argument(
'--user',
action='store',
help='Username to use for Nexus upload',
required=True)
parser.add_argument(
'--method',
action='store',
nargs='?',
const='post',
choices=['post', 'put'],
help=('Method used for uploading files to nexus. '
'Apt repositories use post, Helm uses put.'),
required=True)
parser.add_argument(
'--password',
action='store',
help=("Password to use for Nexus upload. "
"If parameter not given, we'll interactively ask"))
# Positional arguments
parser.add_argument('file', nargs='+', type=str)
def handle(self, *args, **options):
"""Upload debian package to nexus repository"""
if options.get('password') is None:
options['password'] = getpass()
responses = {}
url = 'https://%(url)s/repository/%(repo)s//' % options
method = options.get('method')
exit_code = 0
for file in options.get('file'):
if method == 'post':
responses[file] = requests.post(url, data=open(file, mode='rb'),
auth=(options.get('user'), options.get('password')))
else:
responses[file] = requests.put(url+file, data=open(file, mode='rb'),
auth=(options.get('user'), options.get('password')))
self.stdout.write('Upload results:\n')
sep = '-' * 60
self.stdout.write('%s\n' % sep)
for path, response in responses.items():
self.stdout.write('%-55s: %d\n' % (path, response.status_code))
if response.status_code >= 400:
exit_code = 1
self.stdout.write('%s\n' % sep)
exit(exit_code)

View File

@ -1,4 +1,4 @@
# Generated by Django 2.1.5 on 2019-02-08 10:42
# Generated by Django 2.1.7 on 2019-02-16 09:10
import uuid
@ -68,13 +68,7 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Rule',
name='Policy',
fields=[
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
@ -89,7 +83,7 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='RuleModel',
name='PolicyModel',
fields=[
('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)),
@ -99,6 +93,12 @@ class Migration(migrations.Migration):
'abstract': False,
},
),
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='UserSourceConnection',
fields=[
@ -111,51 +111,82 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='Application',
fields=[
('rulemodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.RuleModel')),
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField()),
('launch_url', models.URLField(blank=True, null=True)),
('icon_url', models.TextField(blank=True, null=True)),
('skip_authorization', models.BooleanField(default=False)),
('provider', models.OneToOneField(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='passbook_core.Provider')),
('provider', models.OneToOneField(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='passbook_core.Provider')),
],
options={
'abstract': False,
},
bases=('passbook_core.rulemodel',),
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='DebugRule',
name='DebugPolicy',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Rule')),
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('result', models.BooleanField(default=False)),
('wait_min', models.IntegerField(default=5)),
('wait_max', models.IntegerField(default=30)),
],
options={
'verbose_name': 'Debug Rule',
'verbose_name_plural': 'Debug Rules',
'verbose_name': 'Debug Policy',
'verbose_name_plural': 'Debug Policys',
},
bases=('passbook_core.rule',),
bases=('passbook_core.policy',),
),
migrations.CreateModel(
name='FieldMatcherRule',
name='Factor',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Rule')),
('user_field', models.TextField(choices=[('username', 'username'), ('first_name', 'first_name'), ('last_name', 'last_name'), ('email', 'email'), ('is_staff', 'is_staff'), ('is_active', 'is_active'), ('data_joined', 'data_joined')])),
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField(unique=True)),
('order', models.IntegerField()),
('type', models.TextField(unique=True)),
('enabled', models.BooleanField(default=True)),
],
options={
'abstract': False,
},
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='FieldMatcherPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('user_field', models.TextField(choices=[('username', 'Username'), ('first_name', 'First Name'), ('last_name', 'Last Name'), ('email', 'E-Mail'), ('is_staff', 'Is staff'), ('is_active', 'Is active'), ('data_joined', 'Date joined')])),
('match_action', models.CharField(choices=[('startswith', 'Starts with'), ('endswith', 'Ends with'), ('endswith', 'Contains'), ('regexp', 'Regexp'), ('exact', 'Exact')], max_length=50)),
('value', models.TextField()),
],
options={
'verbose_name': 'Field matcher Rule',
'verbose_name_plural': 'Field matcher Rules',
'verbose_name': 'Field matcher Policy',
'verbose_name_plural': 'Field matcher Policys',
},
bases=('passbook_core.rule',),
bases=('passbook_core.policy',),
),
migrations.CreateModel(
name='PasswordPolicyPolicy',
fields=[
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('amount_uppercase', models.IntegerField(default=0)),
('amount_lowercase', models.IntegerField(default=0)),
('amount_symbols', models.IntegerField(default=0)),
('length_min', models.IntegerField(default=0)),
('symbol_charset', models.TextField(default='!\\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ')),
],
options={
'verbose_name': 'Password Policy Policy',
'verbose_name_plural': 'Password Policy Policys',
},
bases=('passbook_core.policy',),
),
migrations.CreateModel(
name='Source',
fields=[
('rulemodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.RuleModel')),
('policymodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PolicyModel')),
('name', models.TextField()),
('slug', models.SlugField()),
('enabled', models.BooleanField(default=True)),
@ -163,12 +194,12 @@ class Migration(migrations.Migration):
options={
'abstract': False,
},
bases=('passbook_core.rulemodel',),
bases=('passbook_core.policymodel',),
),
migrations.CreateModel(
name='WebhookRule',
name='WebhookPolicy',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Rule')),
('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')),
('url', models.URLField()),
('method', models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PATCH', 'PATCH'), ('DELETE', 'DELETE'), ('PUT', 'PUT')], max_length=10)),
('json_body', models.TextField()),
@ -177,15 +208,15 @@ class Migration(migrations.Migration):
('result_json_value', models.TextField()),
],
options={
'verbose_name': 'Webhook Rule',
'verbose_name_plural': 'Webhook Rules',
'verbose_name': 'Webhook Policy',
'verbose_name_plural': 'Webhook Policys',
},
bases=('passbook_core.rule',),
bases=('passbook_core.policy',),
),
migrations.AddField(
model_name='rulemodel',
name='rules',
field=models.ManyToManyField(blank=True, to='passbook_core.Rule'),
model_name='policymodel',
name='policies',
field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
),
migrations.AddField(
model_name='user',

View File

@ -1,35 +0,0 @@
# Generated by Django 2.1.5 on 2019-02-08 15:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='PasswordPolicyRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Rule')),
('amount_uppercase', models.IntegerField(default=0)),
('amount_lowercase', models.IntegerField(default=0)),
('amount_symbols', models.IntegerField(default=0)),
('length_min', models.IntegerField(default=0)),
('symbol_charset', models.TextField(default='!\\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ ')),
],
options={
'verbose_name': 'Password Policy Rule',
'verbose_name_plural': 'Password Policy Rules',
},
bases=('passbook_core.rule',),
),
migrations.AlterField(
model_name='fieldmatcherrule',
name='user_field',
field=models.TextField(choices=[('username', 'Username'), ('first_name', 'First Name'), ('last_name', 'Last Name'), ('email', 'E-Mail'), ('is_staff', 'Is staff'), ('is_active', 'Is active'), ('data_joined', 'Date joined')]),
),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,8 +5,8 @@ from random import SystemRandom
from time import sleep
from uuid import uuid4
import reversion
from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
@ -39,7 +39,6 @@ class User(AbstractUser):
applications = models.ManyToManyField('Application')
groups = models.ManyToManyField('Group')
@reversion.register()
class Provider(models.Model):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
@ -51,20 +50,73 @@ class Provider(models.Model):
return getattr(self, 'name')
return super().__str__()
class RuleModel(UUIDModel, CreatedUpdatedModel):
"""Base model which can have rules applied to it"""
class PolicyModel(UUIDModel, CreatedUpdatedModel):
"""Base model which can have policies applied to it"""
rules = models.ManyToManyField('Rule', blank=True)
policies = models.ManyToManyField('Policy', blank=True)
def passes(self, user: User) -> bool:
"""Return true if user passes, otherwise False or raise Exception"""
for rule in self.rules:
if not rule.passes(user):
for policy in self.policies.all():
if not policy.passes(user):
return False
return True
@reversion.register()
class Application(RuleModel):
class Factor(PolicyModel):
"""Authentication factor, multiple instances of the same Factor can be used"""
name = models.TextField()
slug = models.SlugField(unique=True)
order = models.IntegerField()
enabled = models.BooleanField(default=True)
objects = InheritanceManager()
type = ''
form = ''
def has_user_settings(self):
"""Entrypoint to integrate with User settings. Can either return False if no
user settings are available, or a tuple or string, string, string where the first string
is the name the item has, the second string is the icon and the third is the view-name."""
return False
def __str__(self):
return "Factor %s" % self.slug
class PasswordFactor(Factor):
"""Password-based Django-backend Authentication Factor"""
backends = ArrayField(models.TextField())
type = 'passbook.core.auth.factors.password.PasswordFactor'
form = 'passbook.core.forms.factors.PasswordFactorForm'
def has_user_settings(self):
return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password'
def __str__(self):
return "Password Factor %s" % self.slug
class Meta:
verbose_name = _('Password Factor')
verbose_name_plural = _('Password Factors')
class DummyFactor(Factor):
"""Dummy factor, mostly used to debug"""
type = 'passbook.core.auth.factors.dummy.DummyFactor'
form = 'passbook.core.forms.factors.DummyFactorForm'
def __str__(self):
return "Dummy Factor %s" % self.slug
class Meta:
verbose_name = _('Dummy Factor')
verbose_name_plural = _('Dummy Factors')
class Application(PolicyModel):
"""Every Application which uses passbook for authentication/identification/authorization
needs an Application record. Other authentication types can subclass this Model to
add custom fields and other properties"""
@ -73,7 +125,7 @@ class Application(RuleModel):
slug = models.SlugField()
launch_url = models.URLField(null=True, blank=True)
icon_url = models.TextField(null=True, blank=True)
provider = models.OneToOneField('Provider', null=True,
provider = models.OneToOneField('Provider', null=True, blank=True,
default=None, on_delete=models.SET_DEFAULT)
skip_authorization = models.BooleanField(default=False)
@ -81,14 +133,13 @@ class Application(RuleModel):
def user_is_authorized(self, user: User) -> bool:
"""Check if user is authorized to use this application"""
from passbook.core.rules import RuleEngine
return RuleEngine(self.rules.all()).for_user(user).result
from passbook.core.policies import PolicyEngine
return PolicyEngine(self.policies.all()).for_user(user).result
def __str__(self):
return self.name
@reversion.register()
class Source(RuleModel):
class Source(PolicyModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
name = models.TextField()
@ -111,7 +162,6 @@ class Source(RuleModel):
def __str__(self):
return self.name
@reversion.register()
class UserSourceConnection(CreatedUpdatedModel):
"""Connection between User and Source."""
@ -122,9 +172,8 @@ class UserSourceConnection(CreatedUpdatedModel):
unique_together = (('user', 'source'),)
@reversion.register()
class Rule(UUIDModel, CreatedUpdatedModel):
"""Rules which specify if a user is authorized to use an Application. Can be overridden by
class Policy(UUIDModel, CreatedUpdatedModel):
"""Policies which specify if a user is authorized to use an Application. Can be overridden by
other types to add other fields, more logic, etc."""
ACTION_ALLOW = 'allow'
@ -147,12 +196,11 @@ class Rule(UUIDModel, CreatedUpdatedModel):
return "%s action %s" % (self.name, self.action)
def passes(self, user: User) -> bool:
"""Check if user instance passes this rule"""
"""Check if user instance passes this policy"""
raise NotImplementedError()
@reversion.register()
class FieldMatcherRule(Rule):
"""Rule which checks if a field of the User model matches/doesn't match a
class FieldMatcherPolicy(Policy):
"""Policy which checks if a field of the User model matches/doesn't match a
certain pattern"""
MATCH_STARTSWITH = 'startswith'
@ -164,7 +212,7 @@ class FieldMatcherRule(Rule):
MATCHES = (
(MATCH_STARTSWITH, _('Starts with')),
(MATCH_ENDSWITH, _('Ends with')),
(MATCH_ENDSWITH, _('Contains')),
(MATCH_CONTAINS, _('Contains')),
(MATCH_REGEXP, _('Regexp')),
(MATCH_EXACT, _('Exact')),
)
@ -183,7 +231,7 @@ class FieldMatcherRule(Rule):
match_action = models.CharField(max_length=50, choices=MATCHES)
value = models.TextField()
form = 'passbook.core.forms.rules.FieldMatcherRuleForm'
form = 'passbook.core.forms.policies.FieldMatcherPolicyForm'
def __str__(self):
description = "%s, user.%s %s '%s'" % (self.name, self.user_field,
@ -200,13 +248,13 @@ class FieldMatcherRule(Rule):
LOGGER.debug("Checked '%s' %s with '%s'...",
user_field_value, self.match_action, self.value)
passes = False
if self.match_action == FieldMatcherRule.MATCH_STARTSWITH:
if self.match_action == FieldMatcherPolicy.MATCH_STARTSWITH:
passes = user_field_value.startswith(self.value)
if self.match_action == FieldMatcherRule.MATCH_ENDSWITH:
if self.match_action == FieldMatcherPolicy.MATCH_ENDSWITH:
passes = user_field_value.endswith(self.value)
if self.match_action == FieldMatcherRule.MATCH_CONTAINS:
if self.match_action == FieldMatcherPolicy.MATCH_CONTAINS:
passes = self.value in user_field_value
if self.match_action == FieldMatcherRule.MATCH_REGEXP:
if self.match_action == FieldMatcherPolicy.MATCH_REGEXP:
pattern = re.compile(self.value)
passes = bool(pattern.match(user_field_value))
if self.negate:
@ -216,12 +264,11 @@ class FieldMatcherRule(Rule):
class Meta:
verbose_name = _('Field matcher Rule')
verbose_name_plural = _('Field matcher Rules')
verbose_name = _('Field matcher Policy')
verbose_name_plural = _('Field matcher Policies')
@reversion.register()
class PasswordPolicyRule(Rule):
"""Rule to make sure passwords have certain properties"""
class PasswordPolicy(Policy):
"""Policy to make sure passwords have certain properties"""
amount_uppercase = models.IntegerField(default=0)
amount_lowercase = models.IntegerField(default=0)
@ -229,7 +276,7 @@ class PasswordPolicyRule(Rule):
length_min = models.IntegerField(default=0)
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
form = 'passbook.core.forms.rules.PasswordPolicyRuleForm'
form = 'passbook.core.forms.policies.PasswordPolicyForm'
def passes(self, user: User) -> bool:
# Only check if password is being set
@ -250,13 +297,12 @@ class PasswordPolicyRule(Rule):
class Meta:
verbose_name = _('Password Policy Rule')
verbose_name_plural = _('Password Policy Rules')
verbose_name = _('Password Policy')
verbose_name_plural = _('Password Policies')
@reversion.register()
class WebhookRule(Rule):
"""Rule that asks webhook"""
class WebhookPolicy(Policy):
"""Policy that asks webhook"""
METHOD_GET = 'GET'
METHOD_POST = 'POST'
@ -279,7 +325,7 @@ class WebhookRule(Rule):
result_jsonpath = models.TextField()
result_json_value = models.TextField()
form = 'passbook.core.forms.rules.WebhookRuleForm'
form = 'passbook.core.forms.policies.WebhookPolicyForm'
def passes(self, user: User):
"""Call webhook asynchronously and report back"""
@ -287,31 +333,30 @@ class WebhookRule(Rule):
class Meta:
verbose_name = _('Webhook Rule')
verbose_name_plural = _('Webhook Rules')
verbose_name = _('Webhook Policy')
verbose_name_plural = _('Webhook Policies')
@reversion.register()
class DebugRule(Rule):
"""Rule used for debugging the RuleEngine. Returns a fixed result,
class DebugPolicy(Policy):
"""Policy used for debugging the PolicyEngine. Returns a fixed result,
but takes a random time to process."""
result = models.BooleanField(default=False)
wait_min = models.IntegerField(default=5)
wait_max = models.IntegerField(default=30)
form = 'passbook.core.forms.rules.DebugRuleForm'
form = 'passbook.core.forms.policies.DebugPolicyForm'
def passes(self, user: User):
"""Wait random time then return result"""
wait = SystemRandom().randrange(self.wait_min, self.wait_max)
LOGGER.debug("Rule '%s' waiting for %ds", self.name, wait)
LOGGER.debug("Policy '%s' waiting for %ds", self.name, wait)
sleep(wait)
return self.result
class Meta:
verbose_name = _('Debug Rule')
verbose_name_plural = _('Debug Rules')
verbose_name = _('Debug Policy')
verbose_name_plural = _('Debug Policies')
class Invitation(UUIDModel):
"""Single-use invitation link"""

48
passbook/core/policies.py Normal file
View File

@ -0,0 +1,48 @@
"""passbook core policy engine"""
from logging import getLogger
from celery import group
from passbook.core.celery import CELERY_APP
from passbook.core.models import Policy, User
LOGGER = getLogger(__name__)
@CELERY_APP.task()
def _policy_engine_task(user_pk, policy_pk, **kwargs):
"""Task wrapper to run policy checking"""
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)
return policy_obj.passes(user_obj)
class PolicyEngine:
"""Orchestrate policy checking, launch tasks and return result"""
policies = None
_group = None
def __init__(self, policies):
self.policies = policies
def for_user(self, user):
"""Check policies for user"""
signatures = []
kwargs = {
'__password__': getattr(user, '__password__', None)
}
for policy in self.policies:
signatures.append(_policy_engine_task.s(user.pk, policy.pk.hex, **kwargs))
self._group = group(signatures)()
return self
@property
def result(self):
"""Get policy-checking result"""
for policy_result in self._group.get():
if policy_result is False:
return False
return True

View File

@ -1,5 +1,4 @@
django>=2.0
django-reversion
django-model-utils
djangorestframework
PyYAML
@ -8,6 +7,6 @@ markdown
colorlog
celery
redis<3.0
pymysql
psycopg2
idna<2.8,>=2.5
cherrypy

View File

@ -1,47 +0,0 @@
"""passbook core rule engine"""
from logging import getLogger
from celery import group
from passbook.core.celery import CELERY_APP
from passbook.core.models import Rule, User
LOGGER = getLogger(__name__)
@CELERY_APP.task()
def _rule_engine_task(user_pk, rule_pk, **kwargs):
"""Task wrapper to run rule checking"""
rule_obj = Rule.objects.filter(pk=rule_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 rule `%s`#%s for user %s...", rule_obj.name, rule_obj.pk.hex, user_obj)
return rule_obj.passes(user_obj)
class RuleEngine:
"""Orchestrate rule checking, launch tasks and return result"""
rules = None
_group = None
def __init__(self, rules):
self.rules = rules
def for_user(self, user):
"""Check rules for user"""
signatures = []
kwargs = {
'__password__': getattr(user, '__password__')
}
for rule in self.rules:
signatures.append(_rule_engine_task.s(user.pk, rule.pk.hex, **kwargs))
self._group = group(signatures)()
return self
@property
def result(self):
"""Get rule-checking result"""
for rule_result in self._group.get():
if rule_result is False:
return False
return True

View File

@ -50,11 +50,6 @@ AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend',
'passbook.oauth_client.backends.AuthorizedServiceBackend'
]
AUTHENTICATION_FACTORS = [
'passbook.core.auth.backend_factor.AuthenticationBackendFactor',
'passbook.core.auth.dummy.DummyFactor',
'passbook.captcha_factor.factor.CaptchaFactor',
]
# Application definition
@ -65,9 +60,8 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'reversion',
'rest_framework',
'rest_framework_swagger',
'drf_yasg',
'passbook.core.apps.PassbookCoreConfig',
'passbook.admin.apps.PassbookAdminConfig',
'passbook.api.apps.PassbookAPIConfig',
@ -77,7 +71,7 @@ INSTALLED_APPS = [
'passbook.oauth_client.apps.PassbookOAuthClientConfig',
'passbook.oauth_provider.apps.PassbookOAuthProviderConfig',
'passbook.saml_idp.apps.PassbookSAMLIDPConfig',
'passbook.totp.apps.PassbookTOTPConfig',
'passbook.otp.apps.PassbookOTPConfig',
'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig',
]
@ -135,9 +129,6 @@ WSGI_APPLICATION = 'passbook.core.wsgi.application'
DATABASES = {}
for db_alias, db_config in CONFIG.get('databases').items():
if 'mysql' in db_config.get('engine'):
import pymysql
pymysql.install_as_MySQLdb()
DATABASES[db_alias] = {
'ENGINE': db_config.get('engine'),
'HOST': db_config.get('host'),

View File

@ -0,0 +1,18 @@
function convertToSlug(Text) {
return Text
.toLowerCase()
.replace(/[^\w ]+/g, '')
.replace(/ +/g, '-')
;
}
const $source = $('input[name=name]');
const $result = $('input[name=slug]');
const typeHandler = function (e) {
$result.val(convertToSlug(e.target.value));
}
$source.on('input', typeHandler) // register for oninput
$source.on('propertychange', typeHandler) // for IE8

View File

@ -15,6 +15,12 @@
<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' %}">
<style>
.login-pf {
background-attachment: fixed;
scroll-behavior: smooth;
}
</style>
{% block head %}
{% endblock %}
</head>
@ -24,6 +30,7 @@
<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">

View File

@ -5,53 +5,57 @@
{% block head %}
<style>
.login-pf-page .login-pf-page-footer-links {
padding: 15px;
background-color: #fff;
border-top: 2px solid transparent;
box-shadow: 0 1px 1px rgba(3,3,3,.175);
}
.login-pf-page .login-pf-page-footer-links {
padding: 15px;
background-color: #fff;
border-top: 2px solid transparent;
box-shadow: 0 1px 1px rgba(3, 3, 3, .175);
}
.login-pf-page .login-pf-page-footer-link {
color: #72767b;
}
.login-pf-page .login-pf-page-footer-link {
color: #72767b;
}
.login-pf-page .login-pf-page-footer-links li:not(:last-of-type):after {
color: #72767b;
}
.login-pf-page .login-pf-page-footer-links li:not(:last-of-type):after {
color: #72767b;
}
</style>
{% endblock %}
{% block body %}
<div class="login-pf-page">
<div class="container-fluid">
<div class="row">
<div class="col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3">
<header class="login-pf-page-header">
<img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}" alt="PatternFly logo" />
{% if config.login.subtext %}
<p>{{ config.login.subtext }}</p>
{% endif %}
</header>
<div class="container-fluid">
<div class="row">
{% block row %}
<div class="col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-8 col-lg-offset-2">
<div class="card-pf">
{% block card %}
{% endblock %}
</div><!-- card -->
<footer class="login-pf-page-footer">
<ul class="login-pf-page-footer-links list-unstyled">
<li><a class="login-pf-page-footer-link" href="#">Terms of Use</a></li>
<li><a class="login-pf-page-footer-link" href="#">Help</a></li>
<li><a class="login-pf-page-footer-link" href="#">Privacy Policy</a></li>
</ul>
</footer>
</div><!-- col -->
{% endblock %}
<div class="col-md-6 col-md-offset-3">
{% include 'partials/messages.html' %}
</div>
<div class="col-sm-6 col-sm-offset-3 col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4">
<header class="login-pf-page-header">
<img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}"
alt="PatternFly logo" />
{% if config.login.subtext %}
<p>{{ config.login.subtext }}</p>
{% endif %}
</header>
<div class="row">
{% block row %}
<div class="col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-8 col-lg-offset-2">
<div class="card-pf">
{% block card %}
{% endblock %}
</div><!-- card -->
<footer class="login-pf-page-footer">
<ul class="login-pf-page-footer-links list-unstyled">
<li><a class="login-pf-page-footer-link" href="#">Terms of Use</a></li>
<li><a class="login-pf-page-footer-link" href="#">Help</a></li>
<li><a class="login-pf-page-footer-link" href="#">Privacy Policy</a></li>
</ul>
</footer>
</div><!-- col -->
{% endblock %}
</div><!-- row -->
</div><!-- col -->
</div><!-- row -->
</div><!-- col -->
</div><!-- row -->
</div><!-- container -->
</div><!-- container -->
</div><!-- login-pf-page -->
{% endblock %}

View File

@ -1,8 +1,4 @@
{% extends 'login/form.html' %}
{% extends 'login/form_with_user.html' %}
{% load i18n %}
{% block above_form %}
<span class="pficon pficon-unlocked"></span>
{% trans "This is a text" %}
{% endblock %}

View File

@ -7,7 +7,6 @@
<header class="login-pf-header">
<h1>{% trans title %}</h1>
</header>
{% include 'partials/messages.html' %}
<form method="POST">
{% csrf_token %}
{% block above_form %}

View File

@ -0,0 +1,31 @@
{% extends 'login/form.html' %}
{% load i18n %}
{% load utils %}
{% block head %}
{{ block.super }}
<style>
.login-pf-settings img {
max-height: 32px;
border-radius: 100%;
border-width: 1px;
border-color: #000;
}
.login-pf-settings a {
padding-top: 3px;
padding-bottom: 3px;
line-height: 32px;
}
</style>
{% endblock %}
{% block above_form %}
<div class="form-group login-pf-settings">
<p class="form-control-static">
<img src="{% gravatar user.email %}" alt="">
{{ user.username }}
</p>
<a href="{% url 'passbook_core:auth-login' %}">{% trans 'Not you?' %}</a>
</div>
{% endblock %}

View File

@ -1,68 +0,0 @@
{% extends "base/skeleton.html" %}
{% load static %}
{% block body %}
<div class="login-pf-page">
<div class="container-fluid">
<div class="row">
<div class="col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3">
<header class="login-pf-page-header">
<img class="login-pf-brand" src="{% static 'img/Logo_Horizontal_Reversed.svg' %}" alt=" logo" />
</header>
<div class="row">
<div class="col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-8 col-lg-offset-2">
<div class="card-pf">
<header class="login-pf-header">
<select class="selectpicker">
<option>English</option>
<option>French</option>
<option>Italian</option>
</select>
<h1>Single Sign-On</h1>
<p class="text-center">Log in to <strong>Application</strong></p>
</header>
<form>
<div class="form-group">
<label class="sr-only" for="exampleInputEmail1">Email address</label>
<input type="email" class="form-control input-lg" id="exampleInputEmail1" placeholder="Email address">
</div>
<div class="form-group">
<label class="sr-only" for="exampleInputPassword1">Password
</label>
<input type="password" class="form-control input-lg" id="exampleInputPassword1" placeholder="Password">
</div>
<div class="form-group login-pf-settings">
<label class="checkbox-label">
<input type="checkbox"> Keep me logged in for 30 days
</label>
<a href="#">Forgot password?</a>
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg">Log In</button>
</form>
<p class="login-pf-signup">Need an account?<a href="#">Sign up</a></p>
</div><!-- card -->
<footer class="login-pf-page-footer">
<div class="login-pf-page-footer-sso-services">
<p>One account for all your company services</p>
<ul class="login-pf-page-footer-sso-services-logos">
<li><img src="{% static 'img/google-drive.svg' %}" alt="google drive icon" /></li>
<li><img src="{% static 'img/gmail.svg' %}" alt="gmail icon" /></li>
<li><img src="{% static 'img/google-calendar.svg' %}" alt="google calendar icon" /></li>
</ul>
</div>
<ul class="login-pf-page-footer-links list-unstyled">
<li><a class="login-pf-page-footer-link" href="#">Terms of Use</a></li>
<li><a class="login-pf-page-footer-link" href="#">Help</a></li>
<li><a class="login-pf-page-footer-link" href="#">Privacy Policy</a></li>
</ul>
</footer>
</div><!-- col -->
</div><!-- row -->
</div><!-- col -->
</div><!-- login-pf-page -->
</div>
<!--row-->
</div>
<!--container-->
{% endblock %}

View File

@ -14,7 +14,7 @@
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img src="{% static 'img/brand.svg' %}" alt="PatternFly Enterprise Application" />
<img src="{% static 'img/brand.svg' %}" alt="passbook" />
</a>
</div>
<div class="collapse navbar-collapse navbar-collapse-1">
@ -40,6 +40,9 @@
<li>
<a href="{% url 'passbook_core:user-settings' %}">{% trans 'User Settings' %}</a>
</li>
<li>
<a href="{% url 'passbook_core:user-change-password' %}">{% trans 'Change Password' %}</a>
</li>
<li class="divider"></li>
<li>
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
@ -63,7 +66,9 @@
</div>
</nav>
<div class="container-fluid container-cards-pf">
{% include 'partials/messages.html' %}
<div class="container">
{% include 'partials/messages.html' %}
</div>
{% block content %}
{% endblock %}
</div>

View File

@ -2,17 +2,28 @@
{% load i18n %}
{% load is_active %}
{% load passbook_user_settings %}
{% block content %}
<div class="container">
<div class="col-md-3 ">
<div class="nav-category">
<h2>{% trans 'User Profile'%}</h2>
<h2>{% trans 'User Settings'%}</h2>
<ul class="nav nav-pills nav-stacked">
<li class="{% is_active 'passbook_core:user-settings' %}"><a href="{% url 'passbook_core:user-settings' %}"><i class="fa fa-desktop"></i>{% trans 'Details' %}</a></li>
<li><a href="#"><i class="fa fa-cog"></i>System Services</a></li>
<li><a href="#"><i class="fa fa-file-text-o"></i>Journal</a></li>
<li><a href="#"><i class="fa fa-cloud"></i>Storage</a></li>
<li class="{% is_active 'passbook_core:user-settings' %}">
<a href="{% url 'passbook_core:user-settings' %}">
<i class="fa pficon-edit"></i> {% trans 'Details' %}
</a>
</li>
<li class="nav-divider"></li>
{% user_factors as uf %}
{% for name, icon, link in uf %}
<li class="{% is_active link %}">
<a href="{% url link %}">
<i class="{{ icon }}"></i> {{ name }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>

View File

View File

@ -0,0 +1,19 @@
"""passbook user settings template tags"""
from django import template
from passbook.core.models import Factor
register = template.Library()
@register.simple_tag(takes_context=True)
def user_factors(context):
"""Return list of all factors which apply to user"""
user = context.get('request').user
_all_factors = Factor.objects.filter(enabled=True).order_by('order').select_subclasses()
matching_factors = []
for factor in _all_factors:
_link = factor.has_user_settings()
if factor.passes(user) and _link:
matching_factors.append(_link)
return matching_factors

View File

@ -6,7 +6,7 @@ from django.contrib import admin
from django.urls import include, path
from django.views.generic import RedirectView
from passbook.core.auth import mfa
from passbook.core.auth import view
from passbook.core.views import authentication, overview, user
from passbook.lib.utils.reflection import get_apps
@ -19,11 +19,14 @@ core_urls = [
path('auth/login/', authentication.LoginView.as_view(), name='auth-login'),
path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'),
path('auth/sign_up/', authentication.SignUpView.as_view(), name='auth-sign-up'),
path('auth/mfa/', mfa.MultiFactorAuthenticator.as_view(), name='mfa'),
path('auth/mfa/denied/', mfa.MFAPermissionDeniedView.as_view(), name='mfa-denied'),
path('auth/process/denied/', view.FactorPermissionDeniedView.as_view(), name='auth-denied'),
path('auth/process/', view.AuthenticationView.as_view(), name='auth-process'),
path('auth/process/<slug:factor>/', view.AuthenticationView.as_view(), name='auth-process'),
# User views
path('user/', user.UserSettingsView.as_view(), name='user-settings'),
path('user/delete/', user.UserDeleteView.as_view(), name='user-delete'),
path('user/change_password/', user.UserChangePasswordView.as_view(),
name='user-change-password'),
# Overview
path('', overview.OverviewView.as_view(), name='overview'),
]

View File

@ -11,7 +11,7 @@ from django.utils.translation import ugettext as _
from django.views import View
from django.views.generic import FormView
from passbook.core.auth.mfa import MultiFactorAuthenticator
from passbook.core.auth.view import AuthenticationView
from passbook.core.forms.authentication import LoginForm, SignUpForm
from passbook.core.models import Invitation, Source, User
from passbook.core.signals import invitation_used, user_signed_up
@ -62,10 +62,9 @@ class LoginView(UserPassesTestMixin, FormView):
if not pre_user:
# No user found
return self.invalid_login(self.request)
if MultiFactorAuthenticator.SESSION_FACTOR in self.request.session:
del self.request.session[MultiFactorAuthenticator.SESSION_FACTOR]
self.request.session[MultiFactorAuthenticator.SESSION_PENDING_USER] = pre_user.pk
return redirect(reverse('passbook_core:mfa'))
self.request.session.flush()
self.request.session[AuthenticationView.SESSION_PENDING_USER] = pre_user.pk
return redirect(reverse('passbook_core:auth-process'))
def invalid_login(self, request: HttpRequest, disabled_user: User = None) -> HttpResponse:
"""Handle login for disabled users/invalid login attempts"""

View File

@ -1,11 +1,12 @@
"""passbook core user views"""
from django.contrib import messages
from django.contrib.auth import logout
from django.urls import reverse
from django.contrib.auth import logout, update_session_auth_hash
from django.shortcuts import redirect, reverse
from django.utils.translation import gettext as _
from django.views.generic import DeleteView, UpdateView
from django.views.generic import DeleteView, FormView, UpdateView
from passbook.core.forms.user import UserDetailForm
from passbook.core.forms.users import PasswordChangeForm, UserDetailForm
from passbook.lib.config import CONFIG
class UserSettingsView(UpdateView):
@ -28,3 +29,23 @@ class UserDeleteView(DeleteView):
messages.success(self.request, _('Successfully deleted user.'))
logout(self.request)
return reverse('passbook_core:auth-login')
class UserChangePasswordView(FormView):
"""View for users to update their password"""
form_class = PasswordChangeForm
template_name = 'login/form_with_user.html'
def form_valid(self, form: PasswordChangeForm):
self.request.user.set_password(form.cleaned_data.get('password'))
self.request.user.save()
update_session_auth_hash(self.request, self.request.user)
messages.success(self.request, _('Successfully changed password'))
return redirect('passbook_core:overview')
def get_context_data(self, **kwargs):
kwargs['config'] = CONFIG.get('passbook')
kwargs['is_login'] = True
kwargs['title'] = _('Change Password')
kwargs['primary_action'] = _('Change')
return super().get_context_data(**kwargs)

View File

@ -1,2 +1,2 @@
"""Passbook ldap app Header"""
__version__ = '0.0.4-alpha'
__version__ = '0.0.7-alpha'

View File

@ -1,6 +1,7 @@
"""passbook LDAP Forms"""
from django import forms
from django.utils.translation import gettext_lazy as _
from passbook.admin.forms.source import SOURCE_FORM_FIELDS
from passbook.ldap.models import LDAPSource
@ -14,7 +15,7 @@ class LDAPSourceForm(forms.ModelForm):
model = LDAPSource
fields = SOURCE_FORM_FIELDS + ['server_uri', 'bind_cn', 'bind_password',
'type', 'domain', 'base_dn', 'create_user',
'reset_password', 'rules']
'reset_password']
widgets = {
'name': forms.TextInput(),
'server_uri': forms.TextInput(),
@ -23,6 +24,11 @@ class LDAPSourceForm(forms.ModelForm):
'domain': forms.TextInput(),
'base_dn': forms.TextInput(),
}
labels = {
'server_uri': _('Server URI'),
'bind_cn': _('Bind CN'),
'base_dn': _('Base DN'),
}
# class GeneralSettingsForm(SettingsForm):
# """general settings form"""

View File

@ -1,4 +1,4 @@
# Generated by Django 2.1.4 on 2018-12-10 09:16
# Generated by Django 2.1.7 on 2019-02-16 09:13
import django.db.models.deletion
from django.db import migrations, models

View File

@ -1,2 +1,2 @@
"""passbook lib"""
__version__ = '0.0.4-alpha'
__version__ = '0.0.7-alpha'

View File

@ -0,0 +1,12 @@
"""passbook django boilerplate code"""
from django.utils.decorators import method_decorator
from django.views.decorators.cache import never_cache
class NeverCacheMixin():
"""Use never_cache as mixin for CBV"""
@method_decorator(never_cache)
def dispatch(self, *args, **kwargs):
"""Use never_cache as mixin for CBV"""
return super().dispatch(*args, **kwargs)

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