audit: rewrite to be independent of django http requests, allow custom actions
This commit is contained in:
parent
6c358c4e0a
commit
807cbbeaaf
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 2.2.8 on 2019-12-05 14:07
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import passbook.audit.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_audit', '0002_auto_20191028_0829'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='event',
|
||||||
|
options={'verbose_name': 'Audit Event', 'verbose_name_plural': 'Audit Events'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='event',
|
||||||
|
name='action',
|
||||||
|
field=models.TextField(choices=[('LOGIN', 'login'), ('LOGIN_FAILED', 'login_failed'), ('LOGOUT', 'logout'), ('AUTHORIZE_APPLICATION', 'authorize_application'), ('SUSPICIOUS_REQUEST', 'suspicious_request'), ('SIGN_UP', 'sign_up'), ('PASSWORD_RESET', 'password_reset'), ('INVITE_CREATED', 'invitation_created'), ('INVITE_USED', 'invitation_used'), ('CUSTOM', 'custom')]),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 2.2.8 on 2019-12-05 15:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('passbook_audit', '0003_auto_20191205_1407'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='event',
|
||||||
|
name='request_ip',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='event',
|
||||||
|
name='client_ip',
|
||||||
|
field=models.GenericIPAddressField(null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,10 +1,16 @@
|
||||||
"""passbook audit models"""
|
"""passbook audit models"""
|
||||||
|
from enum import Enum
|
||||||
|
from inspect import getmodule, stack
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.contrib.postgres.fields import JSONField
|
from django.contrib.postgres.fields import JSONField
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.http import HttpRequest
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from guardian.shortcuts import get_anonymous_user
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.lib.models import UUIDModel
|
from passbook.lib.models import UUIDModel
|
||||||
|
@ -12,64 +18,86 @@ from passbook.lib.utils.http import get_client_ip
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
class EventAction(Enum):
|
||||||
|
"""All possible actions to save into the audit log"""
|
||||||
|
|
||||||
|
LOGIN = 'login'
|
||||||
|
LOGIN_FAILED = 'login_failed'
|
||||||
|
LOGOUT = 'logout'
|
||||||
|
AUTHORIZE_APPLICATION = 'authorize_application'
|
||||||
|
SUSPICIOUS_REQUEST = 'suspicious_request'
|
||||||
|
SIGN_UP = 'sign_up'
|
||||||
|
PASSWORD_RESET = 'password_reset' # noqa # nosec
|
||||||
|
INVITE_CREATED = 'invitation_created'
|
||||||
|
INVITE_USED = 'invitation_used'
|
||||||
|
CUSTOM = 'custom'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def as_choices():
|
||||||
|
"""Generate choices of actions used for database"""
|
||||||
|
return tuple((x, y.value) for x, y in EventAction.__members__.items())
|
||||||
|
|
||||||
|
|
||||||
class Event(UUIDModel):
|
class Event(UUIDModel):
|
||||||
"""An individual audit log event"""
|
"""An individual audit log event"""
|
||||||
|
|
||||||
ACTION_LOGIN = 'login'
|
|
||||||
ACTION_LOGIN_FAILED = 'login_failed'
|
|
||||||
ACTION_LOGOUT = 'logout'
|
|
||||||
ACTION_AUTHORIZE_APPLICATION = 'authorize_application'
|
|
||||||
ACTION_SUSPICIOUS_REQUEST = 'suspicious_request'
|
|
||||||
ACTION_SIGN_UP = 'sign_up'
|
|
||||||
ACTION_PASSWORD_RESET = 'password_reset' # noqa # nosec
|
|
||||||
ACTION_INVITE_CREATED = 'invitation_created'
|
|
||||||
ACTION_INVITE_USED = 'invitation_used'
|
|
||||||
ACTIONS = (
|
|
||||||
(ACTION_LOGIN, ACTION_LOGIN),
|
|
||||||
(ACTION_LOGIN_FAILED, ACTION_LOGIN_FAILED),
|
|
||||||
(ACTION_LOGOUT, ACTION_LOGOUT),
|
|
||||||
(ACTION_AUTHORIZE_APPLICATION, ACTION_AUTHORIZE_APPLICATION),
|
|
||||||
(ACTION_SUSPICIOUS_REQUEST, ACTION_SUSPICIOUS_REQUEST),
|
|
||||||
(ACTION_SIGN_UP, ACTION_SIGN_UP),
|
|
||||||
(ACTION_PASSWORD_RESET, ACTION_PASSWORD_RESET),
|
|
||||||
(ACTION_INVITE_CREATED, ACTION_INVITE_CREATED),
|
|
||||||
(ACTION_INVITE_USED, ACTION_INVITE_USED),
|
|
||||||
)
|
|
||||||
|
|
||||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL)
|
||||||
action = models.TextField(choices=ACTIONS)
|
action = models.TextField(choices=EventAction.as_choices())
|
||||||
date = models.DateTimeField(auto_now_add=True)
|
date = models.DateTimeField(auto_now_add=True)
|
||||||
app = models.TextField()
|
app = models.TextField()
|
||||||
context = JSONField(default=dict, blank=True)
|
context = JSONField(default=dict, blank=True)
|
||||||
request_ip = models.GenericIPAddressField()
|
client_ip = models.GenericIPAddressField(null=True)
|
||||||
created = models.DateTimeField(auto_now_add=True)
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create(action, request, **kwargs):
|
def _get_app_from_request(request: HttpRequest) -> str:
|
||||||
"""Create Event from arguments"""
|
if not isinstance(request, HttpRequest):
|
||||||
client_ip = get_client_ip(request)
|
return ""
|
||||||
if not hasattr(request, 'user'):
|
return request.resolver_match.app_name
|
||||||
user = None
|
|
||||||
else:
|
@staticmethod
|
||||||
user = request.user
|
def new(action: EventAction,
|
||||||
if isinstance(user, AnonymousUser):
|
app: Optional[str] = None,
|
||||||
user = kwargs.get('user', None)
|
_inspect_offset: int = 1,
|
||||||
entry = Event.objects.create(
|
**kwargs) -> 'Event':
|
||||||
action=action,
|
"""Create new Event instance from arguments. Instance is NOT saved."""
|
||||||
user=user,
|
if not isinstance(action, EventAction):
|
||||||
# User 255.255.255.255 as fallback if IP cannot be determined
|
raise ValueError(f"action must be EventAction instance but was {type(action)}")
|
||||||
request_ip=client_ip or '255.255.255.255',
|
if not app:
|
||||||
|
app = getmodule(stack()[_inspect_offset][0]).__name__
|
||||||
|
event = Event(
|
||||||
|
action=action.value,
|
||||||
|
app=app,
|
||||||
context=kwargs)
|
context=kwargs)
|
||||||
LOGGER.debug("Created Audit entry", action=action,
|
LOGGER.debug("Created Audit event", action=action, context=kwargs)
|
||||||
user=user, from_ip=client_ip, context=kwargs)
|
return event
|
||||||
return entry
|
|
||||||
|
def from_http(self, request: HttpRequest,
|
||||||
|
user: Optional[settings.AUTH_USER_MODEL] = None) -> 'Event':
|
||||||
|
"""Add data from a Django-HttpRequest, allowing the creation of
|
||||||
|
Events independently from requests.
|
||||||
|
`user` arguments optionally overrides user from requests."""
|
||||||
|
if hasattr(request, 'user'):
|
||||||
|
if isinstance(request.user, AnonymousUser):
|
||||||
|
self.user = get_anonymous_user()
|
||||||
|
else:
|
||||||
|
self.user = request.user
|
||||||
|
if user:
|
||||||
|
self.user = user
|
||||||
|
# User 255.255.255.255 as fallback if IP cannot be determined
|
||||||
|
self.client_ip = get_client_ip(request) or '255.255.255.255'
|
||||||
|
# If there's no app set, we get it from the requests too
|
||||||
|
if not self.app:
|
||||||
|
self.app = Event._get_app_from_request(request)
|
||||||
|
self.save()
|
||||||
|
return self
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if not self._state.adding:
|
if not self._state.adding:
|
||||||
raise ValidationError("you may not edit an existing %s" % self._meta.model_name)
|
raise ValidationError("you may not edit an existing %s" % self._meta.model_name)
|
||||||
super().save(*args, **kwargs)
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _('Audit Entry')
|
verbose_name = _('Audit Event')
|
||||||
verbose_name_plural = _('Audit Entries')
|
verbose_name_plural = _('Audit Events')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
from django.contrib.auth.signals import user_logged_in, user_logged_out
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from passbook.audit.models import Event
|
from passbook.audit.models import Event, EventAction
|
||||||
from passbook.core.signals import (invitation_created, invitation_used,
|
from passbook.core.signals import (invitation_created, invitation_used,
|
||||||
user_signed_up)
|
user_signed_up)
|
||||||
|
|
||||||
|
@ -10,26 +10,24 @@ from passbook.core.signals import (invitation_created, invitation_used,
|
||||||
@receiver(user_logged_in)
|
@receiver(user_logged_in)
|
||||||
def on_user_logged_in(sender, request, user, **kwargs):
|
def on_user_logged_in(sender, request, user, **kwargs):
|
||||||
"""Log successful login"""
|
"""Log successful login"""
|
||||||
Event.create(Event.ACTION_LOGIN, request)
|
Event.new(EventAction.LOGIN).from_http(request)
|
||||||
|
|
||||||
@receiver(user_logged_out)
|
@receiver(user_logged_out)
|
||||||
def on_user_logged_out(sender, request, user, **kwargs):
|
def on_user_logged_out(sender, request, user, **kwargs):
|
||||||
"""Log successfully logout"""
|
"""Log successfully logout"""
|
||||||
Event.create(Event.ACTION_LOGOUT, request)
|
Event.new(EventAction.LOGOUT).from_http(request)
|
||||||
|
|
||||||
@receiver(user_signed_up)
|
@receiver(user_signed_up)
|
||||||
def on_user_signed_up(sender, request, user, **kwargs):
|
def on_user_signed_up(sender, request, user, **kwargs):
|
||||||
"""Log successfully signed up"""
|
"""Log successfully signed up"""
|
||||||
Event.create(Event.ACTION_SIGN_UP, request)
|
Event.new(EventAction.SIGN_UP).from_http(request)
|
||||||
|
|
||||||
@receiver(invitation_created)
|
@receiver(invitation_created)
|
||||||
def on_invitation_created(sender, request, invitation, **kwargs):
|
def on_invitation_created(sender, request, invitation, **kwargs):
|
||||||
"""Log Invitation creation"""
|
"""Log Invitation creation"""
|
||||||
Event.create(Event.ACTION_INVITE_CREATED, request,
|
Event.new(EventAction.INVITE_CREATED, invitation_uuid=invitation.uuid.hex).from_http(request)
|
||||||
invitation_uuid=invitation.uuid.hex)
|
|
||||||
|
|
||||||
@receiver(invitation_used)
|
@receiver(invitation_used)
|
||||||
def on_invitation_used(sender, request, invitation, **kwargs):
|
def on_invitation_used(sender, request, invitation, **kwargs):
|
||||||
"""Log Invitation usage"""
|
"""Log Invitation usage"""
|
||||||
Event.create(Event.ACTION_INVITE_USED, request,
|
Event.new(EventAction.INVITE_USED, invitation_uuid=invitation.uuid.hex).from_http(request)
|
||||||
invitation_uuid=invitation.uuid.hex)
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ from qrcode import make
|
||||||
from qrcode.image.svg import SvgPathImage
|
from qrcode.image.svg import SvgPathImage
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.audit.models import Event, EventAction
|
||||||
from passbook.factors.otp.forms import OTPSetupForm
|
from passbook.factors.otp.forms import OTPSetupForm
|
||||||
from passbook.factors.otp.utils import otpauth_url
|
from passbook.factors.otp.utils import otpauth_url
|
||||||
from passbook.lib.boilerplate import NeverCacheMixin
|
from passbook.lib.boilerplate import NeverCacheMixin
|
||||||
|
@ -55,12 +56,7 @@ class DisableView(LoginRequiredMixin, View):
|
||||||
token.delete()
|
token.delete()
|
||||||
messages.success(request, 'Successfully disabled OTP')
|
messages.success(request, 'Successfully disabled OTP')
|
||||||
# Create event with email notification
|
# Create event with email notification
|
||||||
# Event.create(
|
Event.new(EventAction.CUSTOM, message='User disabled OTP.').from_http(request)
|
||||||
# user=request.user,
|
|
||||||
# message=_('You disabled TOTP.'),
|
|
||||||
# current=True,
|
|
||||||
# request=request,
|
|
||||||
# send_notification=True)
|
|
||||||
return redirect(reverse('passbook_factors_otp:otp-user-settings'))
|
return redirect(reverse('passbook_factors_otp:otp-user-settings'))
|
||||||
|
|
||||||
class EnableView(LoginRequiredMixin, FormView):
|
class EnableView(LoginRequiredMixin, FormView):
|
||||||
|
@ -77,7 +73,7 @@ class EnableView(LoginRequiredMixin, FormView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
kwargs['config'] = CONFIG.y('passbook')
|
kwargs['config'] = CONFIG.y('passbook')
|
||||||
kwargs['is_login'] = True
|
kwargs['is_login'] = True
|
||||||
kwargs['title'] = _('Configue OTP')
|
kwargs['title'] = _('Configure OTP')
|
||||||
kwargs['primary_action'] = _('Setup')
|
kwargs['primary_action'] = _('Setup')
|
||||||
return super().get_context_data(**kwargs)
|
return super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
@ -134,14 +130,7 @@ class EnableView(LoginRequiredMixin, FormView):
|
||||||
self.static_device.confirmed = True
|
self.static_device.confirmed = True
|
||||||
self.static_device.save()
|
self.static_device.save()
|
||||||
del self.request.session[OTP_SETTING_UP_KEY]
|
del self.request.session[OTP_SETTING_UP_KEY]
|
||||||
# Create event with email notification
|
Event.new(EventAction.CUSTOM, message='User enabled OTP.').from_http(self.request)
|
||||||
# TODO: Create Audit Log entry
|
|
||||||
# Event.create(
|
|
||||||
# user=self.request.user,
|
|
||||||
# message=_('You activated TOTP.'),
|
|
||||||
# current=True,
|
|
||||||
# request=self.request,
|
|
||||||
# send_notification=True)
|
|
||||||
return redirect('passbook_factors_otp:otp-user-settings')
|
return redirect('passbook_factors_otp:otp-user-settings')
|
||||||
|
|
||||||
class QRView(NeverCacheMixin, View):
|
class QRView(NeverCacheMixin, View):
|
||||||
|
|
|
@ -8,7 +8,7 @@ from django.utils.translation import ugettext as _
|
||||||
from oauth2_provider.views.base import AuthorizationView
|
from oauth2_provider.views.base import AuthorizationView
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.audit.models import Event
|
from passbook.audit.models import Event, EventAction
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.core.views.access import AccessMixin
|
from passbook.core.views.access import AccessMixin
|
||||||
from passbook.core.views.utils import LoadingView, PermissionDeniedView
|
from passbook.core.views.utils import LoadingView, PermissionDeniedView
|
||||||
|
@ -77,9 +77,8 @@ class PassbookAuthorizationView(AccessMixin, AuthorizationView):
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
# User has clicked on "Authorize"
|
# User has clicked on "Authorize"
|
||||||
Event.create(
|
Event.new(EventAction.AUTHORIZE_APPLICATION,
|
||||||
action=Event.ACTION_AUTHORIZE_APPLICATION,
|
authorized_application=self._application).from_http(self.request)
|
||||||
request=self.request,
|
LOGGER.debug('User authorized Application',
|
||||||
app=str(self._application))
|
user=self.request.user, application=self._application)
|
||||||
LOGGER.debug('user %s authorized %s', self.request.user, self._application)
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from django.contrib import messages
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.audit.models import Event
|
from passbook.audit.models import Event, EventAction
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.policies.engine import PolicyEngine
|
from passbook.policies.engine import PolicyEngine
|
||||||
|
|
||||||
|
@ -28,9 +28,7 @@ def check_permissions(request, user, client):
|
||||||
messages.error(request, policy_message)
|
messages.error(request, policy_message)
|
||||||
return redirect('passbook_providers_oauth:oauth2-permission-denied')
|
return redirect('passbook_providers_oauth:oauth2-permission-denied')
|
||||||
|
|
||||||
Event.create(
|
Event.new(EventAction.AUTHORIZE_APPLICATION,
|
||||||
action=Event.ACTION_AUTHORIZE_APPLICATION,
|
authorized_application=application,
|
||||||
request=request,
|
skipped_authorization=False).from_http(request)
|
||||||
app=application.name,
|
|
||||||
skipped_authorization=False)
|
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -13,7 +13,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||||
from signxml.util import strip_pem_header
|
from signxml.util import strip_pem_header
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.audit.models import Event
|
from passbook.audit.models import Event, EventAction
|
||||||
from passbook.core.models import Application
|
from passbook.core.models import Application
|
||||||
from passbook.lib.mixins import CSRFExemptMixin
|
from passbook.lib.mixins import CSRFExemptMixin
|
||||||
from passbook.lib.utils.template import render_to_string
|
from passbook.lib.utils.template import render_to_string
|
||||||
|
@ -123,11 +123,9 @@ class LoginProcessView(AccessRequiredView):
|
||||||
if self.provider.application.skip_authorization:
|
if self.provider.application.skip_authorization:
|
||||||
ctx = self.provider.processor.generate_response()
|
ctx = self.provider.processor.generate_response()
|
||||||
# Log Application Authorization
|
# Log Application Authorization
|
||||||
Event.create(
|
Event.new(EventAction.AUTHORIZE_APPLICATION,
|
||||||
action=Event.ACTION_AUTHORIZE_APPLICATION,
|
authorized_application=self.provider.application,
|
||||||
request=request,
|
skipped_authorization=True).from_http(request)
|
||||||
app=self.provider.application.name,
|
|
||||||
skipped_authorization=True)
|
|
||||||
return RedirectToSPView.as_view()(
|
return RedirectToSPView.as_view()(
|
||||||
request=request,
|
request=request,
|
||||||
acs_url=ctx['acs_url'],
|
acs_url=ctx['acs_url'],
|
||||||
|
@ -145,11 +143,9 @@ class LoginProcessView(AccessRequiredView):
|
||||||
# Check if user has access
|
# Check if user has access
|
||||||
if request.POST.get('ACSUrl', None):
|
if request.POST.get('ACSUrl', None):
|
||||||
# User accepted request
|
# User accepted request
|
||||||
Event.create(
|
Event.new(EventAction.AUTHORIZE_APPLICATION,
|
||||||
action=Event.ACTION_AUTHORIZE_APPLICATION,
|
authorized_application=self.provider.application,
|
||||||
request=request,
|
skipped_authorization=False).from_http(request)
|
||||||
app=self.provider.application.name,
|
|
||||||
skipped_authorization=False)
|
|
||||||
return RedirectToSPView.as_view()(
|
return RedirectToSPView.as_view()(
|
||||||
request=request,
|
request=request,
|
||||||
acs_url=request.POST.get('ACSUrl'),
|
acs_url=request.POST.get('ACSUrl'),
|
||||||
|
|
|
@ -11,8 +11,8 @@ from django.utils.translation import ugettext as _
|
||||||
from django.views.generic import RedirectView, View
|
from django.views.generic import RedirectView, View
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
|
from passbook.audit.models import Event, EventAction
|
||||||
from passbook.factors.view import AuthenticationView, _redirect_with_qs
|
from passbook.factors.view import AuthenticationView, _redirect_with_qs
|
||||||
from passbook.lib.utils.reflection import app
|
|
||||||
from passbook.sources.oauth.clients import get_client
|
from passbook.sources.oauth.clients import get_client
|
||||||
from passbook.sources.oauth.models import (OAuthSource,
|
from passbook.sources.oauth.models import (OAuthSource,
|
||||||
UserOAuthSourceConnection)
|
UserOAuthSourceConnection)
|
||||||
|
@ -180,17 +180,8 @@ class OAuthCallback(OAuthClientMixin, View):
|
||||||
access.user = user
|
access.user = user
|
||||||
access.save()
|
access.save()
|
||||||
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
|
UserOAuthSourceConnection.objects.filter(pk=access.pk).update(user=user)
|
||||||
if app('passbook_audit'):
|
Event.new(EventAction.CUSTOM, message="Linked OAuth Source",
|
||||||
pass
|
source=source).from_http(self.request)
|
||||||
# TODO: Create audit entry
|
|
||||||
# from passbook.audit.models import something
|
|
||||||
# something.event(user=user,)
|
|
||||||
# Event.create(
|
|
||||||
# user=user,
|
|
||||||
# message=_("Linked user with OAuth source %s" % self.source.name),
|
|
||||||
# request=self.request,
|
|
||||||
# hidden=True,
|
|
||||||
# current=False)
|
|
||||||
if was_authenticated:
|
if was_authenticated:
|
||||||
messages.success(self.request, _("Successfully linked %(source)s!" % {
|
messages.success(self.request, _("Successfully linked %(source)s!" % {
|
||||||
'source': self.source.name
|
'source': self.source.name
|
||||||
|
|
Reference in New Issue