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:
parent
a8c06a001c
commit
b99f2078a1
@ -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
0
synergy/auth/__init__.py
Normal file
30
synergy/auth/plugin.py
Normal file
30
synergy/auth/plugin.py
Normal 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!")
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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")
|
||||||
|
@ -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):
|
||||||
|
@ -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):
|
||||||
|
@ -19,3 +19,7 @@ permissions and limitations under the License."""
|
|||||||
|
|
||||||
class SynergyError(Exception):
|
class SynergyError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationError(Exception):
|
||||||
|
pass
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user