Compare commits
	
		
			16 Commits
		
	
	
		
			version/0.
			...
			version/0.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 250b6691d4 | |||
| e3b02a6e78 | |||
| e94ef34d8f | |||
| 49e945307a | |||
| edfe0e5450 | |||
| 06b65a7882 | |||
| ff9bc8aa70 | |||
| 28da67abe6 | |||
| 39d9fe9bf0 | |||
| 750117b0fd | |||
| 983462f80d | |||
| 4ae31d409b | |||
| 98b414f3e2 | |||
| a0d42092e3 | |||
| f2569b6424 | |||
| 9d344d887c | 
| @ -1,5 +1,5 @@ | ||||
| [bumpversion] | ||||
| current_version = 0.0.8-alpha | ||||
| current_version = 0.0.10-alpha | ||||
| tag = True | ||||
| commit = True | ||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) | ||||
|  | ||||
| @ -52,9 +52,9 @@ package-docker: | ||||
|     name: gcr.io/kaniko-project/executor:debug | ||||
|     entrypoint: [""] | ||||
|   before_script: | ||||
|     - echo "{\"auths\":{\"https://docker.$NEXUS_URL/\":{\"username\":\"$NEXUS_USER\",\"password\":\"$NEXUS_PASS\"}}}" > /kaniko/.docker/config.json | ||||
|     - echo "{\"auths\":{\"docker.$NEXUS_URL\":{\"auth\":\"$NEXUS_AUTH\"}}}" > /kaniko/.docker/config.json | ||||
|   script: | ||||
|     - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination docker.pkg.beryju.org/passbook:latest --destination docker.pkg.beryju.org/passbook:0.0.8-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.10-alpha | ||||
|   stage: build | ||||
|   only: | ||||
|     - tags | ||||
| @ -65,7 +65,7 @@ package-helm: | ||||
|     - curl https://raw.githubusercontent.com/helm/helm/master/scripts/get | bash | ||||
|     - helm init --client-only | ||||
|     - helm package helm/passbook | ||||
|     - ./manage.py nexus_upload --method put --url $NEXUS_URL --user $NEXUS_USER --password $NEXUS_PASS --repo helm *.tgz | ||||
|     - ./manage.py nexus_upload --method put --url $NEXUS_URL --auth $NEXUS_AUTH --repo helm *.tgz | ||||
|   only: | ||||
|     - tags | ||||
|     - /^version/.*$/ | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| apiVersion: v1 | ||||
| appVersion: "0.0.8-alpha" | ||||
| appVersion: "0.0.10-alpha" | ||||
| description: A Helm chart for passbook. | ||||
| name: passbook | ||||
| version: 1.0.0 | ||||
| version: "0.0.10-alpha" | ||||
| icon: https://passbook.beryju.org/images/logo.png | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook admin""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
| @ -137,5 +137,43 @@ | ||||
|             </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="pficon-bundle"></span> | ||||
|                     <span class="card-pf-aggregate-status-count"></span> {% trans 'Version' %} | ||||
|                 </a> | ||||
|             </h2> | ||||
|             <div class="card-pf-body"> | ||||
|                 <p class="card-pf-aggregate-status-notifications"> | ||||
|                     <span class="card-pf-aggregate-status-notification"> | ||||
|                         <a href="#"> | ||||
|                             {{ version }} | ||||
|                         </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="pficon-server"></span> | ||||
|                     <span class="card-pf-aggregate-status-count"></span> {% trans 'Worker(s)' %} | ||||
|                 </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>{{ worker_count }} | ||||
|                         </a> | ||||
|                     </span> | ||||
|                 </p> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
|  | ||||
| @ -31,6 +31,8 @@ | ||||
|                         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> | ||||
|                     <a class="btn btn-default btn-sm" | ||||
|                         href="{% url 'passbook_admin:user-password-reset' pk=user.pk %}?back={{ request.get_full_path }}">{% trans 'Reset Password' %}</a> | ||||
|                 </td> | ||||
|             </tr> | ||||
|             {% endfor %} | ||||
|  | ||||
| @ -56,6 +56,8 @@ urlpatterns = [ | ||||
|          users.UserUpdateView.as_view(), name='user-update'), | ||||
|     path('users/<int:pk>/delete/', | ||||
|          users.UserDeleteView.as_view(), name='user-delete'), | ||||
|     path('users/<int:pk>/reset/', | ||||
|          users.UserPasswordResetView.as_view(), name='user-password-reset'), | ||||
|     # Audit Log | ||||
|     path('audit/', audit.AuditEntryListView.as_view(), name='audit-log'), | ||||
|     # Groups | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """passbook Application administration""" | ||||
| from django.contrib import messages | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| @ -45,5 +46,10 @@ class ApplicationDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView) | ||||
|  | ||||
|     model = Application | ||||
|  | ||||
|     template_name = 'generic/delete.html' | ||||
|     success_url = reverse_lazy('passbook_admin:applications') | ||||
|     success_message = _('Successfully updated Application') | ||||
|     success_message = _('Successfully deleted Application') | ||||
|  | ||||
|     def delete(self, request, *args, **kwargs): | ||||
|         messages.success(self.request, self.success_message) | ||||
|         return super().delete(request, *args, **kwargs) | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """passbook Factor administration""" | ||||
| from django.contrib import messages | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.http import Http404 | ||||
| from django.urls import reverse_lazy | ||||
| @ -73,7 +74,11 @@ class FactorDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView): | ||||
|     model = Factor | ||||
|     template_name = 'generic/delete.html' | ||||
|     success_url = reverse_lazy('passbook_admin:factors') | ||||
|     success_message = _('Successfully updated Factor') | ||||
|     success_message = _('Successfully deleted Factor') | ||||
|  | ||||
|     def get_object(self, queryset=None): | ||||
|         return Factor.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) | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """passbook Invitation administration""" | ||||
| from django.contrib import messages | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.http import HttpResponseRedirect | ||||
| from django.urls import reverse_lazy | ||||
| @ -42,4 +43,8 @@ class InvitationDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView): | ||||
|     model = Invitation | ||||
|     template_name = 'generic/delete.html' | ||||
|     success_url = reverse_lazy('passbook_admin:invitations') | ||||
|     success_message = _('Successfully updated Invitation') | ||||
|     success_message = _('Successfully deleted Invitation') | ||||
|  | ||||
|     def delete(self, request, *args, **kwargs): | ||||
|         messages.success(self.request, self.success_message) | ||||
|         return super().delete(request, *args, **kwargs) | ||||
|  | ||||
| @ -2,6 +2,8 @@ | ||||
| from django.views.generic import TemplateView | ||||
|  | ||||
| from passbook.admin.mixins import AdminRequiredMixin | ||||
| from passbook.core import __version__ | ||||
| from passbook.core.celery import CELERY_APP | ||||
| from passbook.core.models import (Application, Factor, Invitation, Policy, | ||||
|                                   Provider, Source, User) | ||||
|  | ||||
| @ -19,4 +21,6 @@ class AdministrationOverviewView(AdminRequiredMixin, TemplateView): | ||||
|         kwargs['source_count'] = len(Source.objects.all()) | ||||
|         kwargs['factor_count'] = len(Factor.objects.all()) | ||||
|         kwargs['invitation_count'] = len(Invitation.objects.all()) | ||||
|         kwargs['version'] = __version__ | ||||
|         kwargs['worker_count'] = len(CELERY_APP.control.ping(timeout=0.5)) | ||||
|         return super().get_context_data(**kwargs) | ||||
|  | ||||
| @ -68,11 +68,15 @@ class PolicyDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView): | ||||
|     model = Policy | ||||
|     template_name = 'generic/delete.html' | ||||
|     success_url = reverse_lazy('passbook_admin:policies') | ||||
|     success_message = _('Successfully updated Policy') | ||||
|     success_message = _('Successfully deleted Policy') | ||||
|  | ||||
|     def get_object(self, queryset=None): | ||||
|         return Policy.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) | ||||
|  | ||||
|  | ||||
| class PolicyTestView(AdminRequiredMixin, DetailView, FormView): | ||||
|     """View to test policy(s)""" | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """passbook Provider administration""" | ||||
| from django.contrib import messages | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.http import Http404 | ||||
| from django.urls import reverse_lazy | ||||
| @ -64,7 +65,11 @@ class ProviderDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView): | ||||
|     model = Provider | ||||
|     template_name = 'generic/delete.html' | ||||
|     success_url = reverse_lazy('passbook_admin:providers') | ||||
|     success_message = _('Successfully updated Provider') | ||||
|     success_message = _('Successfully deleted Provider') | ||||
|  | ||||
|     def get_object(self, queryset=None): | ||||
|         return Provider.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) | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """passbook Source administration""" | ||||
| from django.contrib import messages | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.http import Http404 | ||||
| from django.urls import reverse_lazy | ||||
| @ -66,9 +67,13 @@ class SourceDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView): | ||||
|     """Delete source""" | ||||
|  | ||||
|     model = Source | ||||
|  | ||||
|     template_name = 'generic/delete.html' | ||||
|     success_url = reverse_lazy('passbook_admin:sources') | ||||
|     success_message = _('Successfully updated Source') | ||||
|     success_message = _('Successfully deleted Source') | ||||
|  | ||||
|     def get_object(self, queryset=None): | ||||
|         return Source.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) | ||||
|  | ||||
| @ -1,12 +1,15 @@ | ||||
| """passbook User administration""" | ||||
| from django.contrib import messages | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.shortcuts import get_object_or_404, redirect | ||||
| from django.urls import reverse, reverse_lazy | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.views import View | ||||
| from django.views.generic import DeleteView, ListView, UpdateView | ||||
|  | ||||
| from passbook.admin.mixins import AdminRequiredMixin | ||||
| from passbook.core.forms.users import UserDetailForm | ||||
| from passbook.core.models import User | ||||
| from passbook.core.models import Nonce, User | ||||
|  | ||||
|  | ||||
| class UserListView(AdminRequiredMixin, ListView): | ||||
| @ -31,6 +34,24 @@ class UserDeleteView(SuccessMessageMixin, AdminRequiredMixin, DeleteView): | ||||
|     """Delete user""" | ||||
|  | ||||
|     model = User | ||||
|  | ||||
|     template_name = 'generic/delete.html' | ||||
|     success_url = reverse_lazy('passbook_admin:users') | ||||
|     success_message = _('Successfully updated User') | ||||
|     success_message = _('Successfully deleted User') | ||||
|  | ||||
|     def delete(self, request, *args, **kwargs): | ||||
|         messages.success(self.request, self.success_message) | ||||
|         return super().delete(request, *args, **kwargs) | ||||
|  | ||||
|  | ||||
| class UserPasswordResetView(AdminRequiredMixin, View): | ||||
|     """Get Password reset link for user""" | ||||
|  | ||||
|     # pylint: disable=invalid-name | ||||
|     def get(self, request, pk): | ||||
|         """Create nonce for user and return link""" | ||||
|         user = get_object_or_404(User, pk=pk) | ||||
|         nonce = Nonce.objects.create(user=user) | ||||
|         link = request.build_absolute_uri(reverse( | ||||
|             'passbook_core:auth-password-reset', kwargs={'nonce': nonce.uuid})) | ||||
|         messages.success(request, _('Password reset link: <pre>%(link)s</pre>' % {'link': link})) | ||||
|         return redirect('passbook_admin:users') | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook api""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook audit Header""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
| @ -51,7 +51,10 @@ class AuditEntry(UUIDModel): | ||||
|     def create(action, request, **kwargs): | ||||
|         """Create AuditEntry from arguments""" | ||||
|         client_ip, _ = get_client_ip(request) | ||||
|         user = request.user | ||||
|         if not hasattr(request, 'user'): | ||||
|             user = None | ||||
|         else: | ||||
|             user = request.user | ||||
|         if isinstance(user, AnonymousUser): | ||||
|             user = kwargs.get('user', None) | ||||
|         entry = AuditEntry.objects.create( | ||||
| @ -60,7 +63,7 @@ class AuditEntry(UUIDModel): | ||||
|             # User 255.255.255.255 as fallback if IP cannot be determined | ||||
|             request_ip=client_ip or '255.255.255.255', | ||||
|             context=kwargs) | ||||
|         LOGGER.debug("Logged %s from %s (%s)", action, request.user, client_ip) | ||||
|         LOGGER.debug("Logged %s from %s (%s)", action, user, client_ip) | ||||
|         return entry | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook captcha_factor Header""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook core""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
| @ -12,6 +12,7 @@ from django.views.generic import FormView | ||||
| from passbook.core.auth.factor import AuthenticationFactor | ||||
| from passbook.core.auth.view import AuthenticationView | ||||
| from passbook.core.forms.authentication import PasswordFactorForm | ||||
| from passbook.core.models import Nonce | ||||
| from passbook.lib.config import CONFIG | ||||
|  | ||||
| LOGGER = getLogger(__name__) | ||||
| @ -29,7 +30,8 @@ class PasswordFactor(FormView, AuthenticationFactor): | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         if 'password-forgotten' in request.GET: | ||||
|             # TODO: Save nonce key in database for password reset | ||||
|             nonce = Nonce.objects.create(user=self.pending_user) | ||||
|             LOGGER.debug("DEBUG %s", str(nonce.uuid)) | ||||
|             # TODO: Send email to user | ||||
|             self.authenticator.cleanup() | ||||
|             messages.success(request, _('Check your E-Mails for a password reset link.')) | ||||
|  | ||||
| @ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from passbook.core.models import User | ||||
| from passbook.lib.config import CONFIG | ||||
| from passbook.lib.utils.ui import human_list | ||||
|  | ||||
| LOGGER = getLogger(__name__) | ||||
|  | ||||
| @ -15,13 +16,16 @@ class LoginForm(forms.Form): | ||||
|     """Allow users to login""" | ||||
|  | ||||
|     title = _('Log in to your account') | ||||
|     uid_field = forms.CharField(widget=forms.TextInput(attrs={'placeholder': _('UID')})) | ||||
|     uid_field = forms.CharField() | ||||
|     remember_me = forms.BooleanField(required=False) | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         if CONFIG.y('passbook.uid_fields') == ['email']: | ||||
|         if CONFIG.y('passbook.uid_fields') == ['e-mail']: | ||||
|             self.fields['uid_field'] = forms.EmailField() | ||||
|         self.fields['uid_field'].widget.attrs = { | ||||
|             'placeholder': _(human_list([x.title() for x in CONFIG.y('passbook.uid_fields')])) | ||||
|         } | ||||
|  | ||||
|     def clean_uid_field(self): | ||||
|         """Validate uid_field after EmailValidator if 'email' is the only selected uid_fields""" | ||||
|  | ||||
| @ -27,7 +27,7 @@ class InvitationForm(forms.ModelForm): | ||||
|     class Meta: | ||||
|  | ||||
|         model = Invitation | ||||
|         fields = ['expires', 'fixed_username', 'fixed_email'] | ||||
|         fields = ['expires', 'fixed_username', 'fixed_email', 'needs_confirmation'] | ||||
|         labels = { | ||||
|             'fixed_username': "Force user's username (optional)", | ||||
|             'fixed_email': "Force user's email (optional)", | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| """passbook nexus_upload management command""" | ||||
| from getpass import getpass | ||||
| from base64 import b64decode | ||||
|  | ||||
| import requests | ||||
| from django.core.management.base import BaseCommand | ||||
| @ -24,9 +24,9 @@ class Command(BaseCommand): | ||||
|             help='Nexus root URL', | ||||
|             required=True) | ||||
|         parser.add_argument( | ||||
|             '--user', | ||||
|             '--auth', | ||||
|             action='store', | ||||
|             help='Username to use for Nexus upload', | ||||
|             help='base64-encoded string of username:password', | ||||
|             required=True) | ||||
|         parser.add_argument( | ||||
|             '--method', | ||||
| @ -37,29 +37,21 @@ class Command(BaseCommand): | ||||
|             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() | ||||
|         auth = tuple(b64decode(options.get('auth')).decode('utf-8').split(':', 1)) | ||||
|         responses = {} | ||||
|         url = 'https://%(url)s/repository/%(repo)s//' % options | ||||
|         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'))) | ||||
|                 responses[file] = requests.post(url, data=open(file, mode='rb'), auth=auth) | ||||
|             else: | ||||
|                 responses[file] = requests.put(url+file, data=open(file, mode='rb'), | ||||
|                                                auth=(options.get('user'), options.get('password'))) | ||||
|                 responses[file] = requests.put(url+file, data=open(file, mode='rb'), auth=auth) | ||||
|         self.stdout.write('Upload results:\n') | ||||
|         sep = '-' * 60 | ||||
|         self.stdout.write('%s\n' % sep) | ||||
|  | ||||
							
								
								
									
										31
									
								
								passbook/core/migrations/0012_nonce.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								passbook/core/migrations/0012_nonce.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| # Generated by Django 2.1.7 on 2019-02-25 19:12 | ||||
|  | ||||
| import uuid | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.conf import settings | ||||
| from django.db import migrations, models | ||||
|  | ||||
| import passbook.core.models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('passbook_core', '0011_auto_20190225_1438'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name='Nonce', | ||||
|             fields=[ | ||||
|                 ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), | ||||
|                 ('expires', models.DateTimeField(default=passbook.core.models.default_nonce_duration)), | ||||
|                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||||
|             ], | ||||
|             options={ | ||||
|                 'verbose_name': 'Nonce', | ||||
|                 'verbose_name_plural': 'Nonces', | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
| @ -0,0 +1,18 @@ | ||||
| # Generated by Django 2.1.7 on 2019-02-25 19:57 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('passbook_core', '0012_nonce'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AddField( | ||||
|             model_name='invitation', | ||||
|             name='needs_confirmation', | ||||
|             field=models.BooleanField(default=True), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										25
									
								
								passbook/core/migrations/0014_auto_20190226_0850.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								passbook/core/migrations/0014_auto_20190226_0850.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| # Generated by Django 2.1.7 on 2019-02-26 08:50 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| def create_initial_factor(apps, schema_editor): | ||||
|     """Create initial PasswordFactor if none exists""" | ||||
|     PasswordFactor = apps.get_model("passbook_core", "PasswordFactor") | ||||
|     if not PasswordFactor.objects.exists(): | ||||
|         PasswordFactor.objects.create( | ||||
|             name='password', | ||||
|             slug='password', | ||||
|             order=0, | ||||
|             backends=[] | ||||
|         ) | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('passbook_core', '0013_invitation_needs_confirmation'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RunPython(create_initial_factor) | ||||
|     ] | ||||
| @ -1,5 +1,6 @@ | ||||
| """passbook core models""" | ||||
| import re | ||||
| from datetime import timedelta | ||||
| from logging import getLogger | ||||
| from random import SystemRandom | ||||
| from time import sleep | ||||
| @ -18,6 +19,11 @@ from passbook.lib.models import CreatedUpdatedModel, UUIDModel | ||||
|  | ||||
| LOGGER = getLogger(__name__) | ||||
|  | ||||
|  | ||||
| def default_nonce_duration(): | ||||
|     """Default duration a Nonce is valid""" | ||||
|     return now() + timedelta(hours=4) | ||||
|  | ||||
| class Group(UUIDModel): | ||||
|     """Custom Group model which supports a basic hierarchy""" | ||||
|  | ||||
| @ -386,6 +392,7 @@ class Invitation(UUIDModel): | ||||
|     expires = models.DateTimeField(default=None, blank=True, null=True) | ||||
|     fixed_username = models.TextField(blank=True, default=None) | ||||
|     fixed_email = models.TextField(blank=True, default=None) | ||||
|     needs_confirmation = models.BooleanField(default=True) | ||||
|  | ||||
|     @property | ||||
|     def link(self): | ||||
| @ -399,3 +406,17 @@ class Invitation(UUIDModel): | ||||
|  | ||||
|         verbose_name = _('Invitation') | ||||
|         verbose_name_plural = _('Invitations') | ||||
|  | ||||
| class Nonce(UUIDModel): | ||||
|     """One-time link for password resets/signup-confirmations""" | ||||
|  | ||||
|     expires = models.DateTimeField(default=default_nonce_duration) | ||||
|     user = models.ForeignKey('User', on_delete=models.CASCADE) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return "Nonce %s (expires=%s)" % (self.uuid.hex, self.expires) | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         verbose_name = _('Nonce') | ||||
|         verbose_name_plural = _('Nonces') | ||||
|  | ||||
| @ -74,6 +74,7 @@ INSTALLED_APPS = [ | ||||
|     'passbook.otp.apps.PassbookOTPConfig', | ||||
|     'passbook.captcha_factor.apps.PassbookCaptchaFactorConfig', | ||||
|     'passbook.hibp_policy.apps.PassbookHIBPConfig', | ||||
|     'passbook.pretend.apps.PassbookPretendConfig', | ||||
| ] | ||||
|  | ||||
| # Message Tag fix for bootstrap CSS Classes | ||||
|  | ||||
| @ -5,16 +5,20 @@ | ||||
|  | ||||
| {% block content %} | ||||
| <div class="container"> | ||||
|   {% block above_form %} | ||||
|     <h1>{% trans 'Delete' %}</h1> | ||||
|   {% endblock %} | ||||
|   <div class=""> | ||||
|     <form method="post" class="form-horizontal"> | ||||
|       {% csrf_token %} | ||||
|       <p>Are you sure you want to delete "{{ object }}"?</p> | ||||
|       <a href="{% back %}" class="btn btn-default">{% trans 'Back' %}</a> | ||||
|       <input type="submit" class="btn btn-danger" value="{% trans 'Delete' %}" /> | ||||
|     </form> | ||||
|   </div> | ||||
|     {% block above_form %} | ||||
|     <h1>{% blocktrans with object_type=object|fieldtype|title %}Delete {{ object_type }}{% endblocktrans %}</h1> | ||||
|     {% endblock %} | ||||
|     <div class=""> | ||||
|         <form method="post" class="form-horizontal"> | ||||
|             {% csrf_token %} | ||||
|             <p> | ||||
|                 {% blocktrans with object_type=object|fieldtype|title name=object %} | ||||
|                 Are you sure you want to delete {{ object_type }} "{{ object }}"? | ||||
|                 {% endblocktrans %} | ||||
|             </p> | ||||
|             <a href="{% back %}" class="btn btn-default">{% trans 'Back' %}</a> | ||||
|             <input type="submit" class="btn btn-danger" value="{% trans 'Delete' %}" /> | ||||
|         </form> | ||||
|     </div> | ||||
| </div> | ||||
| {% endblock %} | ||||
| {% endblock %} | ||||
|  | ||||
| @ -29,7 +29,7 @@ | ||||
| <div class="login-pf-page"> | ||||
|     <div class="container-fluid"> | ||||
|         <div class="row"> | ||||
|             <div class="col-sm-6 col-sm-offset-3 col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4"> | ||||
|             <div class="col-sm-12 col-md-8 col-md-offset-2 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="passbook logo" /> | ||||
|  | ||||
| @ -18,7 +18,6 @@ | ||||
| <header class="login-pf-header"> | ||||
|   <h1>{% trans title %}</h1> | ||||
| </header> | ||||
| {% include 'partials/messages.html' %} | ||||
| <form method="POST"> | ||||
|   {% csrf_token %} | ||||
|   {% include 'partials/form_login.html' %} | ||||
|  | ||||
| @ -1,10 +0,0 @@ | ||||
| """passbook core login test""" | ||||
|  | ||||
| from django.test import TestCase | ||||
|  | ||||
|  | ||||
| class LoginTest(TestCase): | ||||
|     """Test login""" | ||||
|  | ||||
|     def test(self): | ||||
|         """Stub test""" | ||||
							
								
								
									
										147
									
								
								passbook/core/tests/test_views_authentication.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								passbook/core/tests/test_views_authentication.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,147 @@ | ||||
| """passbook Core Account Test""" | ||||
|  | ||||
| from django.test import TestCase | ||||
| from django.urls import reverse | ||||
|  | ||||
| from passbook.core.forms.authentication import LoginForm, SignUpForm | ||||
| from passbook.core.models import User | ||||
|  | ||||
|  | ||||
| class TestAuthenticationViews(TestCase): | ||||
|     """passbook Core Account Test""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         super().setUp() | ||||
|         self.sign_up_data = { | ||||
|             'first_name': 'Test', | ||||
|             'last_name': 'User', | ||||
|             'username': 'beryjuorg', | ||||
|             'email': 'unittest@passbook.beryju.org', | ||||
|             'password': 'B3ryju0rg!', | ||||
|             'password_repeat': 'B3ryju0rg!', | ||||
|         } | ||||
|         self.login_data = { | ||||
|             'uid_field': 'unittest@example.com', | ||||
|         } | ||||
|         self.user = User.objects.create_superuser( | ||||
|             username='unittest user', | ||||
|             email='unittest@example.com', | ||||
|             password='test123') | ||||
|  | ||||
|     def test_sign_up_view(self): | ||||
|         """Test account.sign_up view (Anonymous)""" | ||||
|         self.client.logout() | ||||
|         response = self.client.get(reverse('passbook_core:auth-sign-up')) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|  | ||||
|     def test_login_view(self): | ||||
|         """Test account.login view (Anonymous)""" | ||||
|         self.client.logout() | ||||
|         response = self.client.get(reverse('passbook_core:auth-login')) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         # test login with post | ||||
|         form = LoginForm(self.login_data) | ||||
|         self.assertTrue(form.is_valid()) | ||||
|  | ||||
|         response = self.client.post(reverse('passbook_core:auth-login'), data=form.cleaned_data) | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|  | ||||
|     def test_logout_view(self): | ||||
|         """Test account.logout view""" | ||||
|         self.client.force_login(self.user) | ||||
|         response = self.client.get(reverse('passbook_core:auth-logout')) | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|  | ||||
|     def test_sign_up_view_auth(self): | ||||
|         """Test account.sign_up view (Authenticated)""" | ||||
|         self.client.force_login(self.user) | ||||
|         response = self.client.get(reverse('passbook_core:auth-logout')) | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|  | ||||
|     def test_login_view_auth(self): | ||||
|         """Test account.login view (Authenticated)""" | ||||
|         self.client.force_login(self.user) | ||||
|         response = self.client.get(reverse('passbook_core:auth-login')) | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|  | ||||
|     def test_login_view_post(self): | ||||
|         """Test account.login view POST (Anonymous)""" | ||||
|         login_response = self.client.post(reverse('passbook_core:auth-login'), data=self.login_data) | ||||
|         self.assertEqual(login_response.status_code, 302) | ||||
|         self.assertEqual(login_response.url, reverse('passbook_core:auth-process')) | ||||
|  | ||||
|     def test_sign_up_view_post(self): | ||||
|         """Test account.sign_up view POST (Anonymous)""" | ||||
|         form = SignUpForm(self.sign_up_data) | ||||
|         self.assertTrue(form.is_valid()) | ||||
|  | ||||
|         response = self.client.post(reverse('passbook_core:auth-sign-up'), data=form.cleaned_data) | ||||
|         self.assertEqual(response.status_code, 302) | ||||
|  | ||||
|     # def test_reset_password_init_view(self): | ||||
|     #     """Test account.reset_password_init view POST (Anonymous)""" | ||||
|     #     form = SignUpForm(self.sign_up_data) | ||||
|     #     self.assertTrue(form.is_valid()) | ||||
|  | ||||
|     #     res = test_request(accounts.SignUpView.as_view(), | ||||
|     #                        method='POST', | ||||
|     #                        req_kwargs=form.cleaned_data) | ||||
|     #     self.assertEqual(res.status_code, 302) | ||||
|  | ||||
|     #     res = test_request(accounts.PasswordResetInitView.as_view()) | ||||
|     #     self.assertEqual(res.status_code, 200) | ||||
|  | ||||
|     # def test_resend_confirmation(self): | ||||
|     #     """Test AccountController.resend_confirmation""" | ||||
|     #     form = SignUpForm(self.sign_up_data) | ||||
|     #     self.assertTrue(form.is_valid()) | ||||
|  | ||||
|     #     res = test_request(accounts.SignUpView.as_view(), | ||||
|     #                        method='POST', | ||||
|     #                        req_kwargs=form.cleaned_data) | ||||
|     #     self.assertEqual(res.status_code, 302) | ||||
|     #     user = User.objects.get(email=self.sign_up_data['email']) | ||||
|     #     # Invalidate all other links for this user | ||||
|     #     old_acs = AccountConfirmation.objects.filter( | ||||
|     #         user=user) | ||||
|     #     for old_ac in old_acs: | ||||
|     #         old_ac.confirmed = True | ||||
|     #         old_ac.save() | ||||
|     #     # Create Account Confirmation UUID | ||||
|     #     new_ac = AccountConfirmation.objects.create(user=user) | ||||
|     #     self.assertFalse(new_ac.is_expired) | ||||
|     #     on_user_confirm_resend.send( | ||||
|     #         sender=None, | ||||
|     #         user=user, | ||||
|     #         request=None) | ||||
|  | ||||
|     # def test_reset_passowrd(self): | ||||
|     #     """Test reset password POST""" | ||||
|     #     # Signup user first | ||||
|     #     sign_up_form = SignUpForm(self.sign_up_data) | ||||
|     #     self.assertTrue(sign_up_form.is_valid()) | ||||
|  | ||||
|     #     sign_up_res = test_request(accounts.SignUpView.as_view(), | ||||
|     #                               method='POST', | ||||
|     #                               req_kwargs=sign_up_form.cleaned_data) | ||||
|     #     self.assertEqual(sign_up_res.status_code, 302) | ||||
|  | ||||
|     #     user = User.objects.get(email=self.sign_up_data['email']) | ||||
|     #     # Invalidate all other links for this user | ||||
|     #     old_acs = AccountConfirmation.objects.filter( | ||||
|     #         user=user) | ||||
|     #     for old_ac in old_acs: | ||||
|     #         old_ac.confirmed = True | ||||
|     #         old_ac.save() | ||||
|     #     # Create Account Confirmation UUID | ||||
|     #     new_ac = AccountConfirmation.objects.create(user=user) | ||||
|     #     self.assertFalse(new_ac.is_expired) | ||||
|     #     uuid = AccountConfirmation.objects.filter(user=user).first().pk | ||||
|     #     reset_res = test_request(accounts.PasswordResetFinishView.as_view(), | ||||
|     #                              method='POST', | ||||
|     #                              user=user, | ||||
|     #                              url_kwargs={'uuid': uuid}, | ||||
|     #                              req_kwargs=self.change_data) | ||||
|  | ||||
|     #     self.assertEqual(reset_res.status_code, 302) | ||||
|     #     self.assertEqual(reset_res.url, reverse('common-index')) | ||||
							
								
								
									
										21
									
								
								passbook/core/tests/test_views_overview.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								passbook/core/tests/test_views_overview.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| """passbook user view tests""" | ||||
| from django.shortcuts import reverse | ||||
| from django.test import TestCase | ||||
|  | ||||
| from passbook.core.models import User | ||||
|  | ||||
|  | ||||
| class TestOverviewViews(TestCase): | ||||
|     """Test Overview Views""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         super().setUp() | ||||
|         self.user = User.objects.create_superuser( | ||||
|             username='unittest user', | ||||
|             email='unittest@example.com', | ||||
|             password='test123') | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|     def test_overview(self): | ||||
|         """Test UserSettingsView""" | ||||
|         self.assertEqual(self.client.get(reverse('passbook_core:overview')).status_code, 200) | ||||
							
								
								
									
										43
									
								
								passbook/core/tests/test_views_user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								passbook/core/tests/test_views_user.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | ||||
| """passbook user view tests""" | ||||
| from django.shortcuts import reverse | ||||
| from django.test import TestCase | ||||
|  | ||||
| from passbook.core.forms.users import PasswordChangeForm | ||||
| from passbook.core.models import User | ||||
|  | ||||
|  | ||||
| class TestUserViews(TestCase): | ||||
|     """Test User Views""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         super().setUp() | ||||
|         self.user = User.objects.create_superuser( | ||||
|             username='unittest user', | ||||
|             email='unittest@example.com', | ||||
|             password='test123') | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|     def test_user_settings(self): | ||||
|         """Test UserSettingsView""" | ||||
|         self.assertEqual(self.client.get(reverse('passbook_core:user-settings')).status_code, 200) | ||||
|  | ||||
|     def test_user_delete(self): | ||||
|         """Test UserDeleteView""" | ||||
|         self.assertEqual(self.client.post(reverse('passbook_core:user-delete')).status_code, 302) | ||||
|         self.assertEqual(User.objects.filter(username='unittest user').exists(), False) | ||||
|         self.setUp() | ||||
|  | ||||
|     def test_user_change_password(self): | ||||
|         """Test UserChangePasswordView""" | ||||
|         form_data = { | ||||
|             'password': 'test2', | ||||
|             'password_repeat': 'test2' | ||||
|         } | ||||
|         form = PasswordChangeForm(data=form_data) | ||||
|         self.assertTrue(form.is_valid()) | ||||
|         self.assertEqual(self.client.get( | ||||
|             reverse('passbook_core:user-change-password')).status_code, 200) | ||||
|         self.assertEqual(self.client.post( | ||||
|             reverse('passbook_core:user-change-password'), data=form_data).status_code, 302) | ||||
|         self.user.refresh_from_db() | ||||
|         self.assertTrue(self.user.check_password('test2')) | ||||
							
								
								
									
										25
									
								
								passbook/core/tests/test_views_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								passbook/core/tests/test_views_utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | ||||
| """passbook util view tests""" | ||||
|  | ||||
| from django.test import RequestFactory, TestCase | ||||
|  | ||||
| from passbook.core.views.utils import LoadingView, PermissionDeniedView | ||||
|  | ||||
|  | ||||
| class TestUtilViews(TestCase): | ||||
|     """Test Utility Views""" | ||||
|  | ||||
|     def setUp(self): | ||||
|         self.factory = RequestFactory() | ||||
|  | ||||
|     def test_loading_view(self): | ||||
|         """Test loading view""" | ||||
|         request = self.factory.get('something') | ||||
|         response = LoadingView.as_view(target_url='somestring')(request) | ||||
|         response.render() | ||||
|         self.assertIn('somestring', response.content.decode('utf-8')) | ||||
|  | ||||
|     def test_permission_denied_view(self): | ||||
|         """Test PermissionDeniedView""" | ||||
|         request = self.factory.get('something') | ||||
|         response = PermissionDeniedView.as_view()(request) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
| @ -19,13 +19,17 @@ core_urls = [ | ||||
|     path('auth/login/', authentication.LoginView.as_view(), name='auth-login'), | ||||
|     path('auth/logout/', authentication.LogoutView.as_view(), name='auth-logout'), | ||||
|     path('auth/sign_up/', authentication.SignUpView.as_view(), name='auth-sign-up'), | ||||
|     path('auth/sign_up/<uuid:nonce>/confirm/', authentication.SignUpConfirmView.as_view(), | ||||
|          name='auth-sign-up-confirm'), | ||||
|     path('auth/process/denied/', view.FactorPermissionDeniedView.as_view(), name='auth-denied'), | ||||
|     path('auth/password/reset/<uuid:nonce>/', authentication.PasswordResetView.as_view(), | ||||
|          name='auth-password-reset'), | ||||
|     path('auth/process/', view.AuthenticationView.as_view(), name='auth-process'), | ||||
|     path('auth/process/<slug:factor>/', view.AuthenticationView.as_view(), name='auth-process'), | ||||
|     # User views | ||||
|     path('user/', user.UserSettingsView.as_view(), name='user-settings'), | ||||
|     path('user/delete/', user.UserDeleteView.as_view(), name='user-delete'), | ||||
|     path('user/change_password/', user.UserChangePasswordView.as_view(), | ||||
|     path('_/user/', user.UserSettingsView.as_view(), name='user-settings'), | ||||
|     path('_/user/delete/', user.UserDeleteView.as_view(), name='user-delete'), | ||||
|     path('_/user/change_password/', user.UserChangePasswordView.as_view(), | ||||
|          name='user-change-password'), | ||||
|     # Overview | ||||
|     path('', overview.OverviewView.as_view(), name='overview'), | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| """passbook access helper classes""" | ||||
| from logging import getLogger | ||||
|  | ||||
| from django.http import Http404 | ||||
| from django.contrib import messages | ||||
| from django.utils.translation import gettext as _ | ||||
|  | ||||
| from passbook.core.models import Application | ||||
|  | ||||
| @ -11,14 +12,18 @@ class AccessMixin: | ||||
|     """Mixin class for usage in Authorization views. | ||||
|     Provider functions to check application access, etc""" | ||||
|  | ||||
|     # request is set by view but since this Mixin has no base class | ||||
|     request = None | ||||
|  | ||||
|     def provider_to_application(self, provider): | ||||
|         """Lookup application assigned to provider, throw error if no application assigned""" | ||||
|         try: | ||||
|             return provider.application | ||||
|         except Application.DoesNotExist as exc: | ||||
|             # TODO: Log that no provider has no application assigned | ||||
|             LOGGER.warning('Provider "%s" has no application assigned...', provider) | ||||
|             raise Http404 from exc | ||||
|             messages.error(self.request, _('Provider "%(name)s" has no application assigned' % { | ||||
|                 'name': provider | ||||
|                 })) | ||||
|             raise exc | ||||
|  | ||||
|     def user_has_access(self, application, user): | ||||
|         """Check if user has access to application.""" | ||||
|  | ||||
| @ -3,17 +3,17 @@ from logging import getLogger | ||||
| from typing import Dict | ||||
|  | ||||
| from django.contrib import messages | ||||
| from django.contrib.auth import logout | ||||
| from django.contrib.auth import login, logout | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.shortcuts import redirect, reverse | ||||
| from django.shortcuts import get_object_or_404, redirect, reverse | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.views import View | ||||
| from django.views.generic import FormView | ||||
|  | ||||
| from passbook.core.auth.view import AuthenticationView | ||||
| from passbook.core.forms.authentication import LoginForm, SignUpForm | ||||
| from passbook.core.models import Invitation, Source, User | ||||
| from passbook.core.models import Invitation, Nonce, Source, User | ||||
| from passbook.core.signals import invitation_used, user_signed_up | ||||
| from passbook.lib.config import CONFIG | ||||
|  | ||||
| @ -52,6 +52,9 @@ class LoginView(UserPassesTestMixin, FormView): | ||||
|     def get_user(self, uid_value) -> User: | ||||
|         """Find user instance. Returns None if no user was found.""" | ||||
|         for search_field in CONFIG.y('passbook.uid_fields'): | ||||
|             # Workaround for E-Mail -> email | ||||
|             if search_field == 'e-mail': | ||||
|                 search_field = 'email' | ||||
|             users = User.objects.filter(**{search_field: uid_value}) | ||||
|             if users.exists(): | ||||
|                 LOGGER.debug("Found user %s with uid_field %s", users.first(), search_field) | ||||
| @ -136,6 +139,15 @@ class SignUpView(UserPassesTestMixin, FormView): | ||||
|     def form_valid(self, form: SignUpForm) -> HttpResponse: | ||||
|         """Create user""" | ||||
|         self._user = SignUpView.create_user(form.cleaned_data, self.request) | ||||
|         needs_confirmation = True | ||||
|         if self._invitation and not self._invitation.needs_confirmation: | ||||
|             needs_confirmation = False | ||||
|         if needs_confirmation: | ||||
|             nonce = Nonce.objects.create(user=self._user) | ||||
|             LOGGER.debug(str(nonce.uuid)) | ||||
|             # TODO: Send E-Mail to user | ||||
|             self._user.is_active = False | ||||
|             self._user.save() | ||||
|         self.consume_invitation() | ||||
|         messages.success(self.request, _("Successfully signed up!")) | ||||
|         LOGGER.debug("Successfully signed up %s", | ||||
| @ -182,8 +194,35 @@ class SignUpView(UserPassesTestMixin, FormView): | ||||
|             sender=SignUpView, | ||||
|             user=new_user, | ||||
|             request=request) | ||||
|         # TODO: Implement Verification, via email or others | ||||
|         # if needs_confirmation: | ||||
|         #     Create Account Confirmation UUID | ||||
|         #     AccountConfirmation.objects.create(user=new_user) | ||||
|         return new_user | ||||
|  | ||||
| class SignUpConfirmView(View): | ||||
|     """Confirm registration from Nonce""" | ||||
|  | ||||
|     def get(self, request, nonce): | ||||
|         """Verify UUID and activate user""" | ||||
|         nonce = get_object_or_404(Nonce, uuid=nonce) | ||||
|         nonce.user.is_active = True | ||||
|         nonce.user.save() | ||||
|         # Workaround: hardcoded reference to ModelBackend, needs testing | ||||
|         nonce.user.backend = 'django.contrib.auth.backends.ModelBackend' | ||||
|         login(request, nonce.user) | ||||
|         nonce.delete() | ||||
|         messages.success(request, _('Successfully confirmed registration.')) | ||||
|         return redirect('passbook_core:overview') | ||||
|  | ||||
|  | ||||
| class PasswordResetView(View): | ||||
|     """Temporarily authenticate User and allow them to reset their password""" | ||||
|  | ||||
|     def get(self, request, nonce): | ||||
|         """Authenticate user with nonce and redirect to password change view""" | ||||
|         # 3. (Optional) Trap user in password change view | ||||
|         nonce = get_object_or_404(Nonce, uuid=nonce) | ||||
|         # Workaround: hardcoded reference to ModelBackend, needs testing | ||||
|         nonce.user.backend = 'django.contrib.auth.backends.ModelBackend' | ||||
|         login(request, nonce.user) | ||||
|         nonce.delete() | ||||
|         messages.success(request, _(('Temporarily authenticated with Nonce, ' | ||||
|                                      'please change your password'))) | ||||
|         return redirect('passbook_core:user-change-password') | ||||
|  | ||||
| @ -11,6 +11,7 @@ from passbook.lib.config import CONFIG | ||||
|  | ||||
| class UserSettingsView(UpdateView): | ||||
|     """Update User settings""" | ||||
|  | ||||
|     template_name = 'user/settings.html' | ||||
|     form_class = UserDetailForm | ||||
|  | ||||
|  | ||||
							
								
								
									
										17
									
								
								passbook/hibp_policy/migrations/0002_auto_20190225_1912.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								passbook/hibp_policy/migrations/0002_auto_20190225_1912.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| # Generated by Django 2.1.7 on 2019-02-25 19:12 | ||||
|  | ||||
| from django.db import migrations | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ('passbook_hibp_policy', '0001_initial'), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterModelOptions( | ||||
|             name='haveibeenpwendpolicy', | ||||
|             options={'verbose_name': 'have i been pwned Policy', 'verbose_name_plural': 'have i been pwned Policies'}, | ||||
|         ), | ||||
|     ] | ||||
| @ -1,2 +1,2 @@ | ||||
| """Passbook ldap app Header""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook lib""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
							
								
								
									
										7
									
								
								passbook/lib/utils/ui.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								passbook/lib/utils/ui.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,7 @@ | ||||
| """passbook UI utils""" | ||||
|  | ||||
| def human_list(_list) -> str: | ||||
|     """Convert a list of items into 'a, b or c'""" | ||||
|     last_item = _list.pop() | ||||
|     result = ', '.join(_list) | ||||
|     return '%s or %s' % (result, last_item) | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook oauth_client Header""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook oauth_provider Header""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
| @ -22,6 +22,8 @@ OAUTH2_PROVIDER = { | ||||
|     'SCOPES': { | ||||
|         'openid:userinfo': 'Access OpenID Userinfo', | ||||
|         # 'write': 'Write scope', | ||||
|         # 'groups': 'Access to your groups' | ||||
|         # 'groups': 'Access to your groups', | ||||
|         'user:email': 'GitHub Compatibility: User E-Mail', | ||||
|         'read:org': 'GitHub Compatibility: User Groups', | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -11,7 +11,6 @@ | ||||
| <header class="login-pf-header"> | ||||
|   <h1>{% trans 'Authorize Application' %}</h1> | ||||
| </header> | ||||
| {% include 'partials/messages.html' %} | ||||
| <form method="POST"> | ||||
|     {% csrf_token %} | ||||
|     {% if not error %} | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| """passbook oauth_provider urls""" | ||||
|  | ||||
| from django.urls import include, path | ||||
| from django.urls import path | ||||
| from oauth2_provider import views | ||||
|  | ||||
| from passbook.oauth_provider.views import oauth2 | ||||
|  | ||||
| @ -13,5 +14,8 @@ urlpatterns = [ | ||||
|     path('authorize/permission_denied/', oauth2.OAuthPermissionDenied.as_view(), | ||||
|          name='oauth2-permission-denied'), | ||||
|     # OAuth API | ||||
|     path('', include('oauth2_provider.urls', namespace='oauth2_provider')), | ||||
|     path("authorize/", views.AuthorizationView.as_view(), name="authorize"), | ||||
|     path("token/", views.TokenView.as_view(), name="token"), | ||||
|     path("revoke_token/", views.RevokeTokenView.as_view(), name="revoke-token"), | ||||
|     path("introspect/", views.IntrospectTokenView.as_view(), name="introspect"), | ||||
| ] | ||||
|  | ||||
| @ -7,6 +7,7 @@ from django.utils.translation import ugettext as _ | ||||
| from oauth2_provider.views.base import AuthorizationView | ||||
|  | ||||
| from passbook.audit.models import AuditEntry | ||||
| from passbook.core.models import Application | ||||
| from passbook.core.views.access import AccessMixin | ||||
| from passbook.core.views.utils import LoadingView, PermissionDeniedView | ||||
| from passbook.oauth_provider.models import OAuth2Provider | ||||
| @ -38,14 +39,17 @@ class PassbookAuthorizationView(AccessMixin, AuthorizationView): | ||||
|         # Get client_id to get provider, so we can update skip_authorization field | ||||
|         client_id = request.GET.get('client_id') | ||||
|         provider = get_object_or_404(OAuth2Provider, client_id=client_id) | ||||
|         application = self.provider_to_application(provider) | ||||
|         try: | ||||
|             application = self.provider_to_application(provider) | ||||
|         except Application.DoesNotExist: | ||||
|             return redirect('passbook_oauth_provider:oauth2-permission-denied') | ||||
|         # Update field here so oauth-toolkit does work for us | ||||
|         provider.skip_authorization = application.skip_authorization | ||||
|         provider.save() | ||||
|         self._application = application | ||||
|         # Check permissions | ||||
|         if not self.user_has_access(self._application, request.user): | ||||
|             return redirect(reverse('passbook_oauth_provider:oauth2-permission-denied')) | ||||
|             return redirect('passbook_oauth_provider:oauth2-permission-denied') | ||||
|         actual_response = super().dispatch(request, *args, **kwargs) | ||||
|         if actual_response.status_code == 400: | ||||
|             LOGGER.debug(request.GET.get('redirect_uri')) | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook otp Header""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
| @ -6,7 +6,7 @@ from logging import getLogger | ||||
| from django.contrib import messages | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.http import Http404, HttpRequest, HttpResponse | ||||
| from django.shortcuts import redirect | ||||
| from django.shortcuts import get_object_or_404, redirect | ||||
| from django.urls import reverse | ||||
| from django.utils.translation import ugettext as _ | ||||
| from django.views import View | ||||
| @ -41,28 +41,27 @@ class UserSettingsView(LoginRequiredMixin, TemplateView): | ||||
|         kwargs['state'] = totp_devices.exists() and static.exists() | ||||
|         return kwargs | ||||
|  | ||||
| class DisableView(LoginRequiredMixin, TemplateView): | ||||
| class DisableView(LoginRequiredMixin, View): | ||||
|     """Disable TOTP for user""" | ||||
|     # TODO: Use Django DeleteView with custom delete? | ||||
|     # def | ||||
|     # # Delete all the devices for user | ||||
|     # static = get_object_or_404(StaticDevice, user=request.user, confirmed=True) | ||||
|     # static_tokens = StaticToken.objects.filter(device=static).order_by('token') | ||||
|     # totp = TOTPDevice.objects.filter(user=request.user, confirmed=True) | ||||
|     # static.delete() | ||||
|     # totp.delete() | ||||
|     # for token in static_tokens: | ||||
|     #     token.delete() | ||||
|     # messages.success(request, 'Successfully disabled TOTP') | ||||
|     # # Create event with email notification | ||||
|     # # Event.create( | ||||
|     # #     user=request.user, | ||||
|     # #     message=_('You disabled TOTP.'), | ||||
|     # #     current=True, | ||||
|     # #     request=request, | ||||
|     # #     send_notification=True) | ||||
|     # return redirect(reverse('passbook_core:overview')) | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         """Delete all the devices for user""" | ||||
|         static = get_object_or_404(StaticDevice, user=request.user, confirmed=True) | ||||
|         static_tokens = StaticToken.objects.filter(device=static).order_by('token') | ||||
|         totp = TOTPDevice.objects.filter(user=request.user, confirmed=True) | ||||
|         static.delete() | ||||
|         totp.delete() | ||||
|         for token in static_tokens: | ||||
|             token.delete() | ||||
|         messages.success(request, 'Successfully disabled OTP') | ||||
|         # Create event with email notification | ||||
|         # Event.create( | ||||
|         #     user=request.user, | ||||
|         #     message=_('You disabled TOTP.'), | ||||
|         #     current=True, | ||||
|         #     request=request, | ||||
|         #     send_notification=True) | ||||
|         return redirect(reverse('passbook_otp:otp-user-settings')) | ||||
|  | ||||
| class EnableView(LoginRequiredMixin, FormView): | ||||
|     """View to set up OTP""" | ||||
|  | ||||
							
								
								
									
										0
									
								
								passbook/pretend/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/pretend/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										11
									
								
								passbook/pretend/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								passbook/pretend/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| """passbook pretend config""" | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class PassbookPretendConfig(AppConfig): | ||||
|     """passbook pretend config""" | ||||
|  | ||||
|     name = 'passbook.pretend' | ||||
|     label = 'passbook_pretend' | ||||
|     verbose_name = 'passbook Pretender' | ||||
|     mountpoint = '' | ||||
							
								
								
									
										16
									
								
								passbook/pretend/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								passbook/pretend/urls.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| """passbook pretend urls""" | ||||
| from django.urls import include, path | ||||
| from oauth2_provider.views import TokenView | ||||
|  | ||||
| from passbook.oauth_provider.views.oauth2 import PassbookAuthorizationView | ||||
| from passbook.pretend.views.github import GitHubUserView | ||||
|  | ||||
| github_urlpatterns = [ | ||||
|     path('login/oauth/authorize', PassbookAuthorizationView.as_view(), name='github-authorize'), | ||||
|     path('login/oauth/access_token', TokenView.as_view(), name='github-access-token'), | ||||
|     path('user', GitHubUserView.as_view(), name='github-user'), | ||||
| ] | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path('', include(github_urlpatterns)) | ||||
| ] | ||||
							
								
								
									
										0
									
								
								passbook/pretend/views/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/pretend/views/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										55
									
								
								passbook/pretend/views/github.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								passbook/pretend/views/github.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| """passbook pretend GitHub Views""" | ||||
| from django.http import JsonResponse | ||||
| from django.views import View | ||||
|  | ||||
|  | ||||
| class GitHubUserView(View): | ||||
|     """Emulate GitHub's /user API Endpoint""" | ||||
|  | ||||
|     def get(self, request): | ||||
|         """Emulate GitHub's /user API Endpoint""" | ||||
|         return JsonResponse({ | ||||
|             "login": request.user.username, | ||||
|             "id": request.user.pk, | ||||
|             "node_id": "", | ||||
|             "avatar_url": "", | ||||
|             "gravatar_id": "", | ||||
|             "url": "", | ||||
|             "html_url": "", | ||||
|             "followers_url": "", | ||||
|             "following_url": "", | ||||
|             "gists_url": "", | ||||
|             "starred_url": "", | ||||
|             "subscriptions_url": "", | ||||
|             "organizations_url": "", | ||||
|             "repos_url": "", | ||||
|             "events_url": "", | ||||
|             "received_events_url": "", | ||||
|             "type": "User", | ||||
|             "site_admin": False, | ||||
|             "name": "%s %s" % (request.user.first_name, request.user.last_name), | ||||
|             "company": "", | ||||
|             "blog": "", | ||||
|             "location": "", | ||||
|             "email": request.user.email, | ||||
|             "hireable": False, | ||||
|             "bio": "", | ||||
|             "public_repos": 0, | ||||
|             "public_gists": 0, | ||||
|             "followers": 0, | ||||
|             "following": 0, | ||||
|             "created_at": request.user.date_joined, | ||||
|             "updated_at": request.user.date_joined, | ||||
|             "private_gists": 0, | ||||
|             "total_private_repos": 0, | ||||
|             "owned_private_repos": 0, | ||||
|             "disk_usage": 0, | ||||
|             "collaborators": 0, | ||||
|             "two_factor_authentication": True, | ||||
|             "plan": { | ||||
|                 "name": "None", | ||||
|                 "space": 0, | ||||
|                 "private_repos": 0, | ||||
|                 "collaborators": 0 | ||||
|             } | ||||
|         }) | ||||
| @ -1,2 +1,2 @@ | ||||
| """passbook saml_idp Header""" | ||||
| __version__ = '0.0.8-alpha' | ||||
| __version__ = '0.0.10-alpha' | ||||
|  | ||||
| @ -11,7 +11,6 @@ | ||||
| <header class="login-pf-header"> | ||||
|   <h1>{% trans 'Authorize Application' %}</h1> | ||||
| </header> | ||||
| {% include 'partials/messages.html' %} | ||||
| <form method="POST" action="{{ acs_url }}">> | ||||
|   {% csrf_token %} | ||||
|   <input type="hidden" name="ACSUrl" value="{{ acs_url }}"> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	