Rules -> Policies, more things

This commit is contained in:
Jens Langhammer 2019-02-21 16:06:57 +01:00
parent d3d75737ed
commit c941107d42
31 changed files with 333 additions and 175 deletions

View File

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

View File

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

View File

@ -16,6 +16,18 @@
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Sources' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ source_count }}</a></span>
</p>
</div>
</div>
</div>
<div 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">
@ -28,6 +40,30 @@
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Factors' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ factor_count }}</a></span>
</p>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">
<a href="#"><span class="fa fa-shield"></span><span class="card-pf-aggregate-status-count"></span> {% trans 'Invitation' %}</a>
</h2>
<div class="card-pf-body">
<p class="card-pf-aggregate-status-notifications">
<span class="card-pf-aggregate-status-notification"><a href="#"><span class="pficon pficon-ok"></span>{{ invitation_count }}</a></span>
</p>
</div>
</div>
</div>
<div class="col-xs-6 col-sm-2 col-md-2">
<div class="card-pf card-pf-accented card-pf-aggregate-status">
<h2 class="card-pf-title">

View File

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

View File

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

@ -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,10 +1,10 @@
"""passbook audit models"""
from datetime import timedelta
from json import dumps, loads
from logging import getLogger
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.contrib.postgres.fields import JSONField
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
@ -43,18 +43,10 @@ class AuditEntry(UUIDModel):
action = models.TextField(choices=ACTIONS)
date = models.DateTimeField(auto_now_add=True)
app = models.TextField()
_context = models.TextField()
_context_cache = None
context = JSONField(default=dict, blank=True)
request_ip = models.GenericIPAddressField()
created = models.DateTimeField(auto_now_add=True)
@property
def context(self):
"""Load context data and load json"""
if not self._context_cache:
self._context_cache = loads(self._context)
return self._context_cache
@staticmethod
def create(action, request, **kwargs):
"""Create AuditEntry from arguments"""
@ -67,7 +59,7 @@ class AuditEntry(UUIDModel):
user=user,
# User 255.255.255.255 as fallback if IP cannot be determined
request_ip=client_ip or '255.255.255.255',
_context=dumps(kwargs))
context=kwargs)
LOGGER.debug("Logged %s from %s (%s)", action, request.user, client_ip)
return entry

View File

@ -15,8 +15,9 @@ class AuthenticationFactor(TemplateView):
form = None
required = True
authenticator = None
pending_user = None
request = None
template_name = 'login/form.html'
template_name = 'login/factors/base.html'
def __init__(self, authenticator):
self.authenticator = authenticator
@ -26,4 +27,5 @@ class AuthenticationFactor(TemplateView):
kwargs['is_login'] = True
kwargs['title'] = _('Log in to your account')
kwargs['primary_action'] = _('Log in')
kwargs['pending_user'] = self.pending_user
return super().get_context_data(**kwargs)

View File

@ -67,6 +67,7 @@ class AuthenticationView(UserPassesTestMixin, View):
# Instantiate Next Factor and pass request
factor = path_to_class(factor_class)
self._current_factor = factor(self)
self._current_factor.pending_user = self.pending_user
self._current_factor.request = request
return super().dispatch(request, *args, **kwargs)
@ -93,7 +94,8 @@ class AuthenticationView(UserPassesTestMixin, View):
self.pending_factors
self.request.session[AuthenticationView.SESSION_FACTOR] = next_factor
LOGGER.debug("Rendering Factor is %s", next_factor)
return redirect(reverse('passbook_core:auth-process', kwargs={'factor': next_factor}))
# return redirect(reverse('passbook_core:auth-process', kwargs={'factor': next_factor}))
return redirect(reverse('passbook_core:auth-process'))
# User passed all factors
LOGGER.debug("User passed all factors, logging in")
return self._user_passed()
@ -102,6 +104,7 @@ class AuthenticationView(UserPassesTestMixin, View):
"""Show error message, user cannot login.
This should only be shown if user authenticated successfully, but is disabled/locked/etc"""
LOGGER.debug("User invalid")
self._cleanup()
return redirect(reverse('passbook_core:auth-denied'))
def _user_passed(self):

View File

@ -1,5 +1,6 @@
"""passbook Core Application forms"""
from django import forms
from django.utils.translation import gettext_lazy as _
from passbook.core.models import Application, Provider
@ -19,3 +20,7 @@ class ApplicationForm(forms.ModelForm):
'launch_url': forms.TextInput(),
'icon_url': forms.TextInput(),
}
labels = {
'launch_url': _('Launch URL'),
'icon_url': _('Icon URL'),
}

View File

@ -18,6 +18,11 @@ class LoginForm(forms.Form):
uid_field = forms.CharField(widget=forms.TextInput(attrs={'placeholder': _('UID')}))
remember_me = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if CONFIG.y('passbook.uid_fields') == ['email']:
self.fields['uid_field'] = forms.EmailField()
def clean_uid_field(self):
"""Validate uid_field after EmailValidator if 'email' is the only selected uid_fields"""
if CONFIG.y('passbook.uid_fields') == ['email']:

View File

@ -17,7 +17,7 @@ class FactorForm(forms.ModelForm):
class Meta:
model = Factor
fields = ['name', 'slug', 'order', 'policies', 'type', 'enabled']
fields = ['name', 'slug', 'order', 'policies', 'type', 'enabled', 'arguments']
widgets = {
'type': forms.Select(choices=get_factors()),
'name': forms.TextInput(),

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

@ -6,6 +6,7 @@ from time import sleep
from uuid import uuid4
from django.contrib.auth.models import AbstractUser
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
@ -69,6 +70,7 @@ class Factor(PolicyModel):
order = models.IntegerField()
type = models.TextField(unique=True)
enabled = models.BooleanField(default=True)
arguments = JSONField(default=dict, blank=True)
def __str__(self):
return "Factor %s" % self.slug

View File

@ -32,7 +32,7 @@ class PolicyEngine:
"""Check policies for user"""
signatures = []
kwargs = {
'__password__': getattr(user, '__password__')
'__password__': getattr(user, '__password__', None)
}
for policy in self.policies:
signatures.append(_policy_engine_task.s(user.pk, policy.pk.hex, **kwargs))

View File

@ -61,7 +61,7 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework_swagger',
'drf_yasg',
'passbook.core.apps.PassbookCoreConfig',
'passbook.admin.apps.PassbookAdminConfig',
'passbook.api.apps.PassbookAPIConfig',

View File

@ -26,7 +26,7 @@
<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">
<div class="col-sm-6 col-sm-offset-3 col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4">
<header class="login-pf-page-header">
<img class="login-pf-brand" style="max-height: 10rem;" src="{% static 'img/logo.svg' %}" alt="PatternFly logo" />
{% if config.login.subtext %}

View File

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

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 pending_user.email %}" alt="">
{{ pending_user.username }}
</p>
<a href="{% url 'passbook_core:auth-login' %}">{% trans 'Not you?' %}</a>
</div>
{% endblock %}

View File

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

View File

@ -14,7 +14,7 @@
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">
<img src="{% static 'img/brand.svg' %}" alt="PatternFly Enterprise Application" />
<img src="{% static 'img/brand.svg' %}" alt="passbook" />
</a>
</div>
<div class="collapse navbar-collapse navbar-collapse-1">
@ -63,7 +63,9 @@
</div>
</nav>
<div class="container-fluid container-cards-pf">
<div class="container">
{% include 'partials/messages.html' %}
</div>
{% block content %}
{% endblock %}
</div>

View File

@ -9,10 +9,18 @@
<div class="nav-category">
<h2>{% trans 'User Profile'%}</h2>
<ul class="nav nav-pills nav-stacked">
<li class="{% is_active 'passbook_core:user-settings' %}"><a href="{% url 'passbook_core:user-settings' %}"><i class="fa fa-desktop"></i>{% trans 'Details' %}</a></li>
<li><a href="#"><i class="fa fa-cog"></i>System Services</a></li>
<li><a href="#"><i class="fa fa-file-text-o"></i>Journal</a></li>
<li><a href="#"><i class="fa fa-cloud"></i>Storage</a></li>
<li class="{% is_active 'passbook_core:user-settings' %}">
<a href="{% url 'passbook_core:user-settings' %}">
<i class="fa fa-desktop"></i> {% trans 'Details' %}
</a>
</li>
<li class="{% is_active 'passbook_core:user-settings' %}">
<a href="{% url 'passbook_core:user-settings' %}">
<i class="pficon pficon-locked"></i> {% trans 'Change Password' %}
</a>
</li>
<li><a href="#"><i class="fa fa-file-text-o"></i> Journal</a></li>
<li><a href="#"><i class="fa fa-cloud"></i> Storage</a></li>
</ul>
</div>
</div>

View File

