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
tcp_keepidle = 600
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):
self.name = name
self.token = None
def getName(self):
return self.name
def setToken(self, token):
self.token = token
def configureParser(self, subparser):
raise NotImplementedError("not implemented!")
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()
try:

View File

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

View File

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

View File

@ -21,9 +21,9 @@ permissions and limitations under the License."""
CONF = cfg.CONF
service_opts = [
cfg.StrOpt("topic", default="synergy_topic", help="the topic"),
cfg.StrOpt("exchange", default="synergy_exchange", help="the exchange"),
auth_opts = [
cfg.StrOpt("plugin", default="noauth", help="the authorization plugin"),
cfg.StrOpt("policy_file", default="policy.json", help="the plucy file"),
]
wsgi_opts = [
@ -67,9 +67,9 @@ manager_opts = [
cfg.IntOpt("rate", default=60)
]
cfg.CONF.register_opts(service_opts)
cfg.CONF.register_opts(wsgi_opts, group="WSGI")
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):

View File

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

View File

@ -19,3 +19,7 @@ permissions and limitations under the License."""
class SynergyError(Exception):
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.serializer import SynergyEncoder
from synergy.common.service import Service
from synergy.common import utils
from synergy.common.wsgi import Server
from synergy.exception import AuthorizationError
from synergy.exception import SynergyError
@ -91,9 +93,17 @@ class Synergy(Service):
self.managers = {}
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):
LOG.info("loading manager %s", entry.name)
LOG.info("loading the %s manager", entry.name)
try:
CONF.register_opts(config.manager_opts, group=entry.name)
@ -143,6 +153,28 @@ class Synergy(Service):
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):
result = []
@ -156,6 +188,7 @@ class Synergy(Service):
start_response("200 OK", [("Content-Type", "text/html")])
return ["%s" % json.dumps(result, cls=SynergyEncoder)]
@authorizationRequired
def getManagerStatus(self, environ, start_response):
manager_list = None
result = []
@ -194,10 +227,10 @@ class Synergy(Service):
start_response("200 OK", [("Content-Type", "text/html")])
return ["%s" % json.dumps(result, cls=SynergyEncoder)]
@authorizationRequired
def executeCommand(self, environ, start_response):
manager_name = None
command = None
query = environ.get("QUERY_STRING", None)
if not query:
@ -251,6 +284,7 @@ class Synergy(Service):
[("Content-Type", "text/plain")])
return ["error: %s" % ex]
@authorizationRequired
def startManager(self, environ, start_response):
manager_list = None
result = []
@ -308,10 +342,10 @@ class Synergy(Service):
start_response("200 OK", [("Content-Type", "text/html")])
return ["%s" % json.dumps(result, cls=SynergyEncoder)]
@authorizationRequired
def stopManager(self, environ, start_response):
manager_list = None
result = []
query = environ.get("QUERY_STRING", None)
if not query:

View File

@ -47,5 +47,5 @@ class TestHTTPCommand(base.TestCase):
as m:
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)