Compare commits

...

51 Commits

Author SHA1 Message Date
7e9154a0ea bump version: 0.0.7-alpha -> 0.0.8-alpha 2019-02-25 17:39:39 +01:00
e0ef061771 fix pylint errors.... 2019-02-25 17:32:52 +01:00
b8694a7ade fix bandit error (SHA1 has to be used) 2019-02-25 17:23:42 +01:00
10d6a30f2c add experimental HaveIBeenPwned Password Policy 2019-02-25 17:21:56 +01:00
8c94aef6d0 add stub test so coverage doesn't crash 2019-02-25 17:21:06 +01:00
19bd3bfffb fix allauth imports 2019-02-25 17:20:53 +01:00
8611ac624c Make links on admin overview site actually useful 2019-02-25 17:11:52 +01:00
fa93b59a8c switch to toast notifications everywhere 2019-02-25 16:41:53 +01:00
8b66b40f0d move forgot password to PasswordFactor 2019-02-25 16:41:33 +01:00
c2756f15fc Correctly display action on Create/Update templates 2019-02-25 16:40:46 +01:00
408e205c5f add signal for password change, add field for password policies 2019-02-25 15:41:36 +01:00
5f3ab49535 fix bug when Empty username is given to LoginAttempt.attempt 2019-02-25 14:10:29 +01:00
33431ae013 improve OAuth Source Setup process, fix login template, closes #3 2019-02-25 14:10:10 +01:00
b40ac6dc5d more Icons cause everyone loves icons 2019-02-25 13:31:11 +01:00
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
b4cb157257 bump version: 0.0.3-alpha -> 0.0.4-alpha 2019-02-11 17:44:42 +01:00
d9ccbdd962 fix bumpversion typo 2019-02-11 17:44:40 +01:00
d5ab20ee12 fix coverage failing 2019-02-11 17:36:36 +01:00
0e73702fca add PasswordPolicyRule (not used yet) 2019-02-10 20:09:47 +01:00
58ebd15ada fix mismatched Version numbers and missing verbose_names 2019-02-10 20:08:29 +01:00
1a998e5020 bump version: 0.0.2-alpha -> 0.0.3-alpha 2019-02-08 15:19:31 +01:00
d8eb926a76 fix migration import order 2019-02-08 15:19:29 +01:00
171 changed files with 2761 additions and 1712 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.0.2-alpha current_version = 0.0.8-alpha
tag = True tag = True
commit = True commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
@ -20,6 +20,8 @@ values =
[bumpversion:file:passbook/__init__.py] [bumpversion:file:passbook/__init__.py]
[bumpversion:file:passbook/api/__init__.py]
[bumpversion:file:passbook/core/__init__.py] [bumpversion:file:passbook/core/__init__.py]
[bumpversion:file:passbook/admin/__init__.py] [bumpversion:file:passbook/admin/__init__.py]
@ -30,11 +32,13 @@ values =
[bumpversion:file:passbook/ldap/__init__.py] [bumpversion:file:passbook/ldap/__init__.py]
[bumpversion:file:passbook/lib/__init__.py]
[bumpversion:file:passbook/saml_idp/__init__.py] [bumpversion:file:passbook/saml_idp/__init__.py]
[bumpversion:file:passbook/audit/__init__.py] [bumpversion:file:passbook/audit/__init__.py]
[bumpversion:file:passbook/oauth_provider/__init__.py] [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 env
helm helm
passbook-ui passbook-ui
static

2
.gitignore vendored
View File

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

View File

@ -8,7 +8,15 @@ stages:
- test - test
- build - build
- docs - 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: include:
- /allauth/.gitlab-ci.yml - /allauth/.gitlab-ci.yml
@ -46,7 +54,7 @@ package-docker:
before_script: before_script:
- echo "{\"auths\":{\"https://docker.$NEXUS_URL/\":{\"username\":\"$NEXUS_USER\",\"password\":\"$NEXUS_PASS\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"https://docker.$NEXUS_URL/\":{\"username\":\"$NEXUS_USER\",\"password\":\"$NEXUS_PASS\"}}}" > /kaniko/.docker/config.json
script: 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.2-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.8-alpha
stage: build stage: build
only: only:
- tags - tags
@ -55,6 +63,7 @@ package-helm:
stage: build stage: build
script: script:
- curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash - curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash
- helm init --client-only
- helm package helm/passbook - helm package helm/passbook
- ./manage.py nexus_upload --method put --url $NEXUS_URL --user $NEXUS_USER --password $NEXUS_PASS --repo helm *.tgz - ./manage.py nexus_upload --method put --url $NEXUS_URL --user $NEXUS_USER --password $NEXUS_PASS --repo helm *.tgz
only: 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 FROM python:3.6-slim-stretch as build
# LABEL version="1.8.8"
COPY ./passbook/ /app/passbook COPY ./passbook/ /app/passbook
COPY ./static/ /app/static
COPY ./manage.py /app/ COPY ./manage.py /app/
COPY ./requirements.txt /app/ COPY ./requirements.txt /app/
WORKDIR /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 && \ RUN pip install -r requirements.txt && \
pip install psycopg2 && \ pip install psycopg2 && \
adduser --system --home /app/ passbook && \ 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,6 +1,5 @@
"""passbook provider""" """passbook provider"""
from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns from allauth.socialaccount.providers.oauth2.urls import default_urlpatterns
from allauth_passbook.provider import PassbookProvider from allauth_passbook.provider import PassbookProvider
urlpatterns = default_urlpatterns(PassbookProvider) urlpatterns = default_urlpatterns(PassbookProvider)

View File

@ -1,10 +1,10 @@
"""passbook adapter""" """passbook adapter"""
import requests import requests
from allauth.socialaccount import app_settings from allauth.socialaccount import app_settings
from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter, from allauth.socialaccount.providers.oauth2.views import (OAuth2Adapter,
OAuth2CallbackView, OAuth2CallbackView,
OAuth2LoginView) OAuth2LoginView)
from allauth_passbook.provider import PassbookProvider from allauth_passbook.provider import PassbookProvider

View File

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

View File

@ -1,2 +1,2 @@
"""passbook""" """passbook"""
__version__ = '0.0.2-alpha' __version__ = '0.0.8-alpha'

View File

@ -1,2 +1,2 @@
"""passbook admin""" """passbook admin"""
__version__ = '0.0.2-alpha' __version__ = '0.0.8-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""" """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 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.groups import GroupViewSet
from passbook.admin.api.v1.users import UserViewSet
router = DefaultRouter() 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

@ -8,3 +8,4 @@ class PassbookAdminConfig(AppConfig):
name = 'passbook.admin' name = 'passbook.admin'
label = 'passbook_admin' label = 'passbook_admin'
mountpoint = 'administration/' mountpoint = 'administration/'
verbose_name = 'passbook Admin'

View File

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

View File

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

View File

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

View File

@ -9,7 +9,9 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Applications" %}</h1> <h1><span class="pficon-applications"></span> {% 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"> <a href="{% url 'passbook_admin:application-create' %}" class="btn btn-primary">
{% trans 'Create...' %} {% trans 'Create...' %}
</a> </a>
@ -28,8 +30,10 @@
<td>{{ application.name }}</td> <td>{{ application.name }}</td>
<td>{{ application.provider }}</td> <td>{{ application.provider }}</td>
<td> <td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:application-update' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> <a class="btn btn-default btn-sm"
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:application-delete' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> href="{% url 'passbook_admin:application-update' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:application-delete' pk=application.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -8,9 +8,8 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container"> <h1><span class="pficon-catalog"></span> {% trans "Audit Log" %}</h1>
<h1>{% trans "Audit Log" %}</h1> <div id="pf-list-standard" class="list-group list-view-pf list-view-pf-view">
<div id="pf-list-standard" class="list-group list-view-pf list-view-pf-view">
{% for entry in object_list %} {% for entry in object_list %}
<div class="list-group-item"> <div class="list-group-item">
<div class="list-view-pf-main-info"> <div class="list-view-pf-main-info">
@ -23,6 +22,7 @@
{{ entry.action }} {{ entry.action }}
</div> </div>
<div class="list-group-item-text"> <div class="list-group-item-text">
{{ entry.context }}
</div> </div>
</div> </div>
<div class="list-view-pf-additional-info"> <div class="list-view-pf-additional-info">
@ -30,20 +30,23 @@
<span class="pficon pficon-user"></span> <span class="pficon pficon-user"></span>
<strong>{{ entry.user }}</strong> <strong>{{ entry.user }}</strong>
</div> </div>
<div class="list-view-pf-additional-info-item">
<span class="pficon pficon-screen"></span>
<strong>{{ entry.request_ip }}</strong>
</div>
<div class="list-view-pf-additional-info-item"> <div class="list-view-pf-additional-info-item">
<span class="pficon pficon-cluster"></span> <span class="pficon pficon-cluster"></span>
<strong>{{ entry.app|default:'-' }}</strong> <strong>{{ entry.app|default:'-' }}</strong>
</div> </div>
<div class="list-view-pf-additional-info-item">
<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>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
// Row Checkbox Selection // 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' %}"> <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> <a href="{% url 'passbook_admin:providers' %}">{% trans 'Providers' %}</a>
</li> </li>
<li class="{% is_active 'passbook_admin:rules' 'passbook_admin:rule-create' 'passbook_admin:rule-update' 'passbook_admin:rule-delete' 'passbook_admin:rule-test' %}"> <li class="{% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
<a href="{% url 'passbook_admin:rules' %}">{% trans 'Rules' %}</a> <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>
<li class="{% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}"> <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> <a href="{% url 'passbook_admin:invitations' %}">{% trans 'Invitations' %}</a>

View File

@ -0,0 +1,62 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load utils %}
{% load admin_reflection %}
{% block title %}
{% title %}
{% endblock %}
{% block content %}
<div class="container">
<h1><span class="pficon-plugged"></span> {% 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

@ -9,7 +9,9 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Invitations" %}</h1> <h1><span class="pficon-migration"></span> {% 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"> <a href="{% url 'passbook_admin:invitation-create' %}" class="btn btn-primary">
{% trans 'Create...' %} {% trans 'Create...' %}
</a> </a>
@ -26,9 +28,12 @@
{% for invitation in object_list %} {% for invitation in object_list %}
<tr> <tr>
<td>{{ invitation.expires|default:"Never" }}</td> <td>{{ invitation.expires|default:"Never" }}</td>
<td><pre>{{ invitation.link }}</pre></td>
<td> <td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:invitation-delete' pk=invitation.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> <pre>{{ invitation.link }}</pre>
</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:invitation-delete' pk=invitation.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -7,11 +7,18 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Applications' %}</a> <a href="{% url 'passbook_admin:applications' %}">
<span class="pficon-applications"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Applications' %}
</a>
</h2> </h2>
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <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> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:applications' %}">
<span class="pficon pficon-ok"></span>{{ application_count }}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>
@ -19,11 +26,18 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Providers' %}</a> <a href="{% url 'passbook_admin:sources' %}">
<span class="pficon-resource-pool"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Sources' %}
</a>
</h2> </h2>
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <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> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:sources' %}">
<span class="pficon pficon-ok"></span>{{ source_count }}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>
@ -31,11 +45,18 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Rules' %}</a> <a href="{% url 'passbook_admin:providers' %}">
<span class="pficon-integration"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Providers' %}
</a>
</h2> </h2>
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <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> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:providers' %}">
<span class="pficon pficon-ok"></span>{{ provider_count }}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>
@ -43,11 +64,75 @@
<div class="col-xs-6 col-sm-2 col-md-2"> <div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status"> <div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title"> <h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Users' %}</a> <a href="{% url 'passbook_admin:factors' %}">
<span class="pficon-plugged"></span>
<span class="card-pf-aggregate-status-count"></span> {% trans 'Factors' %}
</a>
</h2> </h2>
<div class="card-pf-body"> <div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications"> <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> <span class="card-pf-aggregate-status-notification">
<a href="{% url 'passbook_admin:factors' %}">
<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="{% url 'passbook_admin:policies' %}">
<span class="pficon-infrastructure"></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="{% url 'passbook_admin:policies' %}">
<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="{% url 'passbook_admin:invitations' %}">
<span class="pficon-migration"></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="{% url 'passbook_admin:invitations' %}">
<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="{% url 'passbook_admin:users' %}">
<span class="pficon-users"></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="{% url 'passbook_admin:users' %}">
<span class="pficon pficon-ok"></span>{{ user_count }}
</a>
</span>
</p> </p>
</div> </div>
</div> </div>

View File

@ -0,0 +1,54 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load utils %}
{% block title %}
{% title %}
{% endblock %}
{% block content %}
<div class="container">
<h1><span class="pficon-infrastructure"></span> {% 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...' %}
<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:policy-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 policy in object_list %}
<tr>
<td>{{ policy.name }}</td>
<td>{{ policy|fieldtype }}</td>
<td>
<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 %}

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

@ -10,7 +10,9 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Providers" %}</h1> <h1><span class="pficon-integration"></span> {% trans "Providers" %}</h1>
<span>{% trans "Authentication Protocol Provider, used as Protocol behind an Application." %}</span>
<hr>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown"> <button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %} {% trans 'Create...' %}
@ -18,7 +20,8 @@
</button> </button>
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown"> <ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %} {% for type, name in types.items %}
<li role="presentation"><a role="menuitem" tabindex="-1" href="{% url 'passbook_admin:provider-create' %}?type={{ type }}">{{ name }}</a></li> <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:provider-create' %}?type={{ type }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -37,11 +40,14 @@
<td>{{ provider.name }}</td> <td>{{ provider.name }}</td>
<td>{{ provider|fieldtype }}</td> <td>{{ provider|fieldtype }}</td>
<td> <td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:provider-update' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> <a class="btn btn-default btn-sm"
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:provider-delete' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> href="{% url 'passbook_admin:provider-update' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:provider-delete' pk=provider.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
{% get_links provider as links %} {% get_links provider as links %}
{% for name, href in links.items %} {% for name, href in links.items %}
<a class="btn btn-default btn-sm" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a> <a class="btn btn-default btn-sm"
href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>

View File

