2 # -------------------------------------------------------------------------
3 # Copyright (c) 2015-2017 AT&T Intellectual Property
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 # -------------------------------------------------------------------------
22 from yaml.constructor import ConstructorError
24 from notario import decorators
25 from notario.validators import types
26 from oslo_log import log
28 from pecan_notario import validate
30 from conductor.api.controllers import error
31 from conductor.api.controllers import string_or_dict
32 from conductor.api.controllers import validator
33 from conductor.i18n import _, _LI
34 from oslo_config import cfg
36 from conductor.api.adapters.aaf import aaf_authentication as aaf_auth
40 LOG = log.getLogger(__name__)
42 CONDUCTOR_API_OPTS = [
43 cfg.StrOpt('server_url',
45 help='Base URL for plans.'),
46 cfg.StrOpt('username',
48 help='username for plans.'),
49 cfg.StrOpt('password',
51 help='password for plans.'),
52 cfg.BoolOpt('basic_auth_secure',
54 help='auth toggling.'),
57 CONF.register_opts(CONDUCTOR_API_OPTS, group='conductor_api')
60 (decorators.optional('files'), types.dictionary),
61 (decorators.optional('id'), types.string),
62 (decorators.optional('limit'), types.integer),
63 (decorators.optional('name'), types.string),
64 (decorators.optional('num_solution'), types.string),
65 ('template', string_or_dict),
66 (decorators.optional('template_url'), types.string),
67 (decorators.optional('timeout'), types.integer),
71 class PlansBaseController(object):
72 """Plans Base Controller - Common Methods"""
74 def plan_link(self, plan_id):
77 "href": "%(url)s/v1/%(endpoint)s/%(id)s" %
79 'url': pecan.request.application_url,
87 def plans_get(self, plan_id=None):
89 auth_flag = CONF.conductor_api.basic_auth_secure or CONF.aaf_api.is_aaf_enabled
91 # TBD - is healthcheck properly supported?
92 if plan_id == 'healthcheck' or \
94 (auth_flag and check_auth()):
95 return self.plan_getid(plan_id)
97 def plan_getid(self, plan_id):
101 args = {'plan_id': plan_id}
102 LOG.debug('Plan {} requested.'.format(plan_id))
105 LOG.debug('All plans requested.')
109 client = pecan.request.controller
110 result = client.call(ctx, method, args)
111 plans = result and result.get('plans')
113 for the_plan in plans:
114 the_plan_id = the_plan.get('id')
115 the_plan['links'] = [self.plan_link(the_plan_id)]
116 plans_list.append(the_plan)
119 if len(plans_list) == 1:
122 # For a single plan, we return None if not found
125 # For all plans, it's ok to return an empty list
128 def plan_create(self, args):
130 method = 'plan_create'
132 # TODO(jdandrea): Enhance notario errors to use similar syntax
133 # valid_keys = ['files', 'id', 'limit', 'name',
134 # 'template', 'template_url', 'timeout']
135 # if not set(args.keys()).issubset(valid_keys):
136 # invalid = [name for name in args if name not in valid_keys]
137 # invalid_str = ', '.join(invalid)
138 # error('/errors/invalid',
139 # _('Invalid keys found: {}').format(invalid_str))
140 # required_keys = ['template']
141 # if not set(required_keys).issubset(args):
142 # required = [name for name in required_keys if name not in args]
143 # required_str = ', '.join(required)
144 # error('/errors/invalid',
145 # _('Missing required keys: {}').format(required_str))
147 LOG.debug('Plan creation requested (name "{}").'.format(
150 client = pecan.request.controller
152 transaction_id = pecan.request.headers.get('transaction-id')
154 args['template']['transaction-id'] = transaction_id
156 result = client.call(ctx, method, args)
157 plan = result and result.get('plan')
160 plan_name = plan.get('name')
161 plan_id = plan.get('id')
162 plan['links'] = [self.plan_link(plan_id)]
163 LOG.info(_LI('Plan {} (name "{}") created.').format(
168 def plan_delete(self, plan):
170 method = 'plans_delete'
172 plan_name = plan.get('name')
173 plan_id = plan.get('id')
174 LOG.debug('Plan {} (name "{}") deletion requested.'.format(
177 args = {'plan_id': plan_id}
178 client = pecan.request.controller
179 client.call(ctx, method, args)
180 LOG.info(_LI('Plan {} (name "{}") deleted.').format(
184 class PlansItemController(PlansBaseController):
185 """Plans Item Controller /v1/plans/{plan_id}"""
187 def __init__(self, uuid4):
190 self.plan = self.plans_get(plan_id=self.uuid)
193 error('/errors/not_found',
194 _('Plan {} not found').format(self.uuid))
195 pecan.request.context['plan_id'] = self.uuid
199 """Allowed methods"""
202 @pecan.expose(generic=True, template='json')
204 """Catchall for unallowed methods"""
205 message = _('The {} method is not allowed.').format(
206 pecan.request.method)
207 kwargs = {'allow': self.allow()}
208 error('/errors/not_allowed', message, **kwargs)
210 @index.when(method='OPTIONS', template='json')
211 def index_options(self):
213 pecan.response.headers['Allow'] = self.allow()
214 pecan.response.status = 204
216 @index.when(method='GET', template='json')
219 return {"plans": [self.plan]}
221 @index.when(method='DELETE', template='json')
222 def index_delete(self):
224 self.plan_delete(self.plan)
225 pecan.response.status = 204
228 class PlansController(PlansBaseController):
229 """Plans Controller /v1/plans"""
233 """Allowed methods"""
236 @pecan.expose(generic=True, template='json')
238 """Catchall for unallowed methods"""
239 message = _('The {} method is not allowed.').format(
240 pecan.request.method)
241 kwargs = {'allow': self.allow()}
242 error('/errors/not_allowed', message, **kwargs)
244 @index.when(method='OPTIONS', template='json')
245 def index_options(self):
247 pecan.response.headers['Allow'] = self.allow()
248 pecan.response.status = 204
250 @index.when(method='GET', template='json')
252 """Get all the plans"""
253 plans = self.plans_get()
254 return {"plans": plans}
256 @index.when(method='POST', template='json')
257 @validate(CREATE_SCHEMA, '/errors/schema')
258 def index_post(self):
261 # Look for duplicate keys in the YAML/JSON, first in the
262 # entire request, and then again if the template parameter
263 # value is itself an embedded JSON/YAML string.
264 where = "API Request"
266 parsed = yaml.load(pecan.request.text, validator.UniqueKeyLoader)
267 if 'template' in parsed:
269 template = parsed['template']
270 if isinstance(template, six.string_types):
271 yaml.load(template, validator.UniqueKeyLoader)
272 except ConstructorError as exc:
273 # Only bail on the duplicate key problem (problem and problem_mark
274 # attributes are available in ConstructorError):
276 validator.UniqueKeyLoader.DUPLICATE_KEY_PROBLEM_MARK:
277 # ConstructorError messages have a two line snippet.
278 # Grab it, get rid of the second line, and strip any
279 # remaining whitespace so we can fashion a one line msg.
280 snippet = exc.problem_mark.get_snippet()
281 snippet = snippet.split('\n')[0].strip()
282 msg = _('{} has a duplicate key on line {}: {}')
283 error('/errors/invalid',
284 msg.format(where, exc.problem_mark.line + 1, snippet))
285 except Exception as exc:
286 # Let all others pass through for now.
289 args = pecan.request.json
291 # Print request id from SNIOR at the beginning of API component
292 if args and args['name']:
293 LOG.info('Plan name: {}'.format(args['name']))
295 auth_flag = CONF.conductor_api.basic_auth_secure or CONF.aaf_api.is_aaf_enabled
297 # Create the plan only when the basic authentication is disabled or pass the authenticaiton check
298 if not auth_flag or \
299 (auth_flag and check_auth()):
300 plan = self.plan_create(args)
303 error('/errors/server_error', _('Unable to create Plan.'))
305 pecan.response.status = 201
309 def _lookup(self, uuid4, *remainder):
310 """Pecan subcontroller routing callback"""
311 return PlansItemController(uuid4), remainder
316 Returns True/False if the username/password of Basic Auth match/not match
317 Will also check role-based access controls if AAF integration configured
318 :return boolean value
322 if pecan.request.headers['Authorization'] and verify_user(pecan.request.headers['Authorization']):
323 LOG.debug("Authorized username and password")
327 auth_str = pecan.request.headers['Authorization']
328 user_pw = auth_str.split(' ')[1]
329 decode_user_pw = base64.b64decode(user_pw)
330 list_id_pw = decode_user_pw.split(':')
331 LOG.error("Incorrect username={} / password={}".format(list_id_pw[0], list_id_pw[1]))
333 error('/errors/basic_auth_error', _('Unauthorized: The request does not '
334 'provide any HTTP authentication (basic authentication)'))
338 error('/errors/authentication_error', _('Invalid credentials: username or password is incorrect'))
343 def verify_user(authstr):
345 authenticate user as per config file or AAF authentication service
347 :return boolean value
351 user_pw = auth_str.split(' ')[1]
352 user_pw = user_pw.encode() # below function needs user_pw in bytes object in python 3 so converting that
353 decode_user_pw = base64.b64decode(user_pw)
354 list_id_pw = decode_user_pw.decode().split(':')
355 user_dict['username'] = str(list_id_pw[0])
356 user_dict['password'] = str(list_id_pw[1])
357 password = CONF.conductor_api.password
358 username = CONF.conductor_api.username
360 # print ("plans.verify_user(): Expected username/password: {}/{}".format(username, password))
361 # print ("plans.verify_user(): Provided username/password: {}/{}".format(user_dict['username'], user_dict['password']))
365 if CONF.aaf_api.is_aaf_enabled:
366 retVal = aaf_auth.authenticate(user_dict['username'], user_dict['password'])
368 if username == user_dict['username'] and password == user_dict['password']: