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
|
||||
tcp_keepidle = 600
|
||||
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):
|
||||
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:
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -19,3 +19,7 @@ permissions and limitations under the License."""
|
||||
|
||||
class SynergyError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationError(Exception):
|
||||
pass
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user