app_gw(minor): remove current implementation

This commit is contained in:
Langhammer, Jens
2019-10-04 09:27:29 +02:00
parent 64b75cab84
commit c7322a32a0
24 changed files with 12 additions and 966 deletions

View File

@ -179,8 +179,8 @@
<span class="card-pf-aggregate-status-notification">
<a href="#">
{% if worker_count < 1%}
<span class="pficon-error-circle-o" data-toggle="tooltip" data-placement="right"
title="{% trans 'No workers connected. Policies will not work and you may expect other issues.' %}"></span> {{ worker_count }}
<span class="pficon-warning-triangle-o" data-toggle="tooltip" data-placement="right"
title="{% trans 'No workers connected.' %}"></span> {{ worker_count }}
{% else %}
<span class="pficon pficon-ok"></span>{{ worker_count }}
{% endif %}

Binary file not shown.

View File

@ -1,6 +1,4 @@
"""passbook Application Security Gateway app"""
from importlib import import_module
from django.apps import AppConfig
@ -10,7 +8,4 @@ class PassbookApplicationApplicationGatewayConfig(AppConfig):
name = 'passbook.app_gw'
label = 'passbook_app_gw'
verbose_name = 'passbook Application Security Gateway'
mountpoint = 'app_gw/'
def ready(self):
import_module('passbook.app_gw.signals')
# mountpoint = 'app_gw/'

View File

@ -1,13 +0,0 @@
"""
ASGI entrypoint. Configures Django and then runs the application
defined in the ASGI_APPLICATION setting.
"""
import os
import django
from channels.routing import get_default_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "passbook.root.settings")
django.setup()
application = get_default_application()

View File

@ -1,29 +0,0 @@
"""passbook app_gw webserver management command"""
from daphne.cli import CommandLineInterface
from django.core.management.base import BaseCommand
from django.utils import autoreload
from structlog import get_logger
from passbook.lib.config import CONFIG
LOGGER = get_logger(__name__)
class Command(BaseCommand):
"""Run Daphne Webserver for app_gw"""
def handle(self, *args, **options):
"""passbook daphne server"""
autoreload.run_with_reloader(self.daphne_server)
def daphne_server(self):
"""Run daphne server within autoreload"""
autoreload.raise_last_exception()
CommandLineInterface().run([
'-p', str(CONFIG.y('app_gw.port', 8000)),
'-b', CONFIG.y('app_gw.listen', '0.0.0.0'), # nosec
'--access-log', '/dev/null',
'--application-close-timeout', '500',
'passbook.app_gw.asgi:application'
])

View File

@ -1,33 +0,0 @@
"""passbook app_gw middleware"""
from django.views.generic import RedirectView
from passbook.app_gw.proxy.handler import RequestHandler
from passbook.lib.config import CONFIG
class ApplicationGatewayMiddleware:
"""Check if request should be proxied or handeled normally"""
_app_gw_cache = {}
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Rudimentary cache
host_header = request.META.get('HTTP_HOST')
if host_header not in self._app_gw_cache:
self._app_gw_cache[host_header] = RequestHandler.find_app_gw_for_request(request)
if self._app_gw_cache[host_header]:
return self.dispatch(request, self._app_gw_cache[host_header])
return self.get_response(request)
def dispatch(self, request, app_gw):
"""Build proxied request and pass to upstream"""
handler = RequestHandler(app_gw, request)
if not handler.check_permission():
to_url = 'https://%s/?next=%s' % (CONFIG.y('domains')[0], request.get_full_path())
return RedirectView.as_view(url=to_url)(request)
return handler.get_response()

View File

@ -1,8 +0,0 @@
"""Exception classes"""
class ReverseProxyException(Exception):
"""Base for revproxy exception"""
class InvalidUpstream(ReverseProxyException):
"""Invalid upstream set"""

View File

