Skip to content

Commit

Permalink
Merge "Enhance security of discovery server"
Browse files Browse the repository at this point in the history
  • Loading branch information
Zuul authored and opencontrail-ci-admin committed Feb 26, 2016
2 parents 92c647b + 1f508c3 commit 8b88c28
Show file tree
Hide file tree
Showing 9 changed files with 363 additions and 70 deletions.
4 changes: 4 additions & 0 deletions src/config/common/tests/test_utils.py
Expand Up @@ -991,6 +991,7 @@ def __init__(self, app, conf, *args, **kwargs):
auth_protocol = conf['auth_protocol']
auth_host = conf['auth_host']
auth_port = conf['auth_port']
self.delay_auth_decision = conf['delay_auth_decision']
self.request_uri = '%s://%s:%s' % (auth_protocol, auth_host, auth_port)
self.auth_uri = self.request_uri
# print 'FakeAuthProtocol init: auth-uri %s, conf %s' % (self.auth_uri, self.conf)
Expand Down Expand Up @@ -1077,6 +1078,9 @@ def __call__(self, env, start_response):
if user_token:
# print '****** user token %s ***** ' % user_token
pass
elif self.delay_auth_decision:
self._add_headers(env, {'X-Identity-Status': 'Invalid'})
return self.app(env, start_response)
else:
# print 'Missing token or Unable to authenticate token'
return self._reject_request(env, start_response)
Expand Down
70 changes: 39 additions & 31 deletions src/config/utils/discovery_cli.py
Expand Up @@ -14,6 +14,7 @@

EP_DELIM=','
PUBSUB_DELIM=' '
DEFAULT_HEADERS = {'Content-type': 'application/json; charset="UTF-8"'}

def show_usage():
print 'A rule string must be specified for this operation'
Expand Down Expand Up @@ -225,6 +226,43 @@ def get_ks_var(args, name):
server_ip = server[0]
server_port = server[1]

# Validate API server information
api_server = args.api_server.split(':')
if len(api_server) != 2:
print 'API server address must be of the form ip:port, '\
'for example 127.0.0.1:8082'
sys.exit(1)
api_server_ip = api_server[0]
api_server_port = api_server[1]

# Validate keystone credentials
conf = {}
for name in ['username', 'password', 'tenant_name']:
val, rsp = get_ks_var(args, name)
if val is None:
print rsp
sys.exit(1)
conf[name] = val

username = conf['username']
password = conf['password']
tenant_name = conf['tenant_name']

print 'API Server = ', args.api_server
print 'Discovery Server = ', args.server
print 'Username = ', username
print 'Tenant = ', tenant_name
print ''

try:
vnc = VncApi(username, password, tenant_name,
api_server[0], api_server[1])
except Exception as e:
print '*** %s' % str(e)
sys.exit(1)

headers = DEFAULT_HEADERS.copy()
headers['X-AUTH-TOKEN'] = vnc.get_auth_token()

if args.oper_state or args.admin_state or args.oper_state_reason:
if not args.service_id or not args.service_type:
Expand All @@ -240,9 +278,6 @@ def get_ks_var(args, name):
data['oper-state-reason'] = args.oper_state_reason
if args.admin_state:
data['admin-state'] = args.admin_state
headers = {
'Content-type': 'application/json',
}
url = "http://%s:%s/service/%s" % (server_ip, server_port, args.service_id)
r = requests.put(url, data=json.dumps(data), headers=headers)
if r.status_code != 200:
Expand All @@ -255,35 +290,11 @@ def get_ks_var(args, name):
if args.service_id:
print 'Specific service id %s ignored for this operation' % args.service_id
url = "http://%s:%s/load-balance/%s" % (server_ip, server_port, args.service_type)
r = requests.post(url)
r = requests.post(url, headers=headers)
if r.status_code != 200:
print "Operation status %d" % r.status_code
sys.exit(0)

# Validate API server information
api_server = args.api_server.split(':')
if len(api_server) != 2:
print 'Discovery server address must be of the form ip:port, '\
'for example 127.0.0.1:5998'
sys.exit(1)
api_server_ip = api_server[0]
api_server_port = api_server[1]

