Commit 9ffd9126 authored by Lukas Burgey's avatar Lukas Burgey

Implement message transport to the webpages

parent caacb301
......@@ -3,102 +3,215 @@ import logging
import re
from django.http import HttpResponse
from django.contrib.auth import authenticate
from django.contrib.sessions.models import Session
from ...models import User, RabbitMQInstance
LOGGER = logging.getLogger(__name__)
ALLOW = HttpResponse('allow')
DENY = HttpResponse('deny')
def _valid_vhost(request):
if 'vhost' in request.POST and request.POST['vhost'] == RabbitMQInstance.load().vhost:
if request.POST.get('vhost') == RabbitMQInstance.load().vhost:
return True
LOGGER.error('illegal vhost requested')
return False
def _valid_permission(request):
if 'permission' in request.POST and request.POST['permission'] != 'write':
perm = request.POST.get('permission')
if perm != 'write':
return True
LOGGER.error('illegal permission requested')
LOGGER.error('illegal permission requested %s', perm)
return False
def _valid_user(request):
if 'username' in request.POST:
valid = User.objects.filter(
user_type='apiclient',
username=request.POST['username'],
).exists()
if valid:
return True
LOGGER.error('illegal user requested')
return _apiclient_valid(request) or _webpage_client_userid(request)
def _apiclient_valid(request):
valid = User.objects.filter(
user_type='apiclient',
username=request.POST.get('username'),
).exists()
if valid:
return True
return False
def _get_user(request):
if 'username' in request.POST:
user = User.objects.filter(user_type='apiclient').get(username=request.POST['username'])
if user:
return user
def _apiclient_get(request):
user = User.objects.filter(
user_type='apiclient',
).get(
username=request.POST.get('username'),
)
if user:
return user
LOGGER.error('unable to get user for request')
return None
# VIEWS
# client authentication for RabbitMQ
def user_endpoint(request):
if 'username' in request.POST and 'password' in request.POST:
username = request.POST['username']
password = request.POST['password']
user = authenticate(username=username, password=password)
if user:
LOGGER.info('Authenticated client as %s', user)
return HttpResponse("allow")
def _webpage_client_userid(request):
username = request.POST.get('username')
if username.startswith('webpage-client:'):
components = username.split(':', maxsplit=1)
if len(components) == 2:
return components[1]
return ''
def _webpage_client_session(request):
query = Session.objects.filter(
session_key=request.POST.get('password'),
)
if query.exists() and len(query) == 1:
return query.first()
return None
LOGGER.error('Failed to authenticate user for RabbitMQ')
return HttpResponse("deny")
def _webpage_client_valid(request):
userid = _webpage_client_userid(request)
session = _webpage_client_session(request)
if (
_webpage_client_userid(request) != ''
and session.get_decoded().get('_auth_user_id') == userid
):
return True
# client authorization checks for RabbitMQ
def vhost(request):
# check if on the correct virtual host
if _valid_vhost(request) and _valid_user(request):
return HttpResponse("allow")
LOGGER.error('Failed to authenticate webpage client for RabbitMQ')
return False
LOGGER.error('Authorization check for vhost failed for %s', request.POST)
return HttpResponse("deny")
# VIEWS: authentication and authorization for
# apiclients and webpage-clients
def resource(request):
if not _valid_vhost(request) or not _valid_user(request):
return HttpResponse('deny')
def user_endpoint(request):
if _webpage_client_valid(request):
LOGGER.info('Authenticated webpage client')
return ALLOW
if 'resource' in request.POST and 'name' in request.POST:
if request.POST['resource'] == 'queue':
# the temporary queue a client binds to our exchange
if request.POST['name'].startswith('amq.gen-'):
return HttpResponse('allow')
elif request.POST['resource'] == 'exchange' and _valid_permission(request):
# our exchange
if request.POST['name'] == RabbitMQInstance.load().exchange:
return HttpResponse('allow')
elif request.POST['resource'] == 'topic' and _valid_permission(request):
pass
user = authenticate(
username=request.POST.get('username'),
password=request.POST.get('password'),
)
if user:
LOGGER.info('Authenticated client as %s', user)
return ALLOW
LOGGER.error('Authorization check for resource failed for %s', request.POST)
return HttpResponse('deny')
LOGGER.error('Failed to authenticate user for RabbitMQ')
return DENY
def topic(request):
def vhost_endpoint(request):
# check if on the correct virtual host
if not _valid_vhost(request) or not _valid_permission(request) or not _valid_user(request):
return HttpResponse('deny')
if _valid_vhost(request) and _valid_user(request):
return ALLOW
user = _get_user(request)
if user and 'routing_key' in request.POST:
routing_key = request.POST['routing_key']
LOGGER.error('Authorization check for vhost failed for %s', request.POST)
return DENY
def _resource_authorized_webpage_client(request):
resource = request.POST.get('resource')
name = request.POST.get('name', '')
permission = request.POST.get('permission', [])
return (
resource == 'exchange'
and name == 'update'
and not 'write' in permission
) or (
resource == 'queue'
and name.startswith('stomp-subscription-')
) or (
resource == 'topic'
and name == _webpage_client_userid(request)
)
def _resource_authorized_apiclient(request):
resource = request.POST.get('resource')
name = request.POST.get('name', '')
permission = request.POST.get('permission', [])
return (
resource == 'queue'
and name.startswith('amq.gen-')
) or (
resource == 'exchange'
and name == RabbitMQInstance.load().exchange
and not 'write' in permission
)
def resource_endpoint(request):
resource = request.POST.get('resource')
name = request.POST.get('name', '')
permission = request.POST.get('permission', [])
if _valid_vhost(request):
if (
_webpage_client_userid(request)
and _resource_authorized_webpage_client(request)
):
LOGGER.debug(
'Granted %s access to resource %s %s to client',
permission,
resource,
name,
)
return ALLOW
if (
_apiclient_valid(request)
and _resource_authorized_apiclient(request)
):
LOGGER.debug(
'Granted %s access to resource %s %s to client',
permission,
resource,
name,
)
return ALLOW
LOGGER.error(
'Authorization check of resource %s %s for client failed',
resource,
name,
)
return DENY
def topic_endpoint(request):
permission = request.POST.get('permission', [])
resource = request.POST.get('resource', '')
name = request.POST.get('name', '')
routing_key = request.POST.get('routing_key', '')
if not _valid_vhost(request) or not _valid_permission(request):
return DENY
webpage_client_userid = _webpage_client_userid(request)
if webpage_client_userid:
if (
routing_key == webpage_client_userid
and not 'write' in permission
):
LOGGER.debug(
'Granted %s access to %s %s to client',
permission,
resource,
routing_key,
)
return ALLOW
LOGGER.error(
'Authorization check of resource %s %s for client failed',
resource,
name,
)
return DENY
user = _apiclient_get(request)
if user:
routing_key = request.POST.get('routing_key', '')
if routing_key.startswith('service.'):
match = re.search('service.(.+)', routing_key)
if match:
service_name = match.group(1)
for service in user.site.services.all():
if service_name == service.name:
return HttpResponse('allow')
return ALLOW
LOGGER.error('Authorization check for topic failed for %s', request.POST)
return HttpResponse('deny')
return DENY
......@@ -8,7 +8,7 @@ URLPATTERNS = [
url(r'^callback', views.AuthCallback.as_view()),
url(r'^logout', views.LogoutView.as_view()),
url(r'^client/user', csrf_exempt(client_views.user_endpoint)),
url(r'^client/vhost', csrf_exempt(client_views.vhost)),
url(r'^client/resource', csrf_exempt(client_views.resource)),
url(r'^client/topic', csrf_exempt(client_views.topic)),
url(r'^client/vhost', csrf_exempt(client_views.vhost_endpoint)),
url(r'^client/resource', csrf_exempt(client_views.resource_endpoint)),
url(r'^client/topic', csrf_exempt(client_views.topic_endpoint)),
]
......@@ -44,7 +44,7 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = models.User
fields = ['email', 'userinfo', 'ssh_keys', 'groups', 'deployments', 'auth_groups']
fields = ['id', 'email', 'userinfo', 'ssh_keys', 'groups', 'deployments', 'auth_groups']
class ClientSerializer(serializers.HyperlinkedModelSerializer):
......
......@@ -28,18 +28,22 @@ def _api_error_response():
# Response for StateView, LogoutView, and all post requests
def _api_state_response(request):
if not request.user.is_authenticated:
return Response(
{
'logged_in': False,
}
)
response = {
'logged_in': request.user.is_authenticated
'logged_in': True,
'user': serializers.UserSerializer(request.user).data,
'services': serializers.ServiceSerializer(
user_services(request.user),
many=True,
).data,
}
if request.user.is_authenticated:
response['user'] = serializers.UserSerializer(request.user).data
response['services'] = serializers.ServiceSerializer(
user_services(request.user),
many=True,
).data
if 'error' in request.session:
response['error'] = request.session['error']
......@@ -122,6 +126,7 @@ class DeploymentView(views.APIView):
deployment.save()
return _api_state_response(request)
class UserDeletionView(views.APIView):
def delete(self, request):
# this also logs out the user
......
......@@ -15,8 +15,11 @@ from django_mysql.models import JSONField
from .auth.v1.models import OIDCConfig
LOGGER = logging.getLogger(__name__)
# TODO dirty
RECONNECT_TIMEOUT = 5
RECONNECT_RETRIES = 3
RABBITMQ_CONNECTION = None
# singleton for simple configs
......@@ -83,50 +86,83 @@ class RabbitMQInstance(SingletonModel):
self.password,
)
def _init_exchanges(self, channel):
channel.exchange_declare(
exchange=self.exchange,
durable=True,
auto_delete=False,
exchange_type='topic',
)
# TODO put in config
channel.exchange_declare(
exchange='update',
durable=True,
auto_delete=False,
exchange_type='topic',
)
def _init_connection(self):
global RABBITMQ_CONNECTION
LOGGER.info('Opening new BlockingConnection')
RABBITMQ_CONNECTION = pika.BlockingConnection(
pika.ConnectionParameters(
host=self.host,
ssl=True,
heartbeat_interval=60,
)
)
return RABBITMQ_CONNECTION
@property
def _connection(self):
global RABBITMQ_CONNECTION
if RABBITMQ_CONNECTION is not None:
if RABBITMQ_CONNECTION.is_open:
return RABBITMQ_CONNECTION
elif RABBITMQ_CONNECTION.is_closing:
RABBITMQ_CONNECTION.close()
connection = self._init_connection()
channel = connection.channel()
self._init_exchanges(connection.channel())
channel.close()
RABBITMQ_CONNECTION = connection
return connection
@property
def _connection_parameters(self):
return pika.ConnectionParameters(
host=self.host,
ssl=True,
def _channel(self):
channel = self._connection.channel()
channel.confirm_delivery()
return channel
def _publish(self, exchange, routing_key, body):
channel = self._channel
self._channel.basic_publish(
exchange=exchange,
routing_key=routing_key,
body=body,
properties=pika.BasicProperties(
delivery_mode=1,
),
)
channel.close()
# PUBLIC API
# PUBLIC API
def publish_by_service(self, service, msg):
# FIXME dirty
tries = 0
while tries < RECONNECT_RETRIES:
try:
# open connection
connection = pika.BlockingConnection(
self._connection_parameters,
)
# open channel
channel = connection.channel()
channel.exchange_declare(
exchange=self.exchange,
durable=True,
auto_delete=False,
exchange_type='topic',
)
channel.confirm_delivery()
channel.basic_publish(
exchange=self.exchange,
routing_key=service.routing_key,
body=msg,
properties=pika.BasicProperties(
delivery_mode=1,
),
)
channel.close()
connection.close()
return
except:
time.sleep(RECONNECT_TIMEOUT)
self._publish(
self.exchange,
service.routing_key,
msg,
)
tries += 1
def publish_to_webpage(self, user):
LOGGER.debug('Signalling webpage of user %s', user)
self._publish(
'update',
str(user.id),
'<got update>',
)
def user_info_default():
......@@ -295,7 +331,6 @@ class User(AbstractUser):
for dep in self.deployments.all():
dep.activate()
def deactivate(self):
if not self._is_active:
LOGGER.error(self.msg('already deactivated'))
......@@ -674,6 +709,9 @@ class DeploymentTask(models.Model):
# the client acked the receipt and execution of the task for his site
def item_finished(self, site):
RabbitMQInstance.load().publish_to_webpage(
self.user,
)
item = self.task_items.get(site=site)
LOGGER.debug(item.msg('done'))
item.delete()
......
......@@ -17,7 +17,10 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
AUTH_USER_MODEL = 'backend.User'
# cookie settings
SESSION_COOKIE_AGE = 3600
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = False
CSRF_HEADER_NAME = 'HTTP_X_CSRFTOKEN'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment