add PropertyMapping Model, add Subclass for SAML, test with AWS

This commit is contained in:
Jens Langhammer 2019-03-08 12:47:50 +01:00
parent a7b86e46bc
commit 56d872af15
16 changed files with 368 additions and 86 deletions

View File

@ -5,35 +5,45 @@
{% block nav_secondary %} {% block nav_secondary %}
<ul class="nav navbar-nav navbar-persistent"> <ul class="nav navbar-nav navbar-persistent">
<li class="{% is_active 'passbook_admin:overview' %}"> <li class="{% is_active 'passbook_admin:overview' %}">
<a href="{% url 'passbook_admin:overview' %}">{% trans 'Overview' %}</a> <a href="{% url 'passbook_admin:overview' %}">{% trans 'Overview' %}</a>
</li> </li>
<li class="{% is_active 'passbook_admin:applications' 'passbook_admin:application-create' 'passbook_admin:application-update' 'passbook_admin:application-delete' %}"> <li
<a href="{% url 'passbook_admin:applications' %}">{% trans 'Applications' %}</a> class="{% is_active 'passbook_admin:applications' 'passbook_admin:application-create' 'passbook_admin:application-update' 'passbook_admin:application-delete' %}">
</li> <a href="{% url 'passbook_admin:applications' %}">{% trans 'Applications' %}</a>
<li class="{% is_active 'passbook_admin:sources' 'passbook_admin:source-create' 'passbook_admin:source-update' 'passbook_admin:source-delete' %}"> </li>
<a href="{% url 'passbook_admin:sources' %}">{% trans 'Sources' %}</a> <li
</li> class="{% is_active 'passbook_admin:sources' 'passbook_admin:source-create' 'passbook_admin:source-update' 'passbook_admin:source-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:sources' %}">{% trans 'Sources' %}</a>
<a href="{% url 'passbook_admin:providers' %}">{% trans 'Providers' %}</a> </li>
</li> <li
<li class="{% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}"> class="{% is_active 'passbook_admin:providers' 'passbook_admin:provider-create' 'passbook_admin:provider-update' 'passbook_admin:provider-delete' %}">
<a href="{% url 'passbook_admin:factors' %}">{% trans 'Factors' %}</a> <a href="{% url 'passbook_admin:providers' %}">{% trans 'Providers' %}</a>
</li> </li>
<li class="{% is_active 'passbook_admin:policies' 'passbook_admin:policy-create' 'passbook_admin:policy-update' 'passbook_admin:policy-delete' 'passbook_admin:policy-test' %}"> <li
<a href="{% url 'passbook_admin:policies' %}">{% trans 'Policies' %}</a> class="{% is_active 'passbook_admin:property-mappings' 'passbook_admin:property-mapping-create' 'passbook_admin:property-mapping-update' 'passbook_admin:property-mapping-delete' %}">
</li> <a href="{% url 'passbook_admin:property-mappings' %}">{% trans 'Property Mappings' %}</a>
<li class="{% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}"> </li>
<a href="{% url 'passbook_admin:invitations' %}">{% trans 'Invitations' %}</a> <li
</li> class="{% is_active 'passbook_admin:factors' 'passbook_admin:factor-create' 'passbook_admin:factor-update' 'passbook_admin:factor-delete' %}">
<li class="{% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}"> <a href="{% url 'passbook_admin:factors' %}">{% trans 'Factors' %}</a>
<a href="{% url 'passbook_admin:users' %}">{% trans 'Users' %}</a> </li>
</li> <li
<li class="{% is_active 'passbook_admin:audit-log' %}"> 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:audit-log' %}">{% trans 'Audit Log' %}</a> <a href="{% url 'passbook_admin:policies' %}">{% trans 'Policies' %}</a>
</li> </li>
<li class="{% is_active_app 'admin' %}"> <li
<a href="{% url 'admin:index' %}">{% trans 'Django' %}</a> class="{% is_active 'passbook_admin:invitations' 'passbook_admin:invitation-create' 'passbook_admin:invitation-update' 'passbook_admin:invitation-delete' 'passbook_admin:invitation-test' %}">
</li> <a href="{% url 'passbook_admin:invitations' %}">{% trans 'Invitations' %}</a>
</li>
<li class="{% is_active 'passbook_admin:users' 'passbook_admin:user-update' 'passbook_admin:user-delete' %}">
<a href="{% url 'passbook_admin:users' %}">{% trans 'Users' %}</a>
</li>
<li class="{% is_active 'passbook_admin:audit-log' %}">
<a href="{% url 'passbook_admin:audit-log' %}">{% trans 'Audit Log' %}</a>
</li>
<li class="{% is_active_app 'admin' %}">
<a href="{% url 'admin:index' %}">{% trans 'Django' %}</a>
</li>
</ul> </ul>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "administration/base.html" %}
{% load i18n %}
{% load utils %}
{% block title %}
{% title %}
{% endblock %}
{% block content %}
<div class="container">
<h1><span class="fa fa-table"></span> {% trans "Property Mappings" %}</h1>
<span>{% trans "Property Mappings allow you expose provider-specific attributes." %}</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:property-mapping-create' %}?type={{ type }}&back={{ request.get_full_path }}">{{ 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></th>
</tr>
</thead>
<tbody>
{% for property_mapping in object_list %}
<tr>
<td>{{ property_mapping.name }} ({{ property_mapping.slug }})</td>
<td>{{ property_mapping|verbose_name }}</td>
<td>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:property-mapping-update' pk=property_mapping.pk %}?back={{ request.get_full_path }}">{% trans 'Edit' %}</a>
<a class="btn btn-default btn-sm"
href="{% url 'passbook_admin:property-mapping-delete' pk=property_mapping.pk %}?back={{ request.get_full_path }}">{% trans 'Delete' %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

View File

@ -2,8 +2,8 @@
from django.urls import include, path from django.urls import include, path
from passbook.admin.views import (applications, audit, factors, groups, from passbook.admin.views import (applications, audit, factors, groups,
invitations, overview, policy, providers, invitations, overview, policy,
sources, users) property_mapping, providers, sources, users)
urlpatterns = [ urlpatterns = [
path('', overview.AdministrationOverviewView.as_view(), name='overview'), path('', overview.AdministrationOverviewView.as_view(), name='overview'),
@ -43,6 +43,15 @@ urlpatterns = [
factors.FactorUpdateView.as_view(), name='factor-update'), factors.FactorUpdateView.as_view(), name='factor-update'),
path('factors/<uuid:pk>/delete/', path('factors/<uuid:pk>/delete/',
factors.FactorDeleteView.as_view(), name='factor-delete'), factors.FactorDeleteView.as_view(), name='factor-delete'),
# Factors
path('property-mappings/', property_mapping.PropertyMappingListView.as_view(),
name='property-mappings'),
path('property-mappings/create/',
property_mapping.PropertyMappingCreateView.as_view(), name='property-mapping-create'),
path('property-mappings/<uuid:pk>/update/',
property_mapping.PropertyMappingUpdateView.as_view(), name='property-mapping-update'),
path('property-mappings/<uuid:pk>/delete/',
property_mapping.PropertyMappingDeleteView.as_view(), name='property-mapping-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/',

View File

@ -0,0 +1,90 @@
"""passbook PropertyMapping 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, ListView, UpdateView
from passbook.admin.mixins import AdminRequiredMixin
from passbook.core.models import PropertyMapping
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 PropertyMappingListView(AdminRequiredMixin, ListView):
"""Show list of all property_mappings"""
model = PropertyMapping
template_name = 'administration/property_mapping/list.html'
ordering = 'name'
def get_context_data(self, **kwargs):
kwargs['types'] = {
x.__name__: x._meta.verbose_name for x in all_subclasses(PropertyMapping)}
return super().get_context_data(**kwargs)
def get_queryset(self):
return super().get_queryset().select_subclasses()
class PropertyMappingCreateView(SuccessMessageMixin, AdminRequiredMixin, CreateView):
"""Create new PropertyMapping"""
template_name = 'generic/create.html'
success_url = reverse_lazy('passbook_admin:property-mappings')
success_message = _('Successfully created Property Mapping')
def get_context_data(self, **kwargs):
kwargs = super().get_context_data(**kwargs)
property_mapping_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(PropertyMapping)
if x.__name__ == property_mapping_type)
kwargs['type'] = model._meta.verbose_name
return kwargs
def get_form_class(self):
property_mapping_type = self.request.GET.get('type')
model = next(x for x in all_subclasses(PropertyMapping)
if x.__name__ == property_mapping_type)
if not model:
raise Http404
return path_to_class(model.form)
class PropertyMappingUpdateView(SuccessMessageMixin, AdminRequiredMixin, UpdateView):
"""Update property_mapping"""
model = PropertyMapping
template_name = 'generic/update.html'
success_url = reverse_lazy('passbook_admin:property-mappings')
success_message = _('Successfully updated Property Mapping')
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 PropertyMapping.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
class PropertyMappingDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView):
"""Delete property_mapping"""
model = PropertyMapping
template_name = 'generic/delete.html'
success_url = reverse_lazy('passbook_admin:property-mappings')
success_message = _('Successfully deleted Property Mapping')
def get_object(self, queryset=None):
return PropertyMapping.objects.filter(pk=self.kwargs.get('pk')).select_subclasses().first()
def delete(self, request, *args, **kwargs):
messages.success(self.request, self.success_message)
return super().delete(request, *args, **kwargs)

View File

@ -37,7 +37,7 @@ class PasswordFactor(FormView, AuthenticationFactor):
send_email.delay(self.pending_user.email, _('Forgotten password'), send_email.delay(self.pending_user.email, _('Forgotten password'),
'email/account_password_reset.html', { 'email/account_password_reset.html', {
'url': self.request.build_absolute_uri( 'url': self.request.build_absolute_uri(
reverse('passbook_core:passbook_core:auth-password-reset', reverse('passbook_core:auth-password-reset',
kwargs={ kwargs={
'nonce': nonce.uuid 'nonce': nonce.uuid
}) })

View File

@ -0,0 +1,26 @@
# Generated by Django 2.1.7 on 2019-03-08 10:40
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0016_auto_20190227_1355'),
]
operations = [
migrations.CreateModel(
name='PropertyMapping',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.TextField()),
],
options={
'verbose_name': 'Property Mapping',
'verbose_name_plural': 'Property Mappings',
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.1.7 on 2019-03-08 10:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0017_propertymapping'),
]
operations = [
migrations.AddField(
model_name='provider',
name='property_mappings',
field=models.ManyToManyField(blank=True, default=None, to='passbook_core.PropertyMapping'),
),
]

View File

@ -60,6 +60,8 @@ class User(AbstractUser):
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"""
property_mappings = models.ManyToManyField('PropertyMapping', default=None, blank=True)
objects = InheritanceManager() objects = InheritanceManager()
# This class defines no field for easier inheritance # This class defines no field for easier inheritance
@ -412,7 +414,7 @@ class Invitation(UUIDModel):
verbose_name_plural = _('Invitations') verbose_name_plural = _('Invitations')
class Nonce(UUIDModel): class Nonce(UUIDModel):
"""One-time link for password resets/signup-confirmations""" """One-time link for password resets/sign-up-confirmations"""
expires = models.DateTimeField(default=default_nonce_duration) expires = models.DateTimeField(default=default_nonce_duration)
user = models.ForeignKey('User', on_delete=models.CASCADE) user = models.ForeignKey('User', on_delete=models.CASCADE)
@ -424,3 +426,19 @@ class Nonce(UUIDModel):
verbose_name = _('Nonce') verbose_name = _('Nonce')
verbose_name_plural = _('Nonces') verbose_name_plural = _('Nonces')
class PropertyMapping(UUIDModel):
"""User-defined key -> x mapping which can be used by providers to expose extra data."""
name = models.TextField()
form = ''
objects = InheritanceManager()
def __str__(self):
return "Property Mapping %s" % self.name
class Meta:
verbose_name = _('Property Mapping')
verbose_name_plural = _('Property Mappings')

View File

@ -6,13 +6,13 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
{% block above_form %} {% block above_form %}
<h1>{% blocktrans with object_type=object|fieldtype|title %}Delete {{ object_type }}{% endblocktrans %}</h1> <h1>{% blocktrans with object_type=object|verbose_name %}Delete {{ object_type }}{% endblocktrans %}</h1>
{% endblock %} {% endblock %}
<div class=""> <div class="">
<form method="post" class="form-horizontal"> <form method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<p> <p>
{% blocktrans with object_type=object|fieldtype|title name=object %} {% blocktrans with object_type=object|verbose_name name=object %}
Are you sure you want to delete {{ object_type }} "{{ object }}"? Are you sure you want to delete {{ object_type }} "{{ object }}"?
{% endblocktrans %} {% endblocktrans %}
</p> </p>

View File

@ -6,7 +6,6 @@ from logging import getLogger
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from passbook.lib.config import CONFIG
from passbook.saml_idp import exceptions, utils, xml_render from passbook.saml_idp import exceptions, utils, xml_render
MINUTES = 60 MINUTES = 60
@ -52,9 +51,7 @@ class Processor:
_session_index = None _session_index = None
_subject = None _subject = None
_subject_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' _subject_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
_system_params = { _system_params = {}
'ISSUER': CONFIG.y('saml_idp.issuer'),
}
@property @property
def dotted_path(self): def dotted_path(self):
@ -67,7 +64,7 @@ class Processor:
self.name = remote.name self.name = remote.name
self._remote = remote self._remote = remote
self._logger = getLogger(__name__) self._logger = getLogger(__name__)
self._system_params['ISSUER'] = self._remote.issuer
self._logger.info('processor configured') self._logger.info('processor configured')
def _build_assertion(self): def _build_assertion(self):
@ -170,6 +167,16 @@ class Processor:
'Value': self._django_request.user.username, 'Value': self._django_request.user.username,
}, },
] ]
from passbook.saml_idp.models import SAMLPropertyMapping
for mapping in self._remote.property_mappings.all().select_subclasses():
if isinstance(mapping, SAMLPropertyMapping):
mapping_payload = {
'Name': mapping.saml_name,
'ValueArray': mapping.values
}
if mapping.friendly_name:
mapping_payload['FriendlyName'] = mapping.friendly_name
self._assertion_params['ATTRIBUTES'].append(mapping_payload)
self._assertion_xml = xml_render.get_assertion_xml( self._assertion_xml = xml_render.get_assertion_xml(
'saml/xml/assertions/generic.xml', self._assertion_params, signed=True) 'saml/xml/assertions/generic.xml', self._assertion_params, signed=True)
@ -227,7 +234,7 @@ class Processor:
self._subject = sp_config self._subject = sp_config
self._subject_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent' self._subject_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
self._system_params = { self._system_params = {
'ISSUER': CONFIG.y('saml_idp.issuer'), 'ISSUER': self._remote.issuer
} }
def _validate_request(self): def _validate_request(self):