@ -1,233 +0,0 @@
"""passbook app_gw request handler"""
import mimetypes
from random import SystemRandom
from urllib.parse import urlparse
import certifi
import urllib3
from django.core.cache import cache
from django.utils.http import urlencode
from structlog import get_logger
from passbook.app_gw.models import ApplicationGatewayProvider
from passbook.app_gw.proxy.exceptions import InvalidUpstream
from passbook.app_gw.proxy.response import get_django_response
from passbook.app_gw.proxy.rewrite import Rewriter
from passbook.app_gw.proxy.utils import encode_items, normalize_request_headers
from passbook.core.models import Application
from passbook.policy.engine import PolicyEngine
SESSION_UPSTREAM_KEY = 'passbook_app_gw_upstream'
IGNORED_HOSTNAMES_KEY = 'passbook_app_gw_ignored'
LOGGER = get_logger(__name__)
QUOTE_SAFE = r'<.;>\(}*+|~=-$/_:^@)[{]&\'!,"`'
ERRORS_MESSAGES = {
'upstream-no-scheme': ("Upstream URL scheme must be either "
"'http' or 'https' (%s).")
}
HTTP_NO_VERIFY = urllib3.PoolManager()
HTTP = urllib3.PoolManager(
cert_reqs='CERT_REQUIRED',
ca_certs=certifi.where())
IGNORED_HOSTS = cache.get(IGNORED_HOSTNAMES_KEY, [])
POLICY_CACHE = {}
class RequestHandler:
"""Forward requests"""
_parsed_url = None
_request_headers = None
def __init__(self, app_gw, request):
self.app_gw = app_gw
self.request = request
if self.app_gw.pk not in POLICY_CACHE:
POLICY_CACHE[self.app_gw.pk] = self.app_gw.application.policies.all()
@staticmethod
def find_app_gw_for_request(request):
"""Check if a request should be proxied or forwarded to passbook"""
# Check if hostname is in cached list of ignored hostnames
# This saves us having to query the database on each request
host_header = request.META.get('HTTP_HOST')
if host_header in IGNORED_HOSTS:
# LOGGER.debug("%s is ignored", host_header)
return False
# Look through all ApplicationGatewayProviders and check hostnames
matches = ApplicationGatewayProvider.objects.filter(
server_name__contains=[host_header],
enabled=True)
if not matches.exists():
# Mo matching Providers found, add host header to ignored list
IGNORED_HOSTS.append(host_header)
cache.set(IGNORED_HOSTNAMES_KEY, IGNORED_HOSTS)
# LOGGER.debug("Ignoring %s", host_header)
return False
# At this point we're certain there's a matching ApplicationGateway
if len(matches) > 1:
# This should never happen
raise ValueError
app_gw = matches.first()
try:
# Check if ApplicationGateway is associated with application
getattr(app_gw, 'application')
if app_gw:
return app_gw
except Application.DoesNotExist:
pass
# LOGGER.debug("ApplicationGateway not associated with Application")
return True
def _get_upstream(self):
"""Choose random upstream and save in session"""
if SESSION_UPSTREAM_KEY not in self.request.session:
self.request.session[SESSION_UPSTREAM_KEY] = {}
if self.app_gw.pk not in self.request.session[SESSION_UPSTREAM_KEY]:
upstream_index = int(SystemRandom().random() * len(self.app_gw.upstream))
self.request.session[SESSION_UPSTREAM_KEY][self.app_gw.pk] = upstream_index
return self.app_gw.upstream[self.request.session[SESSION_UPSTREAM_KEY][self.app_gw.pk]]
def get_upstream(self):
"""Get upstream as parsed url"""
upstream = self._get_upstream()
self._parsed_url = urlparse(upstream)
if self._parsed_url.scheme not in ('http', 'https'):
raise InvalidUpstream(ERRORS_MESSAGES['upstream-no-scheme'] %
upstream)
return upstream
def _format_path_to_redirect(self):
# LOGGER.debug("Path before: %s", self.request.get_full_path())
rewriter = Rewriter(self.app_gw, self.request)
after = rewriter.build()
# LOGGER.debug("Path after: %s", after)
return after
def get_proxy_request_headers(self):
"""Get normalized headers for the upstream
Gets all headers from the original request and normalizes them.
Normalization occurs by removing the prefix ``HTTP_`` and
replacing and ``_`` by ``-``. Example: ``HTTP_ACCEPT_ENCODING``
becames ``Accept-Encoding``.
.. versionadded:: 0.9.1
:param request: The original HTTPRequest instance
:returns: Normalized headers for the upstream
"""
return normalize_request_headers(self.request)
def get_request_headers(self):
"""Return request headers that will be sent to upstream.
The header REMOTE_USER is set to the current user
if AuthenticationMiddleware is enabled and
the view's add_remote_user property is True.
.. versionadded:: 0.9.8
"""
request_headers = self.get_proxy_request_headers()
if not self.app_gw.authentication_header:
return request_headers
request_headers[self.app_gw.authentication_header] = self.request.user.get_username()
# LOGGER.debug("%s set", self.app_gw.authentication_header)
return request_headers
def check_permission(self):
"""Check if user is authenticated and has permission to access app"""
if not hasattr(self.request, 'user'):
return False
if not self.request.user.is_authenticated:
return False
policy_engine = PolicyEngine(POLICY_CACHE[self.app_gw.pk])
policy_engine.for_user(self.request.user).with_request(self.request).build()
passing, _messages = policy_engine.result
return passing
def get_encoded_query_params(self):
"""Return encoded query params to be used in proxied request"""
get_data = encode_items(self.request.GET.lists())
return urlencode(get_data)
def _created_proxy_response(self, path):
request_payload = self.request.body
# LOGGER.debug("Request headers: %s", self._request_headers)
request_url = self.get_upstream() + path
# LOGGER.debug("Request URL: %s", request_url)
if self.request.GET:
request_url += '?' + self.get_encoded_query_params()
# LOGGER.debug("Request URL: %s", request_url)
http = HTTP
if not self.app_gw.upstream_ssl_verification:
http = HTTP_NO_VERIFY
try:
proxy_response = http.urlopen(self.request.method,
request_url,
redirect=False,
retries=None,
headers=self._request_headers,
body=request_payload,
decode_content=False,
preload_content=False)
# LOGGER.debug("Proxy response header: %s",
# proxy_response.getheaders())
except urllib3.exceptions.HTTPError as error:
LOGGER.exception(error)
raise
return proxy_response
def _replace_host_on_redirect_location(self, proxy_response):
location = proxy_response.headers.get('Location')
if location:
if self.request.is_secure():
scheme = 'https://'
else:
scheme = 'http://'
request_host = scheme + self.request.META.get('HTTP_HOST')
upstream_host_http = 'http://' + self._parsed_url.netloc
upstream_host_https = 'https://' + self._parsed_url.netloc
location = location.replace(upstream_host_http, request_host)
location = location.replace(upstream_host_https, request_host)
proxy_response.headers['Location'] = location
# LOGGER.debug("Proxy response LOCATION: %s",
# proxy_response.headers['Location'])
def _set_content_type(self, proxy_response):
content_type = proxy_response.headers.get('Content-Type')
if not content_type:
content_type = (mimetypes.guess_type(self.request.path)
[0] or self.app_gw.default_content_type)
proxy_response.headers['Content-Type'] = content_type
# LOGGER.debug("Proxy response CONTENT-TYPE: %s",
# proxy_response.headers['Content-Type'])
def get_response(self):
"""Pass request to upstream and return response"""
self._request_headers = self.get_request_headers()
path = self._format_path_to_redirect()
proxy_response = self._created_proxy_response(path)
self._replace_host_on_redirect_location(proxy_response)
self._set_content_type(proxy_response)
response = get_django_response(proxy_response, strict_cookies=False)
# If response has a 'Location' header, we rewrite that location as well
if 'Location' in response:
LOGGER.debug("Rewriting Location header")
for server_name in self.app_gw.server_name:
response['Location'] = response['Location'].replace(
self._parsed_url.hostname, server_name)
LOGGER.debug(response['Location'])
# LOGGER.debug("RESPONSE RETURNED: %s", response)
return response

View File

@ -1,62 +0,0 @@
"""response functions from django-revproxy"""
from django.http import HttpResponse, StreamingHttpResponse
from structlog import get_logger
from passbook.app_gw.proxy.utils import (cookie_from_string,
set_response_headers, should_stream)
#: Default number of bytes that are going to be read in a file lecture
DEFAULT_AMT = 2 ** 16
logger = get_logger(__name__)
def get_django_response(proxy_response, strict_cookies=False):
"""This method is used to create an appropriate response based on the
Content-Length of the proxy_response. If the content is bigger than
MIN_STREAMING_LENGTH, which is found on utils.py,
than django.http.StreamingHttpResponse will be created,
else a django.http.HTTPResponse will be created instead
:param proxy_response: An Instance of urllib3.response.HTTPResponse that
will create an appropriate response
:param strict_cookies: Whether to only accept RFC-compliant cookies
:returns: Returns an appropriate response based on the proxy_response
content-length
"""
status = proxy_response.status
headers = proxy_response.headers
logger.debug('Proxy response headers: %s', headers)
content_type = headers.get('Content-Type')
logger.debug('Content-Type: %s', content_type)
if should_stream(proxy_response):
logger.info('Content-Length is bigger than %s', DEFAULT_AMT)
response = StreamingHttpResponse(proxy_response.stream(DEFAULT_AMT),
status=status,
content_type=content_type)
else:
content = proxy_response.data or b''
response = HttpResponse(content, status=status,
content_type=content_type)
logger.info('Normalizing response headers')
set_response_headers(response, headers)
logger.debug('Response headers: %s', getattr(response, '_headers'))
cookies = proxy_response.headers.getlist('set-cookie')
logger.info('Checking for invalid cookies')
for cookie_string in cookies:
cookie_dict = cookie_from_string(cookie_string,
strict_cookies=strict_cookies)
# if cookie is invalid cookie_dict will be None
if cookie_dict:
response.set_cookie(**cookie_dict)
logger.debug('Response cookies: %s', response.cookies)
return response

