Skip to content
This repository has been archived by the owner on Dec 11, 2021. It is now read-only.

Commit

Permalink
Merge pull request #94 from seomoz/amit/ROGER-1093
Browse files Browse the repository at this point in the history
Added Auth and filtering for v2/apps|groups and scheduler/jobs endpoints
  • Loading branch information
amitbose327 authored Aug 5, 2016
2 parents 9fc3bc7 + 3c3edda commit 17c5974
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 31 deletions.
25 changes: 24 additions & 1 deletion aaad/aaad.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
import threading
import logging
import argparse
import utils
from SocketServer import ThreadingMixIn
from authenticators import FileAuthenticator
from authorizers import FileAuthorizer
from frameworkUtils import FrameworkUtils


class AuthHTTPServer(ThreadingMixIn, HTTPServer, ):
Expand All @@ -17,6 +19,7 @@ class AuthHTTPServer(ThreadingMixIn, HTTPServer, ):
class AuthHandler(BaseHTTPRequestHandler):
ctx = {}
authenticator = None
frameworkUtils = FrameworkUtils()

def do_GET(self):

Expand All @@ -26,6 +29,7 @@ def do_GET(self):
resource = self.headers.get('URI')
action = self.headers.get('method')
client_ip = self.headers.get('X-Forwarded-For')
auth_action = self.headers.get("auth_action", "")

# Carry out Basic Authentication
if auth_header is None or not auth_header.lower().strip().startswith('basic '):
Expand Down Expand Up @@ -54,6 +58,14 @@ def do_GET(self):

content_type = self.headers.getheader("content-type", "")

if auth_action == "filter_response":
filtered_response = self.filter_response(body, resource, act_as_user, permissions)
self.send_response(200)
self.end_headers()
self.wfile.write(filtered_response)
self.wfile.close()
return True

if not self.authenticate_request(user, password):
self.send_response(401)
self.end_headers()
Expand All @@ -63,12 +75,22 @@ def do_GET(self):
self.send_response(403)
self.end_headers()
return False

self.log_message(ctx['action']) # Continue request processing
self.send_response(200)
self.end_headers()
return True

def filter_response(self, body, resource, act_as_user, permissions):
framework = self.frameworkUtils.getFramework(resource)
allowed_namespaces = utils.getAllowedNamespacePatterns(act_as_user, permissions)

if not allowed_namespaces: #Empty list
return ""

response = framework.filterResponseBody(body, allowed_namespaces, resource)
return response

def authenticate_request(self, user, password):
ctx = self.ctx
try:
Expand Down Expand Up @@ -159,6 +181,7 @@ def parse_args():
FORMAT = "[%(asctime)-15s] %(levelname)s - %(name)s - IP:%(clientip)s User:%(user)s ActAs:%(act_as)s - %(message)s"
logging.basicConfig(level=level, format=FORMAT)
logger = logging.getLogger("AAAd")
permissions = utils.parse_permissions_file(permissions_file)
server = AuthHTTPServer(Listen, AuthHandler)
signal.signal(signal.SIGINT, exit_handler)
server.serve_forever()
30 changes: 3 additions & 27 deletions aaad/authorizers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,6 @@ def __init__(self, filename):
self.filename = filename
self.data = utils.parse_permissions_file(filename)

def get_merged_data(self, user, allowed_users, allowed_actions, data, action):
if user in allowed_users:
return
allowed_users.append(user)
if action != '' and 'action' in data[user]:
if data[user]['action'] != None and action in data[user]['action']:
for item in data[user]['action'][action]:
temp_item = {}
if type(item) == str:
temp_item = {}
temp_item[item] = {}
else:
if type(item) == dict:
temp_item = item

if not temp_item in allowed_actions:
allowed_actions.append(temp_item)

if 'can_act_as' not in data[user]:
return

for u in data[user]['can_act_as']:
self.get_merged_data(u, allowed_users, allowed_actions, data, action)

