create suspicious request detector and policy, add request to policy engine
This commit is contained in:
		| @ -1,17 +1,15 @@ | |||||||
| """passbook audit models""" | """passbook audit models""" | ||||||
| from datetime import timedelta |  | ||||||
| from logging import getLogger | from logging import getLogger | ||||||
|  |  | ||||||
|  | from ipware import get_client_ip | ||||||
| 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.utils import timezone |  | ||||||
| from django.utils.translation import gettext as _ | from django.utils.translation import gettext as _ | ||||||
| from ipware import get_client_ip |  | ||||||
|  |  | ||||||
| from passbook.lib.models import CreatedUpdatedModel, UUIDModel | from passbook.lib.models import UUIDModel | ||||||
|  |  | ||||||
| LOGGER = getLogger(__name__) | LOGGER = getLogger(__name__) | ||||||
|  |  | ||||||
|  | |||||||
| @ -65,7 +65,7 @@ class AuthenticationView(UserPassesTestMixin, View): | |||||||
|             self.pending_factors = [] |             self.pending_factors = [] | ||||||
|             for factor in _all_factors: |             for factor in _all_factors: | ||||||
|                 policy_engine = PolicyEngine(factor.policies.all()) |                 policy_engine = PolicyEngine(factor.policies.all()) | ||||||
|                 policy_engine.for_user(self.pending_user) |                 policy_engine.for_user(self.pending_user).with_request(request).build() | ||||||
|                 if policy_engine.result[0]: |                 if policy_engine.result[0]: | ||||||
|                     self.pending_factors.append((factor.uuid.hex, factor.type)) |                     self.pending_factors.append((factor.uuid.hex, factor.type)) | ||||||
|         # Read and instantiate factor from session |         # Read and instantiate factor from session | ||||||
|  | |||||||
| @ -153,7 +153,7 @@ class Application(PolicyModel): | |||||||
|     def user_is_authorized(self, user: User) -> bool: |     def user_is_authorized(self, user: User) -> bool: | ||||||
|         """Check if user is authorized to use this application""" |         """Check if user is authorized to use this application""" | ||||||
|         from passbook.core.policies import PolicyEngine |         from passbook.core.policies import PolicyEngine | ||||||
|         return PolicyEngine(self.policies.all()).for_user(user).result |         return PolicyEngine(self.policies.all()).for_user(user).build().result | ||||||
|  |  | ||||||
|     def get_provider(self): |     def get_provider(self): | ||||||
|         """Get casted provider instance""" |         """Get casted provider instance""" | ||||||
|  | |||||||
| @ -2,12 +2,22 @@ | |||||||
| from logging import getLogger | from logging import getLogger | ||||||
|  |  | ||||||
| from celery import group | from celery import group | ||||||
|  | from django.http import HttpRequest | ||||||
|  |  | ||||||
| from passbook.core.celery import CELERY_APP | from passbook.core.celery import CELERY_APP | ||||||
| from passbook.core.models import Policy, User | from passbook.core.models import Policy, User | ||||||
|  |  | ||||||
| LOGGER = getLogger(__name__) | LOGGER = getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_remote_ip(request: HttpRequest) -> str: | ||||||
|  |     """Return the remote's IP""" | ||||||
|  |     if not request: | ||||||
|  |         return '0.0.0.0'  # nosec | ||||||
|  |     if request.META.get('HTTP_X_FORWARDED_FOR'): | ||||||
|  |         return request.META.get('HTTP_X_FORWARDED_FOR') | ||||||
|  |     return request.META.get('REMOTE_ADDR') | ||||||
|  |  | ||||||
| @CELERY_APP.task() | @CELERY_APP.task() | ||||||
| def _policy_engine_task(user_pk, policy_pk, **kwargs): | def _policy_engine_task(user_pk, policy_pk, **kwargs): | ||||||
|     """Task wrapper to run policy checking""" |     """Task wrapper to run policy checking""" | ||||||
| @ -33,18 +43,33 @@ class PolicyEngine: | |||||||
|  |  | ||||||
|     policies = None |     policies = None | ||||||
|     _group = None |     _group = None | ||||||
|  |     _request = None | ||||||
|  |     _user = None | ||||||
|  |  | ||||||
|     def __init__(self, policies): |     def __init__(self, policies): | ||||||
|         self.policies = policies |         self.policies = policies | ||||||
|  |         self._request = None | ||||||
|  |         self._user = None | ||||||
|  |  | ||||||
|     def for_user(self, user): |     def for_user(self, user): | ||||||
|         """Check policies for user""" |         """Check policies for user""" | ||||||
|  |         self._user = user | ||||||
|  |         return self | ||||||
|  |  | ||||||
|  |     def with_request(self, request): | ||||||
|  |         """Set request""" | ||||||
|  |         self._request = request | ||||||
|  |         return self | ||||||
|  |  | ||||||
|  |     def build(self): | ||||||
|  |         """Build task group""" | ||||||
|         signatures = [] |         signatures = [] | ||||||
|         kwargs = { |         kwargs = { | ||||||
|             '__password__': getattr(user, '__password__', None) |             '__password__': getattr(self._user, '__password__', None), | ||||||
|  |             'remote_ip': get_remote_ip(self._request) | ||||||
|         } |         } | ||||||
|         for policy in self.policies: |         for policy in self.policies: | ||||||
|             signatures.append(_policy_engine_task.s(user.pk, policy.pk.hex, **kwargs)) |             signatures.append(_policy_engine_task.s(self._user.pk, policy.pk.hex, **kwargs)) | ||||||
|         self._group = group(signatures)() |         self._group = group(signatures)() | ||||||
|         return self |         return self | ||||||
|  |  | ||||||
|  | |||||||
| @ -76,6 +76,7 @@ INSTALLED_APPS = [ | |||||||
|     'passbook.hibp_policy.apps.PassbookHIBPConfig', |     'passbook.hibp_policy.apps.PassbookHIBPConfig', | ||||||
|     'passbook.pretend.apps.PassbookPretendConfig', |     'passbook.pretend.apps.PassbookPretendConfig', | ||||||
|     'passbook.password_expiry_policy.apps.PassbookPasswordExpiryPolicyConfig', |     'passbook.password_expiry_policy.apps.PassbookPasswordExpiryPolicyConfig', | ||||||
|  |     'passbook.suspicious_policy.apps.PassbookSuspiciousPolicyConfig', | ||||||
| ] | ] | ||||||
|  |  | ||||||
| # Message Tag fix for bootstrap CSS Classes | # Message Tag fix for bootstrap CSS Classes | ||||||
|  | |||||||
| @ -20,7 +20,7 @@ def password_policy_checker(sender, password, **kwargs): | |||||||
|     _all_factors = PasswordFactor.objects.filter(enabled=True).order_by('order') |     _all_factors = PasswordFactor.objects.filter(enabled=True).order_by('order') | ||||||
|     for factor in _all_factors: |     for factor in _all_factors: | ||||||
|         policy_engine = PolicyEngine(factor.password_policies.all().select_subclasses()) |         policy_engine = PolicyEngine(factor.password_policies.all().select_subclasses()) | ||||||
|         policy_engine.for_user(sender) |         policy_engine.for_user(sender).build() | ||||||
|         passing, messages = policy_engine.result |         passing, messages = policy_engine.result | ||||||
|         if not passing: |         if not passing: | ||||||
|             raise PasswordPolicyInvalid(*messages) |             raise PasswordPolicyInvalid(*messages) | ||||||
|  | |||||||
| @ -16,7 +16,7 @@ def user_factors(context): | |||||||
|     for factor in _all_factors: |     for factor in _all_factors: | ||||||
|         _link = factor.has_user_settings() |         _link = factor.has_user_settings() | ||||||
|         policy_engine = PolicyEngine(factor.policies.all()) |         policy_engine = PolicyEngine(factor.policies.all()) | ||||||
|         policy_engine.for_user(user) |         policy_engine.for_user(user).with_request(context.get('request')).build() | ||||||
|         if policy_engine.result[0] and _link: |         if policy_engine.result[0] and _link: | ||||||
|             matching_factors.append(_link) |             matching_factors.append(_link) | ||||||
|     return matching_factors |     return matching_factors | ||||||
|  | |||||||
| @ -24,7 +24,7 @@ class PasswordExpiryPolicy(Policy): | |||||||
|         """If password change date is more than x days in the past, call set_unusable_password |         """If password change date is more than x days in the past, call set_unusable_password | ||||||
|         and show a notice""" |         and show a notice""" | ||||||
|         actual_days = (now() - user.password_change_date).days |         actual_days = (now() - user.password_change_date).days | ||||||
|         days_since_expiry = now() - (user.password_change_date + timedelta(days=self.days)).days |         days_since_expiry = (now() - (user.password_change_date + timedelta(days=self.days))).days | ||||||
|         if actual_days >= self.days: |         if actual_days >= self.days: | ||||||
|             if not self.deny_only: |             if not self.deny_only: | ||||||
|                 user.set_unusable_password() |                 user.set_unusable_password() | ||||||
|  | |||||||
| @ -104,7 +104,7 @@ class LoginProcessView(ProviderMixin, LoginRequiredMixin, View): | |||||||
|     def _has_access(self): |     def _has_access(self): | ||||||
|         """Check if user has access to application""" |         """Check if user has access to application""" | ||||||
|         policy_engine = PolicyEngine(self.provider.application.policies.all()) |         policy_engine = PolicyEngine(self.provider.application.policies.all()) | ||||||
|         policy_engine.for_user(self.request.user) |         policy_engine.for_user(self.request.user).with_request(self.request).build() | ||||||
|         return policy_engine.result |         return policy_engine.result | ||||||
|  |  | ||||||
|     def get(self, request, application): |     def get(self, request, application): | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								passbook/suspicious_policy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								passbook/suspicious_policy/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | |||||||
|  | """passbook suspicious_policy""" | ||||||
|  | __version__ = '0.1.1-beta' | ||||||
							
								
								
									
										5
									
								
								passbook/suspicious_policy/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								passbook/suspicious_policy/admin.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | """Passbook suspicious_policy Admin""" | ||||||