View File

@ -2,7 +2,8 @@
from django import forms from django import forms
from passbook.saml_idp.models import SAMLProvider, get_provider_choices from passbook.saml_idp.models import (SAMLPropertyMapping, SAMLProvider,
get_provider_choices)
from passbook.saml_idp.utils import CertificateBuilder from passbook.saml_idp.utils import CertificateBuilder
@ -21,7 +22,7 @@ class SAMLProviderForm(forms.ModelForm):
class Meta: class Meta:
model = SAMLProvider model = SAMLProvider
fields = ['name', 'acs_url', 'processor_path', 'issuer', fields = ['name', 'property_mappings', 'acs_url', 'processor_path', 'issuer',
'assertion_valid_for', 'signing', 'signing_cert', 'signing_key', ] 'assertion_valid_for', 'signing', 'signing_cert', 'signing_key', ]
labels = { labels = {
'acs_url': 'ACS URL', 'acs_url': 'ACS URL',
@ -31,3 +32,17 @@ class SAMLProviderForm(forms.ModelForm):
'name': forms.TextInput(), 'name': forms.TextInput(),
'issuer': forms.TextInput(), 'issuer': forms.TextInput(),
} }
class SAMLPropertyMappingForm(forms.ModelForm):
"""SAML Property Mapping form"""
class Meta:
model = SAMLPropertyMapping
fields = ['name', 'saml_name', 'friendly_name', 'values']
widgets = {
'name': forms.TextInput(),
'saml_name': forms.TextInput(),
'friendly_name': forms.TextInput(),
}