# Validate keystone credentials
conf = {}
for name in ['username', 'password', 'tenant_name']:
val, rsp = get_ks_var(args, name)
if val is None:
print rsp
sys.exit(1)
conf[name] = val

username = conf['username']
password = conf['password']
tenant_name = conf['tenant_name']

vnc = VncApi(username, password, tenant_name,
api_server[0], api_server[1])

uuid = args.uuid
# transform uuid if needed
Expand All @@ -295,9 +306,6 @@ def get_ks_var(args, name):
print 'Oper = ', args.op
print 'Name = %s' % fq_name
print 'UUID = %s' % uuid
print 'API Server = ', args.server
print 'Discovery Server = ', args.server
print ''

if args.op == 'add-rule':
if not args.rule:
Expand Down
3 changes: 1 addition & 2 deletions src/discovery/SConscript
Expand Up @@ -46,15 +46,14 @@ for file in setup_sources:

local_sources = [
'__init__.py',
'disc_server_zk.py',
'disc_server.py',
'disc_utils.py',
'disc_consts.py',
'disc_exceptions.py',
'client.py',
'disc_zk.py',
'disc_cassdb.py',
'output.py',
'disc_auth_keystone.py',
]
local_sources_rules = []
for file in local_sources:
Expand Down
40 changes: 40 additions & 0 deletions src/discovery/disc_auth_keystone.py
@@ -0,0 +1,40 @@
#
# Copyright (c) 2013 Juniper Networks, Inc. All rights reserved.
#
#
# authentication/authorization functionality for discovery server
#

try:
from keystoneclient.middleware import auth_token
except ImportError:
from keystonemiddleware import auth_token
except Exception:
pass

class AuthServiceKeystone(object):

def __init__(self, conf):
self._conf_info = conf
# end __init__

# gets called from keystone middleware after token check
def token_valid(self, env, start_response):
status = env.get('HTTP_X_IDENTITY_STATUS')
return True if status != 'Invalid' else False

def validate_user_token(self, request):
# following config forces keystone middleware to always return the result
# back in HTTP_X_IDENTITY_STATUS env variable
conf_info = self._conf_info.copy()
conf_info['delay_auth_decision'] = True

auth_middleware = auth_token.AuthProtocol(self.token_valid, conf_info)
return auth_middleware(request.headers.environ, None)

def is_admin(self, request):
if not self.validate_user_token(request):
return False
roles = request.headers.environ.get('HTTP_X_ROLE', '').split(",")
return 'admin' in [x.lower() for x in roles]
# end class AuthServiceKeystone
68 changes: 68 additions & 0 deletions src/discovery/disc_server.py
Expand Up @@ -52,6 +52,7 @@
from gevent.coros import BoundedSemaphore
from cfgm_common.rest import LinkObject

import disc_auth_keystone

def obj_to_json(obj):
# Non-null fields in object get converted to json fields
Expand Down Expand Up @@ -82,10 +83,15 @@ def __init__(self, args):
'auto_lb': 0,
'db_exc_unknown': 0,
'db_exc_info': '',
'wl_rejects_pub': 0,
'wl_rejects_sub': 0,
'auth_failures': 0,
}
self._ts_use = 1
self.short_ttl_map = {}
self._sem = BoundedSemaphore(1)
self._pub_wl = None
self._sub_wl = None

self._base_url = "http://%s:%s" % (self._args.listen_ip_addr,
self._args.listen_port)
Expand Down Expand Up @@ -225,6 +231,28 @@ def __init__(self, args):
self._sub_data = {}
for (client_id, service_type) in self._db_conn.subscriber_entries():
self.create_sub_data(client_id, service_type)

# build white list
if self._args.white_list_publish:
self._pub_wl = IPSet()
for prefix in self._args.white_list_publish.split(" "):
self._pub_wl.add(prefix)
if self._args.white_list_subscribe:
self._sub_wl = IPSet()
for prefix in self._args.white_list_subscribe.split(" "):
self._sub_wl.add(prefix)

self._auth_svc = None
if self._args.auth == 'keystone':
ks_conf = {
'auth_host': self._args.auth_host,
'auth_port': self._args.auth_port,
'auth_protocol': self._args.auth_protocol,
'admin_user': self._args.admin_user,
'admin_password': self._args.admin_password,
'admin_tenant_name': self._args.admin_tenant_name,
}
self._auth_svc = disc_auth_keystone.AuthServiceKeystone(ks_conf)
# end __init__

def config_log(self, msg, level):
Expand Down Expand Up @@ -374,6 +402,15 @@ def error_handler(self, *args, **kwargs):
raise
return error_handler

# decorator to authenticate request
def authenticate(func):
def wrapper(self, *args, **kwargs):
if self._auth_svc and not self._auth_svc.is_admin(bottle.request):
self._debug['auth_failures'] += 1
bottle.abort(401, 'Unauthorized')
return func(self, *args, **kwargs)
return wrapper

# 404 forces republish
def heartbeat(self, sig):
# self.syslog('heartbeat from "%s"' % sig)
Expand Down Expand Up @@ -425,6 +462,12 @@ def api_heartbeat(self):
@db_error_handler
def api_publish(self, end_point = None):
self._debug['msg_pubs'] += 1

source = bottle.request.headers.get('X-Forwarded-For', None)
if source and self._pub_wl and source not in self._pub_wl:
self._debug['wl_rejects_pub'] += 1
bottle.abort(401, 'Unauthorized request')

ctype = bottle.request.headers['content-type']
json_req = {}
try:
Expand Down Expand Up @@ -670,6 +713,12 @@ def adjust_in_use_list(self, pubs, in_use_list):
@db_error_handler
def api_subscribe(self):
self._debug['msg_subs'] += 1

source = bottle.request.headers.get('X-Forwarded-For', None)
if source and self._sub_wl and source not in self._sub_wl:
self._debug['wl_rejects_sub'] += 1
bottle.abort(401, 'Unauthorized request')

ctype = bottle.request.headers['content-type']
if 'application/json' in ctype:
json_req = bottle.request.json
Expand Down Expand Up @@ -846,6 +895,7 @@ def api_subscribe(self):

# on-demand API to load-balance existing subscribers across all currently available
# publishers. Needed if publisher gets added or taken down
@authenticate
def api_lb_service(self, service_type):
if service_type is None:
bottle.abort(405, "Missing service")
Expand Down Expand Up @@ -998,6 +1048,7 @@ def services_json(self, service_type=None):
return {'services': rsp}
# end services_json

@authenticate
def service_http_put(self, id):
self.syslog('Update service %s' % (id))
try:
Expand Down Expand Up @@ -1270,6 +1321,8 @@ def parse_args(args_str):
'logger_class': None,
'sandesh_send_rate_limit': SandeshSystem.get_sandesh_send_rate_limit(),
'cluster_id': None,
'white_list_publish': None,
'white_list_subscribe': None,
}

# per service options
Expand All @@ -1282,6 +1335,14 @@ def parse_args(args_str):
'cassandra_user' : None,
'cassandra_password' : None,
}
keystone_opts = {
'auth_host': '127.0.0.1',
'auth_port': '35357',
'auth_protocol': 'http',
'admin_user': '',
'admin_password': '',
'admin_tenant_name': '',
}

service_config = {}
cassandra_config = {}
Expand All @@ -1299,11 +1360,15 @@ def parse_args(args_str):
if section == "DEFAULTS":
defaults.update(dict(config.items("DEFAULTS")))
continue
if 'KEYSTONE' in config.sections():
keystone_opts.update(dict(config.items("KEYSTONE")))
continue
service_config[
section.lower()] = default_service_opts.copy()
service_config[section.lower()].update(
dict(config.items(section)))

defaults.update(keystone_opts)
parser.set_defaults(**defaults)

parser.add_argument(
Expand Down Expand Up @@ -1384,6 +1449,9 @@ def parse_args(args_str):
help="Sandesh send rate limit in messages/sec")
parser.add_argument("--cluster_id",
help="Used for database keyspace separation")
parser.add_argument(
"--auth", choices=['keystone'],
help="Type of authentication for user-requests")

args = parser.parse_args(remaining_argv)
args.conf_file = args.conf_file
Expand Down
1 change: 1 addition & 0 deletions src/discovery/test-requirements.txt
Expand Up @@ -15,3 +15,4 @@ vnc_api
discoveryclient
sandesh
sandesh-common
keystonemiddleware

0 comments on commit 8b88c28

Please sign in to comment.