*(minor): small refactor
This commit is contained in:
0
passbook/providers/app_gw/__init__.py
Normal file
0
passbook/providers/app_gw/__init__.py
Normal file
5
passbook/providers/app_gw/admin.py
Normal file
5
passbook/providers/app_gw/admin.py
Normal file
@ -0,0 +1,5 @@
|
||||
"""passbook Application Security Gateway model admin"""
|
||||
|
||||
from passbook.lib.admin import admin_autoregister
|
||||
|
||||
admin_autoregister('passbook_providers_app_gw')
|
||||
11
passbook/providers/app_gw/apps.py
Normal file
11
passbook/providers/app_gw/apps.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""passbook Application Security Gateway app"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class PassbookApplicationApplicationGatewayConfig(AppConfig):
|
||||
"""passbook app_gw app"""
|
||||
|
||||
name = 'passbook.providers.app_gw'
|
||||
label = 'passbook_providers_app_gw'
|
||||
verbose_name = 'passbook Providers.Application Security Gateway'
|
||||
mountpoint = 'application/gateway/'
|
||||
67
passbook/providers/app_gw/forms.py
Normal file
67
passbook/providers/app_gw/forms.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""passbook Application Security Gateway Forms"""
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django import forms
|
||||
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||
from django.forms import ValidationError
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.lib.fields import DynamicArrayField
|
||||
from passbook.providers.app_gw.models import (ApplicationGatewayProvider,
|
||||
RewriteRule)
|
||||
|
||||
|
||||
class ApplicationGatewayProviderForm(forms.ModelForm):
|
||||
"""Security Gateway Provider form"""
|
||||
|
||||
def clean_server_name(self):
|
||||
"""Check if server_name is in DB already, since
|
||||
Postgres ArrayField doesn't suppport keys."""
|
||||
current = self.cleaned_data.get('server_name')
|
||||
if ApplicationGatewayProvider.objects \
|
||||
.filter(server_name__overlap=current) \
|
||||
.exclude(pk=self.instance.pk).exists():
|
||||
raise ValidationError(_("Server Name already in use."))
|
||||
return current
|
||||
|
||||
def clean_upstream(self):
|
||||
"""Check that upstream begins with http(s)"""
|
||||
for upstream in self.cleaned_data.get('upstream'):
|
||||
_parsed_url = urlparse(upstream)
|
||||
|
||||
if _parsed_url.scheme not in ('http', 'https'):
|
||||
raise ValidationError(_("URL Scheme must be either http or https"))
|
||||
return self.cleaned_data.get('upstream')
|
||||
|
||||
class Meta:
|
||||
|
||||
model = ApplicationGatewayProvider
|
||||
fields = ['server_name', 'upstream', 'enabled', 'authentication_header',
|
||||
'default_content_type', 'upstream_ssl_verification', 'property_mappings']
|
||||
widgets = {
|
||||
'authentication_header': forms.TextInput(),
|
||||
'default_content_type': forms.TextInput(),
|
||||
'property_mappings': FilteredSelectMultiple(_('Property Mappings'), False)
|
||||
}
|
||||
field_classes = {
|
||||
'server_name': DynamicArrayField,
|
||||
'upstream': DynamicArrayField
|
||||
}
|
||||
labels = {
|
||||
'upstream_ssl_verification': _('Verify upstream SSL Certificates?'),
|
||||
'property_mappings': _('Rewrite Rules')
|
||||
}
|
||||
|
||||
class RewriteRuleForm(forms.ModelForm):
|
||||
"""Rewrite Rule Form"""
|
||||
|
||||
class Meta:
|
||||
|
||||
model = RewriteRule
|
||||
fields = ['name', 'match', 'halt', 'replacement', 'redirect', 'conditions']
|
||||
widgets = {
|
||||
'name': forms.TextInput(),
|
||||
'match': forms.TextInput(attrs={'data-is-monospace': True}),
|
||||
'replacement': forms.TextInput(attrs={'data-is-monospace': True}),
|
||||
'conditions': FilteredSelectMultiple(_('Conditions'), False)
|
||||
}
|
||||
55
passbook/providers/app_gw/middleware.py
Normal file
55
passbook/providers/app_gw/middleware.py
Normal file
@ -0,0 +1,55 @@
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.http import cookie_date
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.factors.view import AuthenticationView
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
class SessionHostDomainMiddleware(SessionMiddleware):
|
||||
|
||||
def process_request(self, request):
|
||||
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
|
||||
request.session = self.SessionStore(session_key)
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""
|
||||
If request.session was modified, or if the configuration is to save the
|
||||
session every time, save the changes and set a session cookie.
|
||||
"""
|
||||
try:
|
||||
accessed = request.session.accessed
|
||||
modified = request.session.modified
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if accessed:
|
||||
patch_vary_headers(response, ('Cookie',))
|
||||
if modified or settings.SESSION_SAVE_EVERY_REQUEST:
|
||||
if request.session.get_expire_at_browser_close():
|
||||
max_age = None
|
||||
expires = None
|
||||
else:
|
||||
max_age = request.session.get_expiry_age()
|
||||
expires_time = time.time() + max_age
|
||||
expires = cookie_date(expires_time)
|
||||
# Save the session data and refresh the client cookie.
|
||||
# Skip session save for 500 responses, refs #3881.
|
||||
if response.status_code != 500:
|
||||
request.session.save()
|
||||
hosts = [request.get_host().split(':')[0]]
|
||||
if AuthenticationView.SESSION_FORCE_COOKIE_HOSTNAME in request.session:
|
||||
hosts.append(request.session[AuthenticationView.SESSION_FORCE_COOKIE_HOSTNAME])
|
||||
LOGGER.debug("Setting hosts for session", hosts=hosts)
|
||||
for host in hosts:
|
||||
response.set_cookie(settings.SESSION_COOKIE_NAME,
|
||||
request.session.session_key, max_age=max_age,
|
||||
expires=expires, domain=host,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
secure=settings.SESSION_COOKIE_SECURE or None,
|
||||
httponly=settings.SESSION_COOKIE_HTTPONLY or None)
|
||||
return response
|
||||
50
passbook/providers/app_gw/migrations/0001_initial.py
Normal file
50
passbook/providers/app_gw/migrations/0001_initial.py
Normal file
@ -0,0 +1,50 @@
|
||||
# Generated by Django 2.2.6 on 2019-10-07 14:07
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('passbook_core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ApplicationGatewayProvider',
|
||||
fields=[
|
||||
('provider_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.Provider')),
|
||||
('server_name', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)),
|
||||
('upstream', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), size=None)),
|
||||
('enabled', models.BooleanField(default=True)),
|
||||
('authentication_header', models.TextField(blank=True, default='X-Remote-User')),
|
||||
('default_content_type', models.TextField(default='application/octet-stream')),
|
||||
('upstream_ssl_verification', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Application Gateway Provider',
|
||||
'verbose_name_plural': 'Application Gateway Providers',
|
||||
},
|
||||
bases=('passbook_core.provider',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RewriteRule',
|
||||
fields=[
|
||||
('propertymapping_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='passbook_core.PropertyMapping')),
|
||||
('match', models.TextField()),
|
||||
('halt', models.BooleanField(default=False)),
|
||||
('replacement', models.TextField()),
|
||||
('redirect', models.CharField(choices=[('internal', 'Internal'), (301, 'Moved Permanently'), (302, 'Found')], max_length=50)),
|
||||
('conditions', models.ManyToManyField(blank=True, to='passbook_core.Policy')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Rewrite Rule',
|
||||
'verbose_name_plural': 'Rewrite Rules',
|
||||
},
|
||||
bases=('passbook_core.propertymapping',),
|
||||
),
|
||||
]
|
||||
0
passbook/providers/app_gw/migrations/__init__.py
Normal file
0
passbook/providers/app_gw/migrations/__init__.py
Normal file
74
passbook/providers/app_gw/models.py
Normal file
74
passbook/providers/app_gw/models.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""passbook app_gw models"""
|
||||
import re
|
||||
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from passbook.core.models import Policy, PropertyMapping, Provider
|
||||
|
||||
|
||||
class ApplicationGatewayProvider(Provider):
|
||||
"""Virtual server which proxies requests to any hostname in server_name to upstream"""
|
||||
|
||||
server_name = ArrayField(models.TextField())
|
||||
upstream = ArrayField(models.TextField())
|
||||
enabled = models.BooleanField(default=True)
|
||||
|
||||
authentication_header = models.TextField(default='X-Remote-User', blank=True)
|
||||
default_content_type = models.TextField(default='application/octet-stream')
|
||||
upstream_ssl_verification = models.BooleanField(default=True)
|
||||
|
||||
form = 'passbook.providers.app_gw.forms.ApplicationGatewayProviderForm'
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""since this model has no name property, return a joined list of server_names as name"""
|
||||
return ', '.join(self.server_name)
|
||||
|
||||
def __str__(self):
|
||||
return "Application Gateway %s" % ', '.join(self.server_name)
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Application Gateway Provider')
|
||||
verbose_name_plural = _('Application Gateway Providers')
|
||||
|
||||
|
||||
class RewriteRule(PropertyMapping):
|
||||
"""Rewrite requests matching `match` with `replacement`, if all polcies in `conditions` apply"""
|
||||
|
||||
REDIRECT_INTERNAL = 'internal'
|
||||
REDIRECT_PERMANENT = 301
|
||||
REDIRECT_FOUND = 302
|
||||
|
||||
REDIRECTS = (
|
||||
(REDIRECT_INTERNAL, _('Internal')),
|
||||
(REDIRECT_PERMANENT, _('Moved Permanently')),
|
||||
(REDIRECT_FOUND, _('Found')),
|
||||
)
|
||||
|
||||
match = models.TextField()
|
||||
halt = models.BooleanField(default=False)
|
||||
conditions = models.ManyToManyField(Policy, blank=True)
|
||||
replacement = models.TextField() # python formatted strings, use {match.1}
|
||||
redirect = models.CharField(max_length=50, choices=REDIRECTS)
|
||||
|
||||
form = 'passbook.providers.app_gw.forms.RewriteRuleForm'
|
||||
|
||||
_matcher = None
|
||||
|
||||
@property
|
||||
def compiled_matcher(self):
|
||||
"""Cache the compiled regex in memory"""
|
||||
if not self._matcher:
|
||||
self._matcher = re.compile(self.match)
|
||||
return self._matcher
|
||||
|
||||
def __str__(self):
|
||||
return "Rewrite Rule %s" % self.name
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _('Rewrite Rule')
|
||||
verbose_name_plural = _('Rewrite Rules')
|
||||
0
passbook/providers/app_gw/provider/__init__.py
Normal file
0
passbook/providers/app_gw/provider/__init__.py
Normal file
8
passbook/providers/app_gw/urls.py
Normal file
8
passbook/providers/app_gw/urls.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""passbook app_gw urls"""
|
||||
from django.urls import path
|
||||
|
||||
from passbook.providers.app_gw.views import NginxCheckView
|
||||
|
||||
urlpatterns = [
|
||||
path('nginx/', NginxCheckView.as_view())
|
||||
]
|
||||
36
passbook/providers/app_gw/views.py
Normal file
36
passbook/providers/app_gw/views.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""passbook app_gw views"""
|
||||
from pprint import pprint
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.views import View
|
||||
from structlog import get_logger
|
||||
|
||||
from passbook.core.views.access import AccessMixin
|
||||
from passbook.providers.app_gw.models import ApplicationGatewayProvider
|
||||
|
||||
ORIGINAL_URL = 'HTTP_X_ORIGINAL_URL'
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class NginxCheckView(AccessMixin, View):
|
||||
|
||||
def dispatch(self, request: HttpRequest) -> HttpResponse:
|
||||
pprint(request.META)
|
||||
parsed_url = urlparse(request.META.get(ORIGINAL_URL))
|
||||
# request.session[AuthenticationView.SESSION_ALLOW_ABSOLUTE_NEXT] = True
|
||||
# request.session[AuthenticationView.SESSION_FORCE_COOKIE_HOSTNAME] = parsed_url.hostname
|
||||
print(request.user)
|
||||
if not request.user.is_authenticated:
|
||||
return HttpResponse(status=401)
|
||||
matching = ApplicationGatewayProvider.objects.filter(
|
||||
server_name__contains=[parsed_url.hostname])
|
||||
if not matching.exists():
|
||||
LOGGER.debug("Couldn't find matching application", host=parsed_url.hostname)
|
||||
return HttpResponse(status=403)
|
||||
application = self.provider_to_application(matching.first())
|
||||
has_access, _ = self.user_has_access(application, request.user)
|
||||
if has_access:
|
||||
return HttpResponse(status=202)
|
||||
LOGGER.debug("User not passing", user=request.user)
|
||||
return HttpResponse(status=401)
|
||||
Reference in New Issue
Block a user