View File

@ -0,0 +1,30 @@
# Generated by Django 2.1.7 on 2019-03-08 10:40
import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('passbook_core', '0017_propertymapping'),
('passbook_saml_idp', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='SAMLPropertyMapping',
fields=[
('propertymapping_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PropertyMapping')),
('saml_name', models.TextField()),
('friendly_name', models.TextField(blank=True, default=None, null=True)),
('values', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)),
],
options={
'verbose_name': 'SAML Property Mapping',
'verbose_name_plural': 'SAML Property Mappings',
},
bases=('passbook_core.propertymapping',),
),
]

View File

@ -1,10 +1,11 @@
"""passbook saml_idp Models""" """passbook saml_idp Models"""
from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
from django.shortcuts import reverse from django.shortcuts import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from passbook.core.models import Provider from passbook.core.models import PropertyMapping, Provider
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.saml_idp.base import Processor from passbook.saml_idp.base import Processor
@ -53,6 +54,23 @@ class SAMLProvider(Provider):
verbose_name_plural = _('SAML Providers') verbose_name_plural = _('SAML Providers')
class SAMLPropertyMapping(PropertyMapping):
"""SAML Property mapping, allowing Name/FriendlyName mapping to a list of strings"""
saml_name = models.TextField()
friendly_name = models.TextField(default=None, blank=True, null=True)
values = ArrayField(models.TextField())
form = 'passbook.saml_idp.forms.SAMLPropertyMappingForm'
def __str__(self):
return "SAML Property Mapping %s" % self.saml_name
class Meta:
verbose_name = _('SAML Property Mapping')
verbose_name_plural = _('SAML Property Mappings')
def get_provider_choices(): def get_provider_choices():
"""Return tuple of class_path, class name of all providers.""" """Return tuple of class_path, class name of all providers."""
return [(class_to_path(x), x.__name__) for x in Processor.__subclasses__()] return [(class_to_path(x), x.__name__) for x in Processor.__subclasses__()]