@ -0,0 +1,14 @@
{% extends "user/base.html" %}
{% load i18n %}
{% block page %}
<h1>{% trans 'Change Password' %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% include 'partials/form.html' %}
<input class="btn btn-primary" type="submit" value="{% trans 'Update' %}">
<a class="btn btn-danger"
href="{% url 'passbook_core:user-delete' %}?back={{ request.get_full_path }}">{% trans 'Delete user' %}</a>
</form>
{% endblock %}

View File

@ -13,6 +13,7 @@ class OverviewView(LoginRequiredMixin, TemplateView):
template_name = 'overview/index.html'
def get_context_data(self, **kwargs):
print(self.request.session.keys())
kwargs['applications'] = self.request.user.applications.all()
if self.request.user.is_superuser:
kwargs['applications'] = Application.objects.all()

View File

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

View File

@ -59,6 +59,7 @@ passbook:
uid_fields:
- username
- email
# Factors to load
factors:
- passbook.core.auth.factors.backend
- passbook.core.auth.factors.dummy
@ -67,22 +68,6 @@ passbook:
remember_age: 2592000 # 60 * 60 * 24 * 30, one month
# Provider-specific settings
ldap:
# # Completely enable or disable LDAP provider
# enabled: false
# # AD Domain, used to generate `userPrincipalName`
# domain: corp.contoso.com
# # Base DN in which passbook should look for users
# base_dn: dn=corp,dn=contoso,dn=com
# # LDAP field which is used to set the django username
# username_field: sAMAccountName
# # LDAP server to connect to, can be set to `<domain_name>`
# server:
# name: corp.contoso.com
# use_tls: false
# # Bind credentials, used for account creation
# bind:
# username: Administraotr@corp.contoso.com
# password: VerySecurePassword!
# Which field from `uid_fields` maps to which LDAP Attribute
login_field_map:
username: sAMAccountName
@ -93,10 +78,6 @@ ldap:
mail: email
given_name: first_name
name: last_name
# # Create new users in LDAP upon sign-up
# create_users: true
# # Reset LDAP password when user reset their password
# reset_password: true
oauth_client:
# List of python packages with sources types to load.
types:
@ -108,7 +89,6 @@ oauth_client:
- passbook.oauth_client.source_types.supervisr
- passbook.oauth_client.source_types.twitter
saml_idp:
issuer: passbook
# List of python packages with provider types to load.
types:
- passbook.saml_idp.processors.generic

View File

@ -6,7 +6,7 @@ from django.db import models
class CreatedUpdatedModel(models.Model):
"""Base Abstract Model to save created and update"""
created = models.DateField(auto_now_add=True)
created = models.DateTimeField(auto_now_add=True)
last_updated = models.DateTimeField(auto_now=True)
class Meta:

View File

@ -2,8 +2,9 @@
import glob
import os
import socket
from hashlib import md5
from importlib import import_module
from urllib.parse import urljoin
from urllib.parse import urlencode, urljoin
from django import template
from django.apps import apps
@ -11,6 +12,7 @@ from django.conf import settings
from django.db.models import Model
from django.template.loaders.app_directories import get_app_template_dirs
from django.urls import reverse
from django.utils.html import escape
from django.utils.translation import ugettext as _
from passbook.lib.config import CONFIG
@ -178,3 +180,30 @@ def app_versions():
ver = '.'.join([str(x) for x in ver])
app_versions[app.verbose_name] = ver
return app_versions
@register.simple_tag
def gravatar(email, size=None, rating=None):
"""
Generates a Gravatar URL for the given email address.
Syntax::
{% gravatar <email> [size] [rating] %}
Example::
{% gravatar someone@example.com 48 pg %}
"""
# gravatar uses md5 for their URLs, so md5 can't be avoided
gravatar_url = "%savatar/%s" % ('https://secure.gravatar.com/',
md5(email.encode('utf-8')).hexdigest()) # nosec
parameters = [p for p in (
('s', size or '158'),
('r', rating or 'g'),
) if p[1]]
if parameters:
gravatar_url += '?' + urlencode(parameters, doseq=True)
return escape(gravatar_url)

View File

@ -19,7 +19,7 @@ class OAuthSource(Source):
consumer_key = models.TextField()
consumer_secret = models.TextField()
form = 'passbook.oauth_client.forms.GitHubOAuthSourceForm'
form = 'passbook.oauth_client.forms.OAuthSourceForm'
@property
def is_link(self):