@ -1,48 +0,0 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load utils %}
{% block title %}
{% title %}
{% endblock %}
{% block content %}
<div class="container">
<h1>{% trans "Rules" %}</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:rule-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 rule in object_list %}
<tr>
<td>{{ rule.name }}</td>
<td>{{ rule|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>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

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,7 +6,9 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Sources" %}</h1> <h1><span class="pficon-resource-pool"></span> {% 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"> <div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown"> <button class="btn btn-primary dropdown-toggle" type="button" id="createDropdown" data-toggle="dropdown">
{% trans 'Create...' %} {% trans 'Create...' %}
@ -14,7 +16,8 @@
</button> </button>
<ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown"> <ul class="dropdown-menu" role="menu" aria-labelledby="createDropdown">
{% for type, name in types.items %} {% 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> <li role="presentation"><a role="menuitem" tabindex="-1"
href="{% url 'passbook_admin:source-create' %}?type={{ type }}">{{ name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -24,6 +27,7 @@
<tr> <tr>
<th>{% trans 'Name' %}</th> <th>{% trans 'Name' %}</th>
<th>{% trans 'Class' %}</th> <th>{% trans 'Class' %}</th>
<th>{% trans 'Additional Info' %}</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -32,12 +36,16 @@
<tr> <tr>
<td>{{ source.name }}</td> <td>{{ source.name }}</td>
<td>{{ source|fieldtype }}</td> <td>{{ source|fieldtype }}</td>
<td>{{ source.additional_info }}</td>
<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"
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:source-delete' pk=source.uuid %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> 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 %} {% get_links source as links %}
{% for name, href in links %} {% for name, href in links %}
<a class="btn btn-default btn-sm" href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a> <a class="btn btn-default btn-sm"
href="{{ href }}?back={{ request.get_full_path }}">{% trans name %}</a>
{% endfor %} {% endfor %}
</td> </td>
</tr> </tr>

View File

@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<h1>{% trans "Users" %}</h1> <h1><span class="pficon-users"></span> {% trans "Users" %}</h1>
<hr> <hr>
<table class="table table-striped table-bordered"> <table class="table table-striped table-bordered">
<thead> <thead>
@ -27,8 +27,10 @@
<td>{{ user.is_active }}</td> <td>{{ user.is_active }}</td>
<td>{{ user.last_login }}</td> <td>{{ user.last_login }}</td>
<td> <td>
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a> <a class="btn btn-default btn-sm"
<a class="btn btn-default btn-sm" href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a> href="{% url 'passbook_admin:user-update' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:user-delete' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -5,3 +5,7 @@
{% block above_form %} {% block above_form %}
<h1>{% trans 'Create' %}</h1> <h1>{% trans 'Create' %}</h1>
{% endblock %} {% endblock %}
{% block action %}
{% trans 'Create' %}
{% endblock %}

View File

@ -11,7 +11,7 @@
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% include 'partials/form.html' with form=form %} {% include 'partials/form.html' with form=form %}
<a class="btn btn-default" href="{% back %}">{% trans "Cancel" %}</a> <a class="btn btn-default" href="{% back %}">{% trans "Cancel" %}</a>
<input type="submit" class="btn btn-primary" value="{% trans 'Create' %}" /> <input type="submit" class="btn btn-primary" value="{% block action %}{% endblock %}" />
</form> </form>
</div> </div>
</div> </div>

View File

@ -5,3 +5,7 @@
{% block above_form %} {% block above_form %}
<h1>{% trans 'Update' %}</h1> <h1>{% trans 'Update' %}</h1>
{% endblock %} {% endblock %}
{% block action %}
{% trans 'Update' %}
{% endblock %}

View File

@ -1,12 +1,9 @@
"""passbook URL Configuration""" """passbook URL Configuration"""
from django.urls import include, path 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 = [ urlpatterns = [
path('', overview.AdministrationOverviewView.as_view(), name='overview'), path('', overview.AdministrationOverviewView.as_view(), name='overview'),
@ -24,12 +21,12 @@ urlpatterns = [
path('sources/create/', sources.SourceCreateView.as_view(), name='source-create'), 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>/update/', sources.SourceUpdateView.as_view(), name='source-update'),
path('sources/<uuid:pk>/delete/', sources.SourceDeleteView.as_view(), name='source-delete'), path('sources/<uuid:pk>/delete/', sources.SourceDeleteView.as_view(), name='source-delete'),
# Rules # Policies
path('rules/', rules.RuleListView.as_view(), name='rules'), path('policies/', policy.PolicyListView.as_view(), name='policies'),
path('rules/create/', rules.RuleCreateView.as_view(), name='rule-create'), path('policies/create/', policy.PolicyCreateView.as_view(), name='policy-create'),
path('rules/<uuid:pk>/update/', rules.RuleUpdateView.as_view(), name='rule-update'), path('policies/<uuid:pk>/update/', policy.PolicyUpdateView.as_view(), name='policy-update'),
path('rules/<uuid:pk>/delete/', rules.RuleDeleteView.as_view(), name='rule-delete'), path('policies/<uuid:pk>/delete/', policy.PolicyDeleteView.as_view(), name='policy-delete'),
path('rules/<uuid:pk>/test/', rules.RuleTestView.as_view(), name='rule-test'), path('policies/<uuid:pk>/test/', policy.PolicyTestView.as_view(), name='policy-test'),
# Providers # Providers
path('providers/', providers.ProviderListView.as_view(), name='providers'), path('providers/', providers.ProviderListView.as_view(), name='providers'),
path('providers/create/', path('providers/create/',
@ -38,6 +35,14 @@ urlpatterns = [
providers.ProviderUpdateView.as_view(), name='provider-update'), providers.ProviderUpdateView.as_view(), name='provider-update'),
path('providers/<int:pk>/delete/', path('providers/<int:pk>/delete/',
providers.ProviderDeleteView.as_view(), name='provider-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 # Invitations
path('invitations/', invitations.InvitationListView.as_view(), name='invitations'), path('invitations/', invitations.InvitationListView.as_view(), name='invitations'),
path('invitations/create/', path('invitations/create/',
@ -56,6 +61,5 @@ urlpatterns = [
# Groups # Groups
path('groups/', groups.GroupListView.as_view(), name='groups'), path('groups/', groups.GroupListView.as_view(), name='groups'),
# API # API
path('api/', schema_view), path('api/', include('passbook.admin.api.urls'))
path('api/v1/', include('passbook.admin.api.v1.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)
factor_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_type)
kwargs['type'] = model._meta.verbose_name
return kwargs
def get_form_class(self):
factor_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(Factor) if x.__name__ == factor_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):
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 Factor.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
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 django.views.generic import TemplateView
from passbook.admin.mixins import AdminRequiredMixin 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): class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
@ -12,7 +13,10 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs['application_count'] = len(Application.objects.all()) 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['user_count'] = len(User.objects.all())
kwargs['provider_count'] = len(Provider.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) 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 django.views.generic import DeleteView, ListView, UpdateView
from passbook.admin.mixins import AdminRequiredMixin 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 from passbook.core.models import User

View File

@ -1,2 +1,2 @@
"""passbook api""" """passbook api"""
__version__ = '0.0.1-alpha' __version__ = '0.0.8-alpha'

View File

@ -9,3 +9,4 @@ class PassbookAPIConfig(AppConfig):
name = 'passbook.api' name = 'passbook.api'
label = 'passbook_api' label = 'passbook_api'
mountpoint = 'api/' mountpoint = 'api/'
verbose_name = 'passbook API'

View File

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

View File

@ -1,2 +1,2 @@
"""passbook audit Header""" """passbook audit Header"""
__version__ = '0.0.2-alpha' __version__ = '0.0.8-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 import uuid
@ -20,13 +20,32 @@ class Migration(migrations.Migration):
name='AuditEntry', name='AuditEntry',
fields=[ fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('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)), ('date', models.DateTimeField(auto_now_add=True)),
('app', models.TextField()), ('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)), ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
], ],
options={ 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""" """passbook audit models"""
from datetime import timedelta from datetime import timedelta
from json import dumps, loads
from logging import getLogger from logging import getLogger
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from ipware import get_client_ip from ipware import get_client_ip
from reversion import register
from passbook.lib.models import CreatedUpdatedModel, UUIDModel from passbook.lib.models import CreatedUpdatedModel, UUIDModel
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@register()
class AuditEntry(UUIDModel): class AuditEntry(UUIDModel):
"""An individual audit log entry""" """An individual audit log entry"""
@ -45,18 +43,10 @@ class AuditEntry(UUIDModel):
action = models.TextField(choices=ACTIONS) action = models.TextField(choices=ACTIONS)
date = models.DateTimeField(auto_now_add=True) date = models.DateTimeField(auto_now_add=True)
app = models.TextField() app = models.TextField()
_context = models.TextField() context = JSONField(default=dict, blank=True)
_context_cache = None
request_ip = models.GenericIPAddressField() request_ip = models.GenericIPAddressField()
created = models.DateTimeField(auto_now_add=True) 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 @staticmethod
def create(action, request, **kwargs): def create(action, request, **kwargs):
"""Create AuditEntry from arguments""" """Create AuditEntry from arguments"""
@ -69,7 +59,7 @@ class AuditEntry(UUIDModel):
user=user, user=user,
# User 255.255.255.255 as fallback if IP cannot be determined # User 255.255.255.255 as fallback if IP cannot be determined
request_ip=client_ip or '255.255.255.255', 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) LOGGER.debug("Logged %s from %s (%s)", action, request.user, client_ip)
return entry return entry
@ -94,6 +84,8 @@ class LoginAttempt(CreatedUpdatedModel):
@staticmethod @staticmethod
def attempt(target_uid, request): def attempt(target_uid, request):
"""Helper function to create attempt or count up existing one""" """Helper function to create attempt or count up existing one"""
if not target_uid:
return
client_ip, _ = get_client_ip(request) client_ip, _ = get_client_ip(request)
# Since we can only use 254 chars for target_uid, truncate target_uid. # Since we can only use 254 chars for target_uid, truncate target_uid.
target_uid = target_uid[:254] target_uid = target_uid[:254]

View File

@ -1,2 +1,2 @@
"""passbook captcha_factor Header""" """passbook captcha_factor Header"""
__version__ = '0.0.2-alpha' __version__ = '0.0.8-alpha'

View File

@ -2,8 +2,25 @@
from captcha.fields import ReCaptchaField from captcha.fields import ReCaptchaField
from django import forms from django import forms
from passbook.captcha_factor.models import CaptchaFactor
from passbook.core.forms.factors import GENERAL_FIELDS
class CaptchaForm(forms.Form): class CaptchaForm(forms.Form):
"""passbook captcha factor form""" """passbook captcha factor form"""
captcha = ReCaptchaField() 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""" """passbook core"""
__version__ = '0.0.2-alpha' __version__ = '0.0.8-alpha'

View File

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

View File

View File

@ -1,26 +1,41 @@
"""passbook multi-factor authentication engine""" """passbook multi-factor authentication engine"""
from logging import getLogger from logging import getLogger
from django.contrib import messages
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.shortcuts import redirect
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import FormView from django.views.generic import FormView
from passbook.core.auth.factor import AuthenticationFactor from passbook.core.auth.factor import AuthenticationFactor
from passbook.core.auth.mfa import MultiFactorAuthenticator from passbook.core.auth.view import AuthenticationView
from passbook.core.forms.authentication import AuthenticationBackendFactorForm from passbook.core.forms.authentication import PasswordFactorForm
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
class AuthenticationBackendFactor(FormView, AuthenticationFactor): class PasswordFactor(FormView, AuthenticationFactor):
"""Authentication factor which authenticates against django's AuthBackend""" """Authentication factor which authenticates against django's AuthBackend"""
form_class = AuthenticationBackendFactorForm form_class = PasswordFactorForm
template_name = 'login/factors/backend.html' template_name = 'login/factors/backend.html'
def get_context_data(self, **kwargs):
kwargs['show_password_forget_notice'] = CONFIG.y('passbook.password_reset.enabled')
return super().get_context_data(**kwargs)
def get(self, request, *args, **kwargs):
if 'password-forgotten' in request.GET:
# TODO: Save nonce key in database for password reset
# TODO: Send email to user
self.authenticator.cleanup()
messages.success(request, _('Check your E-Mails for a password reset link.'))
return redirect('passbook_core:auth-login')
return super().get(request, *args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
"""Authenticate against django's authentication backend""" """Authenticate against django's authentication backend"""
uid_fields = CONFIG.y('passbook.uid_fields') uid_fields = CONFIG.y('passbook.uid_fields')
@ -34,7 +49,7 @@ class AuthenticationBackendFactor(FormView, AuthenticationFactor):
if user: if user:
# User instance returned from authenticate() has .backend property set # User instance returned from authenticate() has .backend property set
self.authenticator.pending_user = user 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() return self.authenticator.user_ok()
# No user was found -> invalid credentials # No user was found -> invalid credentials
LOGGER.debug("Invalid credentials") LOGGER.debug("Invalid credentials")

View File

@ -1,20 +1,20 @@
"""passbook multi-factor authentication engine""" """passbook multi-factor authentication engine"""
from logging import getLogger from logging import getLogger
from django.conf import settings
from django.contrib.auth import login from django.contrib.auth import login
from django.contrib.auth.mixins import UserPassesTestMixin from django.contrib.auth.mixins import UserPassesTestMixin
from django.shortcuts import get_object_or_404, redirect, reverse from django.shortcuts import get_object_or_404, redirect, reverse
from django.views.generic import View 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.core.views.utils import PermissionDeniedView
from passbook.lib.utils.reflection import class_to_path, path_to_class from passbook.lib.utils.reflection import class_to_path, path_to_class
from passbook.lib.utils.urls import is_url_absolute
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
class MultiFactorAuthenticator(UserPassesTestMixin, View): class AuthenticationView(UserPassesTestMixin, View):
"""Wizard-like Multi-factor authenticator""" """Wizard-like Multi-factor authenticator"""
SESSION_FACTOR = 'passbook_factor' SESSION_FACTOR = 'passbook_factor'
@ -25,67 +25,84 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
pending_user = None pending_user = None
pending_factors = [] pending_factors = []
factors = settings.AUTHENTICATION_FACTORS.copy() _current_factor_class = None
_current_factor = None current_factor = None
# Allow only not authenticated users to login # Allow only not authenticated users to login
def test_func(self): def test_func(self):
return self.request.user.is_authenticated is False return self.request.user.is_authenticated is False
def handle_no_permission(self): def handle_no_permission(self):
# Function from UserPassesTestMixin
if 'next' in self.request.GET: if 'next' in self.request.GET:
return redirect(self.request.GET.get('next')) return redirect(self.request.GET.get('next'))
return redirect(reverse('passbook_core:overview')) return redirect(reverse('passbook_core:overview'))
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
# Extract pending user from session (only remember uid) # 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( 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: else:
# No Pending user, redirect to login screen # No Pending user, redirect to login screen
return redirect(reverse('passbook_core:auth-login')) return redirect(reverse('passbook_core:auth-login'))
# Write pending factors to session # Write pending factors to session
if MultiFactorAuthenticator.SESSION_PENDING_FACTORS in request.session: if AuthenticationView.SESSION_PENDING_FACTORS in request.session:
self.pending_factors = request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS] self.pending_factors = request.session[AuthenticationView.SESSION_PENDING_FACTORS]
else: 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 # Read and instantiate factor from session
factor_class = None factor_uuid, factor_class = None, None
if MultiFactorAuthenticator.SESSION_FACTOR not in request.session: if AuthenticationView.SESSION_FACTOR not in request.session:
factor_class = self.pending_factors[0] # 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: 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) factor = path_to_class(factor_class)
self._current_factor = factor(self) self._current_factor_class = factor(self)
self._current_factor.request = request self._current_factor_class.pending_user = self.pending_user
self._current_factor_class.request = request
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
"""pass get request to current factor""" """pass get request to current factor"""
LOGGER.debug("Passing GET to %s", class_to_path(self._current_factor.__class__)) LOGGER.debug("Passing GET to %s", class_to_path(self._current_factor_class.__class__))
return self._current_factor.get(request, *args, **kwargs) return self._current_factor_class.get(request, *args, **kwargs)
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
"""pass post request to current factor""" """pass post request to current factor"""
LOGGER.debug("Passing POST to %s", class_to_path(self._current_factor.__class__)) LOGGER.debug("Passing POST to %s", class_to_path(self._current_factor_class.__class__))
return self._current_factor.post(request, *args, **kwargs) return self._current_factor_class.post(request, *args, **kwargs)
def user_ok(self): def user_ok(self):
"""Redirect to next Factor""" """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 # Remove passed factor from pending factors
if class_to_path(self._current_factor.__class__) in self.pending_factors: current_factor_tuple = (self.current_factor.uuid.hex,
self.pending_factors.remove(class_to_path(self._current_factor.__class__)) 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 next_factor = None
if self.pending_factors: if self.pending_factors:
next_factor = self.pending_factors.pop() next_factor = self.pending_factors.pop()
self.request.session[MultiFactorAuthenticator.SESSION_PENDING_FACTORS] = \ self.request.session[AuthenticationView.SESSION_PENDING_FACTORS] = \
self.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) 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 # User passed all factors
LOGGER.debug("User passed all factors, logging in") LOGGER.debug("User passed all factors, logging in")
return self._user_passed() return self._user_passed()
@ -94,26 +111,30 @@ class MultiFactorAuthenticator(UserPassesTestMixin, View):
"""Show error message, user cannot login. """Show error message, user cannot login.
This should only be shown if user authenticated successfully, but is disabled/locked/etc""" This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
LOGGER.debug("User invalid") LOGGER.debug("User invalid")
return redirect(reverse('passbook_core:mfa-denied')) self.cleanup()
return redirect(reverse('passbook_core:auth-denied'))
def _user_passed(self): def _user_passed(self):
"""User Successfully passed all factors""" """User Successfully passed all factors"""
# user = authenticate(request=self.request, ) # 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) login(self.request, self.pending_user, backend=backend)
LOGGER.debug("Logged in user %s", self.pending_user) LOGGER.debug("Logged in user %s", self.pending_user)
# Cleanup # Cleanup
self._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')) return redirect(reverse('passbook_core:overview'))
def _cleanup(self): def cleanup(self):
"""Remove temporary data from session""" """Remove temporary data from session"""
session_keys = ['SESSION_FACTOR', 'SESSION_PENDING_FACTORS', session_keys = [self.SESSION_FACTOR, self.SESSION_PENDING_FACTORS,
'SESSION_PENDING_USER', 'SESSION_USER_BACKEND', ] self.SESSION_PENDING_USER, self.SESSION_USER_BACKEND, ]
for key in session_keys: for key in session_keys:
if key in self.request.session: if key in self.request.session:
del self.request.session[key] del self.request.session[key]
LOGGER.debug("Cleaned up sessions") LOGGER.debug("Cleaned up sessions")
class MFAPermissionDeniedView(PermissionDeniedView): class FactorPermissionDeniedView(PermissionDeniedView):
"""User could not be authenticated""" """User could not be authenticated"""

View File

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

View File

@ -1,5 +1,6 @@
"""passbook Core Application forms""" """passbook Core Application forms"""
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _
from passbook.core.models import Application, Provider from passbook.core.models import Application, Provider
@ -7,15 +8,20 @@ from passbook.core.models import Application, Provider
class ApplicationForm(forms.ModelForm): class ApplicationForm(forms.ModelForm):
"""Application Form""" """Application Form"""
provider = forms.ModelChoiceField(queryset=Provider.objects.all().select_subclasses()) provider = forms.ModelChoiceField(queryset=Provider.objects.all().select_subclasses(),
required=False)
class Meta: class Meta:
model = Application model = Application
fields = ['name', 'slug', 'launch_url', 'icon_url', fields = ['name', 'slug', 'launch_url', 'icon_url',
'rules', 'provider', 'skip_authorization'] 'policies', 'provider', 'skip_authorization']
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
'launch_url': forms.TextInput(), 'launch_url': forms.TextInput(),
'icon_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')})) uid_field = forms.CharField(widget=forms.TextInput(attrs={'placeholder': _('UID')}))
remember_me = forms.BooleanField(required=False) 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): def clean_uid_field(self):
"""Validate uid_field after EmailValidator if 'email' is the only selected uid_fields""" """Validate uid_field after EmailValidator if 'email' is the only selected uid_fields"""
if CONFIG.y('passbook.uid_fields') == ['email']: if CONFIG.y('passbook.uid_fields') == ['email']:
validate_email(self.cleaned_data.get('uid_field')) validate_email(self.cleaned_data.get('uid_field'))
return 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): class SignUpForm(forms.Form):
"""SignUp Form""" """SignUp Form"""
@ -74,6 +75,16 @@ class SignUpForm(forms.Form):
def clean_password_repeat(self): def clean_password_repeat(self):
"""Check if Password adheres to filter and if passwords matche""" """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? # TODO: Password policy? Via Plugin? via Policy?
# return check_password(self) # return check_password(self)
return self.cleaned_data.get('password_repeat') return self.cleaned_data.get('password_repeat')
class PasswordFactorForm(forms.Form):
"""Password authentication form"""
password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': _('Password')}))

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', 'password_policies']
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 import forms
from django.utils.translation import gettext as _ 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', ] GENERAL_FIELDS = ['name', 'action', 'negate', 'order', ]
class FieldMatcherRuleForm(forms.ModelForm): class FieldMatcherPolicyForm(forms.ModelForm):
"""FieldMatcherRule Form""" """FieldMatcherPolicy Form"""
class Meta: class Meta:
model = FieldMatcherRule model = FieldMatcherPolicy
fields = GENERAL_FIELDS + ['user_field', 'match_action', 'value', ] fields = GENERAL_FIELDS + ['user_field', 'match_action', 'value', ]
widgets = { widgets = {
'name': forms.TextInput(), 'name': forms.TextInput(),
@ -20,12 +20,12 @@ class FieldMatcherRuleForm(forms.ModelForm):
} }
class WebhookRuleForm(forms.ModelForm): class WebhookPolicyForm(forms.ModelForm):
"""WebhookRuleForm Form""" """WebhookPolicyForm Form"""
class Meta: class Meta:
model = WebhookRule model = WebhookPolicy
fields = GENERAL_FIELDS + ['url', 'method', 'json_body', 'json_headers', fields = GENERAL_FIELDS + ['url', 'method', 'json_body', 'json_headers',
'result_jsonpath', 'result_json_value', ] 'result_jsonpath', 'result_json_value', ]
widgets = { widgets = {
@ -37,12 +37,12 @@ class WebhookRuleForm(forms.ModelForm):
} }
class DebugRuleForm(forms.ModelForm): class DebugPolicyForm(forms.ModelForm):
"""DebugRuleForm Form""" """DebugPolicyForm Form"""
class Meta: class Meta:
model = DebugRule model = DebugPolicy
fields = GENERAL_FIELDS + ['result', 'wait_min', 'wait_max'] fields = GENERAL_FIELDS + ['result', 'wait_min', 'wait_max']
widgets = { widgets = {
'name': forms.TextInput(), '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,12 +1,13 @@
# 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
from django.conf import settings
import django.contrib.auth.models import django.contrib.auth.models
import django.contrib.auth.validators import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
import uuid from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
@ -67,13 +68,7 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='Provider', name='Policy',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='Rule',
fields=[ fields=[
('created', models.DateField(auto_now_add=True)), ('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)), ('last_updated', models.DateTimeField(auto_now=True)),
@ -88,7 +83,7 @@ class Migration(migrations.Migration):
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name='RuleModel', name='PolicyModel',
fields=[ fields=[
('created', models.DateField(auto_now_add=True)), ('created', models.DateField(auto_now_add=True)),
('last_updated', models.DateTimeField(auto_now=True)), ('last_updated', models.DateTimeField(auto_now=True)),
@ -98,6 +93,12 @@ class Migration(migrations.Migration):
'abstract': False, 'abstract': False,
}, },
), ),
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel( migrations.CreateModel(
name='UserSourceConnection', name='UserSourceConnection',
fields=[ fields=[
@ -110,51 +111,82 @@ class Migration(migrations.Migration):
migrations.CreateModel( migrations.CreateModel(
name='Application', name='Application',
fields=[ 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()), ('name', models.TextField()),
('slug', models.SlugField()), ('slug', models.SlugField()),
('launch_url', models.URLField(blank=True, null=True)), ('launch_url', models.URLField(blank=True, null=True)),
('icon_url', models.TextField(blank=True, null=True)), ('icon_url', models.TextField(blank=True, null=True)),
('skip_authorization', models.BooleanField(default=False)), ('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={ options={
'abstract': False, 'abstract': False,
}, },
bases=('passbook_core.rulemodel',), bases=('passbook_core.policymodel',),
), ),
migrations.CreateModel( migrations.CreateModel(
name='DebugRule', name='DebugPolicy',
fields=[ 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)), ('result', models.BooleanField(default=False)),
('wait_min', models.IntegerField(default=5)), ('wait_min', models.IntegerField(default=5)),
('wait_max', models.IntegerField(default=30)), ('wait_max', models.IntegerField(default=30)),
], ],
options={ options={
'verbose_name': 'Debug Rule', 'verbose_name': 'Debug Policy',
'verbose_name_plural': 'Debug Rules', 'verbose_name_plural': 'Debug Policys',
}, },
bases=('passbook_core.rule',), bases=('passbook_core.policy',),
), ),
migrations.CreateModel( migrations.CreateModel(
name='FieldMatcherRule', name='Factor',
fields=[ 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')), ('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')),
('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')])), ('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)), ('match_action', models.CharField(choices=[('startswith', 'Starts with'), ('endswith', 'Ends with'), ('endswith', 'Contains'), ('regexp', 'Regexp'), ('exact', 'Exact')], max_length=50)),
('value', models.TextField()), ('value', models.TextField()),
], ],
options={ options={
'verbose_name': 'Field matcher Rule', 'verbose_name': 'Field matcher Policy',
'verbose_name_plural': 'Field matcher Rules', '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( migrations.CreateModel(
name='Source', name='Source',
fields=[ 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()), ('name', models.TextField()),
('slug', models.SlugField()), ('slug', models.SlugField()),
('enabled', models.BooleanField(default=True)), ('enabled', models.BooleanField(default=True)),
@ -162,12 +194,12 @@ class Migration(migrations.Migration):
options={ options={
'abstract': False, 'abstract': False,
}, },
bases=('passbook_core.rulemodel',), bases=('passbook_core.policymodel',),
), ),
migrations.CreateModel( migrations.CreateModel(
name='WebhookRule', name='WebhookPolicy',
fields=[ 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()), ('url', models.URLField()),
('method', models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PATCH', 'PATCH'), ('DELETE', 'DELETE'), ('PUT', 'PUT')], max_length=10)), ('method', models.CharField(choices=[('GET', 'GET'), ('POST', 'POST'), ('PATCH', 'PATCH'), ('DELETE', 'DELETE'), ('PUT', 'PUT')], max_length=10)),
('json_body', models.TextField()), ('json_body', models.TextField()),
@ -176,15 +208,15 @@ class Migration(migrations.Migration):
('result_json_value', models.TextField()), ('result_json_value', models.TextField()),
], ],
options={ options={
'verbose_name': 'Webhook Rule', 'verbose_name': 'Webhook Policy',
'verbose_name_plural': 'Webhook Rules', 'verbose_name_plural': 'Webhook Policys',
}, },
bases=('passbook_core.rule',), bases=('passbook_core.policy',),
), ),
migrations.AddField( migrations.AddField(
model_name='rulemodel', model_name='policymodel',
name='rules', name='policies',
field=models.ManyToManyField(blank=True, to='passbook_core.Rule'), field=models.ManyToManyField(blank=True, to='passbook_core.Policy'),
), ),
migrations.AddField( migrations.AddField(
model_name='user', model_name='user',

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

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

View File

@ -5,13 +5,15 @@ from random import SystemRandom
from time import sleep from time import sleep
from uuid import uuid4 from uuid import uuid4
import reversion
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.timezone import now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from passbook.core.signals import password_changed
from passbook.lib.models import CreatedUpdatedModel, UUIDModel from passbook.lib.models import CreatedUpdatedModel, UUIDModel
LOGGER = getLogger(__name__) LOGGER = getLogger(__name__)
@ -38,8 +40,13 @@ class User(AbstractUser):
sources = models.ManyToManyField('Source', through='UserSourceConnection') sources = models.ManyToManyField('Source', through='UserSourceConnection')
applications = models.ManyToManyField('Application') applications = models.ManyToManyField('Application')
groups = models.ManyToManyField('Group') groups = models.ManyToManyField('Group')
password_change_date = models.DateTimeField(auto_now_add=True)
def set_password(self, password):
password_changed.send(sender=self, user=self, password=password)
self.password_change_date = now()
return super().set_password(password)
@reversion.register()
class Provider(models.Model): class Provider(models.Model):
"""Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application""" """Application-independent Provider instance. For example SAML2 Remote, OAuth2 Application"""
@ -51,20 +58,81 @@ class Provider(models.Model):
return getattr(self, 'name') return getattr(self, 'name')
return super().__str__() return super().__str__()
class RuleModel(UUIDModel, CreatedUpdatedModel): class PolicyModel(UUIDModel, CreatedUpdatedModel):
"""Base model which can have rules applied to it""" """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: def passes(self, user: User) -> bool:
"""Return true if user passes, otherwise False or raise Exception""" """Return true if user passes, otherwise False or raise Exception"""
for rule in self.rules: for policy in self.policies.all():
if not rule.passes(user): if not policy.passes(user):
return False return False
return True return True
@reversion.register() class Factor(PolicyModel):
class Application(RuleModel): """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())
password_policies = models.ManyToManyField('Policy', blank=True)
type = 'passbook.core.auth.factors.password.PasswordFactor'
form = 'passbook.core.forms.factors.PasswordFactorForm'
def has_user_settings(self):
return _('Change Password'), 'pficon-key', 'passbook_core:user-change-password'
def password_passes(self, user: User) -> bool:
"""Return true if user's password passes, otherwise False or raise Exception"""
for policy in self.policies.all():
if not policy.passes(user):
return False
return True
def __str__(self):
return "Password Factor %s" % self.slug
class Meta:
verbose_name = _('Password Factor')
verbose_name_plural = _('Password Factors')
class DummyFactor(Factor):
"""Dummy factor, mostly used to debug"""
type = 'passbook.core.auth.factors.dummy.DummyFactor'
form = 'passbook.core.forms.factors.DummyFactorForm'
def __str__(self):
return "Dummy Factor %s" % self.slug
class Meta:
verbose_name = _('Dummy Factor')
verbose_name_plural = _('Dummy Factors')
class Application(PolicyModel):
"""Every Application which uses passbook for authentication/identification/authorization """Every Application which uses passbook for authentication/identification/authorization
needs an Application record. Other authentication types can subclass this Model to needs an Application record. Other authentication types can subclass this Model to
add custom fields and other properties""" add custom fields and other properties"""
@ -73,7 +141,7 @@ class Application(RuleModel):
slug = models.SlugField() slug = models.SlugField()
launch_url = models.URLField(null=True, blank=True) launch_url = models.URLField(null=True, blank=True)
icon_url = models.TextField(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) default=None, on_delete=models.SET_DEFAULT)
skip_authorization = models.BooleanField(default=False) skip_authorization = models.BooleanField(default=False)
@ -81,14 +149,13 @@ class Application(RuleModel):
def user_is_authorized(self, user: User) -> bool: def user_is_authorized(self, user: User) -> bool:
"""Check if user is authorized to use this application""" """Check if user is authorized to use this application"""
from passbook.core.rules import RuleEngine from passbook.core.policies import PolicyEngine
return RuleEngine(self).for_user(user).result return PolicyEngine(self.policies.all()).for_user(user).result
def __str__(self): def __str__(self):
return self.name return self.name
@reversion.register() class Source(PolicyModel):
class Source(RuleModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
name = models.TextField() name = models.TextField()
@ -104,14 +171,18 @@ class Source(RuleModel):
return False return False
@property @property
def get_url(self): def get_login_button(self):
"""Return URL used for logging in""" """Return a tuple of URL, Icon name and Name"""
raise NotImplementedError raise NotImplementedError
@property
def additional_info(self):
"""Return additional Info, such as a callback URL. Show in the administration interface."""
return None
def __str__(self): def __str__(self):
return self.name return self.name
@reversion.register()
class UserSourceConnection(CreatedUpdatedModel): class UserSourceConnection(CreatedUpdatedModel):
"""Connection between User and Source.""" """Connection between User and Source."""
@ -122,9 +193,8 @@ class UserSourceConnection(CreatedUpdatedModel):
unique_together = (('user', 'source'),) unique_together = (('user', 'source'),)
@reversion.register() class Policy(UUIDModel, CreatedUpdatedModel):
class Rule(UUIDModel, CreatedUpdatedModel): """Policies which specify if a user is authorized to use an Application. Can be overridden by
"""Rules which specify if a user is authorized to use an Application. Can be overridden by
other types to add other fields, more logic, etc.""" other types to add other fields, more logic, etc."""
ACTION_ALLOW = 'allow' ACTION_ALLOW = 'allow'
@ -147,12 +217,11 @@ class Rule(UUIDModel, CreatedUpdatedModel):
return "%s action %s" % (self.name, self.action) return "%s action %s" % (self.name, self.action)
def passes(self, user: User) -> bool: def passes(self, user: User) -> bool:
"""Check if user instance passes this rule""" """Check if user instance passes this policy"""
raise NotImplementedError() raise NotImplementedError()
@reversion.register() class FieldMatcherPolicy(Policy):
class FieldMatcherRule(Rule): """Policy which checks if a field of the User model matches/doesn't match a
"""Rule which checks if a field of the User model matches/doesn't match a
certain pattern""" certain pattern"""
MATCH_STARTSWITH = 'startswith' MATCH_STARTSWITH = 'startswith'
@ -160,29 +229,30 @@ class FieldMatcherRule(Rule):
MATCH_CONTAINS = 'contains' MATCH_CONTAINS = 'contains'
MATCH_REGEXP = 'regexp' MATCH_REGEXP = 'regexp'
MATCH_EXACT = 'exact' MATCH_EXACT = 'exact'
MATCHES = ( MATCHES = (
(MATCH_STARTSWITH, _('Starts with')), (MATCH_STARTSWITH, _('Starts with')),
(MATCH_ENDSWITH, _('Ends with')), (MATCH_ENDSWITH, _('Ends with')),
(MATCH_ENDSWITH, _('Contains')), (MATCH_CONTAINS, _('Contains')),
(MATCH_REGEXP, _('Regexp')), (MATCH_REGEXP, _('Regexp')),
(MATCH_EXACT, _('Exact')), (MATCH_EXACT, _('Exact')),
) )
USER_FIELDS = ( USER_FIELDS = (
('username', 'username',), ('username', _('Username'),),
('first_name', 'first_name',), ('first_name', _('First Name'),),
('last_name', 'last_name',), ('last_name', _('Last Name'),),
('email', 'email',), ('email', _('E-Mail'),),
('is_staff', 'is_staff',), ('is_staff', _('Is staff'),),
('is_active', 'is_active',), ('is_active', _('Is active'),),
('data_joined', 'data_joined',), ('data_joined', _('Date joined'),),
) )
user_field = models.TextField(choices=USER_FIELDS) user_field = models.TextField(choices=USER_FIELDS)
match_action = models.CharField(max_length=50, choices=MATCHES) match_action = models.CharField(max_length=50, choices=MATCHES)
value = models.TextField() value = models.TextField()
form = 'passbook.core.forms.rules.FieldMatcherRuleForm' form = 'passbook.core.forms.policies.FieldMatcherPolicyForm'
def __str__(self): def __str__(self):
description = "%s, user.%s %s '%s'" % (self.name, self.user_field, description = "%s, user.%s %s '%s'" % (self.name, self.user_field,
@ -199,13 +269,13 @@ class FieldMatcherRule(Rule):
LOGGER.debug("Checked '%s' %s with '%s'...", LOGGER.debug("Checked '%s' %s with '%s'...",
user_field_value, self.match_action, self.value) user_field_value, self.match_action, self.value)
passes = False passes = False
if self.match_action == FieldMatcherRule.MATCH_STARTSWITH: if self.match_action == FieldMatcherPolicy.MATCH_STARTSWITH:
passes = user_field_value.startswith(self.value) 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) 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 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) pattern = re.compile(self.value)
passes = bool(pattern.match(user_field_value)) passes = bool(pattern.match(user_field_value))
if self.negate: if self.negate:
@ -215,12 +285,45 @@ class FieldMatcherRule(Rule):
class Meta: class Meta:
verbose_name = _('Field matcher Rule') verbose_name = _('Field matcher Policy')
verbose_name_plural = _('Field matcher Rules') verbose_name_plural = _('Field matcher Policies')
@reversion.register() class PasswordPolicy(Policy):
class WebhookRule(Rule): """Policy to make sure passwords have certain properties"""
"""Rule that asks webhook"""
amount_uppercase = models.IntegerField(default=0)
amount_lowercase = models.IntegerField(default=0)
amount_symbols = models.IntegerField(default=0)
length_min = models.IntegerField(default=0)
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
form = 'passbook.core.forms.policies.PasswordPolicyForm'
def passes(self, user: User) -> bool:
# Only check if password is being set
if not hasattr(user, '__password__'):
return True
password = getattr(user, '__password__')
filter_regex = r''
if self.amount_lowercase > 0:
filter_regex += r'[a-z]{%d,}' % self.amount_lowercase
if self.amount_uppercase > 0:
filter_regex += r'[A-Z]{%d,}' % self.amount_uppercase
if self.amount_symbols > 0:
filter_regex += r'[%s]{%d,}' % (self.symbol_charset, self.amount_symbols)
result = bool(re.compile(filter_regex).match(password))
LOGGER.debug("User got %r", result)
return result
class Meta:
verbose_name = _('Password Policy')
verbose_name_plural = _('Password Policies')
class WebhookPolicy(Policy):
"""Policy that asks webhook"""
METHOD_GET = 'GET' METHOD_GET = 'GET'
METHOD_POST = 'POST' METHOD_POST = 'POST'
@ -243,7 +346,7 @@ class WebhookRule(Rule):
result_jsonpath = models.TextField() result_jsonpath = models.TextField()
result_json_value = models.TextField() result_json_value = models.TextField()
form = 'passbook.core.forms.rules.WebhookRuleForm' form = 'passbook.core.forms.policies.WebhookPolicyForm'
def passes(self, user: User): def passes(self, user: User):
"""Call webhook asynchronously and report back""" """Call webhook asynchronously and report back"""
@ -251,31 +354,30 @@ class WebhookRule(Rule):
class Meta: class Meta:
verbose_name = _('Webhook Rule') verbose_name = _('Webhook Policy')
verbose_name_plural = _('Webhook Rules') verbose_name_plural = _('Webhook Policies')
@reversion.register() class DebugPolicy(Policy):
class DebugRule(Rule): """Policy used for debugging the PolicyEngine. Returns a fixed result,
"""Rule used for debugging the RuleEngine. Returns a fixed result,
but takes a random time to process.""" but takes a random time to process."""
result = models.BooleanField(default=False) result = models.BooleanField(default=False)
wait_min = models.IntegerField(default=5) wait_min = models.IntegerField(default=5)
wait_max = models.IntegerField(default=30) wait_max = models.IntegerField(default=30)
form = 'passbook.core.forms.rules.DebugRuleForm' form = 'passbook.core.forms.policies.DebugPolicyForm'
def passes(self, user: User): def passes(self, user: User):
"""Wait random time then return result""" """Wait random time then return result"""
wait = SystemRandom().randrange(self.wait_min, self.wait_max) 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) sleep(wait)
return self.result return self.result
class Meta: class Meta:
verbose_name = _('Debug Rule') verbose_name = _('Debug Policy')
verbose_name_plural = _('Debug Rules') verbose_name_plural = _('Debug Policies')
class Invitation(UUIDModel): class Invitation(UUIDModel):
"""Single-use invitation link""" """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>=2.0
django-reversion
django-model-utils django-model-utils
djangorestframework djangorestframework
PyYAML PyYAML
@ -8,6 +7,6 @@ markdown
colorlog colorlog
celery celery
redis<3.0 redis<3.0
pymysql psycopg2
idna<2.8,>=2.5 idna<2.8,>=2.5
cherrypy cherrypy

View File

@ -1,42 +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):
"""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)
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"""
_rule_model = None
_group = None
def __init__(self, rule_model):
self._rule_model = rule_model
def for_user(self, user):
"""Check rules for user"""
signatures = []
for rule in self._rule_model.rules.all():
signatures.append(_rule_engine_task.s(user.pk, rule.pk.hex))
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

@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/2.1/ref/settings/
import importlib import importlib
import os import os
import sys
from django.contrib import messages from django.contrib import messages
@ -33,7 +34,7 @@ SECRET_KEY = CONFIG.get('secret_key')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.get('debug') DEBUG = CONFIG.get('debug')
INTERNAL_IPS = ['127.0.0.1'] INTERNAL_IPS = ['127.0.0.1']
ALLOWED_HOSTS = CONFIG.get('domains') ALLOWED_HOSTS = CONFIG.get('domains', [])
LOGIN_URL = 'passbook_core:auth-login' LOGIN_URL = 'passbook_core:auth-login'
# CSRF_FAILURE_VIEW = 'passbook.core.views.errors.CSRFErrorView.as_view' # CSRF_FAILURE_VIEW = 'passbook.core.views.errors.CSRFErrorView.as_view'
@ -49,11 +50,6 @@ AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'passbook.oauth_client.backends.AuthorizedServiceBackend' '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 # Application definition
@ -64,9 +60,8 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'reversion',
'rest_framework', 'rest_framework',
'rest_framework_swagger', 'drf_yasg',
'passbook.core.apps.PassbookCoreConfig', 'passbook.core.apps.PassbookCoreConfig',
'passbook.admin.apps.PassbookAdminConfig', 'passbook.admin.apps.PassbookAdminConfig',
'passbook.api.apps.PassbookAPIConfig', 'passbook.api.apps.PassbookAPIConfig',
@ -76,8 +71,9 @@ INSTALLED_APPS = [
'passbook.oauth_client.apps.PassbookOAuthClientConfig', 'passbook.oauth_client.apps.PassbookOAuthClientConfig',
'passbook.oauth_provider.apps.PassbookOAuthProviderConfig', 'passbook.oauth_provider.apps.PassbookOAuthProviderConfig',
'passbook.saml_idp.apps.PassbookSAMLIDPConfig', 'passbook.saml_idp.apps.PassbookSAMLIDPConfig',
'passbook.totp.apps.PassbookTOTPConfig', 'passbook.otp.apps.PassbookOTPConfig',
'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig', 'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig',
'passbook.hibp_policy.apps.PassbookHIBPConfig',
] ]
# Message Tag fix for bootstrap CSS Classes # Message Tag fix for bootstrap CSS Classes
@ -134,9 +130,6 @@ WSGI_APPLICATION = 'passbook.core.wsgi.application'
DATABASES = {} DATABASES = {}
for db_alias, db_config in CONFIG.get('databases').items(): 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] = { DATABASES[db_alias] = {
'ENGINE': db_config.get('engine'), 'ENGINE': db_config.get('engine'),
'HOST': db_config.get('host'), 'HOST': db_config.get('host'),
@ -288,6 +281,16 @@ with CONFIG.cd('log'):
} }
} }
TEST = False
TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
TEST_OUTPUT_VERBOSE = 2
TEST_OUTPUT_FILE_NAME = 'unittest.xml'
if any('test' in arg for arg in sys.argv):
LOGGING = None
TEST = True
_DISALLOWED_ITEMS = ['INSTALLED_APPS', 'MIDDLEWARE', 'AUTHENTICATION_BACKENDS'] _DISALLOWED_ITEMS = ['INSTALLED_APPS', 'MIDDLEWARE', 'AUTHENTICATION_BACKENDS']
# Load subapps's INSTALLED_APPS # Load subapps's INSTALLED_APPS
for _app in INSTALLED_APPS: for _app in INSTALLED_APPS:

View File

@ -9,3 +9,4 @@ from django.core.signals import Signal
user_signed_up = Signal(providing_args=['request', 'user']) user_signed_up = Signal(providing_args=['request', 'user'])
invitation_created = Signal(providing_args=['request', 'invitation']) invitation_created = Signal(providing_args=['request', 'invitation'])
invitation_used = Signal(providing_args=['request', 'invitation', 'user']) invitation_used = Signal(providing_args=['request', 'invitation', 'user'])
password_changed = Signal(providing_args=['user', 'password'])

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="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.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly-additions.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 %} {% block head %}
{% endblock %} {% endblock %}
</head> </head>
@ -24,6 +30,7 @@
<script src="{% static 'js/jquery.min.js' %}"></script> <script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/patternfly.min.js' %}"></script> <script src="{% static 'js/patternfly.min.js' %}"></script>
<script src="{% static 'js/passbook.js' %}"></script>
{% block scripts %} {% block scripts %}
{% endblock %} {% endblock %}
<div class="modals"> <div class="modals">

View File

@ -9,7 +9,7 @@
padding: 15px; padding: 15px;
background-color: #fff; background-color: #fff;
border-top: 2px solid transparent; border-top: 2px solid transparent;
box-shadow: 0 1px 1px rgba(3,3,3,.175); box-shadow: 0 1px 1px rgba(3, 3, 3, .175);
} }
.login-pf-page .login-pf-page-footer-link { .login-pf-page .login-pf-page-footer-link {
@ -23,12 +23,16 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
<div class="toast-notifications-list-pf">
{% include 'partials/messages.html' %}
</div>
<div class="login-pf-page"> <div class="login-pf-page">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <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"> <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"> <header class="login-pf-page-header">
<img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}" alt="PatternFly logo" /> <img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}"
alt="passbook logo" />
{% if config.login.subtext %} {% if config.login.subtext %}
<p>{{ config.login.subtext }}</p> <p>{{ config.login.subtext }}</p>
{% endif %} {% endif %}

View File

@ -1,8 +1,9 @@
{% extends 'login/form.html' %} {% extends 'login/form_with_user.html' %}
{% load i18n %} {% load i18n %}
{% block above_form %} {% block beneath_form %}
<span class="pficon pficon-unlocked"></span> {% if show_password_forget_notice %}
{% trans "This is a text" %} <a href="{% url 'passbook_core:auth-process' %}?password-forgotten">{% trans 'Forgot password?' %}</a>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -7,12 +7,12 @@
<header class="login-pf-header"> <header class="login-pf-header">
<h1>{% trans title %}</h1> <h1>{% trans title %}</h1>
</header> </header>
{% include 'partials/messages.html' %}
<form method="POST"> <form method="POST">
{% csrf_token %}
{% block above_form %} {% block above_form %}
{% endblock %} {% endblock %}
{% include 'partials/form_login.html' %} {% include 'partials/form_login.html' %}
{% block beneath_form %}
{% endblock %}
<button type="submit" class="btn btn-primary btn-block btn-lg">{% trans primary_action %}</button> <button type="submit" class="btn btn-primary btn-block btn-lg">{% trans primary_action %}</button>
</form> </form>
{% if show_sign_up_notice %} {% if show_sign_up_notice %}

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

@ -1,131 +0,0 @@
{% load static %}
<!DOCTYPE html>
<!--[if IE 9]><html lang="en-us" class="ie9 login-pf"><![endif]-->
<!--[if gt IE 9]><!-->
<html lang="en-us" class="login-pf">
<!--<![endif]-->
<head>
<title>Login Social Account (two column) - Red Hat&reg; Common User Experience</title>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="{% static 'img/favicon.ico' %}">
<!-- iPad retina icon -->
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="{% static 'img/apple-touch-icon-precomposed-152.png' %}">
<!-- iPad retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="{% static 'img/apple-touch-icon-precomposed-144.png' %}">
<!-- iPad non-retina icon -->
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="{% static 'img/apple-touch-icon-precomposed-76.png' %}">
<!-- iPad non-retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="{% static 'img/apple-touch-icon-precomposed-72.png' %}">
<!-- iPhone 6 Plus icon -->
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="{% static 'img/apple-touch-icon-precomposed-180.png' %}">
<!-- iPhone retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="{% static 'img/apple-touch-icon-precomposed-114.png' %}">
<!-- iPhone non-retina icon (iOS < 7) -->
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="{% static 'img/apple-touch-icon-precomposed-57.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' %}">
<script src="{% static 'js/jquery.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.12.2/js/bootstrap-select.min.js"></script>
<script src="{% static 'js/patternfly.min.js' %}"></script>
</head>
<div class="toast-notifications-list-pf">
<div class="toast-pf alert alert-warning alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">
<span class="pficon pficon-close"></span>
</button>
<span class="pficon pficon-warning-triangle-o"></span>
These examples are included for development testing purposes. For official documentation, see <a href="https://www.patternfly.org" class="alert-link">https://www.patternfly.org</a> and <a href="http://getbootstrap.com" class="alert-link">http://getbootstrap.com</a>.
</div>
</div>
<body>
<div class="login-pf-page login-pf-page-accounts">
<header class="login-pf-page-header">
<img class="login-pf-brand" src="/" alt="Red Hat&reg; logo" />
</header>
<div class="card-pf login-pf-accounts">
<header class="login-pf-header">
<select class="selectpicker">
<option>English</option>
<option>French</option>
<option>Italian</option>
</select>
<h1>Log In to Your Account</h1>
</header>
<section class="login-pf-social-section" role="contentinfo" aria-label="Log in to your patternfly account">
<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="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>
</section><!--login-pf-section-->
<section class="login-pf-social-section" role="contentinfo" aria-label="Log in with third party account">
<ul class="login-pf-social login-pf-social-double-col list-unstyled">
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/google-logo.svg' %}" alt="Google account login">Google</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/github-logo.svg' %}" alt="github account login">Github</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/facebook-logo.svg' %}" alt="Facebook account login">Facebook</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/twitter-logo.svg' %}" alt="Twitter account login">Twitter</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/linkedin-logo.svg' %}" alt="LinkIn account login">LinkIn</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/stack-exchange-logo.svg' %}" alt="Stack Exchange logo">Stack Exchange</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/open-id-logo.svg' %}" alt="Open ID account login">Open ID</a></li>
<li class="login-pf-social-link"><a href="#"><img src="{% static 'img/instagram-logo.png' %}" alt="Instagram account login">Instagram</a></li>
<li class="login-pf-social-link login-pf-social-link-more"><a href="#"><img src="{% static 'img/git-logo.svg' %}" alt="Git account login">Git</a></li>
<li class="login-pf-social-link login-pf-social-link-more"><a href="#"><img src="{% static 'img/dropbox-logo.svg' %}" alt="dropbox account login">Dropbox</a></li>
<li class="login-pf-social-link login-pf-social-link-more"><a href="#"><img src="{% static 'img/fedora-logo.png' %}" alt="fedora account login">Fedora</a></li>
<li class="login-pf-social-link login-pf-social-link-more"><a href="#"><img src="{% static 'img/skype-logo.svg' %}" alt="skype account logingit ">Skype</a></li>
</ul>
<button type="button" id="socialAccountsToggle" class="btn btn-link login-pf-social-toggle">More<span class="caret"></span></button>
</section><!--login-pf-section-->
<p class="login-pf-signup">Need an account?<a href="#">Sign up</a></p>
</div><!-- card -->
<div class="row">
<div class="col-md-6 col-md-offset-3">
<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>
</div>
</div><!-- login-pf-page -->
<script>
$("#socialAccountsToggle").on("click", function(e) {
var $toggle = $(e.target);
var text = $toggle.contents().first()[0];
var socialContainer = $('.login-pf-social-section > .login-pf-social');
if ($toggle.hasClass('login-pf-social-toggle-active')) {
$toggle.removeClass('login-pf-social-toggle-active');
text.textContent = 'More';
socialContainer.removeClass('login-pf-social-all');
} else {
$toggle.addClass('login-pf-social-toggle-active');
text.textContent = 'Less';
socialContainer.addClass('login-pf-social-all');
}
});
</script>
</body>
</html>

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