View File

@ -11,16 +11,12 @@ class AWSProcessor(Processor):
def _format_assertion(self): def _format_assertion(self):
"""Formats _assertion_params as _assertion_xml.""" """Formats _assertion_params as _assertion_xml."""
self._assertion_params['ATTRIBUTES'] = [ super()._format_assertion()
self._assertion_params['ATTRIBUTES'].append(
{ {
'Name': 'https://aws.amazon.com/SAML/Attributes/RoleSessionName', 'Name': 'https://aws.amazon.com/SAML/Attributes/RoleSessionName',
'Value': self._django_request.user.username, 'Value': self._django_request.user.username,
},
{
'Name': 'https://aws.amazon.com/SAML/Attributes/Role',
# 'Value': 'arn:aws:iam::471432361072:saml-provider/passbook_dev,
# arn:aws:iam::471432361072:role/saml_role'
} }
] )
self._assertion_xml = xml_render.get_assertion_xml( self._assertion_xml = xml_render.get_assertion_xml(
'saml/xml/assertions/generic.xml', self._assertion_params, signed=True) 'saml/xml/assertions/generic.xml', self._assertion_params, signed=True)

View File

@ -9,40 +9,26 @@
{% block card %} {% block card %}
<header class="login-pf-header"> <header class="login-pf-header">
<h1>{% trans 'Authorize Application' %}</h1> <h1>{% trans 'Authorize Application' %}</h1>
</header> </header>
<form method="POST" action="{{ acs_url }}">> <form method="POST" action="{{ acs_url }}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="ACSUrl" value="{{ acs_url }}"> <input type="hidden" name="ACSUrl" value="{{ acs_url }}">
<input type="hidden" name="RelayState" value="{{ relay_state }}" /> <input type="hidden" name="RelayState" value="{{ relay_state }}" />
<input type="hidden" name="SAMLResponse" value="{{ saml_response }}" /> <input type="hidden" name="SAMLResponse" value="{{ saml_response }}" />
<label class="title"> <div class="login-group">
<clr-icon shape="passbook" class="is-info" size="48"></clr-icon> <h3>
{% config 'passbook.branding' %} {% blocktrans with remote=remote.name %}
</label> You're about to sign into {{ remote }}
<label class="subtitle"> {% endblocktrans %}
{% trans 'SSO - Authorize External Source' %} </h3>
</label> <p>
<div class="login-group"> {% blocktrans with user=user %}
<p class="subtitle"> You are logged in as {{ user }}.
{% blocktrans with remote=remote.name %} {% endblocktrans %}
You're about to sign into {{ remote }} <a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Not you?' %}</a>
{% endblocktrans %} </p>
</p> <input class="btn btn-primary btn-block btn-lg" type="submit" value="{% trans 'Continue' %}" />
<p>
{% blocktrans with user=user %}
You are logged in as {{ user }}. Not you?
{% endblocktrans %}
<a href="{% url 'passbook_core:auth-logout' %}">{% trans 'Logout' %}</a>
</p>
<div class="row">
<div class="col-md-6">
<input class="btn btn-success btn-block" type="submit" value="{% trans "Continue" %}" />
</div>
<div class="col-md-6">
<a href="{% url 'passbook_core:overview' %}" class="btn btn-outline btn-block">{% trans "Cancel" %}</a>
</div>
</div> </div>
</div>
</form> </form>
{% endblock %} {% endblock %}

View File

@ -1,7 +1,14 @@
<saml:AttributeStatement> <saml:AttributeStatement>
{% for attr in attributes %} {% for attr in attributes %}
<saml:Attribute {% if attr.FriendlyName %}FriendlyName="{{ attr.FriendlyName }}" {% endif %}Name="{{ attr.Name }}"> <saml:Attribute {% if attr.FriendlyName %}FriendlyName="{{ attr.FriendlyName }}" {% endif %}Name="{{ attr.Name }}">
<saml:AttributeValue>{{ attr.Value }}</saml:AttributeValue> {% if attr.Value %}
<saml:AttributeValue>{{ attr.Value }}</saml:AttributeValue>
{% endif %}
{% if attr.ValueArray %}
{% for value in attr.ValueArray %}
<saml:AttributeValue>{{ value }}</saml:AttributeValue>
{% endfor %}
{% endif %}
</saml:Attribute> </saml:Attribute>
{% endfor %} {% endfor %}
</saml:AttributeStatement> </saml:AttributeStatement>