|  |  | ||||||
|  | from passbook.lib.admin import admin_autoregister | ||||||
|  |  | ||||||
|  | admin_autoregister('passbook_suspicious_policy') | ||||||
							
								
								
									
										15
									
								
								passbook/suspicious_policy/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								passbook/suspicious_policy/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | """Passbook suspicious_policy app config""" | ||||||
|  | from importlib import import_module | ||||||
|  |  | ||||||
|  | from django.apps import AppConfig | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PassbookSuspiciousPolicyConfig(AppConfig): | ||||||
|  |     """Passbook suspicious_policy app config""" | ||||||
|  |  | ||||||
|  |     name = 'passbook.suspicious_policy' | ||||||
|  |     label = 'passbook_suspicious_policy' | ||||||
|  |     verbose_name = 'passbook Suspicious Request Detector' | ||||||
|  |  | ||||||
|  |     def ready(self): | ||||||
|  |         import_module('passbook.suspicious_policy.signals') | ||||||
							
								
								
									
										18
									
								
								passbook/suspicious_policy/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								passbook/suspicious_policy/forms.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | """passbook suspicious request forms""" | ||||||
|  | from django import forms | ||||||
|  |  | ||||||
|  | from passbook.core.forms.policies import GENERAL_FIELDS | ||||||
|  | from passbook.suspicious_policy.models import SuspiciousRequestPolicy | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SuspiciousRequestPolicyForm(forms.ModelForm): | ||||||
|  |     """Form to edit SuspiciousRequestPolicy""" | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |  | ||||||
|  |         model = SuspiciousRequestPolicy | ||||||
|  |         fields = GENERAL_FIELDS + ['check_ip', 'check_username', 'threshold'] | ||||||
|  |         widgets = { | ||||||
|  |             'name': forms.TextInput(), | ||||||
|  |             'value': forms.TextInput(), | ||||||
|  |         } | ||||||
							
								
								
									
										49
									
								
								passbook/suspicious_policy/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								passbook/suspicious_policy/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | |||||||
|  | # Generated by Django 2.1.7 on 2019-03-03 18:17 | ||||||
|  |  | ||||||
|  | import django.db.models.deletion | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     initial = True | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||||
|  |         ('passbook_core', '0016_auto_20190227_1355'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='IPScore', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('ip', models.GenericIPAddressField()), | ||||||
|  |                 ('score', models.IntegerField(default=0)), | ||||||
|  |                 ('updated', models.DateTimeField(auto_now=True)), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='SuspiciousRequestPolicy', | ||||||
|  |             fields=[ | ||||||
|  |                 ('policy_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Policy')), | ||||||
|  |                 ('check_ip', models.BooleanField(default=True)), | ||||||
|  |                 ('check_username', models.BooleanField(default=True)), | ||||||
|  |                 ('threshold', models.IntegerField(default=-5)), | ||||||
|  |             ], | ||||||
|  |             options={ | ||||||
|  |                 'abstract': False, | ||||||
|  |             }, | ||||||
|  |             bases=('passbook_core.policy',), | ||||||
|  |         ), | ||||||
|  |         migrations.CreateModel( | ||||||
|  |             name='UserScore', | ||||||
|  |             fields=[ | ||||||
|  |                 ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), | ||||||
|  |                 ('score', models.IntegerField(default=0)), | ||||||
|  |                 ('updated', models.DateTimeField(auto_now=True)), | ||||||
|  |                 ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), | ||||||
|  |             ], | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -0,0 +1,17 @@ | |||||||
|  | # Generated by Django 2.1.7 on 2019-03-03 18:20 | ||||||
|  |  | ||||||
|  | from django.db import migrations | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('passbook_suspicious_policy', '0001_initial'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterModelOptions( | ||||||
|  |             name='suspiciousrequestpolicy', | ||||||
|  |             options={'verbose_name': 'Suspicious Request Policy', 'verbose_name_plural': 'Suspicious Request Policies'}, | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -0,0 +1,25 @@ | |||||||
|  | # Generated by Django 2.1.7 on 2019-03-03 18:33 | ||||||
|  |  | ||||||
|  | import django.db.models.deletion | ||||||
|  | from django.conf import settings | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ('passbook_suspicious_policy', '0002_auto_20190303_1820'), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='ipscore', | ||||||
|  |             name='ip', | ||||||
|  |             field=models.GenericIPAddressField(unique=True), | ||||||
|  |         ), | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name='userscore', | ||||||
|  |             name='user', | ||||||
|  |             field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
							
								
								
									
										0
									
								
								passbook/suspicious_policy/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								passbook/suspicious_policy/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										51
									
								
								passbook/suspicious_policy/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								passbook/suspicious_policy/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | """passbook suspicious request policy""" | ||||||
|  | from django.db import models | ||||||
|  | from django.utils.translation import gettext as _ | ||||||
|  |  | ||||||
|  | from passbook.core.models import Policy, User | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class SuspiciousRequestPolicy(Policy): | ||||||
|  |     """Return true if request IP/target username's score is below a certain threshold""" | ||||||
|  |  | ||||||
|  |     check_ip = models.BooleanField(default=True) | ||||||
|  |     check_username = models.BooleanField(default=True) | ||||||
|  |     threshold = models.IntegerField(default=-5) | ||||||
|  |  | ||||||
|  |     form = 'passbook.suspicious_policy.forms.SuspiciousRequestPolicyForm' | ||||||
|  |  | ||||||
|  |     def passes(self, user: User): | ||||||
|  |         remote_ip = user.remote_ip | ||||||
|  |         passing = True | ||||||
|  |         if self.check_ip: | ||||||
|  |             ip_scores = IPScore.objects.filter(ip=remote_ip, score__lte=self.threshold) | ||||||
|  |             passing = passing and ip_scores.exists() | ||||||
|  |         if self.check_username: | ||||||
|  |             user_scores = UserScore.objects.filter(user=user, score__lte=self.threshold) | ||||||
|  |             passing = passing and user_scores.exists() | ||||||
|  |         return passing | ||||||
|  |  | ||||||
|  |     class Meta: | ||||||
|  |  | ||||||
|  |         verbose_name = _('Suspicious Request Policy') | ||||||
|  |         verbose_name_plural = _('Suspicious Request Policies') | ||||||
|  |  | ||||||
|  | class IPScore(models.Model): | ||||||
|  |     """Store score coming from the same IP""" | ||||||
|  |  | ||||||
|  |     ip = models.GenericIPAddressField(unique=True) | ||||||
|  |     score = models.IntegerField(default=0) | ||||||
|  |     updated = models.DateTimeField(auto_now=True) | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return "IPScore for %s @ %d" % (self.ip, self.score) | ||||||
|  |  | ||||||
|  | class UserScore(models.Model): | ||||||
|  |     """Store score attempting to log in as the same username""" | ||||||
|  |  | ||||||
|  |     user = models.OneToOneField(User, on_delete=models.CASCADE) | ||||||
|  |     score = models.IntegerField(default=0) | ||||||
|  |     updated = models.DateTimeField(auto_now=True) | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return "UserScore for %s @ %d" % (self.user, self.score) | ||||||
							
								
								
									
										37
									
								
								passbook/suspicious_policy/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								passbook/suspicious_policy/signals.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | """passbook suspicious request signals""" | ||||||
|  | from logging import getLogger | ||||||
|  |  | ||||||
|  | from django.contrib.auth.signals import user_logged_in, user_login_failed | ||||||
|  | from django.dispatch import receiver | ||||||
|  | from ipware import get_client_ip | ||||||
|  |  | ||||||
|  | from passbook.core.models import User | ||||||
|  | from passbook.suspicious_policy.models import IPScore, UserScore | ||||||
|  |  | ||||||
|  | LOGGER = getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def update_score(request, username, amount): | ||||||
|  |     """Update score for IP and User""" | ||||||
|  |     remote_ip = get_client_ip(request) | ||||||
|  |     ip_score, _ = IPScore.objects.update_or_create(ip=remote_ip) | ||||||
|  |     ip_score.score += amount | ||||||
|  |     ip_score.save() | ||||||
|  |     LOGGER.debug("Added %s to score of IP %s", amount, remote_ip) | ||||||
|  |     user = User.objects.filter(username=username) | ||||||
|  |     if not user.exists(): | ||||||
|  |         return | ||||||
|  |     user_score, _ = UserScore.objects.update_or_create(user=user.first()) | ||||||
|  |     user_score.score += amount | ||||||
|  |     user_score.save() | ||||||
|  |     LOGGER.debug("Added %s to score of User %s", amount, username) | ||||||
|  |  | ||||||
|  | @receiver(user_login_failed) | ||||||
|  | def handle_failed_login(sender, request, credentials, **kwargs): | ||||||
|  |     """Lower Score for failed loging attempts""" | ||||||
|  |     update_score(request, credentials.get('username'), -1) | ||||||
|  |  | ||||||
|  | @receiver(user_logged_in) | ||||||
|  | def handle_successful_login(sender, request, user, **kwargs): | ||||||
|  |     """Raise score for successful attempts""" | ||||||
|  |     update_score(request, user.username, 1) | ||||||
| @ -4,7 +4,7 @@ | |||||||
| -r passbook/saml_idp/requirements.txt | -r passbook/saml_idp/requirements.txt | ||||||
| -r passbook/otp/requirements.txt | -r passbook/otp/requirements.txt | ||||||
| -r passbook/oauth_provider/requirements.txt | -r passbook/oauth_provider/requirements.txt | ||||||
| -r passbook/audit/requirements.txt | -r passbook/suspicious_policy/requirements.txt | ||||||
| -r passbook/captcha_factor/requirements.txt | -r passbook/captcha_factor/requirements.txt | ||||||
| -r passbook/admin/requirements.txt | -r passbook/admin/requirements.txt | ||||||
| -r passbook/api/requirements.txt | -r passbook/api/requirements.txt | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer