Missing security support

This fix provides to Synergy a security mechanism highly configurable.
The security policies are pluggable so that it is possible to define any
kind of authorization checks.
This commit includes a very simple authorization plugin (i.e.
synergy.auth.plugin.LocalHostAuthorization) which denies any command
coming from clients having IP address different from the Synergy's one.

Bug: #1691352
Change-Id: I2535b2a3edeea5e56cd8918d01070a6f8a534c3e
Sem-Ver: bugfix
This commit is contained in:
Lisa Zangrando 2017-05-22 12:23:38 +02:00
parent a8c06a001c
commit b99f2078a1
11 changed files with 104 additions and 12 deletions

View File

@ -37,3 +37,6 @@ max_header_line = 16384
retry_until_window = 30 retry_until_window = 30
tcp_keepidle = 600 tcp_keepidle = 600
backlog = 4096 backlog = 4096
[Authorization]
# plugin = synergy.auth.plugin.LocalHostAuthorization

0
synergy/auth/__init__.py Normal file
View File

30
synergy/auth/plugin.py Normal file
View File

@ -0,0 +1,30 @@
from synergy.exception import AuthorizationError
__author__ = "Lisa Zangrando"
__email__ = "lisa.zangrando[AT]pd.infn.it"
__copyright__ = """Copyright (c) 2015 INFN - INDIGO-DataCloud
All Rights Reserved
Licensed under the Apache License, Version 2.0;
you may not use this file except in compliance with the
License. You may obtain a copy of the License at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied.
See the License for the specific language governing
permissions and limitations under the License."""
class LocalHostAuthorization(object):
def authorize(self, context):
server_addr = context.get("SERVER_NAME")
remote_addr = context.get("REMOTE_ADDR")
if not server_addr or not remote_addr or server_addr != remote_addr:
raise AuthorizationError("You are not authorized!")

View File

@ -28,15 +28,27 @@ class HTTPCommand(object):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
self.token = None
def getName(self): def getName(self):
return self.name return self.name
def setToken(self, token):
self.token = token
def configureParser(self, subparser): def configureParser(self, subparser):
raise NotImplementedError("not implemented!") raise NotImplementedError("not implemented!")
def execute(self, synergy_url, payload=None): def execute(self, synergy_url, payload=None):
request = requests.get(synergy_url, params=payload) headers = None
if self.token:
headers = {"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "synergy_client",
"X-Auth-Token": self.token.getId()}
request = requests.get(synergy_url, headers=headers, params=payload)
request.raise_for_status() request.raise_for_status()
try: try:

View File

@ -208,6 +208,11 @@ class KeystoneClient(object):
self.token = Token(token_subject, token_data) self.token = Token(token_subject, token_data)
return self.token
def getToken(self):
return self.token
def getService(self, name): def getService(self, name):
for service in self.token.getCatalog(): for service in self.token.getCatalog():
if service["name"] == name: if service["name"] == name:

View File

@ -124,6 +124,7 @@ def main():
os_cacert = args.os_cacert os_cacert = args.os_cacert
bypass_url = args.bypass_url bypass_url = args.bypass_url
command_name = args.command_name command_name = args.command_name
token = None
if bypass_url: if bypass_url:
synergy_url = bypass_url synergy_url = bypass_url
@ -157,15 +158,14 @@ def main():
project_domain_id=os_project_domain_id, project_domain_id=os_project_domain_id,
project_domain_name=os_project_domain_name) project_domain_name=os_project_domain_name)
client.authenticate() token = client.authenticate()
synergy_endpoint = client.getEndpoint("synergy") synergy_endpoint = client.getEndpoint("synergy")
synergy_url = synergy_endpoint["url"] synergy_url = synergy_endpoint["url"]
if command_name not in commands: if command_name not in commands:
print("command %r not found!" % command_name) print("command %r not found!" % command_name)
commands[command_name].setToken(token)
commands[command_name].execute(synergy_url, args) commands[command_name].execute(synergy_url, args)
except KeyboardInterrupt as e: except KeyboardInterrupt as e:
print("Shutting down synergyclient") print("Shutting down synergyclient")

View File

@ -21,9 +21,9 @@ permissions and limitations under the License."""
CONF = cfg.CONF CONF = cfg.CONF
service_opts = [ auth_opts = [
cfg.StrOpt("topic", default="synergy_topic", help="the topic"), cfg.StrOpt("plugin", default="noauth", help="the authorization plugin"),
cfg.StrOpt("exchange", default="synergy_exchange", help="the exchange"), cfg.StrOpt("policy_file", default="policy.json", help="the plucy file"),
] ]
wsgi_opts = [ wsgi_opts = [
@ -67,9 +67,9 @@ manager_opts = [
cfg.IntOpt("rate", default=60) cfg.IntOpt("rate", default=60)
] ]
cfg.CONF.register_opts(service_opts)
cfg.CONF.register_opts(wsgi_opts, group="WSGI") cfg.CONF.register_opts(wsgi_opts, group="WSGI")
cfg.CONF.register_opts(logger_opts, group="Logger") cfg.CONF.register_opts(logger_opts, group="Logger")
cfg.CONF.register_opts(auth_opts, group="Authorization")
def parseArgs(args=None, usage=None, default_config_files=None): def parseArgs(args=None, usage=None, default_config_files=None):

View File

@ -37,6 +37,10 @@ def import_class(import_str):
(class_str, traceback.format_exception(*sys.exc_info()))) (class_str, traceback.format_exception(*sys.exc_info())))
def instantiate_class(class_str):
return import_class(class_str)()
def objectHookHandler(json_dict): def objectHookHandler(json_dict):
for key, value in json_dict.items(): for key, value in json_dict.items():
if isinstance(value, dict): if isinstance(value, dict):

View File

@ -19,3 +19,7 @@ permissions and limitations under the License."""
class SynergyError(Exception): class SynergyError(Exception):
pass pass
class AuthorizationError(Exception):
pass

View File

@ -15,7 +15,9 @@ from synergy.common import config
from synergy.common.manager import Manager from synergy.common.manager import Manager
from synergy.common.serializer import SynergyEncoder from synergy.common.serializer import SynergyEncoder
from synergy.common.service import Service from synergy.common.service import Service
from synergy.common import utils
from synergy.common.wsgi import Server from synergy.common.wsgi import Server
from synergy.exception import AuthorizationError
from synergy.exception import SynergyError from synergy.exception import SynergyError
@ -91,9 +93,17 @@ class Synergy(Service):
self.managers = {} self.managers = {}
self.wsgi_server = None self.wsgi_server = None
self.auth_plugin = CONF.Authorization.plugin
if self.auth_plugin == "noauth":
LOG.info("the authorization is disabled!")
self.auth_plugin = None
else:
LOG.info("loading the auth_plugin %s" % self.auth_plugin)
self.auth_plugin = utils.instantiate_class(self.auth_plugin)
for entry in iter_entry_points(MANAGER_ENTRY_POINT): for entry in iter_entry_points(MANAGER_ENTRY_POINT):
LOG.info("loading manager %s", entry.name) LOG.info("loading the %s manager", entry.name)
try: try:
CONF.register_opts(config.manager_opts, group=entry.name) CONF.register_opts(config.manager_opts, group=entry.name)
@ -143,6 +153,28 @@ class Synergy(Service):
self.saved_args, self.saved_kwargs = args, kwargs self.saved_args, self.saved_kwargs = args, kwargs
def authorizationRequired(f):
def wrapper(self, *args, **kw):
if self.auth_plugin:
context = args[0]
context["managers"] = self.managers
query = context.get("QUERY_STRING", None)
if query:
context.update(parse_qs(query))
try:
self.auth_plugin.authorize(context)
except AuthorizationError as ex:
args[1]("401 Unauthorized",
[("Content-Type", "text/plain")])
return [ex.message]
return f(self, *args, **kw)
return wrapper
@authorizationRequired
def listManagers(self, environ, start_response): def listManagers(self, environ, start_response):
result = [] result = []
@ -156,6 +188,7 @@ class Synergy(Service):
start_response("200 OK", [("Content-Type", "text/html")]) start_response("200 OK", [("Content-Type", "text/html")])
return ["%s" % json.dumps(result, cls=SynergyEncoder)] return ["%s" % json.dumps(result, cls=SynergyEncoder)]
@authorizationRequired
def getManagerStatus(self, environ, start_response): def getManagerStatus(self, environ, start_response):
manager_list = None manager_list = None
result = [] result = []
@ -194,10 +227,10 @@ class Synergy(Service):
start_response("200 OK", [("Content-Type", "text/html")]) start_response("200 OK", [("Content-Type", "text/html")])
return ["%s" % json.dumps(result, cls=SynergyEncoder)] return ["%s" % json.dumps(result, cls=SynergyEncoder)]
@authorizationRequired
def executeCommand(self, environ, start_response): def executeCommand(self, environ, start_response):
manager_name = None manager_name = None
command = None command = None
query = environ.get("QUERY_STRING", None) query = environ.get("QUERY_STRING", None)
if not query: if not query:
@ -251,6 +284,7 @@ class Synergy(Service):
[("Content-Type", "text/plain")]) [("Content-Type", "text/plain")])
return ["error: %s" % ex] return ["error: %s" % ex]
@authorizationRequired
def startManager(self, environ, start_response): def startManager(self, environ, start_response):
manager_list = None manager_list = None
result = [] result = []
@ -308,10 +342,10 @@ class Synergy(Service):
start_response("200 OK", [("Content-Type", "text/html")]) start_response("200 OK", [("Content-Type", "text/html")])
return ["%s" % json.dumps(result, cls=SynergyEncoder)] return ["%s" % json.dumps(result, cls=SynergyEncoder)]
@authorizationRequired
def stopManager(self, environ, start_response): def stopManager(self, environ, start_response):
manager_list = None manager_list = None
result = [] result = []
query = environ.get("QUERY_STRING", None) query = environ.get("QUERY_STRING", None)
if not query: if not query:

View File

@ -47,5 +47,5 @@ class TestHTTPCommand(base.TestCase):
as m: as m:
result = self.http_command.execute("dummy_url") result = self.http_command.execute("dummy_url")
m.assert_called_once_with("dummy_url", params=None) m.assert_called_once_with("dummy_url", headers=None, params=None)
self.assertEqual({"test": True}, result) self.assertEqual({"test": True}, result)