def resource_check(self, request_uri, body, allowed_actions, content_type):
for item in allowed_actions:
uri = item.keys()
Expand Down Expand Up @@ -105,7 +81,7 @@ def authorize(self, user, act_as, resource, logging, info, data, content_type, a
return False

allowed_users_list = []
self.get_merged_data(user, allowed_users_list, [], self.data, '')
utils.get_merged_data(user, allowed_users_list, [], self.data, '')

if act_as not in allowed_users_list:
logger.warning("User act as failed", extra = info)
Expand All @@ -126,8 +102,8 @@ def authorize(self, user, act_as, resource, logging, info, data, content_type, a
if not temp_item in allowed_actions:
allowed_actions.append(temp_item)

self.get_merged_data(act_as, allowed_users_list, allowed_actions, self.data, action)
utils.get_merged_data(act_as, allowed_users_list, allowed_actions, self.data, action)

result = self.resource_check(resource, data, allowed_actions, content_type)
if result == False:
logger.warning("Unauthorized [{}]".format(resource), extra = info)
Expand Down
31 changes: 31 additions & 0 deletions aaad/chronos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/python

from __future__ import print_function
import os
import sys
import json
import re
from framework import Framework

class Chronos(Framework):

def getName(self):
return "Chronos"

def filterResponseBody(self, body, allowed_namespaces, request_uri):
filtered_response = []
try:
jobs = json.loads(body)
for job in jobs:
if 'name' in job:
job_name = job['name']
for namespace in allowed_namespaces:
pattern = re.compile("^{}$".format(namespace))
result = pattern.match(job_name)
if result:
if not job in filtered_response:
filtered_response.append(job)

return json.dumps(filtered_response)
except (Exception) as e:
return ""
14 changes: 14 additions & 0 deletions aaad/framework.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/python

from __future__ import print_function
import os
import sys
from abc import ABCMeta, abstractmethod

class Framework(object):
__metaclass__ = ABCMeta

@abstractmethod
def filterResponseBody(self, body, allowed_namespaces, request_uri):
pass

23 changes: 23 additions & 0 deletions aaad/frameworkUtils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/python

from __future__ import print_function
import os
import sys
import re
from marathon import Marathon
from chronos import Chronos


class FrameworkUtils:

def getFramework(self, request_uri):
marathon_pattern = re.compile("^{}$".format("/marathon/.*"))
marathon_check = marathon_pattern.match(request_uri)
chronos_pattern = re.compile("^{}$".format("/chronos/.*"))
chronos_check = chronos_pattern.match(request_uri)
if marathon_check:
return Marathon()
elif chronos_check:
return Chronos()
else:
return None
129 changes: 129 additions & 0 deletions aaad/marathon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/python

from __future__ import print_function
import os
import sys
import json
import re
from framework import Framework

class Marathon(Framework):

def getName(self):
return "Marathon"

def filterResponseBody(self, body, allowed_namespaces, request_uri):
uri_pattern = re.compile("^{}$".format("/marathon/*v2/apps"))
uri_match = uri_pattern.match(request_uri)
if uri_match:
response = self.filterV2AppsResponse(body, allowed_namespaces)
return response

uri_pattern = re.compile("^{}$".format("/marathon/*v2/groups"))
uri_match = uri_pattern.match(request_uri)
if uri_match:
response = self.filterV2GroupsResponse(body, allowed_namespaces)
return response

return ""


def filterV2AppsResponse(self, body, allowed_namespaces):
filtered_apps = []
filtered_response = {}
try:
data = json.loads(body)
if 'apps' in data:
apps = data['apps']
if not type(apps) == list:
return ""

for app in apps:
if 'id' in app:
app_id = app['id']
for namespace in allowed_namespaces:
pattern = re.compile("^{}$".format(namespace))
result = pattern.match(app_id)
if result:
if not app in filtered_apps:
filtered_apps.append(app)

filtered_response['apps'] = filtered_apps

return json.dumps(filtered_response)
except (Exception) as e:
return ""

def filterV2GroupsResponse(self, body, allowed_namespaces):
response = {}
filtered_apps = []
filtered_groups = []

try:
data = json.loads(body)
if 'apps' in data.keys():
filtered_apps = self.filterApps(data['apps'], allowed_namespaces)
if 'groups' in data:
filtered_groups = self.filterGroups(data['groups'], allowed_namespaces)

for item in data.keys():
if item != 'apps' and item != 'groups':
response[item] = data[item]

response['apps'] = filtered_apps
response['groups'] = filtered_groups

return json.dumps(response)

except (Exception) as e:
return ""

def matchPattern(self, id_to_match, allowed_namespaces):
for namespace in allowed_namespaces:
pattern = re.compile("^{}$".format(namespace))
result = pattern.match(id_to_match)
if result:
return True

return False

def filterApps(self, apps_list, allowed_namespaces):
allowed_apps = []
for item in apps_list:
if 'id' in item:
app_id = item['id']
match_app_id = self.matchPattern(app_id, allowed_namespaces)
if match_app_id and (not item in allowed_apps):
allowed_apps.append(item)

return allowed_apps

def filterGroups(self, groups_list, allowed_namespaces):
allowed_groups = []
for item in groups_list:
filtered_item = {}
if 'id' in item:
group_id = item['id']
match_group_id = self.matchPattern(group_id, allowed_namespaces)
if match_group_id and (not item in allowed_groups):
allowed_groups.append(item)
else: #The id at this level does not match any allowed_namespaces, but a nested id may match
filtered_apps = []
filtered_groups = []
if 'apps' in item and item['apps']: #Check to see if item['apps'] is not empty
filtered_apps = self.filterApps(item['apps'], allowed_namespaces)

if 'groups' in item and item['groups']: #Check to see if item['groups'] is not empty
filtered_groups = self.filterGroups(item['groups'], allowed_namespaces)

if filtered_apps or filtered_groups: #Either filtered apps or groups is not empty
for elem in item.keys():
if elem != 'apps' and elem != 'groups':
filtered_item[elem] = item[elem]

filtered_item['apps'] = filtered_apps
filtered_item['groups'] = filtered_groups
allowed_groups.append(filtered_item)

return allowed_groups

39 changes: 39 additions & 0 deletions aaad/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,42 @@ def parse_permissions_file(filename):
return yaml.load(data_file)
return ''

def get_merged_data(user, allowed_users, allowed_actions, data, action):
if user in allowed_users:
return
allowed_users.append(user)
if action != '' and 'action' in data[user]:
if data[user]['action'] != None and action in data[user]['action']:
for item in data[user]['action'][action]:
temp_item = {}
if type(item) == str:
temp_item = {}
temp_item[item] = {}
else:
if type(item) == dict:
temp_item = item

if not temp_item in allowed_actions:
allowed_actions.append(temp_item)

if 'can_act_as' not in data[user]:
return

for u in data[user]['can_act_as']:
get_merged_data(u, allowed_users, allowed_actions, data, action)

def getAllowedNamespacePatterns(act_as, permissions):

allowed_users_list = []
get_merged_data(act_as, allowed_users_list, [], permissions, '')

allowed_namespace_patterns = []

for user in allowed_users_list:
if 'allowed_names' in permissions[user]:
for pattern in permissions[user]['allowed_names']:
if not pattern in allowed_namespace_patterns:
allowed_namespace_patterns.append(pattern)

return allowed_namespace_patterns

1 change: 1 addition & 0 deletions ansible/roles/nginx/files/auth_request.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
ngx.req.read_body()
local data = ngx.req.get_body_data()
ngx.req.set_header("auth_action", "auth")
local res = ngx.location.capture("/auth-proxy", {body=data})

if res.status == ngx.HTTP_OK then
Expand Down
Loading

0 comments on commit 17c5974

Please sign in to comment.