View File

@ -1,42 +0,0 @@
"""passbook app_gw rewriter"""
from passbook.app_gw.models import RewriteRule
RULE_CACHE = {}
class Context:
"""Empty class which we dynamically add attributes to"""
class Rewriter:
"""Apply rewrites"""
__application = None
__request = None
def __init__(self, application, request):
self.__application = application
self.__request = request
if self.__application.pk not in RULE_CACHE:
RULE_CACHE[self.__application.pk] = RewriteRule.objects.filter(
provider__in=[self.__application])
def __build_context(self, matches):
"""Build object with .0, .1, etc as groups and give access to request"""
context = Context()
for index, group_match in enumerate(matches.groups()):
setattr(context, "g%d" % (index + 1), group_match)
setattr(context, 'request', self.__request)
return context
def build(self):
"""Run all rules over path and return final path"""
path = self.__request.get_full_path()
for rule in RULE_CACHE[self.__application.pk]:
matches = rule.compiled_matcher.search(path)
if not matches:
continue
replace_context = self.__build_context(matches)
path = rule.replacement.format(context=replace_context)
if rule.halt:
return path
return path

View File

@ -1,226 +0,0 @@
"""Utils from django-revproxy, slightly adjusted"""
import re
from wsgiref.util import is_hop_by_hop
from structlog import get_logger
try:
from http.cookies import SimpleCookie
COOKIE_PREFIX = ''
except ImportError:
from Cookie import SimpleCookie
COOKIE_PREFIX = 'Set-Cookie: '
#: List containing string constant that are used to represent headers that can
#: be ignored in the required_header function
IGNORE_HEADERS = (
'HTTP_ACCEPT_ENCODING', # We want content to be uncompressed so
# we remove the Accept-Encoding from
# original request
'HTTP_HOST',
'HTTP_REMOTE_USER',
)
# Default from HTTP RFC 2616
# See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
#: Variable that represent the default charset used
DEFAULT_CHARSET = 'latin-1'
#: List containing string constants that represents possible html content type
HTML_CONTENT_TYPES = (
'text/html',
'application/xhtml+xml'
)
#: Variable used to represent a minimal content size required for response
#: to be turned into stream
MIN_STREAMING_LENGTH = 4 * 1024 # 4KB
#: Regex used to find charset in a html content type
_get_charset_re = re.compile(r';\s*charset=(?P<charset>[^\s;]+)', re.I)
def is_html_content_type(content_type):
"""Function used to verify if the parameter is a proper html content type
:param content_type: String variable that represent a content-type
:returns: A boolean value stating if the content_type is a valid html
content type
"""
for html_content_type in HTML_CONTENT_TYPES:
if content_type.startswith(html_content_type):
return True
return False
def should_stream(proxy_response):
"""Function to verify if the proxy_response must be converted into
a stream.This will be done by checking the proxy_response content-length
and verify if its length is bigger than one stipulated
by MIN_STREAMING_LENGTH.
:param proxy_response: An Instance of urllib3.response.HTTPResponse
:returns: A boolean stating if the proxy_response should
be treated as a stream
"""
content_type = proxy_response.headers.get('Content-Type')
if is_html_content_type(content_type):
return False
try:
content_length = int(proxy_response.headers.get('Content-Length', 0))
except ValueError:
content_length = 0
if not content_length or content_length > MIN_STREAMING_LENGTH:
return True
return False
def get_charset(content_type):
"""Function used to retrieve the charset from a content-type.If there is no
charset in the content type then the charset defined on DEFAULT_CHARSET
will be returned
:param content_type: A string containing a Content-Type header
:returns: A string containing the charset
"""
if not content_type:
return DEFAULT_CHARSET
matched = _get_charset_re.search(content_type)
if matched:
# Extract the charset and strip its double quotes
return matched.group('charset').replace('"', '')
return DEFAULT_CHARSET
def required_header(header):
"""Function that verify if the header parameter is a essential header
:param header: A string represented a header
:returns: A boolean value that represent if the header is required
"""
if header in IGNORE_HEADERS:
return False
if header.startswith('HTTP_') or header == 'CONTENT_TYPE':
return True
return False
def set_response_headers(response, response_headers):
"""Set response's header"""
for header, value in response_headers.items():
if is_hop_by_hop(header) or header.lower() == 'set-cookie':
continue
response[header.title()] = value
logger.debug('Response headers: %s', getattr(response, '_headers'))
def normalize_request_headers(request):
"""Function used to transform header, replacing 'HTTP\\_' to ''
and replace '_' to '-'
:param request: A HttpRequest that will be transformed
:returns: A dictionary with the normalized headers
"""
norm_headers = {}
for header, value in request.META.items():
if required_header(header):
norm_header = header.replace('HTTP_', '').title().replace('_', '-')
norm_headers[norm_header] = value
return norm_headers
def encode_items(items):
"""Function that encode all elements in the list of items passed as
a parameter
:param items: A list of tuple
:returns: A list of tuple with all items encoded in 'utf-8'
"""
encoded = []
for key, values in items:
for value in values:
encoded.append((key.encode('utf-8'), value.encode('utf-8')))
return encoded
logger = get_logger()
def cookie_from_string(cookie_string, strict_cookies=False):
"""Parser for HTTP header set-cookie
The return from this function will be used as parameters for
django's response.set_cookie method. Because set_cookie doesn't
have parameter comment, this cookie attribute will be ignored.
:param cookie_string: A string representing a valid cookie
:param strict_cookies: Whether to only accept RFC-compliant cookies
:returns: A dictionary containing the cookie_string attributes
"""
if strict_cookies:
cookies = SimpleCookie(COOKIE_PREFIX + cookie_string)
if not cookies.keys():
return None
cookie_name, = cookies.keys()
cookie_dict = {k: v for k, v in cookies[cookie_name].items()
if v and k != 'comment'}
cookie_dict['key'] = cookie_name
cookie_dict['value'] = cookies[cookie_name].value
return cookie_dict
valid_attrs = ('path', 'domain', 'comment', 'expires',
'max_age', 'httponly', 'secure')
cookie_dict = {}
cookie_parts = cookie_string.split(';')
try:
cookie_dict['key'], cookie_dict['value'] = \
cookie_parts[0].split('=', 1)
cookie_dict['value'] = cookie_dict['value'].replace('"', '')
except ValueError:
logger.warning('Invalid cookie: `%s`', cookie_string)
return None
if cookie_dict['value'].startswith('='):
logger.warning('Invalid cookie: `%s`', cookie_string)
return None
for part in cookie_parts[1:]:
if '=' in part:
attr, value = part.split('=', 1)
value = value.strip()
else:
attr = part
value = ''
attr = attr.strip().lower()
if not attr:
continue
if attr in valid_attrs:
if attr in ('httponly', 'secure'):
cookie_dict[attr] = True
elif attr in 'comment':
# ignoring comment attr as explained in the
# function docstring
continue
else:
cookie_dict[attr] = value
else:
logger.warning('Unknown cookie attribute %s', attr)
return cookie_dict

View File

@ -1,5 +0,0 @@
"""Application Security Gateway settings"""
INSTALLED_APPS = [
'channels'
]
ASGI_APPLICATION = "passbook.app_gw.websocket.routing.application"

View File

@ -1,19 +0,0 @@
"""passbook app_gw cache clean signals"""
from django.core.cache import cache
from django.db.models.signals import post_save
from django.dispatch import receiver
from structlog import get_logger
from passbook.app_gw.models import ApplicationGatewayProvider
from passbook.app_gw.proxy.handler import IGNORED_HOSTNAMES_KEY
LOGGER = get_logger(__name__)
@receiver(post_save)
# pylint: disable=unused-argument
def invalidate_app_gw_cache(sender, instance, **kwargs):
"""Invalidate Policy cache when app_gw is updated"""
if isinstance(instance, ApplicationGatewayProvider):
LOGGER.debug("Invalidating cache for ignored hostnames")
cache.delete(IGNORED_HOSTNAMES_KEY)

View File

@ -1,2 +0,0 @@
"""passbook app_gw urls"""
urlpatterns = []

View File

@ -1,83 +0,0 @@
"""websocket proxy consumer"""
import threading
from ssl import CERT_NONE
import websocket
from channels.generic.websocket import WebsocketConsumer
from structlog import get_logger
from passbook.app_gw.models import ApplicationGatewayProvider
LOGGER = get_logger(__name__)
class ProxyConsumer(WebsocketConsumer):
"""Proxy websocket connection to upstream"""
_headers_dict = {}
_app_gw = None
_client = None
_thread = None
def _fix_headers(self, input_dict):
"""Fix headers from bytestrings to normal strings"""
return {
key.decode('utf-8'): value.decode('utf-8')
for key, value in dict(input_dict).items()
}
def connect(self):
"""Extract host header, lookup in database and proxy connection"""
self._headers_dict = self._fix_headers(dict(self.scope.get('headers')))
host = self._headers_dict.pop('host')
query_string = self.scope.get('query_string').decode('utf-8')
matches = ApplicationGatewayProvider.objects.filter(
server_name__contains=[host],
enabled=True)
if matches.exists():
self._app_gw = matches.first()
# TODO: Get upstream that starts with wss or
upstream = self._app_gw.upstream[0].replace('http', 'ws') + self.scope.get('path')
if query_string:
upstream += '?' + query_string
sslopt = {}
if not self._app_gw.upstream_ssl_verification:
sslopt = {"cert_reqs": CERT_NONE}
self._client = websocket.WebSocketApp(
url=upstream,
subprotocols=self.scope.get('subprotocols'),
header=self._headers_dict,
on_message=self._client_on_message_handler(),
on_error=self._client_on_error_handler(),
on_close=self._client_on_close_handler(),
on_open=self._client_on_open_handler())
LOGGER.debug("Accepting connection for %s", host)
self._thread = threading.Thread(target=lambda: self._client.run_forever(sslopt=sslopt))
self._thread.start()
def _client_on_open_handler(self):
return lambda ws: self.accept(self._client.sock.handshake_response.subprotocol)
def _client_on_message_handler(self):
# pylint: disable=unused-argument,invalid-name
def message_handler(ws, message):
if isinstance(message, str):
self.send(text_data=message)
else:
self.send(bytes_data=message)
return message_handler
def _client_on_error_handler(self):
return lambda ws, error: print(error)
def _client_on_close_handler(self):
return lambda ws: self.disconnect(0)
def disconnect(self, code):
self._client.close()
def receive(self, text_data=None, bytes_data=None):
if text_data:
opcode = websocket.ABNF.OPCODE_TEXT
if bytes_data:
opcode = websocket.ABNF.OPCODE_BINARY
self._client.send(text_data or bytes_data, opcode)

View File

@ -1,17 +0,0 @@
"""app_gw websocket proxy"""
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.conf.urls import url
from passbook.app_gw.websocket.consumer import ProxyConsumer
websocket_urlpatterns = [
url(r'^(.*)$', ProxyConsumer),
]
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
),
})

View File

@ -1,7 +1,7 @@
"""passbook policy engine"""
from multiprocessing import Pipe
from multiprocessing.connection import Connection
from typing import List, Tuple
from typing import List, Tuple, Tuple
from django.core.cache import cache
from django.http import HttpRequest
@ -14,14 +14,11 @@ from passbook.policy.struct import PolicyResult, PolicyRequest
LOGGER = get_logger()
def _cache_key(policy, user):
return "policy_%s#%s" % (policy.uuid, user.pk)
return f"policy_{policy.pk}#{user.pk}"
class PolicyEngine:
"""Orchestrate policy checking, launch tasks and return result"""
# __group = None
# __cached = None
policies: List[Policy] = []
__request: HttpRequest
__user: User
@ -53,19 +50,19 @@ class PolicyEngine:
for policy in self.policies:
cached_policy = cache.get(_cache_key(policy, self.__user), None)
if cached_policy:
LOGGER.debug("Taking result from cache for %s", policy.pk.hex)
LOGGER.debug("Taking result from cache", policy=policy.pk.hex)
cached_policies.append(cached_policy)
else:
LOGGER.debug("Looking up real class of policy...")
# TODO: Rewrite this to lookup all policies at once
policy = Policy.objects.get_subclass(pk=policy.pk)
LOGGER.debug("Evaluating policy %s", policy.pk.hex)
LOGGER.debug("Evaluating policy", policy=policy.pk.hex)
our_end, task_end = Pipe(False)
task = PolicyTask()
task.ret = task_end
task.request = request
task.policy = policy
LOGGER.debug("Starting Process %s", task.__class__.__name__)
LOGGER.debug("Starting Process", class_name=task.__class__.__name__)
task.start()
self.__proc_list.append((our_end, task))
# If all policies are cached, we have an empty list here.
@ -75,13 +72,11 @@ class PolicyEngine:
return self
@property
def result(self):
def result(self) -> Tuple[bool, List[str]]:
"""Get policy-checking result"""
results: List[PolicyResult] = []
messages: List[str] = []
for our_end, _ in self.__proc_list:
results.append(our_end.recv())
for policy_result in results:
policy_result = our_end.recv()
# passing = (policy_action == Policy.ACTION_ALLOW and policy_result) or \
# (policy_action == Policy.ACTION_DENY and not policy_result)
LOGGER.debug('Result=%r => %r', policy_result, policy_result.passing)
@ -92,6 +87,6 @@ class PolicyEngine:
return True, messages
@property
def passing(self):
def passing(self) -> bool:
"""Only get true/false if user passes"""
return self.result[0]

View File

@ -34,10 +34,8 @@ STATIC_ROOT = BASE_DIR + '/static'
SECRET_KEY = CONFIG.y('secret_key',
"9$@r!d^1^jrn#fk#1#@ks#9&i$^s#1)_13%$rwjrhd=e8jfi_s") # noqa Debug
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = CONFIG.y_bool('debug')
INTERNAL_IPS = ['127.0.0.1']
# ALLOWED_HOSTS = CONFIG.y('domains', []) + [CONFIG.y('primary_domain')]
ALLOWED_HOSTS = ['*']
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
@ -49,7 +47,6 @@ AUTH_USER_MODEL = 'passbook_core.User'
CSRF_COOKIE_NAME = 'passbook_csrf'
SESSION_COOKIE_NAME = 'passbook_session'
SESSION_COOKIE_DOMAIN = CONFIG.y('primary_domain')
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
LANGUAGE_COOKIE_NAME = 'passbook_language'